痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里实现可靠Flash IAP的方法


大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是双核i.MXRT1180下XIP应用里实现可靠Flash IAP的方法

近期有一个 RT1180 客户在咨询关于双核应用下 Flash IAP 实现的问题,其应用场景是:主核 CM33 运行在 FlexSPI XIP Flash 代码区域,从核 CM7 运行在 ITCM 里且需要对同一颗 Flash 的数据区域进行 IAP 操作。因为目前大部分 NOR Flash 都不支持 Read-While-Write 特性,因此在从核 CM7 执行 IAP 操作过程中,Flash 不能够被读访问,这时候我们就需要特别保证主核 CM33 程序执行不出问题。这显然是一个很有意思的话题,痞子衡今天就来和大家深入讨论一下解决方案。

  • Note:本文虽以 RT1180 为例,但所述方法对其他双核芯片(如 RT1170、RT700)也同样适用。

一、双核通信的三种方法

因为今天这个话题涉及双核架构,有必要先简单和大家谈一谈双核间通信的三种方式及其优缺点,注意这里仅指双核间传递消息数据(侧重通知),而不涉及所谓信号量 semaphore 概念(侧重互斥,比如两个核抢同一个"硬件锁寄存器",谁先抢到,谁就独占某个共享资源)。

1.1 共享内存

第一种方法就是最常见的共享内存,只要是两个核均能访问的 SRAM,Register 等均可以用作消息数据传递。比如定义如下结构体 g_sharedMsg 存储两个 msg 数据,core0 发出的消息存入 core0_msg,core1 发出消息存入 core1_msg,在两个核的工程链接文件里,均将 .shared_mem 段指向同一个物理地址即可(需要是 Non‑Cacheable 属性的内存,且是 4 字节对齐地址)。

C 复制代码
// 对 ARM Cortex‑M 来说,4 字节对齐的 uint32_t 读写是原子的,不会出现"读到一半新、一半旧"的撕裂问题
typedef struct _shared_msg
{
    uint32_t core0_msg;
    uint32_t core1_msg;
} shared_msg_t;

__attribute__((section(".shared_mem")))
volatile shared_msg_t g_sharedMsg;

这种方法的优点是非常通用,不依赖任何硬件,且消息数据量不限(取决于内存大小);缺点是消息数据需要内核以 polling 方式获取,消息交互实时性不高。

1.2 硬件MU模块

第二种方法就是借助专用于双核通信的 Messaging Unit (MU) 模块,这是一个硬件模块,双核 MCU 里一般都会有,其提供了中断驱动的消息传递机制。这种方法的优点是实时性最佳,消息交互通过中断驱动,不需要内核去 polling;缺点是依赖专用硬件模块,且消息寄存器数量有限,比如 RT1180 上 MU 一次最多传递 4 个 uint32_t 数据。

1.3 硬件通信模块

最后一种方法就是借助一般通信外设模块,比如 GPIO/UART/SPI/I2C 等,两个内核各控制一个通信外设,片外通过 pin 脚将两个外设相连。这种方法的优点是实时性也不错,消息交互也可通过通信外设自身中断驱动,并且消息数据量也不限;缺点是消耗硬件通信外设,且占用外部 pin 脚。

二、MU模块及其驱动简介

综合比较,本文选取了第二种方法即借助于片内专用 MU 模块实现双核通信,下面简单介绍一下 MU 模块的基本特性和驱动用法。RT1180 内部共有两个 MU 模块,每个 MU 内部包含两个子模块 MUA 和 MUB,分别对应主核 CM33 (Processor A)和从核 CM7 (Processor B),MUA 与 MUB 子模块之间通过内部总线相连。

每个 MUA/B 模块均提供了 4 个 32-bit 的消息寄存器(发送通过 TR0-3,接收通过 RR0-3)用于数据传递,以及相应的中断机制。比如当 Processor A 向 MUA_TR0 写入数据时,Processor B 可以通过 MUB_RR0 读取该数据,同时会触发 Processor B 的中断;反之亦然。

