拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

FreeRTOS嵌入式实战:从裸机迁移到多任务调度与STM32CubeMX配置

FreeRTOS嵌入式实战:从裸机迁移到多任务调度与STM32CubeMX配置

1. 为什么我要开一个 FreeRTOS 专栏

搞嵌入式这行的朋友,尤其是玩 STM32、GD32 这类 MCU 的,迟早会撞上一个坎:裸机跑着跑着,代码就成了一锅粥。主循环里塞满了各种if-else和延时,按键响应迟钝,串口数据偶尔丢包,几个任务互相打架,改一处崩三处。我早期做工业数据采集项目时就吃过这个亏,一个 4 路串口 + 1 路以太网 + 1 路 SD 卡存储的板子,裸机轮询架构下,SD 卡写入稍微慢一点,串口就丢数据,客户现场调试被折腾得够呛。后来咬牙上了 FreeRTOS,把数据采集、协议解析、存储、通信拆成独立任务,优先级一排序,问题迎刃而解。从那以后,FreeRTOS 就成了我做 MCU 项目的默认选项。

这个专栏我打算系统性地把 FreeRTOS 从入门到项目实战的路径梳理一遍。不是照本宣科翻译官方文档,而是把我这些年踩过的坑、调过的参数、验证过的方案,原原本本分享出来。内容会覆盖 FreeRTOS 的核心机制、STM32CubeMX 图形化配置、任务划分与优先级设计、堆栈溢出检测、内存管理策略、以及和 LVGL、FatFS、W25Q64 这类中间件的整合。适合刚接触 RTOS 的嵌入式新手,也适合已经用过但想深入理解调度原理和避坑技巧的老手。你不需要有多深的操作系统理论基础,只要会写 C 语言、用过 STM32 或者 GD32 的 HAL 库,就能跟着走下来。

2. FreeRTOS 到底解决了什么问题

2.1 裸机开发的三个死结

先说说为什么裸机架构在复杂项目里会力不从心。第一个死结是实时性无法保证。裸机主循环里,如果你在某个环节用了HAL_Delay(100),那这 100ms 内整个系统什么都干不了,按键按下去没反应,串口数据来了只能靠中断收着,但处理逻辑还得等主循环轮询到。第二个死结是代码耦合严重。所有功能模块都挂在while(1)里,模块之间通过全局变量传递状态,改一个模块的逻辑,很可能影响到另一个模块的时序。第三个死结是资源冲突难以管理。多个模块共用 SPI 总线、共用串口,裸机下你得手动加标志位、加状态机,稍不注意就出现总线冲突或者数据覆盖。

FreeRTOS 的核心价值就在于把这些死结一个个解开。它通过任务调度器让多个任务“看起来同时运行”,每个任务有自己的栈空间和上下文,互不干扰。通过优先级抢占保证高优先级任务(比如紧急中断处理、实时控制)能立刻得到 CPU。通过信号量、队列、互斥量这些 IPC 机制,让任务之间的数据传递和资源共享变得规范可控。说白了,FreeRTOS 把裸机时代你手动管理的那些“什么时候该干什么”的调度逻辑,变成了一个可配置、可预测的框架。

2.2 任务调度:从“轮询”到“抢占”

FreeRTOS 默认采用抢占式调度。什么意思呢?假设系统里有三个任务:Task_A 优先级 3,Task_B 优先级 2,Task_C 优先级 1。当 Task_C 正在运行,突然 Task_A 就绪了(比如它等待的信号量到了),调度器会立刻保存 Task_C 的上下文,切换到 Task_A 执行。Task_A 执行完阻塞了,再回到 Task_C 继续。这种机制保证了高优先级任务的响应时间是可预测的,通常在微秒级别。

和抢占式相对的是时间片轮转。当多个任务优先级相同时,FreeRTOS 默认开启时间片轮转,每个任务运行一个 tick(通常 1ms)后切换到下一个同优先级任务。这个机制在需要“公平分配”CPU 的场景下很有用,比如多个相同优先级的通信任务。

我个人的经验是:任务优先级不要设太多层。很多新手喜欢把优先级从 1 排到 10,结果调试时根本理不清谁抢了谁。我的习惯是最多分三层——高优先级给实时控制和安全监控,中优先级给数据处理和协议解析,低优先级给显示刷新、日志存储这类“不急”的活。层数少,逻辑清晰,排查问题也容易。

