FreeRTOS 学习路线

API 一晚上能背完。真正难的是四件事:它什么时候切换、什么叫安全的共享、内存从哪来、中断里能做什么。这几块没搞明白,系统照样能跑,就是上不了产品。

01

调度:先搞懂它什么时候切

FreeRTOS 是优先级抢占 + 同优先级时间片。理解这一点,很多「莫名其妙」的行为就都有解释了。

要建立的直觉:任何时刻,CPU 跑的都是「就绪态里优先级最高的任务」中最早就绪的那个。高优先级任务一旦就绪,立刻抢占——不管低优先级任务正在做什么。

常见误解:以为 vTaskDelay(0) 是「让一下」。它其实是「立即重新放入就绪列表」,同优先级才会让,高优先级本来就在跑。

数字方向:FreeRTOS 里数字越大优先级越高。和某些 RTOS(以及 Linux 的 nice 值)相反,写错方向会导致任务饿死,而且不报错。

空闲任务:优先级最低,负责回收被删除任务的栈。如果你把空闲任务钩子写成了耗时操作,低优先级任务会集体卡住。

验收:画出你的系统的任务优先级表,标出每个任务阻塞在什么对象上,并解释为什么这么排。画不出来,说明还没设计过。

02

同步与通信:选错原语是事故的开始

队列、二值信号量、计数信号量、互斥量、递归互斥量、事件组、任务通知——七种东西,很多人只会用两种。

选择顺序:传数据用队列;单纯唤醒用任务通知(最轻);保护资源用互斥量;统计事件次数用计数信号量;等多个条件同时满足用事件组。

关键差别:互斥量有优先级继承,二值信号量没有。拿二值信号量当锁用,就是在给自己埋优先级反转。

递归互斥量:同一任务需要重复获取同一把锁时才用。普通互斥量重入会自死锁。

别做的事:用全局变量 + 关中断来传递数据。短临界区可以,但一旦涉及「等待」就必须用 RTOS 对象。

验收:统计你的工程里每一处共享数据,指出它是靠什么机制保护的。找不出来的那几处,就是将来的偶发 bug。

03

内存:heap_1 到 heap_5,选哪个决定你能不能上产品

FreeRTOS 自带五种堆实现,很多人从头到尾用默认的 heap_4,从没想过为什么。

heap_1:只分配不释放。最简单、最确定,安全性要求高的产品(医疗、车规)常用它——彻底杜绝碎片。

heap_2:可释放但不合并相邻块。会碎片化,已不推荐。

heap_4:可释放并合并,最常用。长期运行 + 频繁分配释放,仍然会碎。

heap_5:支持多块不连续内存(片内 SRAM + 外部 SDRAM)。

更重要的一点:产品的正确姿势是所有对象在初始化阶段一次性创建,运行期不再动态申请。这样连 heap_1 都够用,也就不用担心碎片。

验收:能说出你的项目用哪种堆、为什么,以及运行 72 小时后 xPortGetFreeHeapSize() 的变化曲线。

04

中断与临界区:最容易出错的一块

FreeRTOS 有一条硬规则:中断里只能调 ...FromISR() 版本的 API。违反它的后果不是报错,是随机崩溃。

为什么:普通 API 会尝试阻塞、会操作调度器的临界区,而中断上下文里不能阻塞,也不能被自己打断。

优先级天花板:configMAX_SYSCALL_INTERRUPT_PRIORITY 之下的中断才能调用 FreeRTOS API;高于它的中断不受 RTOS 管理,也不会被临界区屏蔽。配错这一项,表现是「偶发丢中断」或「临界区保护失效」。数字方向又和直觉相反——数值越大优先级越低。

退出中断要报告:在 ISR 末尾用 portYIELD_FROM_ISR(xHigherPriorityTaskWoken),否则任务要等到下一个 tick 才被调度,实时性直接损失一个 tick 周期。

临界区要短:taskENTER_CRITICAL() 会关中断。在里面做 Flash 擦写或长循环,等于把系统实时性废掉。

验收:能画出一次「串口中断 → 唤醒任务 → 任务处理」的完整路径,并说明每一步的上下文和能不能阻塞。

一个例:数据采集设备的任务怎么分

假设要做一个采样、上报、带屏显和报警的小设备。这是我实际会写的划分,不是标准答案,但每个数字都有来由。

任务优先级栈阻塞在什么上干什么
采集 4(最高) 512 字 / 2 KB ADC 转换完成信号量 每 10 ms 读一轮通道,结果写进数据队列
通信 3 512 字 / 2 KB 数据队列、发送完成通知 取数据、组帧、算校验,走串口 DMA 发出
逻辑 2 768 字 / 3 KB 数据队列(另一份拷贝) 滤波、比阈值、算报警状态,写进共享状态
人机 1 512 字 / 2 KB 按键队列 + 20 ms 周期延时 刷屏、处理按键,读逻辑任务的报警状态
日志 1 384 字 / 1.5 KB 日志队列 写 Flash,队列满时丢最旧的并计数
三个判断依据:
1. 优先级按延迟要求排,不按重要程度排。采集晚 5 ms 就丢数据,所以最高;刷屏晚 100 ms 没人看得出来,所以最低。
2. 队列长度按「最长阻塞时间 × 生产速率」算。10 ms 一条、通信最坏阻塞 500 ms,队列至少要 50 条,拍脑袋给 10 条一定会丢。
3. 栈不要一次给够。先给一个合理值,跑满负载后用 HighWaterMark 量,再按实测留 30% 余量。一开始就给 8 KB 的,通常是没量过。

九个高频错误

  1. 中断里调非 FromISR 版本 —— 从 xQueueSend 改成 xQueueSendFromISR。症状是随机死机或断言触发。
  2. 优先级数字方向搞反 —— 以为 0 是最高。结果关键任务被饿死,系统看起来「很卡」。
  3. configTOTAL_HEAP_SIZE 太小 —— 任务创建失败返回 NULL,但代码没检查,后面用空句柄操作,直接崩。
  4. 任务栈单位是字不是字节 —— 写 128 以为给了 128 字节,实际给了 512;反过来理解就会给太少。
  5. 二值信号量当互斥量用 —— 没有优先级继承,高负载下偶发超时。
  6. 在临界区里调用可能阻塞的函数 —— 关中断期间等信号量 = 死锁。
  7. 中断优先级配到 configMAX_SYSCALL_INTERRUPT_PRIORITY 之上还调 API —— 断言未必开,症状是静默的数据损坏。
  8. 忘记 portYIELD_FROM_ISR —— 功能正常但延迟多一个 tick,压测时才暴露。
  9. 空闲任务钩子里做耗时操作 —— 低优先级任务永远得不到执行时间,内存回收也停了。
怎么验证自己学对了:打开 configUSE_TRACE_FACILITY 和 configGENERATE_RUN_TIME_STATS,把每个任务的 CPU 占用率和栈余量打出来。一个设计良好的系统,空闲任务应该占到相当比例;如果空闲任务接近 0,说明任务划分或阻塞方式有问题。

留言 / 提问

写你卡住的地方:什么芯片、什么现象、已经试过什么。频率高的问题我会写进页面。昵称不填也能发。