ARTICLE DETAIL

资讯详情

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

STM32 FreeRTOS多任务封装实战:CAN通信与Flash存储的V1工程化总结

STM32 FreeRTOS多任务封装实战:CAN通信与Flash存储的V1工程化总结 1. 项目缘起与整体设计思路1.1 为什么会有这个“V1封装”的念头做STM32项目的人大概都有过这种体验第一版功能跑通了代码能跑灯能闪CAN能通Flash能读写但整个工程像一团乱麻。main.c里塞了八百行中断服务函数里直接调业务逻辑FreeRTOS的任务创建和硬件初始化搅在一起换个芯片型号或者加个新功能就得从头翻一遍代码。我管这个叫“能跑的废墟”——功能是有的但没人敢动。这个V1项目封装与总结本质上就是一次“代码收口”。它不是从零开始的新项目而是把之前用STM32CubeIDE搭起来的一套基于FreeRTOS的多任务系统配合CAN总线通信和Flash参数存储做一次结构化的整理和抽象。目标很明确让下一个项目能直接拿这套框架去改而不是重新造轮子。核心关键词就五个STM32、FreeRTOS、CAN、Flash、STM32CubeIDE。这五个词基本覆盖了一个典型工业控制节点的全部要素——主控芯片、实时操作系统、现场总线、非易失存储、开发工具链。把这五样东西封装好后面做类似项目至少省掉百分之六十的重复劳动。适合谁来参考如果你已经能用STM32CubeIDE建工程、点过灯、用过串口但对FreeRTOS的任务划分还停留在“照着例程抄”的阶段对CAN通信只知道发数据不知道怎么组织报文对Flash存储只会调HAL库的擦写函数那这篇总结就是写给你的。我不打算讲寄存器级别的原理那些手册上都有我要讲的是工程层面的组织方式和踩过的坑。1.2 整体架构的分层逻辑封装的核心思路是分层。最底下是硬件抽象层把STM32CubeIDE生成的HAL初始化代码包起来不让上层直接碰HAL_GPIO_WritePin这种底层调用。中间是驱动层CAN驱动、Flash驱动、串口驱动各自独立成模块对外只暴露干净的接口。再往上是服务层FreeRTOS的任务管理、队列、信号量、事件组都在这一层统一配置。最上面是应用层业务逻辑只跟服务层打交道。为什么这么分因为实际项目里最怕的就是“牵一发动全身”。比如你换了一块Flash芯片从W25Q64换成W25Q128如果上层业务代码里到处散落着HAL_SPI_Transmit的调用那你就得满工程搜替换。但如果Flash驱动层封装好了上层只调Flash_Read和Flash_Write换芯片只需要改驱动层一个文件。FreeRTOS的任务划分也是同样的道理。我见过太多项目把所有逻辑塞进一个任务里然后用vTaskDelay硬凑时序。这种写法在功能简单时没问题一旦要加个CAN接收处理或者Flash写入整个时序就乱了。V1封装里我把任务按功能边界切分CAN收发一个任务Flash存储一个任务业务逻辑一个任务再加一个监控任务看门狗。任务之间用队列通信不共享全局变量。STM32CubeIDE在这个架构里扮演的是“脚手架”角色。它生成的初始化代码我不直接改而是用USER CODE BEGIN和USER CODE END区块来插入自己的封装调用。这样下次用CubeIDE重新生成代码时自己的逻辑不会丢。这个习惯救过我很多次后面会细说。1.3 方案选型背后的取舍有人可能会问为什么不用Keil或者IAR答案很简单STM32CubeIDE免费而且和CubeMX无缝集成。对于V1这种需要快速迭代的项目工具链的顺畅程度比编译器的极致优化更重要。Keil5兼容C51和STM32的安装方式虽然经典但版本管理和代码补全体验确实不如CubeIDE。当然如果你团队已经买了IAR授权那另说。FreeRTOS的选择也是类似的逻辑。裸机跑轮询不是不行但一旦涉及CAN通信和Flash写入这种有实时性要求又耗时的操作裸机的中断嵌套和优先级管理会变得非常棘手。FreeRTOS的任务优先级和中断优先级的区分机制恰好能解决这个问题。注意这里说的是“任务优先级”和“中断优先级”是两套独立的体系很多人在这里栽跟头后面会专门讲。CAN总线的必要性不用多说工业现场抗干扰能力强、多主架构、错误检测机制完善。但CAN协议栈的报文解析和过滤配置在STM32CubeIDE里配置起来有点绕尤其是验收滤波器的设置参数算错一个就收不到数据。Flash存储则是为了保存掉电不能丢的参数比如设备地址、校准系数、运行时长统计。用内部Flash还是外部SPI Flash取决于数据量和擦写频率V1里两者都做了封装。2. 核心模块的封装细节与实操要点2.1 FreeRTOS任务划分与优先级配置的实战逻辑FreeRTOS的任务划分不是拍脑袋决定的得根据实时性要求和阻塞特性来。V1里我分了四个任务优先级从高到低排CAN收发任务优先级最高因为CAN报文有实时性要求接收中断触发后需要尽快处理。这个任务大部分时间阻塞在队列上等中断发来的信号。业务逻辑任务优先级中等处理具体的控制算法和状态机。Flash存储任务优先级较低因为Flash写入耗时且不要求实时响应可以等系统空闲时再写。监控任务优先级最低定期喂狗和检查各任务运行状态。这里有个关键点任务优先级和中断优先级的数值方向是相反的。FreeRTOS里任务优先级数值越大优先级越高而Cortex-M的中断优先级数值越小优先级越高。我见过有人把CAN接收中断的抢占优先级设成15然后纳闷为什么任务里收不到数据。因为中断优先级太低被其他中断抢占了。具体配置上CAN接收中断的抢占优先级设成5FreeRTOS的configMAX_SYSCALL_INTERRUPT_PRIORITY设成5这样中断里可以安全调用xQueueSendFromISR。如果中断优先级数值小于5就不能调FreeRTOS的API否则会触发断言。任务间通信全部走队列。CAN任务收到报文后解析出命令字通过队列发给业务任务。业务任务处理完需要保存参数时再通过队列发给Flash任务。队列的长度根据峰值流量来定CAN任务到业务任务的队列我设了16个元素实测在125kbps波特率下不会溢出。注意队列传递的是数据副本不是指针。如果数据量大建议传指针但指针指向的内存必须是静态分配或全局的不能在发送方任务的栈上。2.2 CAN通信的报文组织与过滤器配置CAN通信的封装分三层硬件初始化、报文收发、协议解析。硬件初始化在CubeIDE里配置好波特率和引脚后生成的代码里会有一个MX_CAN_Init函数。我不直接改这个函数而是在它后面加一个CAN_Filter_Config函数来配置验收滤波器。滤波器配置是CAN通信里最容易出错的地方。STM32的CAN外设提供14个过滤器组每个组可以配置为掩码模式或列表模式。V1里我用的是掩码模式只接收特定ID范围的报文。具体参数计算如下假设设备需要接收的标准帧ID范围是0x100到0x1FF那么过滤器ID寄存器写0x100掩码寄存器写0x700。掩码0x700的二进制是0111 0000 0000表示高3位必须匹配低8位任意。这样就能接收0x100到0x1FF之间的所有ID。这个计算过程看起来简单但实际配置时要注意STM32的CAN过滤器寄存器是32位的标准帧和扩展帧的映射方式不同。标准帧的ID需要左移5位再写入寄存器。我当初就是忘了移位结果一个报文都收不到排查了半天。报文发送用HAL_CAN_AddTxMessage这个函数是阻塞的如果邮箱满了会等待。为了避免阻塞任务我在发送前先检查HAL_CAN_GetTxMailboxesFreeLevel如果邮箱不满再发。接收用中断方式在HAL_CAN_RxFifo0MsgPendingCallback回调里把报文存入队列然后通知CAN任务处理。CAN协议解析部分我定义了一个简单的应用层协议第一个字节是命令字第二个字节是数据长度后面是数据负载。命令字包括读参数、写参数、启动、停止、查询状态等。这样业务任务只需要根据命令字做switch-case不用关心CAN底层的细节。2.3 Flash存储的磨损均衡与掉电保护Flash存储的封装要考虑三个问题擦写寿命、掉电保护、数据校验。STM32内部Flash的擦写次数大概是1万次外部SPI Flash一般是10万次。如果频繁写入比如每秒存一次运行数据内部Flash几个月就废了。V1里我用了两个策略。第一参数存储不频繁写只在参数变化时写而且加一个延时比如参数变化后等5秒再写避免频繁擦写。第二运行数据用环形缓冲区的方式存储每次写新数据时擦除最旧的数据块而不是整个扇区擦除。这样能把擦写次数分散到整个Flash空间。掉电保护的关键是写入过程的原子性。Flash写入前要先擦除擦除后如果掉电数据就丢了。我的做法是在Flash里划两个区域A区和B区。写入时先写B区写完后在B区末尾写一个校验标记。下次上电时检查A区和B区的校验标记哪个有效用哪个。这样即使写入过程中掉电至少有一个区域的数据是完整的。数据校验用CRC16每个数据块后面跟两个字节的CRC。读取时先算CRC再比较不匹配就认为数据损坏回退到默认值。这个机制在实际运行中救过一次——有一次设备在写入时断电上电后CRC校验失败自动加载了默认参数没有导致设备死机。提示STM32CubeIDE里配置Flash时注意看门狗的超时时间。Flash擦除一个扇区可能要几百毫秒如果看门狗超时时间设得太短擦除过程中看门狗会复位。V1里我把看门狗超时设成2秒并且在Flash写入前先喂一次狗。2.4 STM32CubeIDE的工程组织与代码生成策略STM32CubeIDE的代码生成机制是“用户代码区块”保护。在.ioc文件里配置好外设后生成的代码里会有/* USER CODE BEGIN XXX */和/* USER CODE END XXX */的注释对。只有在这两个注释之间的代码重新生成时才会保留。我的做法是所有自己的封装代码都写在用户代码区块里或者干脆写到单独的文件里然后在用户代码区块里调用。比如CAN的过滤器配置我写在can_app.c里然后在main.c的USER CODE BEGIN 2区块里调用CAN_App_Init()。这样即使重新生成代码can_app.c不会被覆盖main.c里的调用也会保留。工程目录结构也做了整理Core/ Inc/ can_app.h flash_app.h task_config.h Src/ can_app.c flash_app.c task_config.c main.c freertos.c Drivers/ STM32F1xx_HAL_Driver/ CMSIS/ Middlewares/ Third_Party/FreeRTOS/can_app.c和flash_app.c是驱动层task_config.c是服务层main.c和freertos.c是CubeIDE生成的只做初始化和任务创建。这样分层清晰找代码不用翻整个工程。还有一个细节CubeIDE生成的freertos.c里默认创建了一个defaultTask我把它删了换成自己的四个任务。删除时要注意freertos.c里的MX_FREERTOS_Init函数是在main.c里调用的删任务不影响这个调用但要把defaultTask的句柄和相关代码清理干净否则编译会报未定义。3. 完整实操流程与关键环节实现3.1 从CubeIDE新建工程到外设配置第一步打开STM32CubeIDE新建STM32工程选择芯片型号。V1用的是STM32F103C8T6也就是常说的“蓝板子”。选好芯片后CubeIDE会自动加载对应的芯片包。如果芯片包没安装会提示下载跟着向导走就行。第二步配置时钟。在Clock Configuration标签页里把HSE选成Crystal/Ceramic Resonator然后PLL倍频到72MHz。STM32F103的最大主频是72MHz配到这个频率性能足够而且CubeIDE会自动计算分频系数不用手动算。第三步配置外设。V1用到的外设有CAN1波特率125kbps引脚PA11和PA12。注意PA11是CAN_RXPA12是CAN_TX别接反了。USART1用于调试输出波特率115200引脚PA9和PA10。SPI1用于外部Flash引脚PA5、PA6、PA7片选引脚用PA4。GPIO一个LED接在PC13用于状态指示。RCC使能外部晶振。第四步配置FreeRTOS。在Middleware里选FREERTOS接口选CMSIS_V1。然后在Tasks and Queues标签页里创建任务和队列。V1里创建了四个任务和三个队列具体参数后面细说。第五步配置NVIC。CAN接收中断使能抢占优先级设5子优先级设0。USART1中断使能抢占优先级设6。FreeRTOS的configMAX_SYSCALL_INTERRUPT_PRIORITY在FreeRTOSConfig.h里设成5。第六步生成代码。点击Project菜单的Generate CodeCubeIDE会生成完整的工程文件。3.2 CAN驱动封装的代码实现CAN驱动封装的核心是三个函数初始化、发送、接收回调。初始化函数在can_app.c里#include can_app.h CAN_HandleTypeDef hcan1; QueueHandle_t xCanRxQueue; void CAN_App_Init(void) { CAN_FilterTypeDef filter; // 配置过滤器接收0x100-0x1FF的标准帧 filter.FilterIdHigh 0x100 5; filter.FilterIdLow 0; filter.FilterMaskIdHigh 0x700 5; filter.FilterMaskIdLow 0; filter.FilterFIFOAssignment CAN_RX_FIFO0; filter.FilterBank 0; filter.FilterMode CAN_FILTERMODE_IDMASK; filter.FilterScale CAN_FILTERSCALE_32BIT; filter.FilterActivation ENABLE; HAL_CAN_ConfigFilter(hcan1, filter); HAL_CAN_Start(hcan1); HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); // 创建接收队列 xCanRxQueue xQueueCreate(16, sizeof(CanMsg_t)); }发送函数uint8_t CAN_App_Send(uint16_t std_id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef tx_header; uint32_t tx_mailbox; if (HAL_CAN_GetTxMailboxesFreeLevel(hcan1) 0) { return 1; // 邮箱满发送失败 } tx_header.StdId std_id; tx_header.IDE CAN_ID_STD; tx_header.RTR CAN_RTR_DATA; tx_header.DLC len; if (HAL_CAN_AddTxMessage(hcan1, tx_header, data, tx_mailbox) ! HAL_OK) { return 2; // 发送失败 } return 0; // 发送成功 }接收回调void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; CanMsg_t msg; BaseType_t xHigherPriorityTaskWoken pdFALSE; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, msg.data); msg.id rx_header.StdId; msg.len rx_header.DLC; xQueueSendFromISR(xCanRxQueue, msg, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }这里的关键是portYIELD_FROM_ISR它会在中断退出时触发任务切换让CAN任务立即运行。如果不加这个CAN任务要等到下一个系统节拍才能处理报文延迟会增加。3.3 Flash驱动的读写与校验实现Flash驱动分内部Flash和外部SPI Flash两部分。V1里主要用外部SPI Flash存参数因为内部Flash擦写次数少而且擦除时会阻塞CPU。外部Flash用W25Q648MB容量SPI接口。初始化函数void Flash_App_Init(void) { // SPI初始化在CubeIDE里配置好了这里只需要拉高片选 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }读函数void Flash_App_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd 0x03; // 读命令 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Transmit(hspi1, (uint8_t*)addr, 3, 100); // 24位地址 HAL_SPI_Receive(hspi1, buf, len, 1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }写函数需要先擦除再写void Flash_App_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd; // 擦除扇区W25Q64的扇区大小是4KB Flash_App_EraseSector(addr); // 写使能 cmd 0x06; HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 页写每次最多256字节 cmd 0x02; HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Transmit(hspi1, (uint8_t*)addr, 3, 100); HAL_SPI_Transmit(hspi1, buf, len, 1000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 等待写入完成 Flash_App_WaitBusy(); }Flash_App_WaitBusy函数读状态寄存器等待BUSY位清零。这个等待不能省否则连续写入会失败。CRC校验用软件实现多项式0x1021初始值0xFFFF。每个数据块后面跟两个字节的CRC读取时校验。3.4 FreeRTOS任务创建与队列配置在CubeIDE的FreeRTOS配置界面里创建任务和队列或者直接在freertos.c里手写。V1里我选择手写因为配置界面生成的代码不够灵活。任务创建void MX_FREERTOS_Init(void) { // 创建队列 xCanRxQueue xQueueCreate(16, sizeof(CanMsg_t)); xFlashQueue xQueueCreate(8, sizeof(FlashMsg_t)); xBizQueue xQueueCreate(16, sizeof(BizMsg_t)); // 创建任务 xTaskCreate(CanTask, CAN, 256, NULL, 4, xCanTaskHandle); xTaskCreate(BizTask, BIZ, 512, NULL, 3, xBizTaskHandle); xTaskCreate(FlashTask, FLASH, 256, NULL, 2, xFlashTaskHandle); xTaskCreate(MonitorTask, MON, 128, NULL, 1, xMonitorTaskHandle); // 启动调度器 vTaskStartScheduler(); }任务函数void CanTask(void *argument) { CanMsg_t msg; for (;;) { if (xQueueReceive(xCanRxQueue, msg, portMAX_DELAY) pdTRUE) { // 解析报文转发给业务任务 BizMsg_t biz_msg; biz_msg.cmd msg.data[0]; biz_msg.len msg.data[1]; memcpy(biz_msg.data, msg.data[2], biz_msg.len); xQueueSend(xBizQueue, biz_msg, 0); } } }堆栈大小根据任务里局部变量和函数调用深度来定。CAN任务用了256字业务任务用了512字因为业务任务里可能有浮点运算和较大的局部数组。堆栈溢出检测在FreeRTOSConfig.h里开启configCHECK_FOR_STACK_OVERFLOW设为2这样溢出时会触发vApplicationStackOverflowHook方便定位问题。3.5 系统联调与CAN通信测试联调分三步先测CAN自发自收再测两个节点互发最后测Flash读写和CAN的协同。自发自收测试把CAN设为回环模式发送一帧数据看接收回调能不能收到。这个测试能验证过滤器和中断配置是否正确。如果收不到先检查过滤器ID和掩码的计算再检查中断优先级。两个节点互发用两块板子一块发一块收。发送端每100ms发一帧接收端收到后通过串口打印。如果丢帧检查队列长度和任务优先级。CAN任务优先级要高于业务任务否则报文处理不及时队列会满。Flash和CAN协同测试通过CAN发送写参数命令业务任务收到后把参数发给Flash任务Flash任务写入后通过CAN回复确认。这个测试能验证整个数据流是否通畅。实测下来125kbps波特率下每100ms发一帧连续跑24小时没有丢帧。Flash写入1000次没有出现坏块。系统稳定性满足V1的设计目标。4. 常见问题与排查技巧实录4.1 CAN通信收不到数据的排查思路CAN收不到数据是最常见的问题排查按以下顺序来排查项检查方法常见错误硬件连接用万用表测CAN_H和CAN_L之间的电阻终端电阻没接或者只接了一端波特率确认两端波特率一致一端125k一端250k过滤器读过滤器寄存器的值ID没左移5位掩码算错中断优先级检查NVIC配置优先级数值大于configMAX_SYSCALL_INTERRUPT_PRIORITY模式检查CAN是否在正常模式误设为回环或静默模式我遇到过一次过滤器配置看起来没问题但就是收不到。后来用调试器读寄存器发现FilterIdHigh写进去的值和预期不符。原因是标准帧ID需要左移5位我左移了但移错了方向。标准帧ID是11位在32位寄存器里占bit5到bit15所以左移5位是对的但我当时写成了右移。还有一个坑STM32的CAN外设需要先HAL_CAN_Start再HAL_CAN_ActivateNotification顺序反了中断不会触发。这个在HAL库文档里没写清楚我是看源码发现的。4.2 FreeRTOS任务卡死与堆栈溢出定位任务卡死通常有两个原因堆栈溢出和死锁。堆栈溢出用configCHECK_FOR_STACK_OVERFLOW检测溢出时会调用vApplicationStackOverflowHook在这个函数里打印任务名就能知道是哪个任务溢出了。死锁一般是队列操作不当造成的。比如两个任务互相等对方的队列或者中断里用了非FromISR版本的API。V1里我定了一条规矩中断里只能用FromISR结尾的API任务里只能用非FromISR的API。这条规矩避免了百分之九十的死锁问题。还有一个隐蔽的问题vTaskDelay的参数是系统节拍数不是毫秒。如果configTICK_RATE_HZ设成1000那vTaskDelay(1000)就是1秒。但如果设成100那vTaskDelay(1000)就是10秒。我当初设成100然后纳闷为什么延时不对查了半天才发现是节拍频率的问题。4.3 Flash写入失败与数据丢失的恢复Flash写入失败最常见的原因是忘记擦除。Flash只能把1写成0不能把0写成1所以写入前必须先擦除。W25Q64的擦除最小单位是扇区4KB。如果只写一个字节也要擦除整个扇区。数据丢失的另一个原因是掉电。写入过程中掉电数据可能只写了一半。V1里的双区备份机制解决了这个问题。具体实现是参数区分为A区和B区每个区末尾有一个4字节的序列号。写入时先写序列号小的区写完后序列号加1。读取时比较两个区的序列号用大的那个。如果某个区CRC校验失败就用另一个区。这个机制在测试中模拟掉电100次没有出现数据丢失。实际运行半年有一次现场断电上电后参数正常恢复。提示Flash写入前一定要喂狗。W25Q64擦除一个扇区大概需要100ms如果看门狗超时设成200ms擦除两个扇区就会复位。V1里看门狗超时设成2秒并且在Flash任务里每次擦除前喂狗。4.4 STM32CubeIDE重新生成代码后的冲突处理CubeIDE重新生成代码时用户代码区块之外的代码会被覆盖。如果自己写的函数放在用户代码区块之外重新生成后就丢了。我的做法是所有自己的代码都放在单独的文件里main.c和freertos.c里只保留用户代码区块内的调用。还有一个问题是.ioc文件的版本兼容性。如果团队里有人用旧版CubeIDE有人用新版.ioc文件可能会不兼容。V1里我锁定了CubeIDE版本并且在项目文档里注明避免版本混乱。如果重新生成代码后编译报错先检查是不是用户代码区块的注释对被破坏了。有时候手动修改代码时不小心删了/* USER CODE END */CubeIDE就找不到插入点会把整个文件重新生成。这个坑我踩过丢了一天的代码后来养成了每次改代码前先提交Git的习惯。4.5 系统稳定性优化的几个细节第一个细节FreeRTOS的空闲任务钩子函数里不要做耗时操作。空闲任务优先级最低如果钩子里做了耗时操作会影响低优先级任务的运行。V1里空闲钩子只做一件事喂狗。第二个细节CAN发送时如果邮箱满不要死等。V1里用HAL_CAN_GetTxMailboxesFreeLevel检查满了就返回错误让上层决定是重试还是丢弃。死等会导致任务阻塞影响其他任务。第三个细节Flash写入任务优先级要低于业务任务。因为Flash写入耗时如果优先级太高会抢占业务任务导致控制周期抖动。V1里Flash任务优先级设成2业务任务设成3实测控制周期抖动小于1ms。第四个细节串口打印不要用阻塞方式。V1里调试信息通过队列发给一个低优先级的打印任务打印任务用DMA发送不阻塞其他任务。如果直接用HAL_UART_Transmit115200波特率下发100字节要8.7ms这8.7ms里其他任务都停了。5. 封装成果与后续扩展方向5.1 V1封装的实际效果与复用价值V1封装完成后我拿它做了两个衍生项目。一个是CAN转串口的网关一个是带Flash存储的数据采集器。网关项目只改了业务任务里的协议转换逻辑CAN驱动和Flash驱动直接复用两天就调通了。数据采集器项目加了ADC采样业务任务里多了个ADC处理分支其他部分没动。复用价值主要体现在三个方面一是驱动层接口稳定换芯片型号只需要改驱动层二是FreeRTOS任务框架固定加功能只需要加任务或改业务任务三是Flash存储的双区备份机制通用任何需要掉电保存的项目都能用。代码量方面V1封装的核心文件大概1500行其中CAN驱动300行Flash驱动400行任务框架300行业务逻辑500行。相比封装前的3000多行散乱代码精简了一半而且可读性大幅提升。5.2 从V1到V2的演进思路V1跑通后我总结了几个可以改进的点作为V2的方向。第一CAN协议解析目前是硬编码的switch-caseV2可以做成表驱动的把命令字和回调函数做成映射表加新命令不用改代码。第二Flash存储目前是手动指定地址V2可以做一个简单的文件系统按文件名读写。第三FreeRTOS的任务监控目前只喂狗V2可以加任务运行时间统计和CPU占用率监控。还有一个方向是低功耗。V1没有考虑功耗优化V2可以在空闲任务里进入低功耗模式CAN中断唤醒。这个对电池供电的设备很有用。5.3 给后来者的几点实在建议如果你正准备做类似的项目我的建议是先把FreeRTOS的任务划分想清楚再动手写代码。任务划分错了后面改起来很痛苦。任务划分的原则是“按功能边界切不按代码量切”。一个任务只做一件事任务之间用队列通信不共享全局变量。CAN通信的过滤器配置一定要用调试器读寄存器验证。不要相信自己的计算读出来看一眼确认ID和掩码写对了。这个习惯能省掉很多排查时间。Flash存储一定要做掉电保护。不要觉得“我的设备不会掉电”现场环境什么都有可能发生。双区备份加CRC校验成本不高但能避免数据丢失导致的现场故障。STM32CubeIDE的用户代码区块机制要用好。所有自己的代码都放在用户代码区块里或者单独的文件里。每次重新生成代码前先提交Git万一出问题可以回滚。最后调试信息一定要通过队列异步打印不要用阻塞方式。这个细节对系统实时性的影响比你想象的大。
返回列表