2.3 内存管理: heap_1 到 heap_5 怎么选

FreeRTOS 提供了五种堆管理方案,从heap_1到heap_5,很多人配置的时候直接默认heap_4,但其实不同方案适合不同场景。

方案特点适用场景缺点
heap_1只分配不释放任务创建后不再删除无法回收内存
heap_2可释放但不合并已废弃,不推荐碎片严重
heap_3封装 malloc/free需要标准库支持线程安全性依赖库实现
heap_4可释放且合并相邻块通用场景,最常用仍有碎片风险
heap_5支持多块不连续内存外扩 RAM、多区域堆配置稍复杂

我大多数项目用heap_4,因为它支持内存释放和相邻空闲块合并,能有效延缓碎片化。但如果你的板子外扩了 SRAM 或者用了 GD32F303 这种带 CCM RAM 的芯片,heap_5就更合适,可以把不同物理地址的内存区域都纳入堆管理。选heap_1的场景也有,比如一些超低功耗的传感器节点,任务创建后永远不删,用heap_1反而更省 RAM,因为不需要维护空闲链表。

注意:不管选哪种方案,configTOTAL_HEAP_SIZE一定要根据实际任务数量和栈需求仔细估算。我见过太多项目因为堆给太小,创建任务时xTaskCreate返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,查半天查不出原因。

3. STM32CubeMX 配置 FreeRTOS 的完整流程

3.1 从零开始:CubeMX 工程创建与时钟配置

我用 STM32F407 做演示,这颗芯片资源够,跑 FreeRTOS 加 LVGL 加 FatFS 都没问题。打开 CubeMX,新建工程选 STM32F407ZGT6,第一步先把时钟树配好。外部晶振 8MHz,PLL 倍频到 168MHz,AHB 不分频,APB1 四分频(42MHz),APB2 二分频(84MHz)。这个配置是 F407 的经典跑法,稳定且性能足够。

时钟配好后,进入Middleware选项卡,找到FREERTOS,把Interface从Disabled改成CMSIS_V1或者CMSIS_V2。这里有个选择:CMSIS_V1 对应 FreeRTOS 的旧版 API,CMSIS_V2 对应新版。我建议选CMSIS_V2,因为它是 ARM 官方维护的标准化接口,和 Keil、IAR 的集成更好,而且后续如果要换 RT-Thread 或者 Azure RTOS,API 风格更接近,迁移成本低。

3.2 任务创建:参数怎么填才合理

在Tasks and Queues标签页里,CubeMX 默认给你一个defaultTask。你可以直接改它的名字、优先级、栈大小,也可以点Add加新任务。这里每个参数都有讲究:

  • Task Name:见名知义,比如Task_DataAcq、Task_Comm、Task_Display。
  • Priority:osPriorityLow到osPriorityRealtime,我一般用osPriorityAboveNormal给通信任务,osPriorityNormal给数据处理,osPriorityBelowNormal给显示。
  • Stack Size:单位是字(word),不是字节。STM32 是 32 位机,所以 128 字的栈等于 512 字节。新手最容易在这里翻车,栈给太小,任务跑着跑着就 HardFault。我的经验是:简单任务至少 128 字,带浮点运算或者调用printf的任务至少 256 字,跑 LVGL 的任务至少 512 字。
  • Allocation:选Dynamic就用heap_4动态分配,选Static就静态分配。我一般用Dynamic,方便调试阶段调整。

创建完任务后,CubeMX 会自动生成freertos.c文件,里面帮你把StartDefaultTask的框架搭好了。你只需要在对应的任务函数里写自己的逻辑。

3.3 堆栈溢出检测:别等死机了才后悔

configCHECK_FOR_STACK_OVERFLOW这个宏一定要开。CubeMX 里在Config parameters标签页找到CHECK_FOR_STACK_OVERFLOW,设成Option2。Option1 只检查栈指针是否越界,速度快但漏检率高;Option2 会在任务切换时往栈顶写特定标记,检查标记是否被覆盖,更可靠但稍慢。我实测下来,Option2 在 168MHz 的 F407 上额外开销不到 1%,完全值得。

