ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RT-Thread 双核 BSP 实战:i.MX RT1180 EVK 上 CM33 拉起 CM7 并跑通 RPMsg-Lite 核间通信

RT-Thread 双核 BSP 实战:i.MX RT1180 EVK 上 CM33 拉起 CM7 并跑通 RPMsg-Lite 核间通信

RT-Thread 双核 BSP 实战:i.MX RT1180 EVK 上 CM33 拉起 CM7 并跑通 RPMsg-Lite 核间通信

【免费下载链接】rt-threadRT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/项目地址: https://gitcode.com/gh_mirrors/rt/rt-thread

这篇以 RT-Thread 双核 BSP 中的 i.MX RT1180 EVK 板级支持包为对象,解决一个问题:一颗芯片里装着 800MHz 的 Cortex-M7 和 240MHz 的 Cortex-M33,怎么让两个核各自独立编译运行、由 CM33 主核把 CM7 从核拉起来,再用 RPMsg-Lite 在共享内存上完成 ping-pong 通信。下文全部基于仓库里的 BSP 源码(bsp/nxp/imx/imxrt/imxrt1180-nxp-evk)逐步演示,不涉及任何外部依赖说明。

🎯 场景:一颗 M33 加一颗 M7,各自该干什么活

先交代一下这颗芯片的定位。i.MX RT1180 是 NXP 的跨界 MCU,把两个内核塞在同一个 die 里:

  • Cortex-M7:800MHz,算力担当,适合电机控制、实时信号处理这类重计算任务;
  • Cortex-M33:240MHz,带 TrustZone 安全扩展,适合管安全启动、系统管理和网络协议栈。

外围资源也很"工业":双千兆以太网、多种工业通信接口(含 EtherCAT),宽温范围。所以它面向的就是工业控制、电机驱动这类场景——而这些场景恰恰需要两个核分工协作:一个干活,一个管安全与系统。

BSP 对此的设计是:CM33 当主核(master),CM7 当从核(remote),两个核各有一套完全独立的工程,可以分别编译、分别下载;如果不想手动下两个镜像,也可以用 MCUXpresso Secure Provisioning Tool 生成一个可启动的双核镜像。

小提示:本文所有操作都在这块板子上做,如果你手上是 RT1170 等其他 i.MX RT EVK,流程基本通用,只是串口和控制台配置不同。

⚡ 三条命令跑通:从 pkgs --update 到 Hello_World

编译前先把依赖包拉下来

这个 BSP 依赖NXP IMXRT 驱动包(对应 cm33/rtconfig.h 里的PKG_USING_NXP_IMXRT_DRIVER)。跳过这步编译必挂,因为缺库。在 BSP 目录下打开 ENV 工具后执行:

pkgs --update

生成工程并烧录

MDK5、IAR、GCC 三条路都通了,按自己习惯选:

scons --target=iar # 生成 IAR 工程 scons --target=mdk5 # 生成 KEIL MDK5 工程 scons -j8 # 直接并行编译 GCC 工程

工具链参数和链接脚本由 cm33/rtconfig.py 决定,生成的 IDE 工程落在对应核的目录(如cm33/)下。硬件侧只要两根线:数据线接 PC(板上自带调试器)、打开电源开关。

我以 IAR 为例:双击project.eww打开工程,点编译,然后下载。工程默认走CMSIS-DAP下载——保证 CMSIS-DAP 连接正常后,点下载按钮即可烧录,不用额外配置。

打开串口验证

下载完系统自己就跑了。把开发板串口接 PC,终端按115200-8-1-N打开,复位后先看到启动 banner:

\ | / - RT - Thread Operating System / | \ 5.2.2 build May 15 2026 17:19:35 2006 - 2024 Copyright by RT-Thread team

接着 CM33 工程会打印MIMXRT1180_CM33 Hello_World,并翻转板载 LED(cm33/applications/main.c,引脚GET_PIN(4, 27),500ms 一个来回);CM7 工程则打印MIMXRT1180_CM7 Hello_World。看到这两句,就说明两个核各自活着了。