下面是一个基于 SDK v25.12 里 fsl_mu 驱动代码的简单示例,两个内核各自初始化自己的 MU 子模块,这里定义了两个 MU_CMD,通过 MUx_TR0/RR0 交互,CM7 先通过 MUB 发出 MU_CMD_ECHO 消息,CM33 通过 MUA 中断得到该消息后立刻返回 MU_CMD_ACK 消息,CM7 收到返回消息即结束。

C 复制代码
#include "fsl_mu.h"
typedef enum {
    MU_CMD_ECHO  = 0xA5A50001,  // CM7 -> CM33
    MU_CMD_ACK   = 0xA5A50002,  // CM33 -> CM7
} mu_cmd_t;

// 下列代码应用于 CM33 工程
void mc_cm33_init(void)
{
    MU_Init(MU1_MUA);
    MU_EnableInterrupts(MU1_MUA, kMU_Rx0FullInterruptEnable);
    NVIC_EnableIRQ(MU1_IRQn);
}
void MU1_IRQHandler(void)
{
    uint32_t flag = MU_GetStatusFlags(MU1_MUA);
    if ((flag & kMU_Rx0FullFlag) == kMU_Rx0FullFlag)
    {
        uint32_t msg = MU_ReceiveMsgNonBlocking(MU1_MUA, kMU_MsgReg0);
        if (msg == MU_CMD_ECHO)
        {
            MU_SendMsgNonBlocking(MU1_MUA, kMU_MsgReg0, MU_CMD_ACK);
        }
    }
}

// 下列代码应用于 CM7 工程
void mc_cm7_init(void)
{
    MU_Init(MU1_MUB);
    MU_EnableInterrupts(MU1_MUB, kMU_Rx0FullInterruptEnable);
    NVIC_EnableIRQ(MU1_IRQn);

    MU_SendMsgNonBlocking(MU1_MUB, kMU_MsgReg0, MU_CMD_ECHO);
}
void MU1_IRQHandler(void)
{
    uint32_t flag = MU_GetStatusFlags(MU1_MUB);
    if ((flag & kMU_Rx0FullFlag) == kMU_Rx0FullFlag)
    {
        uint32_t msg = MU_ReceiveMsgNonBlocking(MU1_MUB, kMU_MsgReg0);
        if (msg == MU_CMD_ACK)
        {
            // Do something
        }
    }
}

三、双核管理驱动MCMGR用法

由于 MU 模块仅仅是最底层的裸消息数据传输,不带协议、不带缓冲、不带管理,这里不建议直接在应用程序里大量使用,因为代码易写错、维护成本高。NXP 官方提供了一个更高级的双核管理驱动 MCMGR(Multicore Manager),它是对 MU 模块的进一步封装,提供了更加便捷的双核通信接口。

我们将上一节里的代码示例实现用 MCMGR 驱动改写,这时候完全不用碰 MU 寄存器与驱动,代码变得更加清晰易读。MCMGR 驱动的核心是事件(Event)机制,每个事件都有一个 ID 类型和关联的回调函数,当一个核向另一个核发送事件时,接收端会触发相应的回调函数。

C 复制代码
#include "mcmgr.h"

#define MCMGR_EVENT_ECHO   (1U)  // CM7 -> CM33
#define MCMGR_EVENT_ACK    (2U)  // CM33 -> CM7

// 下列代码应用于 CM33 工程
void mc_cm33_init(void)
{
    (void)MCMGR_Init();
    MCMGR_RegisterEvent(
        kMCMGR_RemoteApplicationEvent,
        cm33_mc_cb,
        NULL);
}
static void cm33_mc_cb(mcmgr_core_t coreNum, uint16_t eventData, void *context)
{
    if (eventData == MCMGR_EVENT_ECHO)
    {
        MCMGR_TriggerEvent(kMCMGR_Core1, kMCMGR_RemoteApplicationEvent, MCMGR_EVENT_ACK);
    }
}