开了检测之后,你还需要实现vApplicationStackOverflowHook函数。CubeMX 生成的代码里默认是弱定义,你可以在freertos.c里重写它:

void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { printf("Stack overflow in task: %s\r\n", pcTaskName); taskDISABLE_INTERRUPTS(); for(;;); }

这样一旦某个任务栈溢出,串口会打印出任务名,你立刻就知道是哪个任务出了问题。我当年调一个 Modbus 从站任务,栈溢出后系统直接死机,没有任何提示,查了两天才发现是栈给少了。自从开了这个钩子函数,类似问题五分钟定位。

4. 任务划分与优先级设计的实战经验

4.1 按“时间约束”划分任务,而不是按“功能模块”

很多教程教人按功能划分任务:串口一个任务、SPI 一个任务、ADC 一个任务。这种分法在简单项目里没问题,但在复杂项目里会导致任务数量爆炸,而且优先级很难排。我的做法是按时间约束划分:把所有“必须在 1ms 内响应”的逻辑放进一个高优先级任务,把所有“可以容忍 10ms 延迟”的逻辑放进中优先级任务,把所有“慢一点无所谓”的逻辑放进低优先级任务。

举个例子,我之前做的一个电机控制项目,需求是这样的:电流环必须 100us 响应一次,速度环 1ms 一次,位置环 10ms 一次,上位机通信 50ms 一次,OLED 显示 100ms 一次。如果按功能分,你得建五个任务;但按时间约束分,电流环和速度环可以合并成一个高优先级任务(因为电流环频率更高,速度环在里面分频执行),位置环单独一个中优先级任务,通信和显示合并成一个低优先级任务。这样只有三个任务,优先级清晰,调度开销也小。

4.2 优先级反转与互斥量的正确用法

优先级反转是 RTOS 里最隐蔽的坑之一。场景是这样的:低优先级任务 A 拿到了互斥量,正在访问共享资源;中优先级任务 B 就绪了,抢占了 A;高优先级任务 C 也就绪了,但 C 需要那个互斥量,只能等 A 释放,而 A 又被 B 抢着跑不了。结果就是高优先级的 C 被中优先级的 B 间接阻塞了。

FreeRTOS 的互斥量(xSemaphoreCreateMutex)自带优先级继承机制:当 C 等待 A 持有的互斥量时,A 的优先级会被临时提升到 C 的级别,这样 B 就抢不过 A 了,A 能尽快执行完释放互斥量。但注意,二值信号量没有优先级继承,所以保护共享资源一定要用互斥量,不要用二值信号量。

实操心得:互斥量的获取和释放一定要成对出现,而且尽量让临界区短小。我见过有人在互斥量保护下做HAL_Delay,这等于把整个系统的实时性都毁了。临界区里只放必要的寄存器操作或内存拷贝,耗时操作放到外面。

4.3 队列:任务间通信的首选

任务之间传递数据,最规范的方式是队列。比如串口中断收到一帧数据,不要直接在中断里解析,而是把数据丢进队列,让一个专门的任务去取出来处理。这样中断服务程序(ISR)执行时间极短,不会阻塞其他中断。

队列的使用有个细节:xQueueSendFromISR和xQueueSend的区别。在 ISR 里必须用带FromISR后缀的版本,而且要用pxHigherPriorityTaskWoken参数来判断是否需要触发任务切换。很多新手在 ISR 里用了xQueueSend,结果系统随机死机,查半天查不出来。这个坑我踩过,后来养成习惯:只要在 ISR 里,所有 FreeRTOS API 都带FromISR。

队列长度也要合理设置。太短了,生产者发不进去会丢数据;太长了,浪费 RAM。我的估算方法是:队列长度 = 最大突发数据量 / 单条数据大小 + 2 的余量。比如串口波特率 115200,一帧 128 字节,最坏情况下连续来 3 帧,那队列长度至少设 5。

5. FreeRTOS 与中间件整合的实战案例

5.1 FreeRTOS + LVGL:显示刷新与任务调度

LVGL 是一个对实时性要求不高的中间件,但它有个特点:lv_task_handler需要周期性调用,而且内部有时序依赖。如果把它放在低优先级任务里,被高优先级任务频繁抢占,可能会导致显示刷新不流畅。我的做法是给 LVGL 单独一个任务,优先级设为osPriorityLow,然后在任务里用vTaskDelay控制刷新周期。

