ARTICLE DETAIL

资讯详情

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

FreeRTOS在Cortex-M23上的移植实践与避坑指南

FreeRTOS在Cortex-M23上的移植实践与避坑指南 我做嵌入式开发也有十来个年头了从最早的8051一路折腾到ARM7、Cortex M0/M3最近一两年明显感觉咨询Cortex M23的同事和同行多了起来。原因也不难理解M23主打的低功耗、高安全特性再加上对FreeRTOS的原生支持正好切中了大量物联网终端、电池供电设备和工业控制节点的需求。但很多从M0或M3转过来的朋友第一次接触M23时并不适应——它看起来像M0的继任者实际用起来却有不少“新时代的规矩”。这篇文章就把我实际把FreeRTOS跑在Cortex M23 MCU上的经验捋一遍聊聊这个内核到底适合谁移植时哪些坑值得提前绕开。1. Cortex M23到底强在哪从M0手里接棒的底气先说个最容易被忽略的事实Cortex M23并不是Cortex M0的简单提频版本而是基于ARMv8-M Baseline架构重新设计的一代内核。这意味着它和M0/M0的关系是“换代”不是“升级”。ARM官方给M23的定位很明确要在最低功耗区间内把安全隔离能力做进去。我们做产品选型时经常会遇到“既要省电又怕固件被抄、数据被读”的尴尬M23就是冲着这个矛盾来的。1.1 与M0/M0的关键参数对比很多老工程师评估内核时第一反应是看主频、看DMIPS这个习惯没错但会漏掉架构层面的差异。下面这张表是我整理给自己团队看的基本能说明M23比M0/M0多出来的硬实力对比项Cortex M0Cortex M23架构版本ARMv6-MARMv8-M BaselineTrustZone安全隔离不支持可选带S后缀型号支持硬件除法指令无无单周期乘法支持支持中断数量最多32个最多32个中断优先级位数2位4级由芯片实现常见4位总线接口单条系统总线I-Code / D-Code / System三条AHB-Lite调试特性SWD、可选MTBSWD、MTBMicro Trace Buffer安全调试认证无可配置安全调试Secure Debug从这个表能看出M23的“内核基础设施”比M0整整高了一代。尤其是三条独立总线指令总线、数据总线、系统总线让内核抓指令和读数据可以并行对实时性是有实际帮助的。我实测过同样的裸机代码在M23上跑复杂状态机时GPIO翻转抖动的概率比M0明显要低虽然幅度不大但做电机控制这类需要确定性响应的场景这点提升是实打实的。1.2 为什么说M23补上了M0最明显的短板M0这么多年被行业广泛采用最大痛点不是性能而是没有硬件级别的安全边界。很多物联网设备为了做固件加密、安全启动、防调试读取只能在软件层想办法比如用外部加密芯片、自己做Bootloader加解密、把Flash读保护打开。这些方法不是不行但防御强度参差不齐而且一旦CF卡复制Flash核心算法还是会被扒走。M23引入TrustZone for ARMv8-M之后系统被硬件强制切分成Secure World和Non-Secure World两个世界安全代码和数据放在Secure侧普通应用代码放在Non-Secure侧硬件总线层面就做了访问控制。普通模式跑飞了、缓冲区溢出了、被外部攻击了也够不到Secure侧的关键数据和密钥。如果说M0是一间没有保险柜的房间M23就是直接把保险柜砌进墙里的房间这就是“换代”二字的真正含义。当然这里必须说清楚带不带TrustZone取决于具体芯片型号。Cortex M23内核本身把TrustZone做成可选项带“S”后缀或官方标注支持TrustZone的型号才有。比如市面上不少主打低功耗的M23单片机其实是没带S的它们走的是“省电ARMv8-M新指令集”的路线绝对不能一听M23就默认自带安全隔离。选型时一定要对着数据手册确认“TrustZone”这几个字母。2. FreeRTOS在M23上的移植启动流程与关键配置清单FreeRTOS官方对ARMv8-M内核的支持已经非常成熟从V10.3.1左右开始portable目录下就有专门的ARM_CM23移植层。如果你用的是带TrustZone的M23还有一套单独适配TZ的port比如ARM_CM23_TZ里面把Secure和Non-Secure侧的上下文切换细节都处理好了。所以现在做M23的FreeRTOS移植基本不需要自己动手写汇编级别的调度代码核心工作量集中在配置、裁剪和验证上。2.1 移植前的三个前提条件网上很多免费教程一上来就教你怎么往工程里拖文件结果读者做完还是跑不起来十有八九是忽略了前提条件。我建议在动手之前先花十分钟把下面三项确认掉第一FreeRTOS版本别用太老的。V8.x及以前的版本对ARMv8-M的支持基本是空白直接硬套M3的port编译能过跑起来立刻进HardFault。我遇到过不止一个同事拿老工程改M23折腾半天最后发现是port版本太旧。尽量用V10.3.1之后的版本当前V10.6.x、V11.x都很稳定。第二确认你的芯片有没有硬件FPU。M23全系都不带FPU这跟M4/M7不一样所以FreeRTOSConfig.h里不要开任何涉及硬浮点的选项编译参数也不要加-mfloat-abihard。如果从M4工程迁移过来这条要重点检查否则链接不会报错但启动第一个任务时就直接卡死。第三启动文件里的堆栈大小必须留足。FreeRTOS的任务栈是自己在堆里分配的但进入main之前C库和系统初始化用的还是启动文件里定义的Stack_Size。这块如果给得太小刚开始初始化就溢出症状非常诡异。我一般设置0x800调试阶段保险。2.2 FreeRTOSConfig.h的必改项与雷区CM23的移植层对FreeRTOSConfig.h里的宏定义非常敏感下面这份模板是我在项目里实际使用的你可以直接抄#define configUSE_PREEMPTION 1 #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 #define configUSE_TICKLESS_IDLE 0 #define configCPU_CLOCK_HZ (64000000UL) #define configTICK_RATE_HZ (1000) #define configMAX_PRIORITIES (32) #define configMINIMAL_STACK_SIZE (128) #define configTOTAL_HEAP_SIZE (8 * 1024) #define configMAX_TASK_NAME_LEN (16) #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 #define configUSE_TASK_NOTIFICATIONS 1 #define configUSE_MUTEXES 1 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_TIMERS 1 #define configTIMER_TASK_PRIORITY (1) #define configTIMER_QUEUE_LENGTH 8 #define configTIMER_TASK_STACK_DEPTH 256这里有几个雷区我一个个说configUSE_16_BIT_TICKS必须设为0。这条是M23移植的死规定。M23是32位内核tick计数器必须用32位设成1会让整个时间系统错乱。很多从M0/M0转过来的人习惯性抄旧配置很容易踩到。configCPU_CLOCK_HZ要和实际系统时钟严格一致。这个值影响vTaskDelay、vTaskDelayUntil等所有时间相关API的换算。不少芯片默认上电用的是内部低速时钟比如32kHz的IRC你如果写的是64MHz跑起来延时偏差会大到离谱。建议在调试初期用逻辑分析仪测一下GPIO翻转周期确认时钟配置和这个宏真的对得上。configTOTAL_HEAP_SIZE根据你实际业务评估heap_4分配策略下8KB可以做不少事了。但我建议一开始别抠得太紧Heap耗尽时FreeRTOS会直接断言前期调试阶段把堆放宽松一点后面优化再慢慢收。关于优先级数量M23内核可以支持多位优先级但实际多少位由芯片的NVIC实现决定。配置configMAX_PRIORITIES时不用和NVIC优先级位数绑定FreeRTOS内部的任务优先级是软件管理的32个优先级不会占满硬件层这块可以放心开到32。2.3 启动流程中的“关键时刻”交叉编译移植完很多人第一步就卡在“创建任务后vTaskStartScheduler()一调用就进HardFault”。我自己排查这个问题时把启动流程整个捋了一遍才知道问题出在哪个环节。M23上FreeRTOS的启动时序大概是这样的系统上电后先从启动文件里的Reset_Handler开始完成时钟初始化、堆栈指针设置、C库初始化然后进入main函数。在main里做外设初始化创建若干任务最后调用vTaskStartScheduler()。这一步内部会先创建空闲任务然后通过SVC指令触发一次系统调用让调度器接管控制权。之后SysTick启动周期性产生tick中断PendSV负责实际的任务切换。如果你卡在“进入vTaskStartScheduler后直接HardFault”优先级最高的怀疑对象就是SysTick和PendSV的优先级没有按要求设置。FreeRTOS要求PendSV和SysTick必须设为最低优先级同时临界区保护依赖configMAX_SYSCALL_INTERRUPT_PRIORITY这个值。如果芯片上电默认把PendSV优先级设得比较高调度器一切换就会抢占正在执行的临界区代码导致栈状态错乱。另一个常见问题是NVIC优先级分组。CM23的NVIC是ARMv8-M风格优先级分组的配置和M3略有差异。FreeRTOS的临界区机制假定抢占优先级是固定位宽的尽量把优先级分组设为组4AIRCR.PRIGROUP0b011也就是所有优先级位都是抢占优先级没有子优先级这样行为最可控。3. 带不带S差异很大TrustZone版本下的FreeRTOS调度思路这一节要展开讲M23上最“挑硬件”的部分。很多人心里有个疑问既然M23能跑FreeRTOS那我把任务直接放到Secure侧来跑是不是更安全这个想法很朴素但实际工程里基本不会这么干。原因在于TrustZone的Secure World和Non-Secure World之间是硬件隔离的两个世界各有各的栈指针、各有各的中断向量表普通RTOS调度器无法跨边界直接切换上下文。3.1 官方的参考架构安全侧跑服务实时侧跑任务在带TrustZone的M23芯片上ARM和Arm Pelion等生态推荐的成熟模式是这样的Non-Secure World运行完整的FreeRTOS实时调度普通业务任务全部放在这里包括网络协议栈、业务逻辑、外设驱动Secure World运行安全固件提供安全启动、密钥管理、安全存储、加密算法等服务通过一组定义好的安全函数接口Secure Function / Veneer对外开放。这种架构的本质是把FreeRTOS完完全全当作Non-Secure侧的一个普通实时内核来跑安全侧根本不跑RTOS而是跑一个精简的安全服务循环。这样做的原因很实在RTOS追求的是实时性和调度灵活性安全侧追求的是最小化和可验证性。把省下的CPU占用和内存开销投给安全算法比在安全侧硬塞一个调度器更有价值。FreeRTOS官方为这个需求提供了一个扩展库叫FreeRTOS Secure Context Integration官方文档里常有S或TZ相关的port。这套机制允许Non-Secure侧在进入安全函数时由硬件自动保存Non-Secure侧的上下文安全函数执行完后自动恢复。也就是说在Non-Secure任务里直接调用Secure侧的安全API不需要自己处理栈切换调用是同步的、阻塞的不会影响任务调度。我实际用下来这套调用看起来就像调用一个普通API一样简单。3.2 两个世界的通信Veneer与IPC机制要在代码层面把Secure和Non-Secure两个世界打通依赖的是这个MCU厂商提供的“安全门”Secure GatewaySG指令。在Secure侧代码中会把允许Non-Secure侧调用的函数定义成“veneers”编译后放入一个特殊区域Non-Secure侧通过函数指针跳转时硬件会执行SG指令完成安全状态切换。注意一个关键点从Non-Secure世界进入Secure世界函数调用的参数和返回值不能在两个世界之间直接传递指针。为什么因为Secure侧访问Non-Secure内存是允许的但反过来不行而Secure侧如果不检查指针合法性就访问Non-Secure内存可能被恶意应用利用。好在CMSIS里提供了安全和非安全内存访问的API加上TrustZone硬件自带的地址过滤MPU这个问题在固件层面基本可控。做两个世界通信时我建议画一张清晰的分区图Secure侧有哪些服务、Non-Secure侧能调哪些API、哪些内存区域是Non-Secure可读的。不要等到Keil工程里出现几十个“xxx_NS.o”文件之后再来理关系那就晚了。另一个经验是安全侧的Veneer接口函数要尽量精简参数用值传递不要传结构体指针这样既安全又省调试时间。3.3 中断安全Secure中断与FreeRTOS调度的协作带TrustZone的M23还有一个特殊处理点——中断。TrustZone把中断也分成了Secure和Non-Secure两类。Non-Secure中断比如普通UART、SPI中断会正常触发FreeRTOS的ISR机制可以在中断里用FromISR结尾的API行为和无TrustZone的芯片一模一样。但Secure中断发生时硬件会直接把控制权切到Secure侧FreeRTOS压根感知不到这个中断的存在。这意味着一个很实际的坑Secure侧中断服务程序里绝对不要调用FreeRTOS的任意API因为你处于“调度器之外”的另一个世界调用任何一个FromISR函数都会导致上下文混乱。正确做法是Secure中断里只做最必要的处理比如置一个标志位、读掉数据寄存器然后把工作“通知”给Non-Secure侧通过Secure侧的专用回调机制触发对应Non-Secure中断让FreeRTOS的ISR机制正常介入处理。4. 实战复现拿GD32L233把FreeRTOS完整跑起来理论讲了一堆不落地等于零。这一节用我手头一直在用的GD32L233这颗典型M23低功耗MCU带你走一遍从建工程到点亮LED的完整流程。选它做演示的原因很直接GD32L233是Cortex-M23内核、主频最高64MHz、主打低功耗价格也亲民资料在国产MCU里算齐全的非常适合第一次接触M23的开发者上手。4.1 准备工作与环境搭建我用的开发环境是Keil MDK 5.38及以上对ARMv8-M和TrustZone支持比较完善GD32L23x标准外设库从兆易创新官网下载版本建议用最新的注意别混用老版本库文件FreeRTOS官方源码V10.6.2直接去官方GitHub仓库拉或者用国内镜像一块GD32L233开发板以及一个DAP-Link/J-Link调试器建好一个空工程后需要把FreeRTOS源码里这几个关键文件加进去FreeRTOS/Source/tasks.c FreeRTOS/Source/queue.c FreeRTOS/Source/list.c FreeRTOS/Source/timers.c FreeRTOS/Source/portable/GCC/ARM_CM23/port.c FreeRTOS/Source/portable/MemMang/heap_4.c FreeRTOS/Source/include/ 整个头文件目录这里有一个容易踩的坑很多人下载完FreeRTOS源码后在porable目录下找port时会看到ARM_CM23和ARM_CM23_TZ两个目录如果芯片不带TrustZone比如GD32L233一定要选ARM_CM23目录下的别选带TZ的版本反过来芯片带TZ的话选错也会编译出一堆诡异错误。然后是启动文件。GD32L23x固件库里自带的startup_gd32l233.s可以直接用它已经初始化好了M23内核的栈指针和向量表。如果你是从别的M23芯片工程改的一定要比对这个启动文件的向量表顺序因为不同芯片外设中断向量数量不同错一个整个中断映射就全乱了。4.2 时钟树配置从内部IRC到PLLM23内核没有独立PLL系统时钟完全由芯片时钟树决定。GD32L233上电后默认跑内部IRC32K这个频率跑FreeRTOS太慢了必须先把主频抬上去。我实际用的配置是将IRC32K倍频到64MHz的系统时钟外设总线APB1和APB2再分频。初始化时钟的代码大致长这样void system_clock_config(void) { /* 使能IRC32K */ rcu_osci_on(RCU_IRC32K); rcu_osci_stab_wait(RCU_IRC32K); /* 配置PLLIRC32K * 2000 / 1 64MHz */ rcu_pll_config(RCU_PLLSRC_IRC32K, 2000, 1); /* 使能PLL并等待稳定 */ rcu_osci_on(RCU_PLL_CK); rcu_osci_stab_wait(RCU_PLL_CK); /* 切换系统时钟到PLL */ rcu_system_clock_source_config(RCU_CKSYSSRC_PLL); rcu_system_clock_source_stab_wait(RCU_CKSYSSRC_PLL); /* 配置总线分频 */ rcu_ahb_clock_config(RCU_AHB_CKSYS_DIV1); rcu_apb1_clock_config(RCU_APB1_CKAHB_DIV2); rcu_apb2_clock_config(RCU_APB2_CKAHB_DIV1); }代码里的倍频系数2000是GD32L233特有的PLL配置方式不同厂商的M23芯片配置PLL的方式完全不同比如NXP的LPC8xx系列又是一种风格。这也是嵌入式开发最需要警惕的一点内核是标准的但芯片周边外设、时钟树、电源管理都是厂商自家设计的没有任何“通用移植”的捷径。4.3 最小工程验证LED闪烁与任务调度时钟配置好之后就可以开始写最简单的双任务LED闪烁验证了。完整流程分三步第一步在main函数里初始化一个GPIO口接一个LED第二步创建两个任务一个控制LED亮灭另一个反复延时并翻转一个测试引脚第三步调用vTaskStartScheduler进入调度。这里我建议用“双任务示波器/逻辑分析仪”的方式验证调度器是否正常工作。只点一个LED你只能知道“系统没死”但看不到调度是否真正按时间片在跑。接一个测试引脚在一个任务里每100ms翻转一次另一个任务每300ms翻转一次用逻辑分析仪同时观察两个引脚能非常直观地看到两个任务的切换时序是否符合预期。创建任务的代码示例void led_task(void *pvParameters) { for (;;) { gpio_bit_toggle(GPIOA, GPIO_PIN_7); vTaskDelay(pdMS_TO_TICKS(200)); } } void test_task(void *pvParameters) { for (;;) { gpio_bit_toggle(GPIOB, GPIO_PIN_0); vTaskDelay(pdMS_TO_TICKS(300)); } } int main(void) { system_clock_config(); gpio_config_led(); gpio_config_test_pin(); xTaskCreate(led_task, LED, 128, NULL, 1, NULL); xTaskCreate(test_task, TEST, 128, NULL, 1, NULL); vTaskStartScheduler(); while (1) {} }这里要注意xTaskCreate里的栈深度单位是“字”Word不是字节。128个字在32位内核上就是512字节。如果你的任务里用了printf、浮点运算这类“栈大户”128字很可能不够跑一段时间任务就莫名其妙崩了。实测经验是任务里只要调用一次复杂的字符串格式化栈最好给到256字以上。4.4 低功耗与Tickless整合的关键点GD32L233主打低功耗不用tickless模式实在可惜。FreeRTOS的tickless idle机制能在空闲任务执行时让MCU进入浅睡眠模式并在下次tick到来前用定时器唤醒从而大幅降低平均功耗。在M23上启用tickless的步骤是把configUSE_TICKLESS_IDLE设为1然后在port.c里实现prvPortSuppressTicksAndSleep函数。这个函数会关闭SysTick配置一个低功耗定时器比如LPTIM让MCU进入睡眠模式等定时器溢出再唤醒。我在GD32L233上实测开启tickless后空闲状态下整板功耗从原来的uA级别降到更低效果非常显著。但这里有一个大坑低功耗定时器的时钟可能是独立的IRC32K其精度和漂移与SysTick所用时钟不一致休眠时间稍微一长累积误差就可能导致vTaskDelay精度变差。如果项目对时间精度要求高需要在休眠结束后校准一次tick计数。若工程用的是普通外部8MHz晶振精度控制相对容易若用内部IRC休眠超过几十秒就会出现几分钟级别的累计误差。5. 调试中容易翻车的地方堆栈溢出、调试口复用、中断优先级最后这部分是实打实的调试经验我把过去半年在M23项目里踩过的坑集中整理一下这些问题在数据手册里都不会明确写但几乎每个M23工程师都会遇到。5.1 任务堆栈溢出检测的三种手段先说任务栈溢出这是RTOS项目最常见的故障源而且症状极具迷惑性——可能表现为另一个不相关任务突然数据错乱而不是直接报错。FreeRTOS提供两种检测方法通过configCHECK_FOR_STACK_OVERFLOW这个宏来控制设为1在任务切换时检查当前任务的栈指针是否越界开销最小但只能抓到“已经溢出”的结果。设为2在任务切换时把任务栈顶的若干字节填入固定模式切换后检查是否被改动能更早发现溢出风险但开销更大。实际项目中我建议调试期直接用2发布前再改成0或1。就算设了检查也不要完全依赖它有些溢出发生在中断上下文里检测点根本来不及触发。最可靠的办法还是任务创建时把栈开大20%然后定期查看任务列表中每个任务的高水位标记uxTaskGetStackHighWaterMark用数据说话。5.2 SWD调试口被复用后救不回来的教训M23芯片引脚紧张很多工程师会把SWDIO/SWCLK复用成普通GPIO用来点灯或驱动外部设备。这是降低成本的好办法但代价是调试器从此连不上芯片。我第一次在M23平台上试这个操作时没提前把复用代码的条件编译关掉结果程序一烧进去J-Link立刻识别不到内核只能按住复位键抢时间擦除整个Flash。从那次之后我的个人经验是开发阶段绝不把SWD引脚复用成GPIO产品量产固件里如果必须复用也要在Bootloader阶段保留一个非常短暂的“调试窗口”比如上电后等待200ms没有收到调试器连接请求才去切换引脚功能。这个窗口不影响量产启动速度却能在关键时刻救回一台砖机。5.3 SysTick、PendSV的优先级设置和临界区中断屏蔽关于中断优先级我给自己的团队立了一条“铁规矩”M23上跑FreeRTOSSysTick和PendSV中断优先级一律设为最低值数值最大例如优先级分组后是15所有应用外设中断的优先级都比它们高这样FreeRTOS的临界区不会被内核自身相关中断干扰。如果某个外设中断需要调用FromISR结尾的API比如xQueueSendFromISR这个中断的优先级必须低于configMAX_SYSCALL_INTERRUPT_PRIORITY否则FreeRTOS会直接断言。这个限制的本质是FreeRTOS在临界区里会屏蔽优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断如果某个中断被更高优先级打断了临界区临界区保护就失效了。还有一个M23特有的注意事项设置中断优先级时尽量通过CMSIS的NVIC_SetPriority函数不要直接写寄存器。ARMv8-M的优先级寄存器位宽是芯片实现的可能是3位、4位或8位直接写寄存器容易把高位覆盖掉导致中断行为异常。用CMSIS函数它会自动把非法位屏蔽掉。5.4 实际项目中“偶发HardFault”的通用排查路径最后分享一个排查M23上偶发HardFault的有效路径。这也是我从一次差点把整个项目搞崩的经历里总结出来的。事情是这样的产品在实验室运行完全正常一到客户现场就偶发死机看门狗复位后又能跑一段时间。抓了很久HardFault的现场都没抓到最后通过JTAG的异常现场分析发现是SPI DMA中断和一个高优先级的UART中断同时触发时栈冲突导致返回地址被破坏。从那以后我排查M23偶发故障的顺序固定成了锁定HardFault现场上电后第一时间挂上调试器开启“硬故障时暂停”选项抓PC和LR寄存器定位异常发生位置。检查中断优先级分组和任务栈使用率先用uxTaskGetStackHighWaterMark排查任务栈再用NVIC的分组信息核对中断优先级特别是DMA中断。检查临界区内是否调用了阻塞型APIFreeRTOS在临界区内禁止调用vTaskDelay、xQueueReceive这类可能触发调度切换的API否则会导致临界区被嵌套破坏。加护身符在HardFault_Handler里写一个“诊断模式”把所有重要的上下文指针PC、LR、PSR、任务句柄存入一个全局结构体并用一个独立GPIO翻转变换状态。这样即使看门狗复位也能通过电平波形判断故障是否复现。我后来给这个项目打了一个补丁在所有DMA传输完成中断里先把中断标志清除再去操作DMA缓冲区。从此这类偶发故障再没出现过。最后再分享一个我个人实际操作中的体会M23这个内核刚上手时你会觉得它“不够快”主频不高、没有浮点单元、连硬件除法都没有但它真正的价值从来不是算力而是低功耗下的安全性、确定性和生态。FreeRTOS在它身上的原生支持意味着你不必为了省电牺牲调度能力这两件事在M23上是可以同时成立的。如果你正打算从M0/M0平台迁过来不要被资料不足吓退找一个带完整SDK和示例工程的芯片厂商照着本文的思路先跑通一个双任务点灯程序后面再慢慢扩展安全特性一定会越来越顺。
返回列表