🗺️ 工程地图:cm33 和 cm7 两套工程怎么分工

目录结构一眼能看明白——同一层下两个平行工程,内部布局完全对称:

imxrt1180-nxp-evk/ ├── README.md / README_zh.md ├── cm33/ # 主核工程 │ ├── applications/ # main.c / multicore_thread.c / mnt.c │ ├── board/ │ │ ├── MCUX_Config/ # NXP 生成的时钟、引脚配置 │ │ ├── linker_scripts/ # RAM / FLEXSPI_NOR / HYPERRAM 三套 │ │ ├── ports/ # flexspi NOR flash 驱动、FAL 移植 │ │ └── Kconfig # 外设配置菜单 │ ├── xip/ # FlexSPI NOR XIP 启动配置 │ ├── rtconfig.h / rtconfig.py │ └── template.ewp / template.uvprojx ... └── cm7/ # 从核工程,结构同 cm33

此时你可能会问:两个工程到底差在哪?答案都写在各自的rtconfig.h里:

对比项cm33/rtconfig.hcm7/rtconfig.h
架构宏ARCH_ARM_CORTEX_M33+ARCH_ARM_CORTEX_SECUREARCH_ARM_CORTEX_M7+ARCH_ARM_CORTEX_FPU
控制台uart1(LPUART1)uart12(LPUART12)
串口 DMA 通道RX ch0 / TX ch1RX ch22 / TX ch23
版本RT_VER_NUM 0x50300(RT-Thread 5.x)同左
默认外设BSP_USING_QSPIFLASH/BSP_USING_DMA/BSP_USING_GPIO/ 对应 LPUART同左

记住一个关键点:两个核的控制台不是同一个串口——CM33 走 UART1,CM7 走 UART12,调试时想同时看两边日志,得接两路串口分别监听。

BSP 目前的外设支持面(以仓库现状为准):

  • 板载:QSPI Flash XIP(外部 NOR Flash 直接跑代码)、flexspi Flash 驱动、CherryUSB(设备/主机两模式)、NETC 交换控制器(双千兆以太网)、EtherCAT;
  • 片上:多核 kick-off、Rpmsg-lite、GPIO、LPUART1~12、LPSPI1~4、LPI2C1/2/3/6、ADC(含 LPADC1 多通道 DMA 示例)、Timer(DWT 周期计数 + 微秒延时)、SDIO(可配 FATFS/LittleFS)、RTC、FLEXCAN3;
  • 暂不支持:PWM,文档标注后续版本规划,用到前先确认。

🔧 外设按需开:menuconfig 里这些开关各管什么

BSP 默认只给最小串口配置,其余外设全靠 ENV 工具开关。标准流程四步:在核工程目录打开 ENV →menuconfig配置并保存 →pkgs --update→ 重新scons --target=mdk5或scons --target=iar。菜单本体在 cm33/board/Kconfig(cm7 对应一份),值得留意的项我归成三组。

片上外设:大部分带 DMA 联动

  • DWT:周期计数器 + 微秒级忙等延时,提供dwt_init()/dwt_get_cycles()/dwt_delay_us();
  • DMA:总开关,勾选任何 UART/SPI/I2C 的 DMA 选项会自动把它选中;
  • GPIO:默认开,自动选RT_USING_PIN,配合GET_PIN()用;
  • RTC:默认关;
  • USB(CherryUSB):先选控制器(OTG1/OTG2)再选角色(Device/Host),Host 模式下可勾BSP_CHERRYUSB_HOST_MOUSE_DEMO跑鼠标 shell 示例(对应 cm33/board/usbh_mouse_demo.c 与 cm33/board/cherryusb_cdc_acm.c);
  • SDIO:开了之后可再勾CODE_STORED_ON_SDCARD,让 SD 卡当启动设备;
  • LPUART1~12:每路独立使能,各自配 RX/TX DMA 通道(0~32,默认递增);LPSPI1~4同理可单独开 DMA;LPI2C1/2/3/6每路在 100kHz/400kHz 里选波特率;
  • CAN:FLEXCAN3,默认开;FLEXSPI1/2:外部 Flash/SRAM 总线控制器,QSPI Flash 和文件系统都压在它上面。