void Task_LVGL(void *argument) { lv_init(); lv_port_disp_init(); lv_port_indev_init(); for(;;) { lv_task_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } }

5ms 的刷新周期对应 200fps,实际 LVGL 处理不了这么快,但lv_task_handler内部会根据lv_tick_get判断是否有任务需要执行,没有的话立刻返回,不会浪费 CPU。关键是lv_tick_inc要在 SysTick 中断里调用,或者用一个高优先级定时器任务来喂 tick。我一般直接在SysTick_Handler里调lv_tick_inc(1),简单可靠。

5.2 FreeRTOS + FatFS + W25Q64:SPI 总线共享

W25Q64 是常见的 SPI Flash,FatFS 是文件系统。这两个和 FreeRTOS 整合时,最大的问题是SPI 总线共享。如果你的系统里还有别的 SPI 设备(比如 TFT 屏、无线模块),那必须用互斥量保护 SPI 总线。

我的做法是创建一个全局互斥量xSPIMutex,所有要访问 SPI 的任务在操作前先xSemaphoreTake,操作完xSemaphoreGive。FatFS 的磁盘 IO 层函数(disk_read、disk_write)里也要加这个互斥量,因为 FatFS 可能在任意任务上下文里被调用。

DSTATUS disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if(xSemaphoreTake(xSPIMutex, pdMS_TO_TICKS(1000)) != pdTRUE) return RES_ERROR; // W25Q64 页编程操作 W25Q64_WritePage(sector, buff, count); xSemaphoreGive(xSPIMutex); return RES_OK; }

这里有个坑:pdMS_TO_TICKS(1000)是等待超时,如果某个任务持有互斥量后卡死了,其他任务等 1 秒后会返回错误,而不是永久阻塞。这个超时机制在调试阶段特别有用,能防止一个任务的 bug 拖垮整个系统。

5.3 GD32F303 移植 FreeRTOS 的注意事项

GD32F303 和 STM32F103 是 Pin-to-Pin 兼容的,但 FreeRTOS 移植时有个细节要注意:SysTick 中断优先级。GD32 的中断优先级分组和 STM32 略有不同,CubeMX 生成的代码默认用HAL_InitTick配置 SysTick,但 FreeRTOS 需要 SysTick 优先级最低(数值最大),否则portYIELD触发 PendSV 时可能被其他中断阻塞。

我的做法是在main.c里HAL_Init()之后、osKernelStart()之前,手动设置 SysTick 优先级:

HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0);

15 是最低优先级,确保 PendSV 和 SysTick 不会抢占其他中断。这个细节在 STM32 上通常没问题,但 GD32 的库函数默认值可能不一样,移植时一定要检查。

6. 常见问题与排查技巧实录

6.1 系统启动就 HardFault 怎么办

这是新手最常遇到的问题。FreeRTOS 启动后立刻 HardFault,原因通常有三个:堆太小、栈太小、中断优先级配置错误。

排查步骤:第一步,检查configTOTAL_HEAP_SIZE,如果创建了多个任务,每个任务栈 256 字,那堆至少给 4KB 以上。第二步,检查configMINIMAL_STACK_SIZE,这个值不能太小,我一般设 128 字。第三步,检查configMAX_SYSCALL_INTERRUPT_PRIORITY,这个宏决定了哪些中断可以调用 FreeRTOS API。如果某个中断的优先级高于这个值,但在 ISR 里调了xQueueSendFromISR,就会触发断言或者 HardFault。

速查表:HardFault 常见原因

现象可能原因排查方法
启动即 HardFault堆/栈不足增大 heap 和 stack
运行一段时间后 HardFault栈溢出开启栈溢出检测
中断里调用 API 后 HardFault中断优先级错误检查 NVIC 优先级分组
任务切换时 HardFaultPendSV 优先级不对设 SysTick 和 PendSV 为最低优先级

6.2 任务卡死但系统没死机

这种情况通常是任务在等待一个永远不会到来的信号量或队列。比如任务 A 等队列,但任务 B 因为某个条件没满足,一直没往队列里发数据。系统其他任务还在跑,所以看门狗没复位,但功能已经异常了。

