FreeRTOS 常见故障排查

从中断优先级、栈溢出到 printf 使用误区

Posted by Yvain Zhang on February 11, 2023 主题:技术

FreeRTOS 出问题时,表面症状经常很随机:

  • 有时一加任务就崩
  • 有时在中断里挂掉
  • 有时刚启动调度器就死
  • 有时只是加了几句 printf() 就开始不稳定

但把常见案例看多了会发现,很多问题其实反复都落在几类原因上。最值得优先排的,通常就是下面三类:

  1. 中断优先级配置不对
  2. 任务栈不够或已经溢出
  3. printf() / sprintf() 用得太随意

1. 为什么中断优先级是 FreeRTOS 第一大坑

很多端口里,最容易把系统搞成“偶发崩溃”的,就是中断优先级设置错误,尤其是在:

  • 中断支持嵌套
  • ISR 里调用了 FreeRTOS API

这时候有一个硬限制:
如果某个中断里会调用 FreeRTOS API,那么它的优先级必须满足端口规定,通常不能高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(或类似命名)。

一旦这个关系错了,后果通常不是立刻编译报错,而是:

  • 临界区失效
  • 上下文切换异常
  • 系统随机挂死

这类问题最麻烦的地方正是“看起来不稳定、难复现”。

2. 为什么 Cortex-M 上更容易搞反

很多 Cortex-M 平台上,中断优先级的数字含义和直觉是反着的:

  • 数值越小,逻辑优先级越高

如果把这个方向搞反,就很容易出现:

  • 你以为自己把某个中断放低了
  • 实际上却把它设成了非常高的优先级

于是只要这个 ISR 再去碰 FreeRTOS API,就很容易出问题。

3. 栈溢出为什么排第二

栈不够是 FreeRTOS 里另一类高频问题。它之所以讨厌,是因为很多崩溃症状看起来不像“单纯栈小了”,而像:

  • 任务跑飞
  • 随机 HardFault
  • 某个任务偶尔异常退出
  • 调度切换后系统状态很怪

在小型嵌入式系统里,任务栈往往分得比较保守,而一旦函数调用层级、局部变量、格式化输出或中断嵌套稍微多一点,就会逼近边界。

4. 用什么方法判断栈够不够

最实用的手段之一就是看高水位线。

uxTaskGetStackHighWaterMark() 能告诉你:

  • 某个任务在运行期间
  • 最深一次栈使用之后
  • 还剩多少未使用空间

这个值越接近 0,越说明任务已经离栈溢出不远了。

如果你在调系统稳定性,别只看“当前有没有崩”,也要看“最坏时剩多少栈”。

5. 为什么建议把 configCHECK_FOR_STACK_OVERFLOW 打开

FreeRTOS 提供了运行时栈检查机制。最常见做法就是把 configCHECK_FOR_STACK_OVERFLOW 打开,并实现:

vApplicationStackOverflowHook()

这不会让系统 magically 恢复,但能极大提升定位效率。因为相比“系统随机死掉”,能明确知道“哪个任务栈炸了”,排查难度会低很多。

6. printf() 为什么经常把问题搞得更糟

很多人遇到问题第一反应是多打日志,这方向本身没错,但在 FreeRTOS 场景里,printf() 本身常常会带来额外风险:

  • 占栈很大
  • 运行时间长
  • 有的实现并不线程安全
  • 有的实现不适合在 ISR 中调用
  • 有的实现内部还会碰 malloc()

结果就是:你原本只是想“多打一行日志看看到底怎么回事”,最后却把系统进一步拖进不稳定状态。

7. 为什么“只加一个任务就崩”经常不是任务本身的问题

这种场景里,常见原因其实很朴素:

  • 堆不够了
  • 任务栈不够了
  • 调度器启动时还要额外创建空闲任务和守护任务

很多演示工程把 heap 配得刚刚够跑示例。你再加一个任务、队列或信号量,马上就把边界顶破。

所以一碰到“新建一个简单任务就挂”的情况,先别急着怀疑任务逻辑,先看:

  • heap 还有多少
  • 栈分得够不够
  • 调度器是否正常启动

8. 为什么 ISR 里只能用 FromISR 版本 API

这是 FreeRTOS 里另一个非常基础但非常容易被忽视的原则:

  • 普通 API 给任务上下文用
  • 中断里只能用 ...FromISR() 版本

原因不是“命名风格不同”,而是它们在内部处理方式上就不是给同一种上下文准备的。

如果在 ISR 里直接调用普通 API,很容易出现:

  • 临界区不匹配
  • 调度器状态异常
  • 返回路径不正确

9. 调度器一启动就崩时先查什么

如果问题发生在 vTaskStartScheduler() 附近,优先查这几件事:

  • heap 是否足够创建空闲任务和 RTOS 守护任务
  • FreeRTOS 的中断处理程序是否正确安装
  • 启动时处理器模式是否符合端口要求
  • 启动前是否已经有会触发上下文切换的中断在乱入

这类问题往往不是应用任务逻辑,而是系统基础配置还没站稳。

10. 开发阶段最值得打开的保护是什么

如果只选一个,我会优先建议:

configASSERT()

因为它能尽早把很多低级但代价很大的错误拦下来,例如:

  • 中断优先级不合法
  • 参数明显异常
  • 某些状态机进入不该进入的路径

它最大的价值不是“帮你修 bug”,而是让错误尽可能早、尽可能明确地暴露。

11. 一套更稳的排查顺序

FreeRTOS 系统出问题时,比较实用的顺序通常是:

  1. 先查 ISR 是否用了错误 API,优先级是否正确
  2. 再查任务栈和中断栈是否够
  3. 再查 heap 是否足够
  4. 再看日志输出、格式化函数和内存分配是否把系统拖垮
  5. 最后再回到具体业务逻辑

这套顺序听起来有点保守,但现实里经常能省掉大量无效时间。

12. 总结

FreeRTOS 的很多“奇怪问题”,最后都不是奇怪问题,而是几个高频错误源在反复出现:

  • 中断优先级没配对
  • 栈空间不够
  • printf() 用得太重
  • ISR 和任务上下文边界搞混

把这几个地方先守住,系统稳定性通常就能明显上一个台阶。剩下真正复杂的业务问题,再慢慢往里拆也不迟。