ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 FreeRTOS实战:CAN通信、Flash存储与PI控制封装总结

STM32 FreeRTOS实战:CAN通信、Flash存储与PI控制封装总结 1. 项目缘起与整体设计思路1.1 为什么会有这个“V1封装”的念头做嵌入式这行十来年我经手的STM32项目少说也有几十个。早期做项目基本是“一竿子捅到底”——裸机写while(1)中断里塞逻辑全局变量满天飞。这种写法在功能单一的小项目上跑得挺欢但只要需求稍微复杂一点比如要同时处理CAN通信、Flash存储、多路传感器采集代码就会迅速变成一团乱麻。改一个功能牵动三个模块加一个任务时序全乱。V1项目最初也是这个路子STM32F103加几个外设裸机跑得还行。但到了后期需求加上了CAN总线数据交互、参数掉电保存、多路闭环控制裸机架构明显扛不住了。于是就有了这个“V1封装与总结”的念头。说白了就是把V1阶段踩过的坑、验证过的方案、沉淀下来的代码结构做一次系统性的封装和复盘。核心目标很明确用FreeRTOS把任务调度管起来用CAN总线做设备间通信用Flash存关键参数用PI控制器做闭环调节最后把这四块整合成一个可复用的工程框架。这个框架不是给实验室演示用的是要能直接拿到下一个项目里改改就能跑的。1.2 整体架构的取舍逻辑架构设计这块我纠结最久的是“要不要上RTOS”。裸机加状态机也能实现多任务效果但状态机嵌套深了之后调试难度指数级上升。FreeRTOS的好处在于任务之间通过队列、信号量、事件组通信逻辑边界清晰每个任务只管自己的事。坏处也明显RAM开销上去了栈空间分配不好容易溢出中断优先级和任务优先级的关系得捋清楚。权衡下来V1项目的外设数量和实时性要求已经到了裸机吃力的程度上FreeRTOS是划算的。CAN总线这块STM32自带的bxCAN控制器够用不需要外挂芯片。CAN的优势在于差分信号抗干扰强多节点通信天然支持仲裁适合设备间中等速率的数据交换。Flash存储选的是STM32内部Flash省去外部存储芯片的成本和布线麻烦但要注意擦写寿命和扇区对齐问题。PI控制器用在电机转速或者温度闭环上比纯比例控制稳态误差小比PID调参简单适合V1阶段快速验证。整个V1封装的核心思路就是任务分层、通信解耦、参数持久化、控制闭环。FreeRTOS负责任务调度和资源管理CAN负责对外通信Flash负责参数保存PI负责执行层面的闭环调节。四者各司其职通过队列和信号量串联起来。1.3 适合谁来参考这套方案这套封装方案适合几类人一是刚接触STM32和FreeRTOS的开发者想看看实际项目里任务怎么划分、优先级怎么定二是做工业控制、车载设备、智能硬件的工程师需要CAN通信和参数存储的参考实现三是在做毕业设计或者课程项目的同学V1这套框架结构清晰改起来不费劲。如果你还在裸机阶段徘徊或者FreeRTOS移植完了不知道怎么用这篇总结应该能帮你省不少时间。提示V1封装不是万能模板它是针对“中等复杂度、多外设、有通信和存储需求”的场景设计的。如果你的项目只是点个灯、读个传感器裸机反而更合适。2. FreeRTOS任务划分与优先级设计2.1 任务划分的底层逻辑FreeRTOS用起来不难难的是任务怎么切。切得太细任务间通信开销大调度器频繁切换上下文CPU利用率反而下降切得太粗又回到了裸机那种大循环里塞逻辑的老路。V1项目里我最终切了五个任务系统监控任务、CAN通信任务、Flash存储任务、PI控制任务、人机交互任务。系统监控任务优先级最低负责喂狗、统计各任务栈使用情况、监测CPU占用率。这个任务不参与实时控制但它是整个系统的“体检医生”一旦发现某个任务栈快满了或者CPU负载异常能及时报警。CAN通信任务优先级中等偏上因为CAN报文有实时性要求接收中断触发后要尽快处理。Flash存储任务优先级最低因为Flash擦写耗时较长放在低优先级任务里慢慢做不阻塞其他任务。PI控制任务优先级最高闭环控制对时序敏感必须保证每个控制周期准时执行。人机交互任务优先级中等处理按键、显示刷新这些对实时性要求不高的操作。这里有个经验任务优先级不要照搬理论要根据实际时序需求来定。我见过有人把串口打印任务设成最高优先级结果打印一多控制任务全被挤掉了。V1项目里PI控制任务的周期是1msCAN接收处理要求在2ms内完成Flash写入可以容忍100ms的延迟。优先级排序就是按这个容忍度来的。2.2 任务间通信的三种方式FreeRTOS提供了队列、信号量、事件组、任务通知等多种通信机制。V1项目里主要用了三种队列传数据、二值信号量同步中断、互斥量保护共享资源。队列用得最多。CAN任务收到报文后解析出控制指令通过队列发给PI控制任务。PI控制任务算完输出后通过另一个队列把状态数据发给CAN任务由CAN任务打包发送。队列的好处是自带阻塞机制发送方和接收方不需要忙等CPU利用率高。队列长度要估算好太短了容易丢数据太长了占RAM。V1项目里CAN接收队列设了16个元素每个元素是一个结构体包含CAN ID、数据长度、数据内容总共占不了多少RAM。二值信号量主要用在中断和任务之间同步。CAN接收中断里释放信号量CAN任务里获取信号量这样中断服务程序尽量短耗时的解析和处理放到任务里做。这里要注意中断里只能调用带FromISR后缀的API比如xSemaphoreGiveFromISR普通版本在中断里调用会出问题。互斥量用来保护共享资源比如Flash操作。Flash存储任务在写Flash时其他任务不能同时读写同一块区域。互斥量确保同一时间只有一个任务能操作Flash。互斥量和二值信号量的区别在于互斥量有优先级继承机制能减少优先级翻转的影响。2.3 栈空间分配与溢出检测FreeRTOS任务栈空间分配是个容易踩坑的地方。栈给少了任务跑着跑着就HardFault栈给多了RAM不够用。V1项目里我的做法是先给一个保守值跑起来后用uxTaskGetStackHighWaterMark查剩余栈空间再逐步调整。具体来说PI控制任务因为要做浮点运算和调用数学库栈给了512字注意FreeRTOS里栈的单位是字不是字节STM32上是4字节。CAN通信任务栈给了256字。Flash存储任务因为要调用Flash擦写函数栈给了384字。系统监控任务和人机交互任务各给了256字。跑稳定后查HighWaterMark发现PI控制任务还剩100多字说明512字够用但不算宽裕后续加功能要留意。栈溢出检测在FreeRTOSConfig.h里配置。V1项目里开了configCHECK_FOR_STACK_OVERFLOW2这个级别会在任务切换时检查栈指针是否越界比级别1更严格。检测到溢出后会调用vApplicationStackOverflowHook在这个钩子函数里可以点亮错误灯或者记录日志。实测下来级别2对性能影响很小建议都开上。注意栈溢出检测只能检测到“栈指针越界”不能检测到“栈使用接近上限但还没越界”的情况。所以HighWaterMark的定期检查不能省。2.4 中断优先级与任务优先级的配合STM32的中断优先级和FreeRTOS的任务优先级是两套体系搞混了会出大问题。Cortex-M内核的中断优先级数值越小优先级越高FreeRTOS的任务优先级数值越大优先级越高。更关键的是FreeRTOS的临界区保护依赖于BASEPRI寄存器如果某个中断的优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY那么这个中断里不能调用任何FreeRTOS的API。V1项目里CAN接收中断的优先级设成了5数值configMAX_SYSCALL_INTERRUPT_PRIORITY设成了5。这意味着CAN中断的优先级等于阈值可以调用FromISR版本的API。SysTick中断优先级设成最低15因为SysTick只负责任务调度不需要抢占其他中断。串口中断优先级设成6高于阈值所以串口中断里不能调用FreeRTOS API只能做纯数据搬运。这里有个口诀中断里要调FreeRTOS API优先级数值必须大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY。记不住的话就把所有中断优先级都设成比阈值大虽然牺牲了一点实时性但不会出玄学问题。3. CAN总线通信的封装与实现3.1 CAN协议栈的裁剪与配置STM32的bxCAN控制器功能很全但V1项目里用不到那么多。我裁剪后的配置是波特率500kbps、正常模式、自动重传开启、接收过滤器用标识符列表模式。500kbps在40米以内的双绞线上跑很稳再高就要考虑线材和终端电阻了。自动重传开启是因为V1项目对可靠性要求高丢一帧可能影响控制效果。接收过滤器用列表模式只接收特定ID的报文减少CPU中断次数。CAN初始化的步骤先使能GPIO和CAN时钟配置GPIO为复用推挽输出CAN_TX和浮空输入CAN_RX然后配置CAN_Mode、CAN_SJW、CAN_BS1、CAN_BS2、CAN_Prescaler这些参数。波特率计算公式是波特率 APB1时钟 / (Prescaler * (1 BS1 BS2))。V1项目里APB1是36MHzPrescaler设4BS1设15BS2设2算下来36M / (4 * (1152)) 500kbps。SJW设1同步跳转宽度为1个时间单元。过滤器配置这块V1项目用了两个过滤器组。过滤器组0接收ID为0x100到0x103的标准帧映射到FIFO0过滤器组1接收ID为0x200的扩展帧映射到FIFO1。FIFO0的中断优先级高于FIFO1因为0x100系列是控制指令0x200是状态查询实时性要求不同。3.2 报文收发与队列对接CAN报文的收发在V1项目里封装成了两个函数CAN_SendMessage和CAN_ReceiveHandler。发送函数接收CAN ID、数据指针、长度填充发送邮箱后等待发送完成。接收处理函数在中断里调用从FIFO读取报文释放信号量通知CAN任务。CAN任务的主体是一个无限循环等待接收信号量然后从FIFO读取报文解析后根据ID分发。ID为0x100的报文是速度设定值解析后通过队列发给PI控制任务ID为0x101是参数配置报文解析后通过队列发给Flash存储任务ID为0x200是状态查询CAN任务直接组装状态数据回复。这里有个细节CAN报文的字节序要统一。V1项目里所有多字节数据都用小端模式和STM32的内存布局一致解析时直接memcpy就行不用做字节序转换。如果和上位机通信上位机那边也要按小端解析否则数据会错乱。队列对接这块CAN任务和PI控制任务之间用了两个队列一个发设定值一个回状态值。队列元素是结构体包含时间戳、数据类型、数值。时间戳用于计算通信延迟调试时很有用。队列满时发送方可以选择阻塞等待或者直接丢弃。V1项目里设定值队列满了就覆盖最旧的数据因为最新的设定值才是有效的状态值队列满了就丢弃因为状态数据丢一帧不影响控制。3.3 错误处理与总线恢复CAN总线出问题的时候排查起来比串口麻烦。V1项目里我加了错误处理和总线恢复机制。CAN控制器有三种错误状态错误主动、错误被动、总线关闭。错误主动时正常通信错误被动时发送会延迟总线关闭时完全不能通信。总线关闭的恢复流程检测到CAN_ESR寄存器的BOFF位被置位后先清除BOFF位然后等待128次11个隐性位的时间再重新初始化CAN控制器。V1项目里把这个流程放在系统监控任务里每100ms检查一次CAN状态发现总线关闭就执行恢复。实测下来总线关闭多半是接线问题或者终端电阻缺失恢复机制只能救急根本解决还得查硬件。错误计数器的监控也很重要。CAN_ESR里的TEC和REC分别记录发送和接收错误计数。TEC超过255时进入总线关闭REC超过127时进入错误被动。V1项目里在调试阶段把TEC和REC打印出来发现TEC涨得快说明发送有问题通常是ACK错误或者位填充错误REC涨得快说明接收有问题可能是波特率不匹配或者干扰太大。实操心得CAN总线两端必须各接一个120欧姆终端电阻中间节点不要接。我见过有人每个节点都焊了终端电阻结果总线负载太重通信距离大幅缩短。4. Flash参数存储的封装与寿命管理4.1 内部Flash的扇区规划STM32F103的内部Flash按页擦除每页1KB或2KB根据型号不同。V1项目用的是STM32F103C8T664KB Flash每页1KB。程序代码占用了前48KB剩下的16KB用来存参数。我把这16KB分成两个区域参数区A和参数区B各8KB。参数区A存当前有效参数参数区B存备份参数。写入时先擦除备份区写入新参数验证成功后更新有效区标志。这样即使写入过程中断电至少有一个区的数据是完整的。参数结构体设计成固定长度包含魔数、版本号、参数数组、CRC校验值。魔数用来判断Flash是否被擦除过擦除后全是0xFF版本号用于参数升级时的兼容处理CRC校验确保数据完整性。V1项目里参数结构体总共256字节8KB的扇区能存32份但我只用了前两份做A/B备份剩下的空间预留给后续扩展。4.2 擦写流程与掉电保护Flash写入的流程是解锁、擦除、写入、上锁。擦除前要确保目标扇区没有正在执行的代码否则会HardFault。V1项目里Flash操作全部放在Flash存储任务里这个任务优先级最低不会打断其他任务。擦除一页1KB大约需要20ms到40ms这段时间CPU可以调度其他任务但Flash存储任务本身会阻塞。掉电保护是Flash存储的难点。V1项目里的做法是先写备份区再写有效标志。具体流程是擦除备份区写入新参数到备份区读取备份区数据做CRC校验校验通过后在有效区写入一个“备份有效”标志最后擦除有效区并写入新参数。如果任何一步断电上电后检查有效区和备份区的标志和CRC选择完整的那份数据恢复。这个流程听起来繁琐但实测下来很稳。我做过断电测试在写入过程中随机断电100次参数没有丢失过。代价是写入耗时增加了一倍但参数保存不是高频操作这点开销可以接受。4.3 磨损均衡与寿命估算STM32内部Flash的擦写寿命是10000次。如果参数每分钟保存一次一天1440次不到一周就写坏了。V1项目里参数保存不是定时触发的而是事件触发只有在参数真正改变时才保存比如用户通过CAN修改了设定值。正常使用下一天可能就保存几次寿命完全够用。如果确实需要高频保存可以用磨损均衡把参数区当成环形缓冲区每次写入下一个扇区记录写入位置。V1项目里预留了这个机制但实际没用上。因为参数保存频率低而且Flash写坏之前设备可能早就因为其他原因退役了。注意Flash擦除后数据是0xFF写入只能把1变成0不能把0变成1。所以写入前必须先擦除。如果忘记擦除直接写写入的数据会是旧数据和新数据的按位与结果不可预测。5. PI控制器的参数整定与实现5.1 PI控制的基本原理与离散化PI控制器由比例项和积分项组成。比例项响应当前误差积分项消除稳态误差。连续域的PI公式是u(t) Kp * e(t) Ki * ∫e(t)dt。在数字控制器里积分项用累加实现离散化后的公式是u(k) Kp * e(k) Ki * Ts * Σe(j)其中Ts是控制周期。V1项目里控制周期是1msTs0.001。Kp和Ki的整定用了试凑法先把Ki设0逐渐增大Kp直到系统开始振荡然后取振荡时Kp的60%作为最终Kp再逐渐增大Ki直到稳态误差在可接受范围内。实测下来电机转速控制的Kp2.5Ki0.8效果不错超调量小于5%调节时间约200ms。积分饱和是PI控制常见的问题。当误差持续存在时积分项会一直累加导致输出超出执行机构范围。V1项目里加了积分限幅积分项累加值限制在[-1000, 1000]之间对应输出限幅的±10%。这样即使误差很大积分项也不会无限增长退出饱和时不会有过大的超调。5.2 定点数与浮点数的选择STM32F103没有硬件浮点单元浮点运算靠软件模拟速度慢。V1项目里PI控制器最初用浮点实现1ms的控制周期里浮点运算占了600usCPU负载偏高。后来改成定点数实现用Q15格式1位符号位15位小数位运算速度提升了3倍多控制周期里PI运算只占180us。Q15格式的乘法要注意溢出两个Q15数相乘结果是Q30需要右移15位变回Q15。右移时要做饱和处理防止溢出。V1项目里封装了Q15的乘加函数用STM32的SMULL指令做64位中间结果然后饱和到32位。实测下来定点数PI的控制效果和浮点数几乎没差别但CPU负载低了很多。如果控制精度要求不高也可以用Q10或Q12格式整数部分留更多位防止大数值溢出。V1项目里误差范围是±1000Q15格式的整数部分只有1位不够用所以实际用的是Q12格式3位整数12位小数范围±8精度0.0002够用了。5.3 控制周期的保障与抖动处理PI控制对周期稳定性要求高周期抖动会导致积分项计算不准。V1项目里PI控制任务用vTaskDelayUntil实现精确周期。vTaskDelayUntil和vTaskDelay的区别在于vTaskDelay是“延迟指定时间”实际周期是“延迟时间任务执行时间”vTaskDelayUntil是“延迟到指定时刻”周期严格等于设定值。实测下来vTaskDelayUntil的周期抖动在±10us以内vTaskDelay的抖动可能到±100us。对于1ms的控制周期10us的抖动可以接受100us就太大了。所以PI控制任务必须用vTaskDelayUntil。如果系统负载突然升高PI控制任务可能错过执行时刻。V1项目里加了错过补偿如果发现当前时刻已经超过预定时刻就跳过这次控制等下一个周期。跳过会导致控制输出保持上一次的值短时间内影响不大总比乱序执行好。6. 常见问题与排查技巧实录6.1 FreeRTOS相关的问题问题一任务跑不起来程序卡在启动调度器之前。最常见的原因是栈空间不够任务创建失败。检查xTaskCreate的返回值如果是errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY说明堆空间不够。V1项目里configTOTAL_HEAP_SIZE设了10KB五个任务加队列和信号量用了大约6KB还剩4KB余量。问题二任务运行一段时间后HardFault。多半是栈溢出。开configCHECK_FOR_STACK_OVERFLOW2在vApplicationStackOverflowHook里打印出错任务名。V1项目里遇到过一次是CAN任务里调用了printf栈需求比预期大把栈从256字加到384字就好了。问题三中断里调用FreeRTOS API导致死机。检查中断优先级是否高于configMAX_SYSCALL_INTERRUPT_PRIORITY。如果是要么降低中断优先级要么把API调用移到任务里。V1项目里串口中断最初优先级设成4高于阈值5调用xQueueSendFromISR时死机改成6就好了。6.2 CAN通信相关的问题问题一CAN发送失败TEC持续增长。检查终端电阻是否接好波特率是否匹配CAN_H和CAN_L是否接反。V1项目里遇到过一次两个节点波特率一个500k一个250kTEC涨到255进入总线关闭。问题二CAN接收不到数据REC不增长。检查过滤器配置是否正确FIFO是否使能接收中断是否开启。V1项目里过滤器掩码模式配错了把不需要的ID也过滤进来了导致FIFO溢出。问题三CAN通信偶尔丢帧。检查队列长度是否够用中断处理是否太长。V1项目里CAN接收队列最初设了8个元素高速通信时偶尔丢帧加到16个就好了。6.3 Flash操作相关的问题问题一Flash写入后读出来是0xFF。忘记擦除了。Flash写入前必须先擦除擦除后数据是0xFF写入才能把1变成0。问题二Flash擦除时程序跑飞。擦除的扇区里有正在执行的代码。检查链接脚本确保参数区不在代码区范围内。V1项目里参数区从0x0800C000开始代码区到0x0800BFFF结束刚好错开。问题三Flash数据偶尔损坏。写入过程中断电了。用A/B备份加CRC校验上电后自动恢复完整的那份数据。6.4 PI控制相关的问题问题一系统振荡输出忽大忽小。Kp太大或者Ki太大。先减小Ki如果振荡消失说明是积分项的问题如果还在振荡减小Kp。问题二稳态误差消不掉。Ki太小或者积分限幅太紧。适当增大Ki或者放宽积分限幅。问题三控制输出有毛刺。误差信号有噪声。在误差计算前加一阶低通滤波截止频率设为控制频率的1/10。V1项目里加了滤波后输出毛刺明显减少。问题现象可能原因排查方法解决方案任务卡死栈溢出开栈溢出检测增大栈空间CAN发送失败终端电阻缺失万用表测电阻两端各接120欧姆Flash写入无效未擦除读Flash内容先擦除再写入PI振荡Kp过大减小Kp观察取振荡时Kp的60%中断死机优先级过高查NVIC配置降低到阈值以下实操心得调试FreeRTOS项目时把configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS打开用vTaskList打印任务状态能快速定位哪个任务卡住了。这个功能占一点RAM和Flash但调试阶段非常值。7. V1封装的代码组织与复用建议7.1 目录结构与模块划分V1项目的代码目录分成五层硬件层、驱动层、系统层、应用层、配置层。硬件层放STM32的启动文件、链接脚本、寄存器定义。驱动层放CAN、Flash、GPIO、定时器的初始化代码。系统层放FreeRTOS的配置和移植文件。应用层放任务函数和业务逻辑。配置层放全局宏定义和参数结构体。这种分层的好处是复用方便。下一个项目如果还是STM32F103硬件层和系统层直接拷贝如果换了STM32F407硬件层要改驱动层和系统层基本不动如果只是应用逻辑变了只改应用层就行。V1项目里我把驱动层和系统层做成了静态库应用层单独编译链接这样代码管理更清晰。7.2 接口设计与命名规范模块间的接口用头文件暴露内部实现放在C文件里。命名规范统一用模块名_函数名的格式比如CAN_SendMessage、Flash_WriteParams、PI_Update。全局变量加g_前缀静态变量加s_前缀宏定义全大写。队列和信号量的句柄统一放在一个全局结构体里比如g_SystemResources里面包含xQueueCANRx、xQueueCANTx、xSemaphoreCANRx等。这样初始化和使用的时候一目了然不会出现句柄满天飞的情况。7.3 移植到新项目的检查清单把V1封装移植到新项目时按这个清单过一遍时钟配置改了吗GPIO引脚改了吗CAN波特率改了吗Flash扇区地址改了吗任务栈大小够吗队列长度够吗中断优先级对吗这八个问题检查完基本就能跑起来了。V1项目里我遇到过移植后CAN不工作的情况查了半天发现是新项目的APB1时钟是42MHz不是36MHz波特率算错了。所以时钟配置一定要先确认波特率、定时器周期、串口波特率都依赖时钟。7.4 后续扩展的方向V1封装目前只做了基础功能后续可以扩展的方向不少。比如加OTA升级通过CAN总线接收固件包写入Flash的备份区重启后跳转到新固件。加文件系统用外部SPI Flash存日志数据。加Modbus协议栈通过串口和上位机通信。加看门狗任务独立监控各任务的心跳。但扩展之前先把V1的稳定性跑透。我见过太多项目基础功能还没测稳就急着加新功能最后问题叠问题排查起来痛苦不堪。V1封装的价值在于它提供了一个经过验证的起点在这个起点上做加法比从零开始搭要靠谱得多。最后分享一个小技巧在系统监控任务里加一个“任务心跳”机制每个任务定期更新一个计数器监控任务检查计数器是否在预期时间内变化。如果某个任务的心跳停了说明它卡住了可以触发报警或者重启。这个机制在V1项目里帮我提前发现了好几次潜在的死锁问题。
返回列表