我的排查方法是加任务运行状态监控。创建一个低优先级的监控任务,定期打印每个任务的状态(Running、Ready、Blocked、Suspended)。FreeRTOS 提供了vTaskGetInfo和uxTaskGetSystemState这两个 API,可以获取任务状态。配合串口打印,一眼就能看出哪个任务卡在 Blocked 状态。

void Task_Monitor(void *argument) { TaskStatus_t taskStatus[10]; UBaseType_t taskCount; for(;;) { taskCount = uxTaskGetSystemState(taskStatus, 10, NULL); for(int i = 0; i < taskCount; i++) { printf("Task: %s, State: %d, Prio: %d\r\n", taskStatus[i].pcTaskName, taskStatus[i].eCurrentState, taskStatus[i].uxCurrentPriority); } vTaskDelay(pdMS_TO_TICKS(5000)); } }

6.3 队列数据丢失的排查思路

队列丢数据通常是因为生产者速度大于消费者速度,队列满了之后xQueueSend返回errQUEUE_FULL,但代码里没处理这个返回值。我的习惯是:所有xQueueSend的返回值都要检查,如果失败,要么重试,要么记录错误计数。

另一个原因是在 ISR 里用了错误的 API。前面说过,ISR 里必须用FromISR版本。如果你在 ISR 里用了xQueueSend,它可能会阻塞,而 ISR 里是不允许阻塞的,结果就是数据丢失或者系统异常。

还有一个隐蔽的原因:队列项大小不对。xQueueCreate的第二个参数是队列项的大小(字节),如果你传的是sizeof(pointer)而不是sizeof(struct),那队列里存的就是指针而不是数据本身,指针指向的内容可能已经被覆盖了。这个坑我在早期项目中踩过,后来养成习惯:队列里直接存结构体,不用指针。

6.4 优先级设置不当导致的“饥饿”问题

如果高优先级任务一直就绪,低优先级任务永远得不到执行,这就是任务饥饿。比如一个高优先级任务里写了while(1)且没有阻塞操作,那整个系统就它一个人在跑,其他任务全部饿死。

FreeRTOS 的调度器不会自动解决饥饿问题,需要你在设计时保证每个任务都有阻塞点。我的原则是:任何任务都不能在while(1)里空转,必须至少有一个vTaskDelay或者等待信号量/队列的操作。哪怕是最高优先级的任务,也要在完成一轮处理后主动让出 CPU。

实操心得:我习惯在任务末尾加一个taskYIELD()或者vTaskDelay(1),哪怕逻辑上不需要延时,也强制让出一次 CPU。这样能避免因为某个任务的 bug 导致整个系统卡死,给调试留出空间。

7. 从裸机到 FreeRTOS 的迁移策略

7.1 不要一次性全部重构

很多朋友一上来就把整个裸机工程推倒重来,结果各种问题集中爆发,调试周期拉得很长。我的建议是渐进式迁移:先保留裸机的主循环,把最耗时的模块(比如 SD 卡写入、网络通信)单独拎出来做成任务,其他模块继续在裸机里跑。等新任务稳定了,再迁移下一个模块。

具体做法是:在main函数里先调用osKernelInitialize()和osKernelStart(),但osKernelStart不会返回,所以裸机代码要放在osKernelStart之前,或者放在一个最低优先级的任务里。我通常把裸机主循环改成一个Task_Legacy,优先级设为最低,这样新任务能正常调度,旧代码也能继续跑。

7.2 全局变量的处理

裸机时代大量使用全局变量传递状态,迁移到 FreeRTOS 后,这些全局变量如果被多个任务访问,就成了竞态条件的源头。我的处理原则是:能改成队列的改成队列,不能改的加互斥量保护。

比如一个全局的uint8_t g_uart_rx_buf[128],裸机下主循环直接读,迁移后如果串口任务和解析任务都访问它,就必须加保护。更好的做法是定义一个结构体,通过队列传递:

typedef struct { uint8_t data[128]; uint16_t len; } UartFrame_t; QueueHandle_t xUartQueue = xQueueCreate(5, sizeof(UartFrame_t));

串口任务收到数据后打包成UartFrame_t发到队列,解析任务从队列取。这样数据所有权清晰,不需要额外的互斥量。

7.3 中断服务程序的改造

