ARTICLE DETAIL

资讯详情

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

STM32+FreeRTOS+CAN+PI+Flash:V1嵌入式项目封装与总结

STM32+FreeRTOS+CAN+PI+Flash:V1嵌入式项目封装与总结 1. 从零到一V1项目到底在做什么第一次拿到“V1项目封装与总结”这个题目的时候我脑子里冒出来的第一个念头就是这肯定不是一个从零开始的新项目而是一个已经跑通了、准备收口归档的阶段性成果。事实也确实如此。这个V1项目核心就是围绕STM32主控、FreeRTOS实时操作系统、CAN总线通信、PI控制算法以及Flash存储这五块内容搭建起来的一套嵌入式控制系统。说白了就是把一个能跑、能控、能存、能通信的完整小系统从散落的代码和调试记录整理成一套可以交付、可以复现、可以继续迭代的工程。我之所以愿意花时间把这个项目重新梳理一遍是因为在实际开发过程中踩了太多坑。STM32的芯片包版本、FreeRTOS的任务优先级和中断优先级到底怎么配、CAN总线的报文过滤和波特率计算、PI参数的整定、Flash的读写寿命和分区管理这些东西单拎出来每一个都能写一篇长文但真正难的是把它们揉在一起还能稳定运行。这个V1项目就是把这些模块全部打通之后的第一个稳定版本适合那些已经学过STM32基础、想进一步做完整项目的人参考也适合正在做毕业设计或者课程设计的同学直接抄作业。这篇文章不会只讲“怎么做”我会把每个关键选择背后的“为什么”也讲清楚。比如为什么用FreeRTOS而不是裸机大循环为什么CAN要加终端电阻为什么PI参数不能照搬别人的为什么Flash要单独划一个分区出来。这些细节才是决定项目能不能稳定跑起来的关键。2. 整体架构设计与方案选型拆解2.1 为什么选STM32加FreeRTOS这套组合STM32在国内嵌入式圈子的地位不用多说资料多、生态全、芯片包安装方便Keil5兼容C51和STM32的安装方式也让很多人一台电脑就能搞定。但选STM32的真正原因不是因为它火而是因为它的外设资源刚好匹配这个项目的需求至少两路CAN、足够的定时器资源、内置Flash、支持DMA的串口这些在F103或者F407系列上都能找到。裸机大循环能不能做能。但问题在于这个项目里同时有CAN通信、PI控制计算、Flash读写、串口调试输出这几件事。如果全部塞进一个while循环里PI控制的周期就会被CAN报文的接收和Flash的写入操作打乱控制效果会明显变差。FreeRTOS的价值就在这里它可以把PI控制放在一个高优先级任务里保证固定的控制周期把CAN报文解析放在中等优先级任务把Flash存储和串口日志放在低优先级任务。这样即使Flash写入耗时较长也不会影响控制环路的实时性。注意FreeRTOS的任务优先级和中断优先级是两个完全不同的概念。任务优先级是调度器用的数值越大优先级越高中断优先级是NVIC用的数值越小优先级越高。这两个东西搞混了会出现任务被中断打断后无法及时恢复的问题。2.2 CAN总线在这个项目里的角色CAN总线在这个项目里承担的是设备间通信的角色。相比串口CAN的差分信号抗干扰能力更强适合在电机、传感器、控制器之间做短距离通信。这个项目里CAN的波特率定在500kbps这是一个在总线长度和通信速率之间比较平衡的选择。波特率的计算依赖于APB1时钟和分频系数具体公式是波特率 APB1时钟 / (分频系数 × (1 BS1 BS2))假设APB1时钟是36MHz想要500kbps那么分频系数取4BS1取5BS2取2算下来就是36M / (4 × (152)) 36M / 32 1.125M不对。重新算36M / (4 × 18) 500k所以BS1BS2118取BS115BS22这样就是36M / (4 × 18) 500kbps。这个计算过程在配置CAN的时候必须自己算一遍不能随便填。CAN报文解析是另一个重点。标准帧的ID是11位扩展帧是29位。这个项目里用的是标准帧ID分配如下0x100是控制指令0x200是状态反馈0x300是参数配置。过滤器配置成只接收这三个ID的报文其他的一律丢弃这样可以减少CPU的处理负担。2.3 PI控制算法的实现位置PI控制放在FreeRTOS的一个独立任务里周期是1ms。这个周期由定时器中断触发中断里只做一件事给控制任务发一个信号量。控制任务拿到信号量之后读取ADC采样值计算PI输出更新PWM占空比。这样做的好处是中断服务函数极短不会阻塞其他中断。PI参数的选择没有捷径必须根据被控对象的特性来整定。这个项目里控制的是一个直流电机的转速先用纯比例环节让系统有响应然后逐渐加大积分环节消除稳态误差。比例系数从0.5开始试积分系数从0.01开始试每次调整后观察阶跃响应的超调量和调节时间。最终定下来的参数是Kp2.5Ki0.08这个组合在1ms控制周期下响应快且超调小。2.4 Flash存储的分区设计STM32内置的Flash除了存程序之外还划出了一块区域用来存参数和日志。这个项目里用的是F103C8T6Flash总共64KB程序占了大约40KB剩下24KB用来做参数存储。参数区放在0x0800C000开始的4KB空间里日志区放在0x0800D000开始的8KB空间里。Flash的写入必须按页擦除F103的一页是1KB。所以参数更新的时候不能直接覆盖写而是先把整页读到RAM里修改对应字节擦除整页再写回去。这个过程耗时大约20ms所以必须放在低优先级任务里做不能放在控制任务里。提示Flash的擦写寿命大约是1万次如果参数更新频繁建议加一个缓存机制只有参数真正变化时才写入Flash而不是每次控制周期都写。3. 核心细节解析与实操要点3.1 FreeRTOS任务划分与堆栈分配这个项目里一共创建了四个任务控制任务、CAN接收任务、Flash存储任务、串口日志任务。控制任务的堆栈给的是256字CAN接收任务给的是128字Flash存储任务给的是256字串口日志任务给的是128字。堆栈大小的单位是字不是字节在STM32上1字等于4字节。堆栈溢出是FreeRTOS项目里最常见的问题之一。FreeRTOS提供了堆栈溢出检测机制在FreeRTOSConfig.h里把configCHECK_FOR_STACK_OVERFLOW设为2然后在vApplicationStackOverflowHook函数里打印出错的任务名。这个功能在调试阶段一定要打开等系统稳定了再关掉以节省开销。任务优先级的分配原则是控制任务优先级最高设为4CAN接收任务设为3Flash存储任务设为2串口日志任务设为1。空闲任务的优先级是0这个不用管。这样分配的原因是控制任务的周期最严格不能被其他任务打断CAN接收任务需要及时响应但可以容忍少量延迟Flash和串口都是低速操作放在最低优先级。3.2 CAN报文的收发与过滤配置CAN的初始化包括引脚配置、波特率配置、过滤器配置、中断配置。引脚用的是PA11和PA12这两个引脚在F103上默认就是CAN功能不需要额外重映射。波特率配置前面已经算过了这里不再重复。过滤器配置是CAN里比较容易出错的地方。F103的CAN有14个过滤器组每个组可以配置成掩码模式或列表模式。这个项目里用的是列表模式把0x100、0x200、0x300三个ID写进过滤器其他ID的报文直接丢弃。过滤器的配置代码如下CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdList; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x100 5; CAN_FilterInitStructure.CAN_FilterIdLow 0x200 5; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x300 5; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);发送CAN报文的时候要注意如果总线上没有其他节点应答发送会失败并触发错误中断。所以在调试阶段最好接一个CAN分析仪或者另一个节点否则发送函数会一直返回失败。3.3 PI控制器的代码实现与参数整定PI控制器的代码本身很简单核心就是一个积分累加和输出限幅。但细节在于积分饱和的处理。如果输出已经达到限幅值积分项就不应该继续累加否则退出限幅的时候会有很大的超调。这个项目里用的是积分分离法当误差大于某个阈值时只使用比例环节当误差小于阈值时才加入积分环节。float PI_Controller(float error) { static float integral 0; float output; if (fabs(error) 50.0f) { integral error * 0.001f; } else { integral 0; } output 2.5f * error 0.08f * integral; if (output 1000.0f) output 1000.0f; if (output 0.0f) output 0.0f; return output; }参数整定的过程需要耐心。先把Ki设为0逐渐增大Kp直到系统开始振荡然后取振荡时Kp的一半作为最终Kp。然后逐渐增大Ki直到稳态误差在可接受范围内。这个过程可能需要反复几次每次都要记录阶跃响应的曲线。3.4 Flash读写操作的注意事项Flash写入之前必须先擦除擦除的最小单位是页。F103的一页是1KB擦除时间大约是20ms到40ms。在擦除期间CPU从Flash取指令会暂停所以如果程序本身也在Flash里运行擦除操作会导致程序短暂卡顿。解决办法是把擦除和写入操作放在RAM里执行或者接受这个卡顿把它安排在系统空闲的时候。Flash的读取没有限制可以直接用指针访问。但要注意地址对齐问题32位读取要求地址是4的倍数。这个项目里参数结构体的大小是32字节正好是4的倍数所以直接按结构体读写没有问题。注意Flash写入之前一定要先解锁写入完成后要重新上锁。忘记上锁会导致意外写入忘记解锁会导致写入失败。这个坑我踩过不止一次。4. 实操过程与核心环节实现4.1 开发环境的搭建与芯片包安装Keil5的安装本身不复杂但要注意Keil5默认不包含STM32的芯片包需要单独下载安装。芯片包的版本要和芯片型号匹配F103系列用Keil.STM32F1xx_DFPF407系列用Keil.STM32F4xx_DFP。安装完芯片包之后在Keil5里新建工程的时候才能看到对应的芯片型号。如果电脑上同时装了Keil5和Keil4或者需要兼容C51建议把两个版本装在不同的目录下不要覆盖安装。Keil5的工程文件格式和Keil4不一样用Keil5打开Keil4的工程会提示转换转换之后Keil4就打不开了。所以最好在转换之前备份一份。FreeRTOS的源码可以从官网下载也可以直接用CubeMX生成。CubeMX生成的FreeRTOS工程已经配置好了时钟和中断省去了很多手动配置的工作。但CubeMX生成的代码里FreeRTOSConfig.h是只读的需要手动改成可写才能修改配置。4.2 系统初始化的顺序与依赖关系系统初始化的顺序很重要搞错了会导致某些外设不工作。正确的顺序是先初始化时钟再初始化GPIO然后初始化串口用于调试输出接着初始化CAN和定时器最后创建FreeRTOS任务并启动调度器。时钟初始化包括系统时钟和APB1、APB2的分频配置。F103的系统时钟最高可以到72MHzAPB1最高36MHzAPB2最高72MHz。CAN挂在APB1上所以CAN的时钟是36MHz。定时器挂在APB1或APB2上具体看用的是哪个定时器。FreeRTOS的启动函数是vTaskStartScheduler()这个函数调用之后就不会返回了所以必须放在所有初始化完成之后。如果启动调度器之后发现某个任务没有运行首先检查任务的堆栈是否够大然后检查任务的优先级是否比空闲任务高。4.3 CAN通信的调试与验证CAN通信的调试需要至少两个节点一个发送一个接收。如果手头没有第二个节点可以用CAN分析仪模拟。调试的步骤是先确认波特率配置正确然后确认过滤器配置正确最后确认发送和接收的中断使能正确。发送CAN报文的函数是CAN_Transmit()这个函数会返回一个邮箱号如果返回CAN_TxStatus_NoMailBox说明发送邮箱满了需要等待。接收CAN报文的函数是CAN_Receive()这个函数在中断里调用把接收到的报文存到缓冲区里然后通过队列发送给CAN接收任务。提示CAN总线的两端都需要接120欧姆的终端电阻否则通信会不稳定甚至完全失败。这个电阻不是可选项是必须的。4.4 PI控制任务的周期保证与调试PI控制任务的周期由定时器中断保证。定时器配置成1ms中断一次中断服务函数里释放一个信号量控制任务等待这个信号量。如果控制任务的执行时间超过了1ms信号量会累积导致控制周期变短。所以在调试的时候要用GPIO翻转或者串口打印来测量控制任务的实际执行时间。控制任务的实际执行时间包括ADC采样、PI计算、PWM更新三部分。ADC采样如果用的是DMA模式时间可以忽略不计PI计算就是几次浮点运算时间也很短PWM更新就是写寄存器时间更短。所以整个控制任务在1ms内完成是没有问题的。调试PI参数的时候最好把误差和输出通过串口打印出来用曲线工具画出来看。光看数字很难判断超调量和调节时间画成曲线就一目了然了。4.5 Flash参数存储的完整流程Flash参数存储的完整流程是系统上电时从Flash读取参数到RAM运行过程中参数变化时先更新RAM里的值然后标记需要写入低优先级任务检测到标记后执行擦除和写入。读取参数的代码很简单就是把Flash地址强制转换成结构体指针然后解引用。写入参数稍微复杂一点需要先解锁Flash然后擦除整页然后按半字写入最后上锁。void Flash_WriteParams(ParamStruct *params) { FLASH_Unlock(); FLASH_ErasePage(PARAM_ADDR); uint16_t *p (uint16_t *)params; for (int i 0; i sizeof(ParamStruct) / 2; i) { FLASH_ProgramHalfWord(PARAM_ADDR i * 2, p[i]); } FLASH_Lock(); }写入完成之后最好再读回来校验一遍确保写入的数据和预期一致。如果校验失败说明Flash可能损坏或者写入过程中断电了需要做异常处理。5. 常见问题与排查技巧实录5.1 FreeRTOS任务跑不起来或者跑飞了任务跑不起来最常见的原因是堆栈太小。FreeRTOS在创建任务的时候会分配堆栈如果堆栈不够任务执行到一半就会触发硬件错误。解决办法是把堆栈调大或者打开堆栈溢出检测功能让FreeRTOS在溢出时调用钩子函数。任务跑飞的另一个原因是优先级配置错误。如果两个任务的优先级相同FreeRTOS会轮流调度它们这本身没有问题。但如果一个任务里用了阻塞式延时另一个任务又一直在运行低优先级的任务可能永远得不到执行。解决办法是合理使用vTaskDelay()让出CPU给其他任务。5.2 CAN通信失败或者丢包CAN通信失败的第一步是检查硬件终端电阻有没有接CANH和CANL有没有接反波特率是否一致。硬件没问题的话检查软件过滤器配置是否正确中断是否使能发送邮箱是否满。丢包的问题通常和中断处理有关。如果CAN接收中断里做了太多事情比如直接解析报文并执行控制逻辑会导致中断处理时间过长后续报文来不及接收。正确的做法是在中断里只把报文存到队列里解析和处理放在任务里做。5.3 PI控制效果不理想PI控制效果不理想的表现是响应慢、超调大、或者稳态误差大。响应慢说明比例系数太小超调大说明比例系数太大或者积分系数太大稳态误差大说明积分系数太小。整定PI参数的时候建议先用纯比例环节把Kp调到系统开始振荡然后取振荡时Kp的一半。然后加入积分环节Ki从很小的值开始逐渐增大直到稳态误差消除。每次调整后都要记录阶跃响应对比不同参数下的效果。5.4 Flash写入失败或者数据丢失Flash写入失败的原因可能是没有解锁、没有擦除、或者地址不对。F103的Flash写入地址必须是偶数写入的数据是半字16位。如果写入地址是奇数会触发硬件错误。数据丢失的原因可能是写入过程中断电了。Flash擦除和写入都需要时间如果在这段时间内断电数据就会丢失。解决办法是在Flash里存两份参数一份是当前值一份是备份值读取的时候校验两份数据如果一份损坏就用另一份恢复。5.5 常见问题速查表问题现象可能原因排查方法解决办法任务不运行堆栈太小打开堆栈溢出检测增大堆栈任务跑飞优先级配置错误检查任务优先级调整优先级CAN通信失败终端电阻未接测量总线电阻接120欧姆电阻CAN丢包中断处理太长测量中断执行时间中断里只存队列PI响应慢Kp太小观察阶跃响应增大KpPI超调大Ki太大观察阶跃响应减小KiFlash写入失败未解锁或未擦除检查代码流程先解锁再擦除Flash数据丢失写入时断电检查电源双备份存储提示以上问题里堆栈溢出和CAN终端电阻是最容易被忽略的两个。我见过太多人在这两个问题上卡了好几天最后发现原因特别简单。6. 项目封装与后续迭代的思考6.1 代码分层与模块化封装V1项目的代码在封装上做了一次重构把硬件相关的操作和业务逻辑分开了。硬件层包括GPIO、CAN、Flash、定时器的初始化函数业务层包括PI控制、报文解析、参数管理。这样做的目的是方便移植如果换一个STM32型号只需要改硬件层的代码业务层基本不用动。模块化的另一个好处是方便测试。每个模块都可以单独测试比如CAN模块可以用回环模式测试Flash模块可以用读写测试PI模块可以用模拟数据测试。单独测试通过之后再集成出问题的概率会小很多。6.2 版本管理与后续扩展方向V1项目的代码用Git做了版本管理每个功能模块一个分支主分支只保留稳定版本。每次提交都要写清楚改了什么、为什么改方便以后回溯。后续的扩展方向有几个一是加入USB虚拟串口方便和上位机通信二是加入LWIP网络协议栈支持以太网通信三是把PI控制升级成更复杂的控制算法比如模糊PID或者自适应PID。这些扩展都需要在V1的基础上做所以V1的代码结构必须清晰否则扩展起来会很痛苦。6.3 我在这个项目里踩过的坑第一个坑是FreeRTOS的任务优先级和中断优先级搞混了。我把一个中断的优先级设成了比任务优先级还低结果中断触发后任务一直不响应。后来查了手册才知道中断优先级是NVIC管的数值越小优先级越高和FreeRTOS的任务优先级完全是两套体系。第二个坑是CAN的过滤器配置。我一开始用的是掩码模式结果发现有些不该接收的报文也进来了。后来改成列表模式只接收指定的三个ID问题就解决了。第三个坑是Flash的擦除时间。我在控制任务里直接调用了Flash写入函数结果控制周期从1ms变成了20ms电机直接失控。后来把Flash操作移到低优先级任务里问题才解决。这些坑在文档里都不会写只有真正做过项目的人才知道。希望这篇总结能帮你少走一些弯路。
返回列表