ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

FreeRTOS与软件架构实战:STM32任务划分、通信与分层设计

FreeRTOS与软件架构实战:STM32任务划分、通信与分层设计 去年年底接手 P5 这个项目的时候板子上的固件还是一个大while(1)里塞了三十几个状态标志位串口、按键、屏幕刷新、参数存储全挤在一起。那种代码跑是能跑但只要其中一个环节耗时多一点屏幕就开始卡按键就开始丢后面加一个功能就得把整个循环捋一遍。所以这次重构我把 freeRTOS 和软件架构绑在一起做先定分层和任务模型再往里面填 OS 的调度、队列、信号量最后才是具体业务。这套打法在 STM32F103C8T6 这种 64KB Flash、20KB RAM 的小容量芯片上一样跑得动实测下来内核加应用常驻 RAM 控在 6KB 左右。这篇文章想聊的不是怎么把 FreeRTOS 编译通过那个半个下午就能搞定。我想聊的是当你已经能跑通点灯任务之后任务怎么切、优先级怎么排、栈给多大、队列和信号量到底该用哪个、分层怎么分才不会半年后自己都看不懂。这些内容偏工程判断文档里要么不提要么一笔带过但恰恰是决定项目能不能维护三年的关键。适合刚把 FreeRTOS 跑起来准备做实际项目的朋友也适合做了几个项目但总觉得代码越写越乱的同行。1. FreeRTOS 加软件架构到底要解决什么问题1.1 裸机大循环的三条死线裸机大循环不是不能用我在很多小项目上照样用。它有三条死线只要不碰就相安无事。第一条是实时性死线任何一个函数里出现HAL_Delay(10)这种阻塞等待整个系统的响应时间就被拉长 10ms如果这个函数还在跟别的外设抢总线实际延迟更大。第二条是耦合死线所有模块共享同一个全局状态A 模块改一个标志B 模块的解析逻辑就得跟着改改动量随模块数平方增长。第三条是可测性死线你想单独测一下协议解析对不对得先把整个 main 函数跑起来喂串口数据这基本没法做单元测试。P5 这个项目三条线全踩了。设备要同时做三件事1kHz 采一路 ADC 并做滑动滤波、100ms 上报一帧数据给上位机、屏幕 20ms 刷一次界面。用裸机做屏幕刷新一定会打断 ADC 的定时采样间隔就抖了。这不是水平问题是结构问题。1.2 P5 项目的分层切法与职责边界我给它切了五层从上到下是应用层、服务层、OS 抽象层、驱动层、硬件层。听起来像教科书但每一层画在哪里是有具体判据的。驱动层只做两件事把寄存器操作封装成函数以及把数据从外设搬到内存缓冲区。它不知道 RTOS 的存在不允许在驱动里调用xQueueSend。这条约束看起来苛刻但收益很大驱动层的代码可以直接拿到无 OS 的项目里复用也能在 PC 上做桩测试。OS 抽象层OSAL是薄薄一层胶水提供osal_task_create、osal_queue_send、osal_mutex_lock这类接口里面再调 FreeRTOS 的 API。多这一层的理由是防止 FreeRTOS 的宏和类型污染业务代码同时也留了后路——万一某个项目要用别的内核改这一层就够了。有人觉得这是过度设计我承认在小项目里确实是负担但 P5 后面还要出三四个衍生型号这层很值。服务层放那些跨模块的能力日志、参数存储、协议解析、故障记录。它们的共同特征是被多个应用任务调用所以必须线程安全内部该加互斥量就加。应用层就是各个任务函数本体只做业务编排取数据、判断、发消息、更新界面。1.3 先定架构再动 OS顺序反了会怎样我见过最常见的翻车方式先兴冲冲把 FreeRTOS 移植进来然后随手xTaskCreate建了七八个任务每个任务里再直接调驱动。跑了两周发现任务之间互相等加锁加到死锁最后干脆退回去用裸机还得出一个RTOS 不适合小芯片的结论。正确的顺序是反的先把模块依赖图画出来标清楚谁是生产者、谁是消费者、谁是被动响应再决定这些数据流用任务边界还是用队列边界切断最后才是建任务。任务不是越多越好P5 最终只有 5 个任务外加 FreeRTOS 自己的空闲任务和定时器任务一共 7 个。任务数量少调度开销小调试的时候用 RTOS 感知插件看状态也一目了然。2. 把 FreeRTOS 移植进工程从目录到配置项2.1 源码裁剪与目录结构官方源码包里有portable目录里面按编译器和内核架构分了几十种组合。Cortex-M3 只需要保留portable/RVDS/ARM_CM3Keil或portable/IAR/ARM_CM3IAR其余全删。MemMang目录下五个heap_x.c只留一个P5 留的是heap_4.c。留错或留多会导致重复定义链接阶段才报错而且报错信息往往指不到根因。我的目录是这么铺的P5_Firmware/ ├── app/ # 应用层任务函数、业务编排 ├── service/ # 服务层log、param、protocol、fault ├── osal/ # OS 抽象层 ├── driver/ # 驱动层adc、uart、spi_lcd、flash ├── bsp/ # 板级引脚定义、时钟配置 ├── third_party/ │ ├── FreeRTOS/ # 内核源码 │ └── lvgl/ # 图形库 └── config/ ├── FreeRTOSConfig.h └── lv_conf.h这个结构的价值在于FreeRTOSConfig.h单独放在config目录而不是塞在FreeRTOS/include里。升级内核版本的时候你只需要覆盖third_party/FreeRTOS自己的配置和业务代码一个字都不用动。踩过一次坑以前把配置文件和内核源码放一起升级时手抖覆盖了一上午白干。2.2 中断向量与三个 Handler 的归属这是移植阶段最容易卡住的地方尤其是从别的例程抄过来的时候。FreeRTOS 的 Cortex-M3 端口需要三个异常处理函数vPortSVCHandler启动第一个任务、xPortPendSVHandler任务切换、xPortSysTickHandler系统节拍。而 STM32 的启动文件startup_stm32f103xb.s里已经定义了SVC_Handler、PendSV_Handler、SysTick_Handler这三个弱符号。两种做法。一种是去改启动文件把弱符号的名字改掉另一种更干净在FreeRTOSConfig.h里做宏映射#define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler注意这两个方案只能二选一。同时用会报重复定义而且报错位置在汇编文件里不太好找。另外别忘了 SysTick 的优先级。FreeRTOS 在xPortStartScheduler里会把 PendSV 和 SysTick 设成最低优先级值最大并且通过configKERNEL_INTERRUPT_PRIORITY自动配置正常情况下不用你手动NVIC_SetPriority。但如果你的main里在调用vTaskStartScheduler之前就手动初始化过 SysTick优先级可能被改掉导致任务切换异常。我遇到过这个现象是任务能跑但节拍不对xTaskGetTickCount走得比实际慢。2.3 FreeRTOSConfig.h 逐项拆解这个文件是移植的核心我按功能分组说明参数不是抄来的都是压着实际需求定的。#define configUSE_PREEMPTION 1 /* 抢占式调度实时性靠它 */ #define configUSE_TIME_SLICING 1 /* 同优先级轮转 */ #define configUSE_TICKLESS_IDLE 0 /* 低功耗模式没启用先关掉 */ #define configCPU_CLOCK_HZ ( 72000000UL ) #define configTICK_RATE_HZ ( 1000 ) #define configMAX_PRIORITIES ( 8 ) #define configMINIMAL_STACK_SIZE ( 128 ) #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 12 * 1024 ) ) #define configUSE_16_BIT_TICKS 0 #define configUSE_MUTEXES 1 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configQUEUE_REGISTRY_SIZE 8 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 1 #define configKERNEL_INTERRUPT_PRIORITY ( 15 ( 8 - 4 ) ) #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( 5 ( 8 - 4 ) ) #define configASSERT( x ) if( ( x ) 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }逐条说几个关键的。configTICK_RATE_HZ我选 1000也就是 1ms 一个节拍。选 100 会省一点中断开销但vTaskDelay(1)的精度就变成 10ms很多传感器驱动的等待时间没法表达。选 1000 的代价是每秒一千次 SysTick 中断在 72MHz 的 M3 上每次中断进出大概 1~2 微秒占用不到 0.2% 的 CPU完全能接受。configMAX_PRIORITIES设 8 而不是默认的 5是因为我要留出清晰的优先级梯队0 给空闲任务1 给日志2 给定时器任务3 给界面4 给通信5 给采样6 和 7 空着留给以后的中断转任务。优先级数字越大优先级越高这一点和某些人的直觉相反写代码的时候多看两眼。configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设为 5 是个临界点。它的含义是优先级数值大于等于 5也就是逻辑优先级更低的中断才允许调用xxxFromISR系列的 API。数值小于 5 的高优先级中断FreeRTOS 关中断的时候屏蔽不了它所以在里面调FromISR会破坏内核数据。STM32 的中断优先级数值越小优先级越高换算的时候容易绕晕我的做法是在所有外设中断初始化里显式写上HAL_NVIC_SetPriority(XXX_IRQn, 5, 0)数值只用 5 到 15 这一段。configCHECK_FOR_STACK_OVERFLOW设 2检测方式最严格代价是每次任务切换多检查 20 个字节实测开销可以忽略具体检测原理后面单独讲。3. 任务模型设计需求怎么翻译成任务和通信机制3.1 任务划分的四条判据任务划分是架构设计里最考验经验的一步。我给自己的四条判据是时间尺度是否独立、是否有阻塞点、耦合度是否高、失效影响范围是否隔离。四条里满足两条以上就值得单独开一个任务。以 P5 为例。ADC 采样要 1kHz 严格周期它在时间尺度上完全独立必须单独任务用vTaskDelayUntil保证周期稳定。通信任务要等串口数据、等上位机应答有明确阻塞点单独任务。界面任务要等 LVGL 的刷新时机单独任务。参数存储只在按键触发时写一次 Flash属于低频且不阻塞直接放应用层函数里同步调用就行不值得开任务。日志任务我犹豫过最后留下了因为它是唯一有消费速度可能跟不上生产速度特性的模块需要独立缓冲。这里有个反例要提醒不要为了看起来清晰而把每个驱动都包成一个任务。我见过一个项目一个串口一个任务、一个 ADC 一个任务、一个按键一个任务最后 15 个任务抢 RAM光栈就吃掉 10KB。任务数量应该由数据流的阻塞特性决定不是由外设数量决定。3.2 优先级与栈深度的估算方法优先级分配有个朴素原则周期越短、截止时间越紧的优先级越高这叫速率单调调度在大部分嵌入式场景够用。P5 的分配是采样任务 5、通信任务 4、界面任务 3、定时器服务任务 2、日志任务 1、空闲任务 0。任务优先级周期/触发栈字栈字节数主要阻塞点采样任务51ms 周期200800vTaskDelayUntil通信任务4事件驱动5122048队列、串口中断信号量界面任务35ms 周期10244096LVGL 内部、互斥量定时器服务2内核管理2561024定时器队列日志任务1事件驱动3841536队列空闲任务0空闲时运行128512无注意栈的单位。Cortex-M3 的StackType_t是uint32_txTaskCreate的usStackDepth参数单位是字不是字节。写 512 代表 2048 字节。这个坑我踩过一开始以为单位是字节结果栈只有标称的四分之一跑一会儿就触发栈溢出钩子。界面任务给 1024 字是因为 LVGL 内部有递归调用和局部数组实测高水位线用到 700 字左右留了 30% 余量。通信任务给 512 字是因为里面有个 256 字节的协议解析临时缓冲放在栈上这个做法其实不好后面改成了静态缓冲。3.3 队列、信号量、互斥量、任务通知怎么选这是新手最容易混的地方我把四个机制的适用场景做成表选型的时候直接对号入座。机制典型用途数据传递能否在 ISR 用开销注意事项队列 Queue任务间传数据块、ISR 转任务支持值拷贝用 FromISR 版本中拷贝的是值大结构体建议传指针二值信号量ISR 通知任务、事件同步不传数据用 FromISR 版本低不能累计连续 Give 会丢计数信号量资源计数、多事件累计不传数据用 FromISR 版本低可以累计到设定上限互斥量 Mutex共享资源加锁不传数据禁止中有优先级继承别当信号量用任务通知一对一任务同步或传值可传 32 位值用 FromISR 版本最低每任务仅一组冲突时需额外缓冲事件组多事件逻辑组合等待位标志用 FromISR 版本中适合等三个事件都到齐几个实际判断。串口中断收到一帧数据要交给通信任务解析用二值信号量就够了数据本身放环形缓冲信号量只做有数据了的通知。如果多个中断源都要往日志任务丢消息每个中断给一次 Give那必须用计数信号量否则中间的会丢。互斥量用在两个地方SPI 总线被界面和 Flash 驱动共享用xSemaphoreCreateMutex加锁LVGL 的 API 被多个任务调用的话也要锁。互斥量绝不能在中断里用因为优先级继承机制需要阻塞当前任务中断上下文没有任务可以阻塞。这条如果违反运行时会触发configASSERT死循环如果你把断言关了那就是随机崩溃非常难查。4. 分层架构落地驱动层、OSAL、服务层与应用层4.1 驱动层和 OSAL 的写法驱动层的接口我统一成打开/读写/控制三段式函数名带前缀参数用句柄。比如串口驱动typedef struct uart_dev *uart_handle_t; int uart_open(uart_handle_t *h, const uart_cfg_t *cfg); int uart_write(uart_handle_t h, const uint8_t *buf, uint32_t len); int uart_read_nonblock(uart_handle_t h, uint8_t *buf, uint32_t len);驱动内部收到字节后只做一件事把数据塞进环形缓冲然后调用注册的回调。回调是谁是 OSAL 层提供的一个通知函数。这样驱动层就不依赖 FreeRTOS依赖关系反转成了驱动调用一个函数指针函数指针由上层注入。OSAL 层大概两百行代码把用到的 FreeRTOS API 包一遍。有人问这层值不值我的判断标准是如果你的项目只做一个型号、生命周期两年内就别加如果要做多个衍生型号、或者团队里有新人需要快速上手加上它。P5 属于后者。4.2 服务层日志、参数存储、协议栈日志服务我用的是环形缓冲加独立任务的模式。其他任务通过log_write往缓冲里写缓冲区满的时候直接丢弃并计数绝不阻塞业务任务。日志任务在低优先级下慢慢往串口或者 RTT 输出。这个设计的核心是日志不能反过来影响业务我见过因为日志串口没接、发送阻塞把整个采样任务拖垮的案例。参数存储用 Flash 的最后两页做双备份写入的时候先擦 A 页写数据、校验通过后擦 B 页写索引掉电也不会两边都坏。不要在任务里直接擦写 FlashSTM32F103 的页擦除要 20~40ms这期间 CPU 取指如果落在同一 Bank 上会被硬件阻塞其他任务直接失去响应。我的做法是把擦写放到一个专门的临界区里先挂起调度器vTaskSuspendAll、操作完再xTaskResumeAll同时保证这段时间不依赖中断响应。更稳的方案是把参数写到外部 EEPROM只有确认没有 EEPROM 才用内部 Flash 兜底。协议解析服务是无状态的输入字节流、输出帧结构方便单独测试。这里有个技巧解析函数不要直接往队列里发而是返回一个状态码由调用方决定怎么处理。这样解析逻辑可以在 PC 上用 GCC 编译跑单元测试不用上板子。4.3 应用层与 LVGL、上位机协议的解耦LVGL 的移植有两个必须做对的地方。第一是心跳lv_tick_inc(1)必须每毫秒调用一次我放在vApplicationTickHook里这个钩子由configUSE_TICK_HOOK打开在 SysTick 中断上下文执行函数体里只做lv_tick_inc(1)这一件事不做别的。第二是刷新lv_timer_handler放在界面任务里用vTaskDelayUntil保证 5ms 一次返回下次需要等待的时间可以拿来动态调整延时。LVGL 本身不是线程安全的。我的处理方式是所有lv_*调用只允许出现在界面任务里其他任务想改界面就把界面请求丢进一个队列界面任务收到后自己调 LVGL。这比到处加互斥量可靠得多因为互斥量只能保证不冲突保证不了调用时序正确。如果非要在别的任务里直接调用lv_async_call也得配合锁我实测下来还是队列方案省心。上位机那边P5 配套的是一个 Qt 写的监控软件。有意思的是Qt 上位机的架构和 MCU 侧几乎是一一对应的。MCU 侧有驱动层UART、协议层帧解析、服务层数据缓存、应用层业务逻辑、显示层LVGLQt 侧就是 QSerialPort、协议解析类、数据模型、业务逻辑类、QWidget 界面。Qt 那边的关键点是别在 UI 线程里做耗时解析。QSerialPort::readyRead信号在 UI 线程触发如果解析函数里有大量计算界面就会卡。我的做法是readyRead里只把readAll()的数据丢进一个缓冲然后用信号槽队列连接发给工作线程里的解析对象解析完再发信号回 UI 线程更新。槽函数的连接类型要显式写成Qt::QueuedConnection不然默认是直连等于没跨线程。帧格式我定的是0xAA 0x55 | 长度 | 命令字 | 负载 | CRC16 低字节 | CRC16 高字节。用 CRC16 而不是简单校验和是因为设备在电机旁边干扰大校验和漏检率太高。CRC 表法计算72MHz 下算 64 字节大概几十微秒放在通信任务里没问题。5. 内存与堆heap_1 到 heap_5 的选择和实测数据5.1 五种 heap 方案的取舍FreeRTOS 提供五个heap_x.c很多人是照抄别人的选择其实每种都有明确适用场景。方案是否可释放碎片处理适用场景P5 是否选用heap_1否不涉及所有对象只在启动时创建否heap_2是不合并相邻空闲块已废弃不建议新项目用否heap_3是依赖 C 库 malloc有成熟 malloc 实现的平台否heap_4是首次适配 相邻合并通用场景绝大多数项目是heap_5是同 heap_4支持多内存区有外部 SRAM/SDRAM 的平台否heap_4 是默认答案它把空闲块用链表串起来分配时首次适配释放时自动合并相邻空闲块能有效抑制碎片。heap_1 虽然最简单最安全但要求所有任务、队列、信号量在调度器启动前一次性建好P5 有个按需创建的调试任务所以不用。heap_5 适合有片外 RAM 的场景vPortDefineHeapRegions把内部 SRAM 和外部 SDRAM 一起交给内核管理。用的时候要注意必须在创建任何内核对象之前调用否则内核会先按默认区域初始化再定义就失效了。5.2 堆剩余量的监控与调参configTOTAL_HEAP_SIZE我定的是 12KB芯片总共 20KB RAM内部 Flash 双备份、LVGL 缓冲、环形缓冲都要占留给堆的就是这么多。调参不能靠猜要在运行一段时间后打印两个数值size_t remain xPortGetFreeHeapSize(); size_t min_ever xPortGetMinimumEverFreeHeapSize();xPortGetMinimumEverFreeHeapSize记录的是历史最低水位这个值比当前剩余量有用得多因为它能告诉你运行过程中的峰值占用。我的判断标准是历史最低水位要留 20% 以上余量。如果最低水位掉到 1KB 以下说明配置得太紧后面加一个队列就会分配失败。堆分配失败的钩子也要打开configUSE_MALLOC_FAILED_HOOK设为 1然后实现vApplicationMallocFailedHook里面把任务名、请求大小打印出来再死循环。这个钩子救过我一次现象是设备偶尔重启最后发现是某个场景下队列创建失败返回了 NULL代码没判空直接解引用。6. 调试与排查栈溢出、优先级反转、死锁现场6.1 栈溢出检测的两种模式configCHECK_FOR_STACK_OVERFLOW设 1 的时候内核在任务切换时检查栈指针是否越界只能发现已经越界的情况属于亡羊补牢。设 2 的时候创建任务时整个栈填 0xA5切换时检查栈末尾 20 个字节是否还是 0xA5 图案能在越界前就发现是主动防御。代价是每次切换多 20 字节比较实测在 M3 上大概多 1~2 微秒。P5 用 2。触发后内核会调用vApplicationStackOverflowHook参数是出问题的任务句柄和任务名void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName ) { taskDISABLE_INTERRUPTS(); printf( STACK OVERFLOW: %s\r\n, pcTaskName ); for( ;; ); }注意钩子里不要调用任何 FreeRTOS API 里带阻塞的接口栈已经崩了再调这些函数就是二次伤害。最简单的做法是关中断然后死循环用调试器看pcTaskName。平时想监控栈余量用uxTaskGetStackHighWaterMark(handle)返回值是历史最小剩余量单位是字。我在调试版本里每 5 秒把所有任务的这个值打一遍哪个任务剩余量低于 20 字就该加大栈了。6.2 常见故障速查表下面这些是我和同事在实际项目里遇到过的按现象归类。现象可能原因排查方法处理方式上电就进 HardFault栈溢出 / 向量表未重映射看 LR 和 PC 寄存器加大栈、检查 SCB-VTOR任务跑一会儿卡死优先级反转或死锁RTOS 感知调试看各任务状态检查互斥量获取顺序vTaskDelay(1)实际延迟很长SysTick 优先级被改 / 有其他高优先级中断测实际节拍统一中断优先级数值串口偶发丢帧ISR 里给了二值信号量导致合并计数信号量替换环形缓冲 计数信号量堆分配返回 NULLconfigTOTAL_HEAP_SIZE偏小打印历史最低水位增大堆或复用对象界面卡顿但 CPU 不忙LVGL 调用分散在多任务检查lv_*调用点集中到界面任务调度器启动前就崩中断里调了非 FromISR 的 API检查启动阶段的中断改成 FromISR 版本优先级反转值得单独说一句。界面任务拿了 SPI 互斥量被通信任务抢占通信任务又要拿同一个互斥量理论上通信任务优先级高会一直等。FreeRTOS 的互斥量有优先级继承机制会把持有锁的任务临时提到等待者的优先级缓解这个问题。但如果链路上有三四个互斥量嵌套继承机制就不好使了只能靠设计上避免嵌套加锁。我的规矩是同一时刻每个任务最多持有一把锁绝不在持锁状态下申请另一把。6.3 RTOS 感知调试与内核切换流程的回看Keil MDK 和 IAR 都支持 RTOS 感知调试能看到当前有哪些任务、各自状态、优先级、栈用量。Keil 里需要在FreeRTOSConfig.h里打开configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS然后在调试窗口里勾选。这个功能我在排查死锁的时候用过一次一眼就看出两个任务都在Blocked状态等待对方手里的信号量比打日志快得多。顺便回顾一下内核切换流程理解了这个很多现象就能自己解释。任务的切换由PendSV 异常完成因为它是可挂起的、优先级最低的不会打断更高优先级的中断。SysTick 中断里如果发现需要切换就置位 PendSV 的挂起位等所有高优先级中断处理完PendSV 才执行在它的处理函数里完成寄存器压栈、栈指针切换、出栈。所以任务切换永远是发生在中断退出之后这也解释了为什么在中断里改的任务状态不会立即生效。6.4 面试里绕不开的几个点既然聊到内核顺带说几个面试常问、也确实反映功底的题目。vTaskDelay和vTaskDelayUntil的区别是什么前者是从现在起延时 N 个节拍每次调用会引入累计误差后者是延时到指定时刻能保证周期稳定适合周期性采样。xQueueSend和xQueueSendFromISR的差别后者不会阻塞靠pxHigherPriorityTaskWoken参数告知是否需要退出中断时触发切换用的时候必须配合portYIELD_FROM_ISR否则被唤醒的高优先级任务要等下一个节拍才运行。还有一个高频问题二值信号量和互斥量的区别。表面看都是 0/1本质区别有三个互斥量有优先级继承、互斥量有所有权概念谁拿谁放、互斥量不能在中断里使用。面试官问这个其实是在确认你有没有真正踩过坑。7. 后续可以这样扩展P5 这套结构后面还有几件事要做。低功耗是第一个configUSE_TICKLESS_IDLE打开后空闲时可以让芯片进入低功耗模式靠定时器唤醒。要注意的是打开这个功能前必须先确认没有任务依赖绝对精确的节拍否则唤醒后的节拍补偿会带来偏差采样任务的时间戳就飘了。再就是单元测试。协议解析、参数校验这些纯逻辑模块我抽出来放到 PC 上跑用 GCC 编译加一套简单的断言不需要上目标板。这样改协议的时候能快速回归比烧一次板子快得多。最后分享一个我自己的小习惯每次改完架构相关的代码我都会在README里记一行为什么这么改。因为半年后你自己一定会忘而这三行字能省下一小时。8. 一个关于外包给纯软件公司的做法讨论最近常有人问能不能把 FreeRTOS 那一层直接外包给纯软件公司自己团队只写业务。从经验看可行但有两个前提。一是外包出去的契约必须写到接口级别OSAL 那层的函数签名、语义、错误码要全部冻结不能允许对方顺便优化。二是必须有懂 RTOS 的人做验收哪怕只招一个因为很多问题在 PC 上测不出来必须上板。我见过一个反例外包方为了交差把configTOTAL_HEAP_SIZE直接设成 16KB在 20KB RAM 的板子上所有功能都能跑但他们没测长时间运行。实际上内部 Flash 模拟 EEPROM 的缓冲和图形缓冲吃掉了几 KB最终在客户现场连续跑三天后堆分配失败重启。纯软件出身的人对这种资源在某些路径上才被消耗的模式不敏感这个必须自己把关。所以我的建议是让外包方做操作系统抽象层之上的业务逻辑和上位机RTOS 配置、堆栈预算、中断优先级这三样核心参数由自己团队定写进接口文档里当成硬约束改动必须走评审。这样分工两边都能发挥长处责任边界也清楚。
返回列表