板载外设与内存布局

  • HyperRAM:BSP_USE_HYPERRAM,把外部 HyperRAM 挂成扩展内存;
  • 以太网(NETC):BSP_USING_ETH会自动选中 lwIP/SAL/netdev;NETC_USE_SWT选模式(0 为 EP0 直连,1 为 switch 交换),PHY 默认开;
  • 文件系统:BSP_USING_FS下可以组合 SD 卡 FATFS(BSP_USING_SDCARD_FATFS)或 QSPI Flash LittleFS(BSP_USING_QSPI_FLASH_FS,依赖 FAL 分区表,见 cm33/board/ports/fal_cfg.h);
  • QSPI Flash:BSP_USING_QSPI_FLASH,FlexSPI1 上 NOR Flash 的读写驱动。

内存布局是三选一,在 Kconfig 末尾的 Linker Script Selection 里,对应 cm33/board/linker_scripts/ 下不同后缀的链接脚本:

  • BSP_LINKER_SCRIPT_RAM:镜像全在内嵌 SRAM 里跑;
  • BSP_LINKER_SCRIPT_FLEXSPI_NOR:外部 NOR Flash 上 XIP 执行,默认;
  • BSP_LINKER_SCRIPT_FLEXSPI_NOR_HYPERRAM:XIP + HyperRAM 当附加 RAM。

rtconfig.py会扫描rtconfig.h里的BSP_LINKER_SCRIPT_*宏自动挑脚本,生成 Keil 工程时还通过update_keil_active_target()把project.uvoptx的当前 Target 同步成rtthread_ram/rtthread_flexspi_nor/rtthread_flexspi_nor_hyperram。也就是说,menuconfig 里换完选项重新生成工程,Keil 里的 Target 自己就跟着换了,不用手动切。

🔍 源码拆解:CM33 是怎么把 CM7 叫醒的

多核逻辑全在 cm33/applications/multicore_thread.c。入口是multicore_init(),通过INIT_APP_EXPORT在应用初始化阶段建一个名叫multicore的线程——栈 2048 字节、优先级 10、时间片 5,线程内按顺序做完 kick-off、RPMsg-Lite 初始化,再跑 ping-pong。

kick-off 序列(对应 Kconfig 的BSP_USE_MULTICORE_KICK_OFF,默认开)是这样的:

  1. MCMGR_Init()初始化多核管理单元;
  2. MCMGR_RegisterEvent(kMCMGR_RemoteApplicationEvent, ...)先注册回调,等 CM7 上报就绪;
  3. BOARD_GetCore1ImageAddrSize()拿到 CM7 镜像的源地址、目的地址、大小、启动地址并打印核对;
  4. BOARD_PrepareCore1()把镜像搬到目标内存(地址信息来自 cm33/board/board.h);
  5. MCMGR_StartCore(kMCMGR_Core1, boot_addr, rpmsg_lite_base, kMCMGR_Start_Synchronous)同步方式启动 CM7,注意第三个参数——RPMsg 共享内存基址是在放核那一刻直接传给从核的;
  6. 之后轮询等 CM7 通过RPMsgRemoteReadyEventData上报 ready,每 10ms 查一次。

为什么顺序不能乱?因为从核放出来之前,主核必须先完成"镜像落位 + 复位向量指向正确地址 + 通信区地址告知"这三件事,否则 CM7 醒来第一件事就是跑飞。Kconfig 里BSP_USE_MULTICORE_KICK_OFF的 help 也写明了:CM33(master)拉起 CM7(remote)时,kick-off 是保证初始化顺序正确所必需的。

📮 RPMsg-Lite:在共享内存上做 100 轮 ping-pong

