ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cortex M23与FreeRTOS:小容量MCU上的低功耗RTOS实战

Cortex M23与FreeRTOS:小容量MCU上的低功耗RTOS实战 “Arm Cortex M23-Based MCUs Feature FreeRTOS Kernel Support”——这句话如果放在十年前很多人会嗤笑一声Cortex M0级别的资源跑什么RTOS裸机循环就够了。但这两年实际做过低功耗产品、传感器节点、电池供电设备的嵌入式工程师应该都能感受到风向变了。Cortex M23作为Arm v8-M架构的入门核心本来就不是单纯的“M0升级版”它带了TrustZone安全扩展主频通常不高大多在40MHz到72MHz之间Flash多在64KB到512KB之间RAM更是抠抠搜搜地给到8KB到128KB。就这么点资源FreeRTOS不仅能跑还跑得相当稳原因用一句话说清楚FreeRTOS内核本身只需要不到6KB的代码空间一个最小任务栈只要几百字节就能转起来这和M23的定位恰好对上了。这篇文章我想围绕“M23 FreeRTOS”这套组合从架构特性、选型逻辑、移植步骤、踩坑实录四个层面展开。适合正在做低功耗物联网终端、Sensor Hub、电机控制副核、或准备把裸机代码迁到RTOS但手里只有一块小容量MCU的工程师看。我尽量把工程中真正会遇到的问题写透而不是停留在“哪个函数调用哪个API”的层面。1. 整体设计与架构拆解1.1 Cortex M23 到底是一颗什么样的内核很多工程师对Cortex M23的第一印象是“M0换皮”。严格来说这种说法只对了一半。M23是Arm在2016年底随v8-M架构一起发布的入门级处理器它保留了M0/M0的极低门数和低功耗优势但指令集层面却往前迈了一大步。从架构特性来看Cortex M23的亮点集中在几个方面支持TrustZone安全分区这是M0/M0完全没有的能力。通过SAUSecurity Attribution Unit和IDAU可以把Flash、RAM、外设划分为安全和非安全两个世界FreeRTOS跑在非安全侧安全侧放密钥和secure boot代码硬件层面隔离攻击面大幅缩小。可选的MPUMemory Protection Unit虽然只有8个region但足以给任务栈、内核数据区做基本保护。采用ARMv8-M Baseline指令集硬件除法、原子操作LDREX/STREX都成了标配这比M0强在同步原语上FreeRTOS的临界区实现可以直接用原子操作来优化。中断延迟比M0/M0更低一般能做到15个周期左右这对实时任务响应非常友好。调试能力支持SWD和JTAG部分型号还支持ETM trace。但要注意M23的“技术规格”并不包括一颗MCU的整体实力。芯片厂商在M23内核外面挂多少Flash、多少SRAM、哪些低功耗模式、哪些外设直接决定了这板子实用不实用。以瑞萨RA2系列、NXP LPC8xx系列、Nordic nRF91系列里的应用处理器部分为例它们普遍把功耗做到几十微安/MHz级别同时在睡眠模式下还能保留RAM数据和RTC唤醒这才让“M23 FreeRTOS 低功耗”成为可落地的方案。1.2 FreeRTOS 是怎么在这点资源里转起来的FreeRTOS的体积优势不是玄学而是切实的内核设计取舍。它没有使用传统Unix风格的进程模型而是轻量级任务task模型所有任务共享同一个地址空间任务间的隔离靠程序员自律和可选的MPU。内核对象只有任务控制块TCB、队列、信号量、互斥量、事件组、软件定时器每种对象的数据结构都尽可能精简。我实测过在IAR和GCC下编译只开任务调度 信号量 队列ROM占用大约4.5KB到6KB如果把软件定时器、事件组、互斥量全部打开ROM也就8KB出头每个任务额外的RAM开销主要来自TCB约90到120字节 任务栈由用户指定最小可用128字节但实际建议至少256字节起步。听起来还是不少但注意M23 MCU出厂时往往带着至少64KB Flash和16KB RAM对一颗跑FreeRTOS的芯片来说这个容量已经够跑4到8个有实际意义的任务了。比如一个典型的传感器终端拆成采集任务、处理任务、通信任务、电源管理任务每个任务栈给512字节总共也就4KB左右的RAM开销加上内核heap依然留有余量。1.3 M23 对比 M0、M3/M4为什么偏偏选它低端MCU选型是个永恒话题。M0够便宜、够普遍但遇到需要固件安全升级、密钥保护、更快的开关中断速度、硬件原子操作的场景时就有点力不从心。M3/M4性能当然更好但功耗和成本对电池供电的小设备也是包袱。我习惯用一张表来对比维度Cortex M0Cortex M23Cortex M3/M4架构版本ARMv6-MARMv8-M BaselineARMv7-MTrustZone不支持支持可裁剪不支持M23优势点硬件除法无有有MPU可选通常4-8个region可选通常8个region可选8个region中断延迟约16周期约15周期约12周期指令集56条81条左右大体一致但更丰富典型主频48MHz以下72MHz以下100MHz以上M23在Arm内核序列里其实是“性价比安全核”的定位。如果你想做一款带OTA升级、需要防抄板、防固件被读出来的产品M0裸奔很难处理M3/M4成本又偏高M23配TrustZone是刚好的中间路线。而且M23功耗曲线很漂亮动态功耗和M0基本持平所以这几年很多低功耗无线SoC内部集成的应用处理器都选了M23。2. 为什么要用 FreeRTOS而不是继续裸机2.1 裸机状态机的痛点在复杂业务下会放大“裸机 状态机 定时器中断”这套方法论在简单项目里完全够用我至今不觉得裸机是过时的技术。但当业务复杂到一定规模比如设备要同时处理传感器轮询、无线协议栈回调、上位机命令解析、参数存储、异常恢复裸机的主循环会变得非常痛苦。具体表现是状态机事件满天飞中断里不小心执行了耗时操作主循环里的标志位互相牵扯一个未处理的边缘情况可能导致整机挂死。更头疼的是一旦来了新需求比如加一个定时上报功能你得在整个状态机里找出合适的插入点牵一发而动全身。FreeRTOS的思路是把并发问题从“事件编排”变成“任务划分”。每个业务模块一个任务任务之间用队列和信号量通信。采集传感器就是一个while(1)里阻塞在队列上收到采集命令才执行通信模块收到数据就往解析队列里丢。这种东西在裸机上写出来也不是不可能但可维护性差很多。2.2 FreeRTOS 在 M23 上的真实资源开销这里给出一组我在实际项目中的配置参考。以某款64KB Flash、16KB RAM的M23 MCU为例我开了如下功能configUSE_PREEMPTION 1 configUSE_TICKLESS_IDLE 1 configUSE_IDLE_HOOK 0 configUSE_TICK_HOOK 0 configUSE_TIMERS 1 configUSE_MUTEXES 1 configQUEUE_REGISTRY_SIZE 8 configUSE_RECURSIVE_MUTEXES 0 configUSE_COUNTING_SEMAPHORES 1 configSUPPORT_STATIC_ALLOCATION 1 // 全部用静态分配不用heap编译后ROM占用约5.8KBRAM除了任务栈之外内核自身占用约1.2KB。也就是说留给业务代码的空间依然很宽裕。如果连这点开销都不想给说实话就不该用RTOS继续裸机就好。2.3 同类轻量内核的横向对比Cortex M23能跑的RTOS不止FreeRTOS。我之前也评估过RT-Thread Nano、Zephyr、Keil RTX5RT-Thread Nano国内生态好组件丰富但它的调度器抢占逻辑做得比FreeRTOS“重”在极小RAM上不如FreeRTOS灵活。Zephyr功能全面、支持设备树但本身的学习成本和代码体积都偏大更适合资源相对宽裕的M33或M4平台。RTX5配合Keil MDK使用时集成度极高CMSIS-RTOS v2接口原生支持但出了Keil生态反而不如FreeRTOS通用。FreeRTOS最大的价值在于它已经被AWS收购后持续维护有大量商业级项目背书且几乎每种MCU厂商的SDK都默认集成了移植层。你换一颗芯片SDK里拉出来就能跑。这种“行业默认选项”的便利性是其他内核很难替代的。3. 工程搭建与移植实操3.1 准备一份可用的工程骨架我们不讨论具体某款芯片的厂商SDK只说通用步骤。M23平台移植FreeRTOS最关键的不是复制粘贴文件而是理解三个层面的东西内核代码本身即tasks.c、queue.c、list.c、timers.c等这部分与芯片无关。移植层就是portable/目录下的代码M23一般使用GCC/ARM_CM33_NON_SECURE或RVDS/ARM_CM33_NON_SECURE这类port注意FreeRTOS官方没有单独的ARM_CM23 port因为v8-M Baseline和v8-M Mainline在调度器层面用的是同一套机制官方port直接兼容M23。配置文件FreeRTOSConfig.h这是整个移植的“灵魂”。工程目录建议这样组织project_root/ ├── bsp/ // 芯片启动文件、时钟、GPIO、UART等 ├── freertos/ // FreeRTOS 内核源码 │ ├── include/ │ ├── portable/ │ └── src/ ├── app/ // 业务任务 │ ├── main.c │ ├── sensor_task.c │ └── comm_task.c └── linker/ // 链接脚本3.2 移植中最容易被忽略的三个点第一FreeRTOSConfig.h里的configCPU_CLOCK_HZ必须和实际系统时钟一致。很多新手把时钟树改到48MHz配置文件里还写着32MHz导致vTaskDelay(1000)实际只等了600ms或1.5秒。排查半天结果是个配置错误。第二M23的PendSV和SysTick中断优先级必须设为最低。具体值是NVIC_SYSPRI2寄存器里PendSV和SysTick的优先级都配置为0xFF。如果这两个异常优先级不是最低一旦在中断里调用了会导致任务切换的API系统就会进入HardFault。这部分代码通常在port.c或startup_M23.s里要确认一下。第三当使用TrustZone时FreeRTOS任务默认运行在Non-Secure状态。Secure状态下的中断如果触发了FreeRTOS API调用需要经过TZ_相关的安全非安全调用接口转换否则会权限错误。简单项目可以直接关闭TrustZone功能把M23当普通M0用但这样就浪费了核心卖点。还有一个链接脚本问题如果开了MPU任务栈必须按照MPU region对齐要求分配。M23的MPU region最小是32字节最大4GB但多数情况下建议让任务栈按8字节或16字节对齐避免栈指针在入栈时产生非对齐访问。3.3 写一个最小可运行的示例下面给出一段最小示例基于CMSIS-RTOS v2接口因为新版FreeRTOS默认推荐这套接口而且厂商SDK几乎都支持。#include FreeRTOS.h #include task.h #include queue.h static void vTask1(void *argument) { for (;;) { // 采集传感器数据或者处理某个业务 vTaskDelay(pdMS_TO_TICKS(500)); } } static void vTask2(void *argument) { for (;;) { // 通信处理 vTaskDelay(pdMS_TO_TICKS(1000)); } } int main(void) { /* 初始化时钟、外设 */ BSP_Init(); /* 如果启用TrustZone必须先完成安全侧初始化 这里简化为直接非安全模式启动 */ xTaskCreate(vTask1, Task1, 256, NULL, 1, NULL); xTaskCreate(vTask2, Task2, 256, NULL, 2, NULL); vTaskStartScheduler(); /* 正常情况下不会走到这里 */ for (;;) { } }任务栈大小我用的是256字即1024字节这是绝大多数任务的“安全起步值”。如果是中断里解析Modbus、JSON这类可能消耗栈的函数建议直接给到512字甚至更高宁可多给不要让栈溢出。3.4 Heap 与内存规划经验如果configSUPPORT_DYNAMIC_ALLOCATION为1FreeRTOS会用一个heap_x.c文件管理内存。x取1到5实现各有不同heap_1.c只分配不释放适合永不删除任务的场景heap_2.c支持释放但不会合并相邻空闲块容易碎片化已被官方标记为不推荐用于新设计heap_3.c直接包一层malloc/free需要C库支持heap_4.c首次适配算法 相邻空闲块合并最常用的方案heap_5.c在heap_4基础上支持非连续内存块适合RAM有多个物理分区的芯片。M23平台我默认推荐heap_4.c用configTOTAL_HEAP_SIZE指定堆大小。工程里如果RAM有16KB任务栈静态分配占掉8KB那么heap可以给4KB剩下4KB留给全局变量和内核。实际项目中我更喜欢全静态分配也就是把configSUPPORT_DYNAMIC_ALLOCATION设为0。这样做的好处是内存占用在编译期就完全确定不会在运行时出现malloc失败导致的偶发问题代码评审也更简单。4. 常见问题与排查技巧实录4.1 HardFault 到底是谁踩了别人的栈RTOS下最恼人的问题就是HardFault尤其是M23这种没有硬件浮点单元的内核出错现场非常隐蔽。根据我多次定位经验按出现频率排序任务栈溢出。M23没有像M3/M4那种可配置的栈溢出检测硬件机制所以要靠FreeRTOS的软件检测。把configCHECK_FOR_STACK_OVERFLOW设为2再补一个vApplicationStackOverflowHook钩子函数。这个钩子触发后用调试器查看当前任务的栈指针附近数据就能知道是谁干的。数组越界。这跟RTOS没直接关系但在多任务环境下会被放大。建议开MPU给关键任务栈设置region越界立刻触发MemManage Fault比事后猜快得多。中断里调用阻塞API。比如在UART中断里直接xQueueSend超时等待这是完全禁止的。中断函数里只能用带FromISR后缀的API。在临界区里执行耗时操作。因为M23的taskENTER_CRITICAL()会屏蔽所有可屏蔽中断临界区里跑一个Flash写入或大数组拷贝会导致中断延迟飙升严重时看门狗复位。排查工具方面强烈建议用Ozone或Keil的RTX/FreeRTOS插件查看任务状态和栈高水位。如果条件有限最简单的方法是做一个“心跳任务”——一个最高优先级的任务每100ms翻转一次调试IO用示波器看它是否还活着这能快速区分“系统崩溃”还是“某任务卡死”。4.2 Tickless 低功耗模式下的坑M23主打低功耗FreeRTOS也提供了configUSE_TICKLESS_IDLE。这个功能让MCU在空闲时进入睡眠SysTick停止计数用低功耗定时器来补时间。听起来完美但有一类问题很典型唤醒后任务调度出现时钟漂移。原因是“睡眠时间计算”依赖vApplicationSleep钩子函数如果你在钩子里没有正确配置唤醒定时器的重新加载值下一次唤醒后的tick计数就会偏慢或偏快。我的建议是低功耗产品第一版先把Tickless关掉正常运行后再开启。开启后对照外部RTC或外部晶振秒信号连续测48小时确认漂移在可接受范围。另外必须考虑哪个外设能唤醒MCU。有些M23芯片的低功耗定时器在深睡眠模式下不工作只能用外部GPIO唤醒。这时Tickless策略要做成“浅睡眠 短周期”而不是一味追求长睡眠时间。4.3 中断优先级和临界区的“玄学”问题Cortex M23支持中断优先级但不同芯片厂商实现的可编程优先级位数不一样有的只支持2位也就是只有4个优先级等级。FreeRTOS里用configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY来限制“能调用FreeRTOS API的中断优先级”。如果某个中断优先级数值比这个限制更高数值更小它就跟SysTick抢占同时又调用了任务切换API结果就是不可预知的。这个问题的经典表现是程序偶尔死机、被调试器暂停时停在奇怪的指令上、跑几个小时才出一次问题。排查技巧把configASSERT打开FreeRTOS会在API调用前检查当前中断优先级是否合法不合法直接触发断言。这个宏是调试神器别再嫌它占空间了。
返回列表