// 下列代码应用于 CM7 工程
void mc_cm7_init(void)
{
    (void)MCMGR_Init();
    MCMGR_RegisterEvent(
        kMCMGR_RemoteApplicationEvent,
        cm7_mc_cb,
        NULL);
    
    MCMGR_TriggerEvent(kMCMGR_Core0, kMCMGR_RemoteApplicationEvent, MCMGR_EVENT_ECHO);
}
static void cm7_mc_cb(mcmgr_core_t coreNum, uint16_t eventData, void *context)
{
    if (eventData == MCMGR_EVENT_ACK)
    {
        // Do something
    }
}

MCMGR 驱动一共定义了 9 种不同类型的事件,其中 kMCMGR_RemoteApplicationEvent 是用于用户自定义跨核事件的唯一正确类型。

C 复制代码
typedef enum _mcmgr_event_type_t
{
    kMCMGR_RemoteCoreUpEvent = 1,       // 内部事件,核状态管理
    kMCMGR_RemoteCoreDownEvent,         // 内部事件,核状态管理
    kMCMGR_RemoteExceptionEvent,        // 内部事件
    kMCMGR_StartupDataEvent,            // 启动数据
    kMCMGR_FeedStartupDataEvent,
    kMCMGR_RemoteRPMsgEvent,            // RPMsg 专用,IPC
    kMCMGR_RemoteApplicationEvent,      // 唯一用户事件
    kMCMGR_FreeRtosMessageBuffersEvent, // 系统用,Buffer 回收
    kMCMGR_EventTableLength
} mcmgr_event_type_t;

四、XIP应用里实现Flash IAP的方法

前面铺垫了那么多,看到这里相信你肯定已经对这个客户需求如何解决有了答案,其实也不复杂。我们可以直接基于 SDK multicore_examples/hello_world 例程来修改实现,该例程 CM33 是 XIP,CM7 在 ITCM 执行,跟客户情况一致,我们只需要在此基础上加上 MCMGR 和 IAP 代码,核心思想是 CM7 做 Flash IAP 之前必须先给 CM33 发 notify 消息让其跳转到 RAM 里驻留等待(脱离 XIP),得到 CM33 发来的 ready 消息时,CM7 才能真正开始做 Flash IAP,等 IAP 结束 CM7 再通知 CM33 重返 XIP,这里最大的注意点是 CM33 端 RAM 驻留函数设计与 FlexSPI 状态清理。

C 复制代码
CM7 端:
    > MCMGR_EVENT_FLASH_IAP_NOTIFY:通知CM33即将做IAP
    < MCMGR_EVENT_FLASH_IAP_READY:得到来自CM33的ready信号后开始做IAP
    > MCMGR_EVENT_FLASH_IAP_PASS:IAP成功后返回结果给CM33
    > MCMGR_EVENT_FLASH_IAP_FAIL:IAP失败后返回结果给CM33

CM33 端:
    < MCMGR_EVENT_FLASH_IAP_NOTIFY:当接收到来自CM7的notify信号后,清除FLEXSPI cache等待bus idle
    > MCMGR_EVENT_FLASH_IAP_READY:此时已驻留进RAM loop中,给CM7发ready信号
    < MCMGR_EVENT_FLASH_IAP_PASS:得到来自CM7的IAP成功结果,直接重返XIP
    < MCMGR_EVENT_FLASH_IAP_FAIL:得到来自CM7的IAP失败结果,需重新初始化FLEXSPI重返XIP(TBD)

具体实现代码如下,目前主要支持 RT1180,未来计划把 RT1170 和 RT700 也加上。

至此,双核i.MXRT1180下XIP应用里实现可靠Flash IAP的方法痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的 博客园主页CSDN主页知乎主页微信公众号 平台上。

微信搜索"痞子衡嵌入式"或者扫描下面二维码,就可以在手机上第一时间看了哦。