
去年我拿到一块STM32MP257的开发板折腾了一段时间的异构多核方案最终把M33核心上的FreeRTOS和A35核心上的Linux通过OpenAMP完整跑通了。这套环境现在已经成为我做实时控制类项目的底子开发效率比之前在A35上硬扛实时任务高了不少。这篇文章把我从零搭建的整个过程、踩过的坑、以及最后稳定运行的配置思路一次说清楚。这套方案的核心是STM32MP257这颗MPU里有两颗Cortex-A35和一颗Cortex-M33A35跑完整的Linux系统负责协议栈、文件系统和应用逻辑M33跑FreeRTOS负责实时采样、伺服控制、IO快速响应这一类任务两者之间用OpenAMP框架做通信。如果你也在做类似的异构多核项目或者正在纠结“要不要把实时任务从A核搬到M核”这篇文章会很有参考价值。1. 为什么要拆成“Linux FreeRTOS”双核架构1.1 STM32MP257这颗芯片的特别之处STM32MP257属于ST的第二代MPU产品线和上一代MP1最大的区别是多了NPU同时把A7换成了A35。我最初看中的是它“一颗芯片包圆”的能力A35核心足够跑完整的嵌入式LinuxM33核心又是一个独立的ARM Cortex-M33带TrustZone、MPU、中断控制器主频400MHz单核性能比大多数低端MCU强得多。最关键的一点M33和A35是完全独立的两个核心各自有独立的异常向量表、时钟控制、复位控制。也就是说M33可以不依赖A35独立运行也可以由A35通过remoteproc机制在启动过程中加载固件、启动、停止。这个特性决定了我们可以在M33上放一个真正意义上的RTOS——FreeRTOS而不是在Linux上打实时补丁或者用preempt_rt去抢时间片。之前我在A35上用PREEMPT_RT跑过运动控制响应抖动在几十微秒级别对普通控制够用但对一些需要硬实时的场合比如电流环、编码器高速采样还是不够稳。把同样的任务挪到M33上之后FreeRTOS的中断响应是硬实时的抖动基本上就是硬件延迟一个M33核心完全承担得起来。1.2 FreeRTOS在M33上承担什么角色FreeRTOS在这个架构里不是“跑个点灯demo”而是承担真正的实时控制逻辑。我在这套系统上分配的任务大概是这样的电机控制的PWM输出和ADC采样周期50μs放FreeRTOS高优先级任务里。编码器数据的读取和位置解算中断触发200μs周期。和A35的通信处理包括命令接收、状态上报。故障保护逻辑比如过流、过温必须在10μs内响应这个靠GPIO外部中断加上FreeRTOS的中断服务函数完成。A35侧则负责Ethernet协议栈、USB、文件系统、日志存储、远程固件升级以及通过OpenAMP向M33下发控制参数。这样划分之后两边各自擅长的领域都发挥得很彻底。Linux那边再也不用为了保证实时性绑CPU核心M33这边也不用管复杂文件系统代码量小很多调试简单粗暴。1.3 M33启动方式的选择M33固件的启动有三种路径一是由A35的Linux系统在启动阶段自动加载二是手动通过sysfs接口控制三是由内部BootROM从外部Flash独立启动。我常用的是前两种组合系统启动时自动加载M33固件调试时手动操作sysfs接口。手动操作的方式在生产调试时特别有用比如在M33固件崩溃后可以直接echo stop /sys/class/remoteproc/remoteproc0/state echo start /sys/class/remoteproc/remoteproc0/state把M33重新启动整个过程不需要重启整个板子A35侧的应用也不受影响。这在开发调试阶段的效率提升是巨大的。2. 双核通信选型为什么要用OpenAMP2.1 自己写共享内存协议的问题很多从单片机转过来的工程师第一反应是“两个核通信嘛划一块共享内存自己定个协议不就完了” 我最初也是这么想的但真正做起来之后发现几个绕不开的坑。第一是内存一致性问题。A35侧的Cache策略和M33侧如果不一致双方看到的同一块内存数据会不一样。这个问题在自研协议里很容易被忽略一旦数据量增大就会偶发出现“读到的值是旧值”这种极其难排查的bug。第二是生命周期管理。M33固件什么时候启动、什么时候停止、崩溃了怎么重启这些逻辑自己写过一遍就知道有多麻烦。remoteproc机制已经把这套东西标准化了直接复用是最省力的。第三是安全隔离问题。M33有TrustZoneA35侧如果要访问M33受保护的内存区域需要复杂的权限配置。OpenAMP的resource table机制把共享区域的定义和管理规范化了规避了这部分风险。2.2 OpenAMP的三大组件OpenAMP这个框架可以拆成三块理解remoteproc负责远程处理器的生命周期管理包括加载固件、启动、停止、资源分配。Linux内核的remoteproc子系统直接对接这个接口。在STM32MP257上ST维护了一个stm32-rproc驱动设备树配好之后Linux系统就能识别M33并管理它。rpmsg负责消息传递基于virtio的环形缓冲区实现。上层看到的是一个字符设备可以像读写文件一样收发消息。每个消息有源端点和目的端点支持多通道。virtio负责底层传输一个典型的virtio设备有发送队列和接收队列数据通过共享内存中的vring环形队列传递用mailbox中断通知对端。三者的关系可以类比成virtio是水管rpmsg是阀门和水表remoteproc是水厂的调度系统。Linux侧已经将这些封装成了标准的内核接口用户态直接读写字符设备节点就行。2.3 resource table的核心地位M33侧固件里有一个特殊的结构叫resource table它描述了这个固件需要的资源共享内存区域在哪里、vring放到什么地址、有几个mbox中断通道。Linux侧remoteproc加载固件的时候会解析这个表并根据表中的信息配置对应的硬件资源。这就意味着M33固件里的链接脚本、共享内存地址、中断通道定义必须和Linux侧设备树里配置完全对应。这块是OpenAMP整个调试过程中最容易出错、也最需要耐心的部分后面我会详细展开。3. M33侧FreeRTOS的移植与配置实操3.1 用STM32CubeMX生成基础工程ST官方在STM32Cube_FW_MP2这个固件包里已经为STM32MP257的M33核心做好了FreeRTOS的底层移植。我建议直接基于CubeMX生成基础工程不要去手写启动文件和时钟初始化因为M33的时钟、电源、复位关系比普通MCU复杂很多。在CubeMX里选择MP257对应的型号然后选择Cortex-M33核心在Middleware里面启用FreeRTOS选择CMSIS_V1或者V2接口这一步就完成了基础知识。生成出来之后工程结构会自动包含startup_stm32mp257xx.sM33的启动文件stm32mp2xx_hal_rcc.c时钟初始化默认配置系统时钟为400MHzfreertos.cFreeRTOS的配置和任务创建代码有一点要注意CUbeMX生成的默认工程是在M33的Secure状态下运行的。如果你的M33固件需要和Linux侧的OpenAMP配合ST官方推荐用Non-Secure模式。这个切换在CubeMX里没有直接的选项需要手动修改链接脚本和启动文件把Secure相关的初始化去掉。我在实际操作中直接使用ST官方OpenAMP例程的工程模板来改比从CubeMX默认工程改造要省事得多。3.2 时钟与SysTick配置的几个坑M33的时钟树从系统复位开始默认是HSI经过PLL分频后工作的。FreeRTOS需要用到SysTick定时器作为系统节拍而SysTick的计数频率取决于SystemCoreClock的值。我犯过一次很低级的错误默认SystemCoreClock 400000000但实际M33的系统时钟被我通过CubeMX改成了200MHz结果FreeRTOS的configTICK_RATE_HZ设为1000时实际节拍变成了500Hz所有任务的延时都慢了一倍。排查起来特别坑因为你不容易直接看到SysTick本身但所有任务的时序都不对。解决方案是在初始化代码里检查SystemCoreClock的实际值用调试器读寄存器确认PLL输出频率。如果M33从FLASH独立启动时钟和从A35 remoproc启动时可能会有细微差别验证一遍时钟配置是最稳妥的。3.3 中断优先级和FreeRTOS的配置要点在M33上跑FreeRTOS中断优先级的设置比普通M3/M4更需要注意。M33的NVIC支持3到8个优先级位STM32MP257上实际使用的是3位也就是优先级范围0到7。FreeRTOS对中断优先级的硬性要求是SVC异常必须设置为最高优先级调用FreeRTOS API会触发SVC。PendSV和SysTick必须设置为最低优先级否则系统节拍中断会抢占临界区。使用configMAX_SYSCALL_INTERRUPT_PRIORITY限定了哪些中断可以从FreeRTOS API如果这个值设置不对会出现随机崩溃。我最终使用的配置是#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 7 #define configKERNEL_INTERRUPT_PRIORITY 7 #define configMAX_SYSCALL_INTERRUPT_PRIORITY 5Mailbox中断用于接收Linux侧发来的消息优先级设置为3保证它能在高优先级任务运行的时候仍然产生中断但不会打断更关键的控制回路。3.4 内存规划与MPU设置M33没有MMU只有MPU地址空间直接暴露整个系统总线。但这意味着每一块内存区域是否可以缓存、是否可执行、是否可写都需要通过MPU配置明确。我建议把M33的RAM区域分成三段区域用途Cache属性0x30000000FreeRTOS堆和任务栈Normal, Write-back0x30040000共享内存区vring rpmsg缓冲区Device/非Cache0x30080000OpenAMP库内部堆Normal, Write-back共享内存区域特别需要注意在M33侧如果将该区域配置成Write-back且没有在发送时执行cache flush就会出现“A35收到不完整数据”的怪问题。我在M33侧的MPU里将共享内存区配置为Device内存彻底避开了缓存一致性问题。FreeRTOS的configTOTAL_HEAP_SIZE我设置为16KB足够支撑OpenAMP库的缓冲区需求。每次发送数据量控制在1KB以内缓冲区不会爆。4. OpenAMP双核通信的完整打通过程4.1 Linux侧设备树配置设备树是OpenAMP配置最先要确认的地方。缺少任何一个节点M33固件可能根本不会被Linux识别加载。我在设备树中添加的节点如下reserved-memory { #address-cells 2; #size-cells 2; ranges; m33_rproc_memory: m33-rproc-memory30000000 { compatible shared-dma-pool; reg 0x0 0x30000000 0x0 0x1000000; no-map; }; }; m33_rproc { clocks rcc CK_M33; clock-names m33; memory-region m33_rproc_memory; firmware-name stm32mp257-m33.elf; status okay; }; ipcc { status okay; };memory-region指定了M33运行时使用哪块物理地址空间firmware-name是Linux启动时通过remoteproc加载的M33固件文件路径位于/lib/firmware/目录下。ipcc是STM32的mailbox外设是M33和A35之间传递中断信号的通道。4.2 M33侧OpenAMP库的集成在M33固件里ST官方使用了OpenAMP库。在FreeRTOS环境中核心实现是openamp库中的virtio和rpmsg模块配合libmetal提供的硬件抽象层。M33的工程里需要引入以下源文件组open-amp/lib/rpmsg/下的rpmsg核心代码open-amp/lib/virtio/下的virtio实现libmetal/lib/下面的设备抽象代码ST提供的平台适配文件处理M33的mailbox中断和时钟链接脚本里必须显式保留共享内存区域我在MPU配置中使用的起始地址为0x30000000链接脚本里对应的结构如下.rsc_table : { . ALIGN(4); KEEP(*(.resource_table)) } SHARED_MEM .vring0 : { . ALIGN(4); . . 4096; } SHARED_MEM .vring1 : { . ALIGN(4); . . 4096; } SHARED_MEM .rpmsg_buf : { . ALIGN(4); . . 16384; } SHARED_MEM4.3 双核握手的关键流程A35侧Linux启动后会通过remoteproc自动加载M33固件。加载过程是Linux内核解析设备树找到m33_rproc节点。读取/lib/firmware/stm32mp257-m33.elf固件文件。解析ELF中各段的加载地址将代码段和数据段拷贝到对应的物理内存地址。解析resource table获取vring地址、mailbox中断配置。配置mailbox中断创建rpmsg通道。释放M33的复位信号M33开始从入口地址执行。M33侧的FreeRTOS启动后会在主任务中调用OpenAMP的初始化函数创建rpmsg端点然后等待Linux侧的连接请求。两边的握手是在一个“服务发现”机制下完成的——Linux侧通过rpmsg发送RPMSG_NS_CREATE消息M33收到后回复确认这个通道就正式建立起来了。我建议不要一开始就写复杂业务先跑通一个“echo”测试A35发一条消息M33收到后原样返回。确认双向通路正常后再往上加业务逻辑。4.4 打通后的第一个示例程序M33侧的核心代码大致如下static int rpmsg_echo_cb(struct rpmsg_device *rdev, void *data, int len, void *priv, unsigned long src) { int ret; ret rpmsg_sendto(rdev, data, len, src); if (ret 0) { printf(rpmsg send failed: %d\n, ret); } return 0; } int app_openamp_init(void) { struct rpmsg_device *rpdev platform_get_rpmsg_device(); if (!rpdev) { return -EINVAL; } rpmsg_create_ept(rpdev, rpmsg_echo_cb, NULL, RPMSG_ADDR_ANY); return 0; }A35侧我写了一个简单的C程序使用rpmsg字符设备接口int fd open(/dev/rpmsg0, O_RDWR); char buf[64] hello from A35; write(fd, buf, strlen(buf) 1); int len read(fd, buf, sizeof(buf)); printf(received: %s\n, buf);要成功创建/dev/rpmsg0节点Linux内核需要启用rpmsg_char驱动并且在设备树中配置好rpmsg虚拟设备。ST的OpenSTLinux默认已开启如果你是自己编的内核要确认CONFIG_RPMSG_CHAR和CONFIG_RPMSG_VIRTIO都已打开。5. 调试过程中遇到的典型问题与排查思路5.1 M33固件没有被Linux加载出现这个现象时先确认/lib/firmware/stm32mp257-m33.elf文件存在然后看内核日志dmesg | grep remoteproc dmesg | grep stm32-rproc常见的内核报错有三种failed to load firmware固件路径不对或固件格式有误。检查文件名和设备树里的firmware-name是否完全一致。resource table not foundM33固件链接脚本中resource table段的地址不对。确认.resource_table段的地址和设备树中memory-region范围匹配。timeout waiting for remote processorM33固件启动后没有完成握手可能是M33侧代码没跑起来或者mailbox中断有问题。我遇到过最隐蔽的一种情况M33固件在OpenAMP初始化前先执行了FreeRTOS的任务创建而任务中用了printf打印。由于M33串口外设还没有初始化在remoteproc加载过程中串口默认未打开printf直接卡死在等待UART发送完成的循环里。M33启动流程被阻塞握手超时。后来我把printf改到OpenAMP初始化完成后再输出问题消失。5.2 rpmsg消息收发但不通现象是echo测试发出去的消息收不到回复但设备节点正常、没有报错。第一可能原因是共享内存缓存一致性问题。M33侧设置成Write-back区域时需要主动维护cache。我后来直接改用非Cache方式整个问题从根上消失。第二是vring地址不对齐。virtio的vring需要按64字节对齐我在链接脚本里加了对齐宏. ALIGN(64);第三是M33侧的mailbox中断没有使能。M33固件初始化时需要调用platform_init()函数这个函数内部会配置mailbox中断并使能。如果漏掉A35发消息时M33根本不会知道有新数据。5.3 M33跑飞或者HardFaultM33跑飞最常见的几个原因任务栈溢出。FreeRTOS默认不检查栈溢出除非开启configCHECK_FOR_STACK_OVERFLOW。排查时可以临时开启发现溢出时vApplicationStackOverflowHook会被调用。MPU配置错误导致访问了被保护的内存区域。中断优先级配置错误导致中断嵌套失控。开启configUSE_IDLE_HOOK和vApplicationIdleHook在空闲任务里加个LED翻转能肉眼看到M33是否在正常调度。5.4 通过GDB调试M33ST的STM32CubeIDE可以连接STM32MP257的M33核心调试。连接后在GDB里可以直接看到M33的寄存器状态、任务栈位置和当前执行位置。我调试HardFault时经常直接看PC指针然后用addr2line工具反查源代码行号arm-none-eabi-addr2line -e build/stm32mp257-m33.elf 0x080012345.5 常见问题速查表现象排查方向解决方案Linux不加载M33固件固件路径、ELF格式、设备树确认/lib/firmware路径及设备树firmware-name握手超时M33卡在初始化检查M33侧printf是否阻塞、时钟是否正常rpmsg收发不通缓存一致性、vring对齐、mailbox中断共享内存配置为Device类型vring按64字节对齐M33跑飞栈溢出、MPU权限、中断优先级开启栈溢出检测MPU逐块排查偶发数据错乱共享内存竞争、缓冲区不足增加信号量保护缩小单包数据量6. 基于个人经验的一些实操建议最后聊几点我折腾下来的实际体会不一定全对但对后来者应该有参考价值。首要建议是你手头如果已经有官方的M33OpenAMP例程不要一上来就自己从零搭工程。直接把官方例程烧进去确认A35侧能正常收发消息再逐渐往里面加自己的任务代码这样排查问题的范围就小很多。优先级分配方面我坚持的原则是控制类任务在FreeRTOS里优先级最高通信任务次之日志和状态上报优先级最低。Linux侧不要通过rpmsg频繁地向M33发送大量的参数数据尽量采用“下发后M33请求-响应”模式避免M33侧缓冲区被突发的数据冲垮。共享内存的空间一开始就留大一点。M33有独立的4MB DDR空间我建议预留至少1MB给OpenAMP的通信缓冲区虽然实际上用不了那么多但后期加功能时不用来回改链接脚本。日志系统方面M33侧直接用串口输出即可但如果你的调试环境不方便接串口可以走rpmsg把日志转发到A35侧。A35侧开一个接收线程把M33的日志写到syslog文件里。这个方式在远程调试时太有用了。调试多核系统耐心和方法同样重要。每改一处配置都要确认它影响的是A35侧还是M33侧是启动阶段还是运行阶段。我养成的习惯是每次只改一处并且记录设备树、链接脚本、MPU配置三个文件的变化单独验证排查效率翻倍。