RPMsg-Lite 是 RPMsg 协议的轻量实现,两个核之间不靠中断消息、全靠一段双方都能访问的共享内存传消息(对应 Kconfig 的BSP_USE_MULTICORE_RPMSG_LITE,默认开)。源码里有几个关键点:

共享内存区怎么放。定义了一个 6144 字节的rpmsg_lite_base[]数组,且必须放进链接脚本约定的段里,三种编译器各有各的写法:

#if defined(__ICCARM__) /* IAR */ #pragma location = "rpmsg_sh_mem_section" static char rpmsg_lite_base[SH_MEM_TOTAL_SIZE]; #elif defined(__CC_ARM) || defined(__ARMCC_VERSION) /* Keil */ static char rpmsg_lite_base[SH_MEM_TOTAL_SIZE] __attribute__((section("rpmsg_sh_mem_section"))); #elif defined(__GNUC__) /* GCC */ static char rpmsg_lite_base[SH_MEM_TOTAL_SIZE] __attribute__((section(".noinit.$rpmsg_sh_mem")));

段名必须和链接脚本里的定义对上,换编译器或改链接脚本时这里最容易翻车。

主核侧初始化链条:rpmsg_lite_master_init()以 master 身份起来 →rpmsg_queue_create()建接收队列 →rpmsg_lite_create_ept()建本地端点(地址默认 40,带接收回调)→rpmsg_ns_bind()绑定名称服务,然后阻塞等待 CM7 的名称服务广播,远端端点地址写进remote_addr。名称服务的作用就是"自我介绍":CM7 侧用约定的字符串广播自己,CM33 侧听到才知道该往哪个地址发消息。

跑通信:rpmsg_lite_communication()循环 100 轮,rpmsg_lite_send()发一个THE_MESSAGE{DATA},rpmsg_queue_recv()阻塞等对端回包,标准 ping-pong 压测。收尾时rpmsg_lite_cleanup()依次销毁端点、队列、名称服务句柄并rpmsg_lite_deinit()。

可配置参数(Kconfig 里BSP_USE_MULTICORE_RPMSG_LITE下的子项,源码用#ifndef兜底,不配也能跑):

  • RPMSG_LITE_LINK_ID:链路 ID,默认 0,即RL_PLATFORM_IMXRT1180_M33_M7_LINK_ID;
  • RPMSG_LOCAL_EPT_ADDR:CM33 侧本地端点地址,默认 40,范围 0~65535;
  • RPMSG_NS_ANNOUNCE_STRING:CM7 广播服务用的字符串,默认rpmsg-openamp-demo-channel;
  • RPMSG_SHARED_MEM_SIZE:共享内存大小,默认 6144 字节。

⚠️ 避坑清单与延伸阅读

踩过的坑我列在这里,都是"顺序不对就白忙"级别的:

  1. pkgs --update必须在编译前执行——NXP IMXRT 驱动包不拉下来,编译直接失败;
  2. CM33 和 CM7 是两个独立工程,各编各的、各下各的;CM33 是主核,负责拉起 CM7,只下 CM7 镜像而没有主核先跑,从核不会自己动;
  3. 控制台串口不一样:CM33 看 UART1,CM7 看 UART12,都是 115200-8-1-N,别拿一根线等两边的日志;
  4. 换内存布局(RAM / FlexSPI NOR / HyperRAM)后一定要重新生成工程,Keil Target 虽会自动同步,但旧工程里的链接配置不会自己更新;
  5. PWM 目前未支持,做电机控制外设规划时注意这一点,别在 menuconfig 里找它。

延伸方向有两个:一是同目录下 README_zh.md 有同系列其他 BSP 的双核外设细节可作对照;二是 bsp/nxp/imx/imxrt/ 下还有 imxrt1021 / 1052 / 1060 / 1064 / 1170 等工程,单核 i.MX RT 的外设驱动套路和 1180 基本一脉相承,可以先拿它们练手。

【免费下载链接】rt-threadRT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/项目地址: https://gitcode.com/gh_mirrors/rt/rt-thread

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进