裸机下的 ISR 通常直接处理数据,迁移到 FreeRTOS 后,ISR 应该尽量短,只做“通知任务”的工作。比如串口接收中断,裸机下可能在 ISR 里直接解析协议,迁移后应该只把数据存入缓冲区,然后通过xQueueSendFromISR通知解析任务。

改造时注意:ISR 里不能调用任何可能阻塞的 API,不能使用malloc/free,不能做浮点运算(除非确认 FPU 上下文已保存)。我的一般原则是:ISR 执行时间控制在 10us 以内,超过这个时间就应该考虑用任务来处理。

8. 调试工具与性能分析方法

8.1 用 GPIO 翻转测量任务执行时间

这是最土但最有效的方法。在任务开始和结束各翻转一个 GPIO,用示波器看波形宽度,就知道任务执行了多久。我调试电机控制任务时,就是靠这个方法发现某个浮点运算耗时 200us,远超预期的 50us,后来改用定点运算才达标。

void Task_Control(void *argument) { for(;;) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 控制逻辑 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); vTaskDelay(pdMS_TO_TICKS(1)); } }

8.2 用 FreeRTOS 的运行时统计功能

configGENERATE_RUN_TIME_STATS这个宏开启后,FreeRTOS 会统计每个任务占用 CPU 的时间比例。你需要提供一个高精度的计时器(比如 TIM2 配成 1us 分辨率),然后在vTaskGetRunTimeStats里打印结果。

void Task_Stats(void *argument) { char buffer[512]; for(;;) { vTaskGetRunTimeStats(buffer); printf("%s\r\n", buffer); vTaskDelay(pdMS_TO_TICKS(10000)); } }

打印出来的表格会显示每个任务的运行时间和占比,一眼就能看出哪个任务最耗 CPU。如果某个任务占比超过 70%,就要考虑优化算法或者拆分任务了。

8.3 用 SEGGER SystemView 做可视化跟踪

如果条件允许,强烈推荐用 SEGGER SystemView。它通过 J-Link 的 RTT 通道实时抓取 FreeRTOS 的调度事件,然后在 PC 端图形化显示。你能看到每个任务的切换时刻、阻塞原因、中断触发点,排查时序问题非常直观。我调一个多任务通信项目时,就是靠 SystemView 发现某个任务因为等待队列超时设置太短,频繁触发超时重试,白白浪费了 30% 的 CPU。

配置方法也不复杂:在 FreeRTOSConfig.h 里加几个宏,把SEGGER_SYSVIEW_ConfInclude包含进来,然后在main里调SEGGER_SYSVIEW_Init和SEGGER_SYSVIEW_Start。具体步骤官方文档写得很清楚,照着做就行。

9. 我个人的一些经验体会

FreeRTOS 这东西,入门容易精通难。刚开始用的时候,觉得能创建任务、能用队列就行了;用得多了才发现,真正的功夫在任务划分和优先级设计上。我见过太多项目,FreeRTOS 用是用了,但任务划分得一塌糊涂,优先级拍脑袋定,结果系统跑起来各种玄学问题。

我的建议是:新项目上手时,先在纸上画任务框图,标清楚每个任务的触发条件、执行时间、优先级、依赖关系。画完了再动手写代码。这个习惯帮我省了无数调试时间。另外,栈大小宁大勿小,STM32F407 有 192KB RAM,多给每个任务几百字节的栈根本不算什么,但栈溢出导致的死机可能让你查一整天。

最后分享一个小技巧:在FreeRTOSConfig.h里把configASSERT定义成自己的断言函数,里面打印文件名和行号。FreeRTOS 内部有大量的参数检查,一旦配置错误或者 API 用法不对,configASSERT会立刻触发,比等到 HardFault 再查要高效得多。

#define configASSERT(x) if((x) == 0) { \ printf("ASSERT FAILED: %s, line %d\r\n", __FILE__, __LINE__); \ taskDISABLE_INTERRUPTS(); \ for(;;); \ }

这个专栏后续还会继续更新 FreeRTOS 的深入内容,包括事件组、任务通知、流缓冲区这些高级特性的实战用法,以及和 TCP/IP 协议栈、USB 协议栈的整合案例。如果你也在用 FreeRTOS 做项目,欢迎一起交流踩坑经验。

返回列表