ARTICLE DETAIL

资讯详情

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

裸机工程快速接入FreeRTOS:任务拆分与移植避坑指南

裸机工程快速接入FreeRTOS:任务拆分与移植避坑指南 前阵子有个朋友把跑了一年多的裸机工程发给我问能不能不推倒重写就把RTOS加进去。他的状态我记得很清楚main 函数的 while(1) 里塞了五六个模块按键扫描、传感器读取、OLED刷新、蜂鸣器控制全挤在一起某个外设偶尔卡一下整个系统界面就跟着僵住。他觉得代码已经快改不动了但又担心引入实时操作系统会带来很大的移植成本。这件事其实很典型。“嵌入式裸机应用”在项目初期效率最高逻辑简单直观可一旦外设数量上来、实时性要求变高超级大循环的弊端就会越来越明显。把RTOS加进去不等于把整个工程推翻重写恰恰相反正确做法是保留驱动层只把调度方式和任务组织改掉。这篇文章就把我整理出来的改造思路完整写出来包括怎么选型、怎么裁剪、任务怎么拆、坑在哪里照着做基本一两天就能把RTOS跑起来。1. 裸机应用到底卡在哪才需要上RTOS1.1 超级大循环的现实瓶颈很多裸机工程实际就是一段 while(1) 里按顺序调用各个模块函数再加上几个中断服务函数。所有任务共享一个CPU执行顺序完全依赖代码书写顺序。while (1) { Key_Scan(); SHT30_Update(); OLED_Refresh(); HAL_Delay(20); }这种结构在模块少的时候没毛病但模块一多就会遇到两个问题。第一是响应时间不可控某个函数如果内部有阻塞等待比如I2C读传感器、Flash擦写后面所有模块都得排队按键响应就会变得很迟钝。第二是逻辑纠缠按键模块想通知显示模块只能靠标志位标志位多了以后代码就变成一团乱麻你根本分不清某个全局变量到底被谁改过。我在不少项目里看到过类似代码一个全局标志位数组里面几十个 bool每个中断和循环任务都在里面写标志互相影响。这种状态已经到了该考虑换架构的时候。1.2 RTOS真正带来的内核价值以及它引入的新问题RTOS做的事情本质上很简单把CPU时间按照任务优先级分片让优先级高的任务先抢占CPU让等待中的任务主动让出CPU去睡觉等延时到了或事件发生了再醒来继续跑。从裸机到RTOS核心变化是从“代码顺序驱动”变成“事件和优先级驱动”。这个改动能带来两个立竿见影的好处。一个是任务隔离每个功能模块有自己的栈有自己的上下文逻辑边界清楚不会因为一个模块卡死导致整个系统瘫掉。另一个是阻塞不浪费CPU比如vTaskDelay会让任务进入阻塞态等时间到了再执行CPU的空闲时间可以被其他任务利用。但RTOS并不是免费的。它引入的代价同样很现实内存开销变大每个任务都需要分配独立的栈空间共享资源需要加锁保护否则会出现数据竞争所有的延时、中断处理逻辑都要重新梳理。还有一个很多人容易忽略的点调试不再像以前那样打断点就完事任务切换可能在你查看变量的一瞬间让代码上下文跳走。所以我要先给一个明确建议如果你的裸机工程还在稳定运行、实时性够用、没有痛点不必为了技术“潮”去硬上RTOS。什么时候该上当多个实时任务出现相互阻塞、中断里要处理重活、或者功能模块多到主循环已经难以维护的时候图的就是调度和隔离这两个收益。2. 快速改造前的决策内核选型、接口与配置裁剪2.1 先分辨裸机代码属于哪种结构拿到一个裸机工程不要急着打开FreeRTOS源码复制粘贴先看代码属于哪一类。不同结构的改造难度差别很大。第一种是纯顺序轮询型while(1)里面从模块A跑到模块Z周而复始。这种最容易被RTOS替代但也要注意模块之间的数据依赖关系。第二种是“中断标志 主循环处理”型中断服务函数里只置标志位主循环轮询标志后处理业务。这种代码上RTOS收益很大因为原来主循环里的处理动作可以挪到独立任务里按优先级执行。第三种是状态机驱动型主循环里维护一个CurrentState每个状态是一个函数指针或switch case。这种结构本身已经具备“事件驱动”的雏形改造时不要破坏状态机的核心逻辑只需要把状态机的Tick和执行环境放到一个RTOS任务里。我自己接手的项目里半数以上是第二种。改造的时候应该按“把每个业务模块归位到独立任务”的思路走不要试图一次把所有模块全拆出来。快速改造不代表一步到位先做最小可运行版本跑通了再继续拆。2.2 内核怎么选我的建议是先上FreeRTOSRTOS选型在嵌入式圈子里讨论得很多RT-Thread、FreeRTOS、uc/OS、Zephyr各有拥趸。如果目标是“给已有裸机项目快速加RTOS”我首选FreeRTOS。原因很实在资料多到泛滥遇到问题搜索引擎随便一翻就有答案商用免费开源许可证对商业项目相对友好源码精简可以只保留一个kernel不引入设备驱动框架和组件层入侵性低也支持切换一层CMSIS-RTOS v2接口后面想在STM32CubeMX里直接配置也非常方便。有些人会强调RT-Thread的设备框架和软件包生态更好这我当然认。但越是老工程越不愿意把驱动代码改成框架规定的写法。FreeRTOS单纯提供内核调度不强迫你用它的driver模型这对“快速添加”是巨大的优势。另一个选择是直接用STM32CubeMX集成的FreeRTOS如果项目原本基于STM32CubeMX生成这是最快的路。CubeMX可以勾选FreeRTOS生成中间件自动处理中断向量、PendSV和SysTick的分配连 heap 实现和参数都能在界面里配置。但对于非CubeMX工程手工添加源码反而更容易控制裁剪范围。2.3 裁剪与最小配置别照抄默认工程把这个表作为首次运行的初始值后续再调。我见过很多移植后系统崩溃的案例不是代码逻辑错误而是configMINIMAL_STACK_SIZE和configTOTAL_HEAP_SIZE设置不合理。最小任务栈可以先设128字512字节但真实工程里任务里如果调用了printf、查表函数、浮点运算128字经常不够最好按256字起步。我习惯的做法是初始给每个任务分配得稍微宽松一点内部元素用uxTaskGetStackHighWaterMark函数去查剩余栈空间把所有任务跑满几十分钟后再缩小到安全裕量。configTOTAL_HEAP_SIZE需要估算每个任务需要自己的栈tick hook、定时器服务任务、队列和信号量都要占用内存。一个包含4个256字任务的小系统加上几个队列heap给8KB~16KB比较稳妥。小内存MCU可以换成heap_1或heap_2比如系统不动态删除任务时heap_1足够而且不会有内存碎片问题。3. 不动驱动层的移植方案5个关键步骤3.1 源文件拷贝与工程目录组织在工程根目录下新建 Middlewares/RTOS 目录简单一点就把tasks.c、queue.c、list.c、portable文件放进去把所有FreeRTOS头文件放进去。拷贝文件时特别提醒portable目录里的内容要根据编译器差别选择对应子目录。GCC就选GCC/ARM_CM4FIAR就选IAR/ARM_CM4FKeil则选RVDS/ARM_CM4F。另外heap实现只需要选一个heap_4.c不要一个不漏地全加进工程里否则编译后会链接冲突。对于没有内存管理模块的裸机工程我一般顺手用heap_4它比heap_1多了内存合并机制更安全。工程配置里要额外添加两个宏定义不要等编译报错再来补救configUSE_PORT_OPTIMISED_TASK_SELECTION基于CM4以上内核建议设为1这样通过硬件CLZ指令快速查找最高优先级就绪任务减少调度开销另一个是configUSE_TIME_SLICING需要在开启时间片轮转调度的时候设为1。3.2 中断优先级分组移植最容易出错的一步如果只拷贝源码然后直接编译运行通常十有八九会跑死。真正让新手栽跟头的不是编译错误而是中断优先级没有配合FreeRTOS的要求。FreeRTOS为了确保系统API调用安全要求能调用API的中断优先级不高于数值上大于等于一个设定阈值而PendSV和SysTick必须设为最低优先级。CM4和CM7内核上NVIC中断优先级分组如果是STM32默认的HAL库设置一般是分组4也就是4位都用于抢占优先级取值0~15。FreeRTOS里要用下面的配置把它们对齐#define configPRIO_BITS 4 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY (8 - configPRIO_BITS) ) #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (8 - configPRIO_BITS) )移植时还要在代码里把所有调用FreeRTOS API的中断优先级设置成数值不低于5比如设置成6、7、8都可以。低于5的中断比如优先级数值3FreeRTOS认为它太重要不会做屏蔽任务切换时可能产生不可预知的风险。而PendSV和SysTick则设置成15最低优先级。如果你原来用过NVIC_PriorityGroup_2或者分组3FreeRTOS会跑得乱七八糟因为抢占优先级位数的含义不对了。最好统一成分组4在main函数最开始就调用HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)并且这个设置必须在任何外设中断初始化之前完成。3.3 main函数重构两件事做完就启动调度器裸机main函数的写法一般都是硬件初始化之后直接进入while(1)死循环。RTOS启动调度器之后vTaskStartScheduler并不会返回所以main函数结构会变成下面这样int main(void) { HAL_Init(); SystemClock_Config(); BSP_Init(); // 所有板级外设初始化保持不变 MX_FREERTOS_Init(); // 内部创建任务 vTaskStartScheduler(); // 启动调度器不会返回 while (1) {} // 正常不会走到这里 }很多人移植后程序启动就HardFault最大原因就是main函数里仍然保留了一段裸机时代的前置初始化操作比如延时等待传感器就绪而这个延时函数依赖SysTick。一旦FreeRTOS接管SysTick和PendSV裸机延时函数可能失效导致外设初始化时序错误。所以移植时的铁律是所有HAL_Init以外的、耗时超过几毫秒的硬件初始化要么放在创建的第一个任务里分步完成要么就要确保初始化代码不依赖会被RTOS占用的系统节拍。一般情况下外设寄存器的初始化在启动调度器之前做都还安全但遇到需要长时间的等待外部复位、校准的情况就要把这个初始化放到任务里执行。3.4 把裸机延时改成任务阻塞不是简单换函数裸机代码里大量存在HAL_Delay、DelayMs这种忙等延时。移植到RTOS后建议先做一次全局搜索把所有while里的小延时分为两类一类是只持续几个微秒的硬件时序比如I2C的ACK等待、模拟SPI的时钟翻转这种延时不能随意改成任务切换因为切换开销可能超过延时本身还会导致时序抖动。另一类是几十毫秒以上的业务延时比如按键消抖、轮询周期、状态保持时间这种就适合用vTaskDelay替换。vTaskDelay和裸机delay最大的区别是参数单位是Tick而且调用方会进入阻塞态让出CPU。做替换的时候注意vTaskDelay最少等待一个Tick如果参数是0则不会阻塞。用pdMS_TO_TICKS来转换毫秒比如vTaskDelay(pdMS_TO_TICKS(10))。有一个高频问题为什么任务里的delay要尽量用vTaskDelay而不用HAL_Delay因为HAL_Delay本质上也是忙等待即使它在裸机下工作正常交给RTOS后它不会触发任务调度器的阻塞机制于是其他相同优先级任务得不到运行机会延迟和抖动问题一点没解决。移植阶段最直接的动作就是把主循环和任务函数里的HAL_Delay批量改成vTaskDelay配合pdMS_TO_TICKS。3.5 ISR的改造中断服务函数只做一件事裸机程序里的中断服务函数很容易写得很“重”不断轮询、清标志、处理数据。RTOS场景下最推荐的模式是中断里只置标志或发送信号/数据把真正耗时的事务放到任务里处理。典型做法是用队列或信号量通知任务如下面的按键中断例子void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { BaseType_t xHigherPriorityTaskWoken pdFALSE; uint32_t keyCode (uint32_t)GPIO_Pin; xQueueSendFromISR(keyEventQueue, keyCode, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }xQueueSendFromISR结尾这个portYIELD_FROM_ISR尤其关键。如果队列等待者的优先级高于当前被打断的任务它会立刻抢在中断返回后进行上下文切换少了这句高优先级任务可能在多个Tick之后才被调度实时性就会打折。这里还要强烈提醒ISR里绝对不能调用vTaskDelay、vTaskDelayUntil这类会阻塞的API因为它们会导致调度器在中断上下文里试图切换任务直接触发断言或死机。中断里如果确实需要发送数据记得选带FromISR后缀的版本。4. 实战把典型超级循环工程改造成FreeRTOS任务4.1 改造前现场超级循环到底卡在哪我拿一个非常典型的小系统来拆解。它没有任何业务需要保密但问题非常有代表性一个带按键、温湿度传感器、OLED屏、蜂鸣器的小设备。改之前的骨架像下面这样int main(void) { HAL_Init(); SystemClock_Config(); BSP_Init(); uint32_t lastKey 0; while (1) { uint32_t key Key_Scan(); if (key ! 0 key ! lastKey) { Buzzer_Beep(); SaveKeyToFlash(key); // Flash写入可能阻塞 20ms 以上 OLED_ShowKey(key); } lastKey key; float temp SHT30_ReadTemp(); // I2C阻塞读最坏几十ms float humi SHT30_ReadHumi(); OLED_UpdateEnv(temp, humi); OLED_Refresh(); // 整屏刷新非常耗时 HAL_Delay(20); } }这个系统的问题很明显当SHT30读取卡住时按键扫描和蜂鸣器完全不响应Flash写操作和OLED刷新叠加整个主循环的一次周期可能达到上百毫秒读到的数据都是主循环按代码写死的顺序来的想提高温湿度采样频率就必须把按键任务拆出去。所有这些问题的根因就是在一个顺序循环里硬塞了两个不同实时要求的事情按键要快速响应屏幕刷新可以慢半拍但刷新时不应该挡住其他事情。4.2 任务划分与优先级分配把这个系统拆成几个任务划分依据不是“有没有现成函数”而是功能周期和实时要求。任务名做的事情周期优先级KeyTask扫描按键、检测按下事件并发送到队列10ms高FlashTask接收存储队列把按键值写入Flash触发型中高EnvTask周期性读取温湿度把结果发送给显示任务500ms中UIRefreshTask接收环境数据、按键消息刷新OLED消息/周期低IDLETask系统空闲可做低功耗统计-最低优先级分配的核心是“谁的实时性要求最高谁就优先”。这里按键扫描虽然本身不重但用户按一下就希望立即有反应所以优先级最高。环境数据读取是周期任务稍微晚几十毫秒影响不大。屏幕刷新最慢而且即使被高优先级任务频繁打断也能分片完成就放在低优先级。Flash写入比较微妙它必须保证不丢数据又不能在写的过程中被打断太多所以做成一个由队列触发的任务优先级中等偏高键盘任务只负责入队不直接执行写入。这个小设计让按键函数不承担耗时的Flash擦写是改造的重点收益之一。4.3 改造后核心代码改造后的工程中main函数只负责硬件和内核初始化。任务创建集中在独立函数里static TaskHandle_t keyTaskHandle; static TaskHandle_t envTaskHandle; static TaskHandle_t uiTaskHandle; static QueueHandle_t keyEventQueue; static QueueHandle_t envDataQueue; void MX_FREERTOS_Init(void) { keyEventQueue xQueueCreate(4, sizeof(uint32_t)); envDataQueue xQueueCreate(2, sizeof(EnvData_t)); xTaskCreate(KeyTask, key, 256, NULL, 3, keyTaskHandle); xTaskCreate(EnvTask, env, 256, NULL, 2, envTaskHandle); xTaskCreate(UIRefreshTask, ui, 384, NULL, 1, uiTaskHandle); xTaskCreate(FlashTask, flash, 256, NULL, 2, NULL); }任务函数内部把原本的业务逻辑几乎原样搬进去唯一明显变化是阻塞手段和通信方式static void KeyTask(void *argument) { uint32_t lastKey 0; for (;;) { uint32_t key Key_Scan(); if (key ! 0 key ! lastKey) { xQueueSend(keyEventQueue, key, 0); Buzzer_BeepTime(30); } lastKey key; vTaskDelay(pdMS_TO_TICKS(10)); } } static void FlashTask(void *argument) { uint32_t keyCode 0; for (;;) { if (xQueueReceive(keyEventQueue, keyCode, portMAX_DELAY) pdTRUE) { SaveKeyToFlash(keyCode); } } } static void EnvTask(void *argument) { EnvData_t envData; for (;;) { envData.temp SHT30_ReadTemp(); envData.humi SHT30_ReadHumi(); xQueueOverwrite(envDataQueue, envData); vTaskDelay(pdMS_TO_TICKS(500)); } } static void UIRefreshTask(void *argument) { EnvData_t envData; uint32_t keyCode 0; for (;;) { if (xQueueReceive(keyEventQueue, keyCode, 0) pdTRUE) { OLED_ShowKey(keyCode); } if (xQueuePeek(envDataQueue, envData, 0) pdTRUE) { OLED_UpdateEnv(envData.temp, envData.humi); } OLED_Refresh(); vTaskDelay(pdMS_TO_TICKS(50)); } }注意有几个细节。FlashTask和UIRefreshTask都接收了同一个keyEventQueue这是个隐患如果两个任务同时等待同一个队列按下事件只会被其中一个拿走。所以实际代码里应该拆成两个队列或者让UI任务只接收显示消息、Flash任务接收存储消息。这种“假共享”问题在裸机时代不明显到了多任务环境很容易出现后面会专门展开讲。EnvTask用xQueueSend还是xQueueOverwrite取决于业务如果希望UI始终显示最新温度而不是排队显示一堆历史数据就用xQueueOverwrite覆盖旧值。UIRefreshTask里用xQueuePeek而不是xQueueReceive读环境数据是为了不把队首数据消费掉保证每次刷新都能读取最新的副本。4.4 队列、信号量与互斥锁什么时候用任务通信和共享资源保护是裸机代码里从没遇到过的新问题。我建议在快速改造阶段按下面这个原则来选任务到任务、中断到任务的数据传递用队列数据量小就用队列发送指针但务必保证指针指向的内存生命周期是整个系统运行期不要发送指向任务栈临时变量的指针。中断通知任务“有活干了”用二值信号量或任务通知比如UART接收完成通知解析任务。多个任务访问同一个外设或同一片内存用互斥锁比如两个任务都要操作OLED如果不加锁可能出现两个任务交替写屏导致花屏。读多写少的共享变量有时直接用volatile加临界区就够不必为了一个小变量动用重量级互斥锁。在“快速添加”的场景里我见过不少人把所有共享数据都套上互斥锁结果系统反而变得复杂。其实很多裸机时代已经通过关中断解决的临界区在RTOS里可以用taskENTER_CRITICAL和taskEXIT_CRITICAL临时关调度器保护几行代码就够了。关键是尽量缩短临界区执行时间不要在临界区内做外部等待或延时。5. 改造后最容易踩的5类坑排查经验一次讲完5.1 现象一跑起来后任务完全不动任务没有任何输出调度器像是没启动。排查时先看编译器和链接器有没有把PendSV、SVC、SysTick这几个中断入口正确提供给内核。裸机工程如果使用了自己的SysTick_Handler并且与FreeRTOS提供的port SysTick冲突系统会卡死。其次检查stack空间是不是太小如果任务栈溢出FreeRTOS的栈溢出检测可以打开在配置里设configCHECK_FOR_STACK_OVERFLOW为1或2同时在FreeRTOSConfig.h里开启钩子函数它会在栈溢出时触发vApplicationStackOverflowHook。判断任务是否真的在跑最简单的方法是给每个任务开始处加一个GPIO翻转用示波器看有没有方波。没有示波器就在任务里写一个全局计数变量调试器定时看它的值变化。这个方法虽然土但能快速确认到底是调度器没动还是任务内部卡住。5.2 现象二进中断后系统直接死机或HardFault中断里用了非FromISR后缀的API是头号原因。比如xQueueSend不带FromISR在中断里调用FreeRTOS会因上下文不对而触发configASSERT。开启configASSERT后类似问题会在出错的瞬间卡在当前行通过调试器调用栈很容易定位。另一个常见原因是中断优先级设置过低或过高。前面讲过能调用FreeRTOS API的中断优先级数值应该不低于configMAX_SYSCALL_INTERRUPT_PRIORITY如果优先级数值小于阈值比如配置成2FreeRTOS在切换任务时会进入临界区保护这些中断但实际上它无法正确遮蔽这种更高优先级的中断导致系统状态异常。整体上STM32项目都建议统一NVIC分组4同时把SysTick和PendSV配成最低优先级15。5.3 现象三高优先级任务把低优先级任务饿死核心代码里如果有一个高优先级任务在for循环里不断vTaskDelay(1)它虽然会释放CPU但因为每个Tick结束后它又是最高优先级就绪任务低优先级任务在没有时间片的情况下根本得不到执行。这就是典型的饥饿现象。解决方式有三种。如果高优先级任务只是在等待一个低频率事件应该用xQueueReceive、xSemaphoreTake配合portMAX_DELAY阻塞等待而不是每隔1个Tick轮询一次。如果确实需要周期性发心跳数据可以把延时拉长到低优先级可以容忍的范围。如果纯粹是偶尔长事务阻塞可以临时降低自己的优先级或者在等待条件满足时阻塞。用时间片轮转也能缓解同优先级任务之间的饿死。把configUSE_TIME_SLICING设为1后同优先级任务会按时间片轮转执行但仍是抢占式调度高优先级任务如果一直不阻塞低优先级任务依然没有机会。真正的饥饿是设计问题不是调度器能兜底的。5.4 现象四任务创建返回NULL内存不足xTaskCreate返回值不是pdPASS说明heap不够用。这种问题在加了新任务、新队列之后特别常见。排查时可以调大configTOTAL_HEAP_SIZE但更要搞清楚内存都去哪了。一个实用工具是调用vTaskList查看每个任务的状态和栈高水位但这个函数需要开启configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS而且会占用不少输出缓冲区。一个256字的栈大概是1024字节看似不大但项目里若有8个任务、3个队列再加上定时器任务和heap管理元数据16KB的heap很快就不够。我习惯在代码里启动后先创建一个看门狗任务周期打印当前heap剩余空间xPortGetFreeHeapSize和每个任务的栈高水位确认安全后再把任务栈调到刚好够用加一定余量的大小。5.5 现象五加了RTOS之后中断响应反而变慢如果只是想用RTOS管理任务调度对外部硬实时中断要求依然非常高比如PWM保护、编码器计数这类功能中断服务函数里的处理逻辑应该保持轻量任何需要调用API的地方都走FromISR后缀版本并且在中断返回时用portYIELD_FROM_ISR做必要的任务切换调度。有时候响应变慢不是因为中断函数本身而是因为某个任务持有了互斥锁且被更高优先级任务抢占导致另一个等待锁的低优先级任务错过了硬实时窗口。遇到这种场景我会写一份记录把关键中断的实际响应延迟用逻辑分析仪或者GPIO翻转测出来确认根因到底是任务切换还是锁竞争再去优化锁的持有时长必要时用关中断而不是互斥锁来保护极小段临界代码。5.6 改造期最有用的三条排查经验补一个属于“实践之后才明白”的小结不一定适合所有项目但至少救过我很多次。第一先不要重新设计业务先原样搬。首次添加RTOS最忌讳一边移植一边重构。先把模块函数原封不动放进新任务里只替换阻塞延时和中断入口确认任务调度正常后再去优化内部逻辑。这样能快速定位到底是内核问题还是业务问题。第二日志输出要尽早支持但不要让打印本身拖慢系统。建议在低优先级任务里维护一个环形缓冲日志区高优先级任务只负责写入专门的LogTask周期性把日志通过串口批量输出既不影响实时性又避免多个任务同时争夺串口。第三临界区的使用要像对待雷区一样谨慎。拿到旧代码看到EA相关的注释就顺手删掉是危险动作在RTOS里正确做法是判断这段代码是否会被其他任务或中断打断再决定用临界区、信号量还是队列。宁可多写一层保护也不要相信这里的变量只有裸机时代的主循环会碰。很多工程师上手RTOS时最担心的是内存和调试复杂度。实际上从一个结构良好的裸机工程出发给系统加上一个内核大概需要动的地方就是main、中断响应、全部HAL_Delay调用点这三类。驱动层、外设初始化、核心算法尽量不动这样即使出了问题也能够快速整体回滚。根据我的经验最好的做法是先挑一个最让系统卡顿的模块做第一个RTOS任务比如频繁阻塞的Flash写入或传感器读取然后在它旁边开一个用vTaskDelay管理的低优先级任务验证调度是否顺畅。一个项目只要第一个任务成功切换起来后面的改造就会顺理成章。当初那个朋友后来告诉我他只用了一个周末就把最头疼的那几个外设模块拆成了独立任务最重要的是他再也不用靠熬夜调那个偶尔卡死的while(1)循环了。
返回列表