ARTICLE DETAIL

资讯详情

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

STM32裸机升级FreeRTOS实战:从任务调度到内存管理完整指南

STM32裸机升级FreeRTOS实战:从任务调度到内存管理完整指南 做嵌入式开发的朋友基本都有过这样的经历裸机程序写了两三年点灯、按键、串口、传感器采集都能玩得转但一旦项目里要同时处理的任务超过四五个或者某个外设的中断一多主循环就开始变得又臭又长一个延时函数卡死全局逻辑改起来牵一发动全身。这时候你就该考虑上RTOS了而在STM32上最主流、资料最多的选择就是FreeRTOS配合HAL库几乎是当前入门实时操作系统最顺的一条路。这篇内容不是把官方文档翻译一遍而是我从零开始把FreeRTOS移植到HAL库工程、再从裸机思维切换到多任务思维的完整经历包含源码结构、内核机制、任务间通信、内存管理、堆栈溢出排查这几个绕不开的坎以及最后用一个带串口命令交互的双任务小系统做收尾。不管你是刚点亮过LED的新手还是想从裸机跳到RTOS但有顾虑的老手这篇都能让你少走不少弯路。1. 裸机写得好好的为什么还要上个操作系统先把最核心的思维转变说清楚裸机程序里的“并发”靠的是主循环轮询加大量的状态标志位而FreeRTOS里的“并发”靠的是任务的优先级抢占和时间片切换。同样是点两个灯裸机代码可能是这样的while (1) { HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); delay_ms(100); HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); delay_ms(300); }这段代码的问题一眼就能看出来LED2的翻转必须等LED1的延时结束两个任务之间是串行关系。如果这时候串口来了数据要处理按键要响应AD采样也要周期执行你就得用非阻塞的状态机把每个功能的延时拆成时间戳判断。拆个两个还行拆到七八个功能的时候代码可读性和维护成本会急剧下降。FreeRTOS解决的就是这个问题。你可以把每个独立的功能封装成一个任务每个任务有自己的堆栈、自己的优先级、自己的延时方式。LED1闪自己的LED2闪自己的串口任务收到数据立刻处理互不阻塞。在我刚开始用FreeRTOS的时候最大的感触不是功能实现了多少而是编写逻辑的心态变了。裸机编程时我每写一个函数都想会不会卡住其他功能RTOS编程时我只关心当前任务自己的逻辑心态轻松得多。不过这里也要泼一盆冷水不是所有项目都适合上RTOS。如果你的项目只有两三个实时性要求不高的功能裸机加状态机完全够用引入RTOS反而增加学习成本和调试复杂度。我的经验是当满足以下任意两条时才值得上FreeRTOS需要同时处理5个以上周期性的或事件触发的功能有多个实时性要求不同的任务比如按键要秒级响应而传感器采集可以慢一点有任务需要阻塞等待某件事发生比如等待串口一帧完整数据等待队列中有数据项目后续大概率要加功能希望代码在模块化方面更清爽关键词是“取舍”。RTOS不是银弹但确实是嵌入式项目从作坊式开发走向工程化开发的标志。2. 把FreeRTOS搬进HAL库工程从源码下载到第一颗任务跑起来2.1 源码获取与版本选择我用的是STM32CubeMX生成的HAL库工程然后把FreeRTOS的源码手动加进去这样能更清楚地知道每个文件是干什么的。FreeRTOS的源码可以从官网或者GitHub获取当前主流稳定版本是V10.x。下载后打开源码目录你会看到FreeRTOS文件夹下面有两个子目录Demo和Source。我们真正需要的核心代码都在Source里FreeRTOS/Source ├── include // 所有对外头文件包括tasks.h、queue.h、semphr.h ├── croutine.c // 协程实现一般用不到 ├── event_groups.c // 事件标志组任务间同步用 ├── list.c // 内核链表实现 ├── queue.c // 队列和信号量都基于此文件 ├── tasks.c // 任务管理核心 ├── timers.c // 软件定时器 └── portable // 硬件相关的移植层核心portable目录里放着针对不同编译器和芯片架构的移植代码这一层是FreeRTOS能跑在STM32上的关键。我用的编译器是ARM CompilerKeil环境下芯片是Cortex-M3/M4所以需要的是portable/RVDS/ARM_CM3/目录下的port.c和portmacro.hportable/MemMang/目录下的heap_x.c我选的heap_4.c如果在CubeMX里用IDE自带的FreeRTOS中间件你不需要手动管这些。但手动添加能让你理解源码结构后面排查问题更有底气。建议你还是花点时间手动移植一遍。2.2 我把源码放到了哪个目录我的工程结构大致是这样Project/ ├── Core/ // HAL库生成的main.c、stm32f1xx_it.c等 ├── Drivers/ // CMSIS和HAL库驱动 ├── FreeRTOS/ │ ├── include/ // 拷贝Source/include下的头文件 │ ├── src/ // 拷贝tasks.c、queue.c、list.c、timers.c │ └── portable/ // 拷贝ARM_CM3目录和MemMang目录 └── MDK-ARM/ // Keil工程文件然后在Keil里新建几个组FreeRTOS/include、FreeRTOS/src、FreeRTOS/portable把对应文件加进去。路径设置方面在C/C选项卡里的Include Paths里加上FreeRTOS/include和FreeRTOS/portable/RVDS/ARM_CM3不要忘了。还有两个关键头文件在Source/include里找不到FreeRTOSConfig.h和portmacro.h。portmacro.h在portable目录的ARM_CM3里会自动被引用。而FreeRTOSConfig.h需要自己建——网上有很多模板但最稳妥的办法是去Demo/CORTEX_STM32F103_Keil里拷贝现成的然后按需求裁剪。FreeRTOSConfig.h是整个FreeRTOS的“操作面板”里面定义了是否使能任务通知、队列、软件定时器、堆栈溢出检测等功能以及configTICK_RATE_HZ系统节拍频率、configMAX_PRIORITIES最大优先级数、configTOTAL_HEAP_SIZE堆大小这些全局参数。我的初始配置重点是这几个#define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 10 * 1024 ) ) #define configMAX_PRIORITIES ( 5 ) #define configUSE_IDLE_HOOK 0 #define configUSE_TICK_HOOK 0 #define configCHECK_FOR_STACK_OVERFLOW 2几个容易踩的坑这里先提前说configTICK_RATE_HZ设为1000意味着系统节拍是1ms一次。如果设为100那么任务延时的最小粒度是10msvTaskDelay(1)会延10ms这个特别容易忽略。configMINIMAL_STACK_SIZE以“字”为单位不是字节。Cortex-M3上1个字等于4字节所以128字等于512字节。任务栈大小传参的时候是字为单位这个单位问题能让新手懵好久。configTOTAL_HEAP_SIZE是FreeRTOS所有动态分配对象的可用总堆单位为字节。10KB对小项目够用但如果你创建一堆任务和队列可能不够后面我会细说。2.3 与HAL库的中断优先级配合在STM32上移植FreeRTOS优先级分组是第一个大坑。Cortex-M3/M4内核使用中断优先级且优先级数值越小优先级越高。FreeRTOS要求configMAX_SYSCALL_INTERRUPT_PRIORITY配置正确通常对应HAL库中的PRIORITYGROUP_4即全部4位都是抢占优先级不使用子优先级。为什么必须统一优先级分组因为FreeRTOS在任务切换时会关闭一部分中断也就是将BASEPRI寄存器设置为某个阈值只有优先级数值大于这个阈值即优先级更低的中断才允许触发。如果优先级分组设置不一致BASEPRI的判定就会出错导致系统崩溃或者中断响应异常。在main()里调用HAL_Init之后立刻设置优先级分组HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);同时FreeRTOSConfig.h里要加上#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系统节拍中断SysTick使用最低优先级15而能从中断服务函数中调用FreeRTOS API的中断优先级数值必须大于等于5。比如串口中断如果优先级设为0~4则中断里不能调用xQueueSendFromISR这类函数否则会破坏临界区保护。这个我在项目里踩过表现为随机死机排查了很久。2.4 中断服务函数里必须处理的“钩子”HAL库生成的工程里stm32f1xx_it.c中已经实现了SysTick_Handler等中断函数。用FreeRTOS之后SysTick由FreeRTOS接管HAL库的时间基准就不能再用SysTick了否则两者会打架。这里有两条路让HAL的时基改用另一个硬件定时器比如TIM6或TIM7。这在CubeMX里直接配置HAL_TIM_Base选一个基本定时器把HAL_TICK从SysTick切过去。如果在裸机工程上改更快的办法是保留SysTick给FreeRTOS然后把HAL_InitTick改成使用TIM6。我推荐在CubeMX里直接选好时基源省去后面的麻烦。如果你手写工程可以在HAL_InitTick()里把uwTickFreq相关的SysTick配置注释掉改成__HAL_RCC_TIM6_CLK_ENABLE(); HAL_NVIC_SetPriority(TIM6_IRQn, 5, 0); // 中断里不调用FreeRTOS API则随意但建议低于configMAX_SYSCALL_INTERRUPT_PRIORITY然后TIM6中断里调用HAL_IncTick()。SVC_Handler、PendSV_Handler这两个中断HAL库没有实现。在stm32f1xx_it.c里找到这两个函数如果是空函数就直接删掉或者注释掉因为它们需要由FreeRTOS的port.c提供真正的实现。如果这两个中断没有正确连接到port.c的函数任务调度器启动后会直接进HardFault这是移植失败最典型的表现。2.5 验证移植成功配置都处理完写一个最简单的双任务灯泡DEMO验证调度器能不能跑void TaskLED1(void *argument) { for (;;) { HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); vTaskDelay(pdMS_TO_TICKS(200)); } } void TaskLED2(void *argument) { for (;;) { HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); vTaskDelay(pdMS_TO_TICKS(500)); } }main.c里先初始化所有外设然后xTaskCreate(TaskLED1, LED1, 128, NULL, 1, NULL); xTaskCreate(TaskLED2, LED2, 128, NULL, 1, NULL); vTaskStartScheduler();vTaskStartScheduler()之后程序就不会返回了如果程序没有正常工作不要执行main后面的代码而应该检查优先级分组、PendSV和SysTick这几个点。我实测的结论LED1以200ms周期闪烁LED2以500ms周期闪烁两个任务互不阻塞这就算基本跑通了。3. 任务从创建到切换优先级、时间片、堆栈到底在发生什么3.1 tick与PendSV调度器的时间骨架任务切换听起来很玄底层其实就是两个中断在配合SysTick和PendSV。SysTick每1ms触发一次由configTICK_RATE_HZ决定在中断服务函数xPortSysTickHandler里FreeRTOS会更新系统时基计数并检查是否有比当前任务优先级更高的任务进入就绪态。比如一个延时到了它的优先级更高SysTick就把PendSV异常挂起。PendSV是可挂起的系统服务异常它不像普通中断那样触发后立即执行而是会等当前指令执行完、更高优先级的中断处理完才运行。FreeRTOS在PendSV里真正做了上下文切换把当前任务的寄存器、堆栈指针保存到它自己的任务控制块TCB然后从下一个要运行的任务的TCB恢复现场。这个机制的好处是如果有高优先级中断正在执行PendSV不会被嵌套进去打断它而是等中断处理完再切换保证了实时性。3.2 任务打开后的世界TCB与任务堆栈每个任务被创建时FreeRTOS都会给它分配一个任务控制块TCB以及一份私有的任务堆栈。TCB里记录的信息包括任务的优先级、状态、堆栈起始地址、当前堆栈指针、任务名称等。任务堆栈的意义通俗讲就是给每个任务一个“个人草稿本”。裸机程序只有一个运行栈你调A函数、B函数返回地址和局部变量都压在同一个栈里。FreeRTOS则是在任务切换时把CPU的当前现场R0~R12、LR、PC、xPSR等寄存器压入当前任务自己的栈然后把下一个任务的现场弹出来。这就是各个任务看起来能同时运行的核心原因。任务的栈深度取决于函数调用的嵌套深度、局部变量大小、中断嵌套深度这在后面会直接影响稳定性。3.3 优先级抢占和时间片轮转什么时候谁运行FreeRTOS的调度规则可以总结成两句话同一时刻只有最高优先级的就绪任务在跑。如果多个任务优先级相同则按时间片轮流跑每个时间片长度是一个tick。举个例子三个任务优先级分别是2、2、1。优先级为2的两个任务会分时使用CPU各占一个时间片优先级为1的任务在两个高优先级任务都阻塞比如延时、等队列的时候才有机会跑。优先级抢占的发生时机有条件SysTick中断里检查到了更高优先级的任务就绪或者从ISR退出时低优先级任务被更高优先级任务抢占。很多新手纠结的“我的高优先级任务是不是会无限霸占CPU”答案是只要高优先级任务一直处于就绪状态低优先级任务确实永远没机会跑。这是RTOS的公平性问题不是bug。解决办法是让高优先级任务用vTaskDelay或等待事件主动让出CPU或者用时间片而不要设计成一个while(1)空转。3.4 一个常见误解优先级越高中断响应越快CPU中断优先级是硬件层的FreeRTOS任务优先级是软件层的两者没有直接换算关系。任务优先级决定的是“这个任务何时获得CPU的使用权”而中断优先级决定的是“中断何时打断CPU”。一个中断即便优先级很低数值大它在触发后依然会打断当前运行的任务——除非该中断的优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY且当前在临界区中。搞清楚这个区别就不会在设计任务优先级时一头雾水了。任务间的实时性需求需要用任务优先级来体现外设的实时性需求用中断优先级来体现互不干扰但两者结合的方式决定了系统整体的实时性。4. 任务间通信三板斧队列、信号量、互斥量创建几个任务只是第一步。真正复杂的是任务之间的数据交换和同步。FreeRTOS提供的核心IPC机制我用一套串口转WiFi模块的收发逻辑来举例说明。4.1 队列任务之间搬数据的“邮局”队列的本质是一个环形缓冲区生产者任务往里放数据消费者任务从里取数据。队列最明显的优势是自带阻塞机制队列满了生产者可以选择等一会儿再放队列空了消费者也可以选择等一会儿再取。这替代了裸机编程里大量的“标志位加轮询”。我的串口接收任务收到一帧数据后解析出关键命令然后通过队列发给LED控制任务// 创建队列每个元素64字节最多5个 xCommandQueue xQueueCreate(5, sizeof(CommandMsg)); // 发送任务 CommandMsg cmd {.id 0x01, .len 2, .data {0xAA, 0x55}}; if (xQueueSend(xCommandQueue, cmd, pdMS_TO_TICKS(100)) ! pdPASS) { // 队列在100ms内没空间处理超时 } // 接收任务 CommandMsg rxCmd; if (xQueueReceive(xCommandQueue, rxCmd, portMAX_DELAY) pdPASS) { ProcessCommand(rxCmd); }这里几个细节值得注意portMAX_DELAY作为阻塞时间表示无限等待。只有确保队列迟早会有数据才用否则任务会永久挂起。队列元素大小在创建时固定元素太大超过几百字节建议传结构体指针避免拷贝耗时太长。队列也可以在中断中发送但中断版本函数是xQueueSendFromISR并且不能阻塞。实际测试中如果在1000Hz的系统节拍下队列的阻塞等待粒度是1ms这个精度对于大部分命令交互场景绰绰有余。4.2 二值信号量与计数信号量干一件事还是攒N件事很多人分不清二值信号量和队列的区别。二值信号量的典型场景是“唤醒”也就是让一个任务从阻塞中醒过来但不需要传递数据本身。我用二值信号量来同步按键扫描任务和主控任务按键按下的中断置位信号量主控任务阻塞在信号量上只负责在被唤醒后执行“开机”或“关机”动作。从机制上讲二值信号量的xSemaphoreGive和xSemaphoreTake这对操作和队列消息的发送接收很相似但它只关心“是/否”不关心内容。而计数信号量增加了一个“计数”的概念适合表示“还有多少个任务待处理”。我推荐一个选择思路需要传递具体数据用队列。只做事件通知不关心事件内容用二值信号量。事件会发生多次比如DMA半传输和全传输中断交替到来需要记录次数用计数信号量。4.3 互斥量与优先级继承互斥量是二值信号量的特化版本额外附加了优先级继承机制。这是RTOS里解决优先级翻转问题的关键工具。优先级翻转是个经典问题低优先级任务持有互斥量高优先级任务等待互斥量此时中等优先级任务抢占了CPU导致高优先级任务一直无法执行。这在汽车、工控等领域可能是致命问题。优先级继承的解决思路是当高优先级任务等待一个被低优先级任务持有的互斥量时系统临时把低优先级任务的优先级提升到高优先级任务的级别让它尽快运行、释放互斥量之后再降回去。这样中等优先级任务就没法插入进来有效缩短了高优先级任务的阻塞时间。互斥量必须在任务上下文获取/释放不能在中断中使用。中断里要保护资源应该用二值信号量或者直接关中断加临界区。一个典型用例是多个任务都要往串口打印日志如果不加互斥量就会出现一条日志被另一个任务打断、混在一块的情况void Log_SafePrint(const char *str) { xSemaphoreTake(printMutex, portMAX_DELAY); HAL_UART_Transmit(huart1, (uint8_t *)str, strlen(str), 1000); xSemaphoreGive(printMutex); }有了互斥量保护任何时刻只有一个任务能占用串口打印完整信息日志不再乱序。5. 内存管理heap_4.c为什么不一定是万能解堆栈溢出怎么查5.1 五种heap实现怎么选FreeRTOS在portable/MemMang目录下提供了heap_1.c到heap_5.c五种内存分配实现。我一开始图省事直接用heap_4.c后来发现不同场景要选不同的heap。实现支持释放碎片处理适用场景heap_1不支持无只创建任务和队列从不删除最简单最安全heap_2支持不合并相邻空闲块频繁创建/删除固定大小的对象heap_3支持由编译器malloc/free实现依赖C库线程安全由FreeRTOS保证heap_4支持合并相邻块低碎片通用场景强烈推荐heap_5支持合并多段堆低碎片内存分布于多个不连续RAM区域对于大部分STM32小项目heap_4是最稳的选择。它把空闲块地址相邻时合并成一个更大的空闲块长期跑删除创建任务、队列的操作也不会出现严重的碎片化。但是heap_4也不是万能的。它的分配策略是首次适应如果有大量大小不一的对象频繁创建删除还是可能出现无法分配到大块的场景。另外heap_4的堆大小由configTOTAL_HEAP_SIZE统一控制如果任务栈分配得过大堆不够xTaskCreate会返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。我习惯在调试阶段把configTOTAL_HEAP_SIZE设得稍大一点跑一段时间后用xPortGetFreeHeapSize()监控剩余堆再根据实测值调整而不是拍脑袋定大小。5.2 堆栈溢出检测configCHECK_FOR_STACK_OVERFLOW 2任务堆栈是最容易出问题的内存区域。函数调用层次太深、局部数组太大都会导致栈溢出。FreeRTOS提供了两种检测机制通过configCHECK_FOR_STACK_OVERFLOW配置设为1在任务切换时检查栈指针是否越界开销小但只能检测当前任务。设为2额外在任务创建时在栈顶和栈底填入特殊标记切换时检查标记是否被覆盖检测更全面。开启检测后如果栈溢出FreeRTOS会调用vApplicationStackOverflowHook你可以在这里打日志或者点亮错误LEDvoid vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { // 进入这里说明任务栈已爆 printf(Stack overflow in %s\r\n, pcTaskName); // 可以稍微停顿方便调试 for (;;); }实测中最难排查的栈溢出问题往往是局部变量。我遇到过一个大数组放在局部变量里导致任务栈瞬间被击穿损坏了相邻任务的TCB表现是另一个任务随机瘫痪。后来把大数组改成static或malloc问题立刻消失。大空间尽量静态分配是RTOS调试的重要经验。5.3 优先级翻转和中断/任务API混用还有哪些坑这一部分的内容很零散但全是实践汇总。关于优先级翻转上面已经讲了互斥量。需要补充的是你还需要开启configUSE_MUTEXES在FreeRTOSConfig.h里定义才能使用互斥量这是很多人容易漏掉的。中断服务函数里使用API的问题。FreeRTOS的API分普通版和FromISR版例如xQueueSend与xQueueSendFromISR、xSemaphoreGive与xSemaphoreGiveFromISR。中断里用普通版会因阻塞机制试图切换上下文出错导致HardFault。工作习惯上我写代码时如果函数可能被中断调用会特意在注释里标明然后用FromISR版。vTaskDelay和vTaskDelayUntil的区别。vTaskDelay(n)表示阻塞n个tick但这个时间是从当前时刻开始的所以如果你在每个循环里调用它做周期任务实际周期会略微大于n个tick因为还要算上任务执行时间和调度延时。需要严格等间隔采样时要用vTaskDelayUntil它基于绝对时间计算延迟能确保任务的执行周期恒定。比如让网口状态LED以精确的时间间隔切换TickType_t xLastWakeTime xTaskGetTickCount(); const TickType_t xFrequency pdMS_TO_TICKS(50); for (;;) { HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin); vTaskDelayUntil(xLastWakeTime, xFrequency); }软件定时器也是常见的稳定性隐患。FreeRTOS的软件定时器回调运行在定时器服务任务configTIMER_TASK_NAME中而不是在调用者任务栈中。所以定时器回调里不能做耗时操作否则会影响其他定时器的执行。很多人的代码在定时器回调里调用延时函数导致定时器服务任务卡住然后整个定时器机制停摆。6. 一个能跑起来的综合实例带串口命令交互的温湿度采集双任务系统写到这原理和坑都讲得差不多了但很多人看完还是不知道实际工程该怎么搭。我把自己做的一个具体小项目拆解出来这个例子涵盖了任务创建、队列、软件定时器、二值信号量很适合作为参考。6.1 项目需求和任务划分需求有一个DHT11温湿度传感器一个OLED屏一个串口。串口每收到命令readtemp就返回当前温湿度OLED每2秒刷新一次显示温湿度同时板载LED每1秒翻转一次表示系统运行。任务划分如下任务优先级功能说明传感器采集任务2周期1s读DHT11把数据写入队列DHT11时序敏感单独占一个任务阻塞在vTaskDelay上显示刷新任务1接收队列里的数据刷新OLEDOLED操作耗时较长使用低优先级串口命令任务3阻塞在串口接收队列上解析命令命令响应实时性要求最高给最高优先级状态LED任务0每1s翻转LED后台保活任务最低优先级DHT11单总线读取有个特点时序驱动期间不能被其他任务抢占否则时序错乱。所以在DHT11的位采样阶段我用了一点临界区保护taskENTER_CRITICAL(); // 读取DHT11的40bit数据每个电平状态用循环计数测量 taskEXIT_CRITICAL();这里用临界区而不是直接在整个读取过程中禁止任务切换是为了尽量缩短关临界区的时间避免影响系统实时性。实测在72MHz主频下DHT11整帧40bit读取大约20ms如果全程关中断又比较危险所以我把短采样循环放入临界区中间允许其他中断触发降低了对系统响应的影响。6.2 核心代码骨架串口接收中断收到完整一行命令后将数据通过队列发送给命令解析任务void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart huart1) { uint8_t cmd rxBuffer[0]; xQueueSendFromISR(rxQueue, cmd, NULL); HAL_UART_Receive_IT(huart1, rxBuffer, 1); } }命令解析任务void CommandTask(void *argument) { uint8_t cmd; char reply[64]; for (;;) { if (xQueueReceive(rxQueue, cmd, portMAX_DELAY) pdPASS) { if (cmd t) { snprintf(reply, sizeof(reply), temperature: %d.%d C\r\n, tempInt, tempDec); HAL_UART_Transmit(huart1, (uint8_t *)reply, strlen(reply), 100); } } } }DHT11采集任务把温湿度包成一个结构体通过队列发给OLED任务typedef struct { int16_t temperature; int16_t humidity; } EnvData;代码逻辑很简单关键是任务优先级和阻塞方式的配合传感器任务和串口命令任务各自等各自的队列显示任务只在收到数据时刷新谁也不会阻塞谁。6.3 实测过程中的几个印象这个系统跑了一个多月我记录到几个不容易在文档里看到的现象OLED刷新会偶尔闪烁。因为OLED刷新时如果来了串口中断显示任务会被打断I2C通信时序有时候会受影响。后来在刷新OLED时我用互斥量把I2C总线锁住并在挂载OLED的I2C设备任务里做了重发机制闪烁问题基本消失。DHT11即使有临界区保护偶尔会读到错误数据。排查后发现是DHT11的读取间隔要求大于1秒我在任务里设置延时950ms等于在临界区外或任务切换期间又发起了读取。把延时改成1100ms之后读数稳定。串口命令偶尔丢失。原因是接收中断把单字节放入队列而我的命令是按行分割的单字节队列没有“行”的概念。我改成用DMA接收不定长数据在空闲中断中判断一帧完成再整帧入队才彻底解决。这个例子告诉你RTOS不是写完任务调度就万事大吉外设的使用方式、临界区的长短、延时的精度都会影响整个系统最终的稳定性。调试RTOS系统比裸机系统更需要系统性思维。7. 关于FreeRTOS和HAL库搭配使用的最后一些复盘跟FreeRTOS和HAL库打了这么多年交道回头总结一下最基础也最容易被忽略的还是“是否清楚内核在做什么”这件事。很多人用CubeMX一键生成FreeRTOS工程很容易跑起来也很顺利但一旦遇到系统卡死就不明所以。如果你想深入排查问题我的建议是花点时间读一下tasks.c里的vTaskSwitchContext、port.c里的xPortPendSVHandler搞明白任务切换的完整流程比背任何面试题都有用。在实际项目里我一般会遵守这几条习惯这也算是长期实践下来的“军规”硬件初始化全部做在vTaskStartScheduler()之前不要在任务里重复初始化外设。每个任务只做一件事做得越纯粹越好降低相互干涉的概率。任何队列、信号量的创建在一开始集中完成并检查返回值。堆栈大小先给足调稳定后再逐步缩小反复验证栈溢出检测是否触发。不要在ISR里做延时不要试图在ISR里通过排队顶替任务逻辑ISR只负责唤醒任务。还有个小事很多人问我用不用CubeMX的FreeRTOS中间件。我的看法是新手学习阶段自己手动移植一次源码能帮你建立完整的概念框架做正式项目时用CubeMX生成是最高效的它的配置和错误处理已经做得很好。两条路并不矛盾可以都走一遍。文章最后再分享一个调试小技巧。我以前排查系统随机死机都是靠点灯。后来养成了一个习惯在工程里加一个心跳任务优先级设为最高每100ms翻转一个GPIO并协议里串口打印tick计数。任何情况下只要发现心跳异常第一时间就能判断是任务调度卡死还是某个任务在跑飞。这个几行代码的成本能省下大量的定位时间。FreeRTOS只是工具理解它的思维方式和运行机制才是真正能写好多任务嵌入式系统的关键。希望这篇内容能帮你迈过从裸机到RTOS的那道坎。
返回列表