Chapter 11 · FreeRTOS 实时操作系统

调试与性能分析

RTOS 出问题往往"偶发、难复现"。本章教你用 configASSERT、栈高水位、运行时统计和可视化 Trace 工具,把死锁、栈溢出、优先级配置错误、CPU 占用异常一网打尽。

课程进度92%

关键概念

configASSERT
FreeRTOS 内部大量断言的开关,是最重要的调试武器。定义 configASSERT(x) 后,内核会在中断优先级配错、在 ISR 里误用非 FromISR API、句柄为空、栈损坏等情况下立刻触发断言,把你停在出错现场。开发期必须开启;它能在问题"变成偶发崩溃"之前就抓住根因。发布版可关闭以省空间。
栈高水位 (Stack High Water Mark)
uxTaskGetStackHighWaterMark(handle) 返回某任务从创建至今栈的最小剩余空间(单位:字)。它靠创建时给栈填充特定字节、运行时看有多少没被覆盖来测量。返回值越接近 0 越危险,说明该任务栈快爆了。是给每个任务"量体裁衣"设置栈大小的唯一可靠依据——先给大,实测后按最坏余量收敛。
栈溢出检测 (Stack Overflow Hook)
configCHECK_FOR_STACK_OVERFLOW 设 1(切换时查栈指针越界)或 2(查栈末尾填充字节是否被改),触发时回调 vApplicationStackOverflowHook(xTask, pcTaskName),告诉你是哪个任务栈爆了。方法 2 更可靠。这是定位"莫名其妙硬 fault / 数据被改"的首选,因为栈溢出正是这类诡异 bug 的高频来源。
运行时统计 (Run Time Stats)
configGENERATE_RUN_TIME_STATS=1 后,内核用一个高频计时器(远快于 tick,如硬件 TIM)累计每个任务占用 CPU 的时间。vTaskGetRunTimeStats() 打印各任务的绝对时间和百分比,让你看清"CPU 到底被谁吃了"、有没有任务饿死、空闲任务占比多少(反映系统负载余量)。是性能调优的核心数据。
任务列表快照 (vTaskList)
configUSE_TRACE_FACILITY=1 + configUSE_STATS_FORMATTING_FUNCTIONS=1 后,vTaskList() 打印所有任务的状态(Running/Ready/Blocked/Suspended/Deleted)、优先级、栈剩余、任务号。一眼看出"某任务是不是卡在 Blocked 再没醒"、"优先级是不是配反了"。配合运行时统计是调试的两大文本利器。
Trace 钩子宏 (traceTASK_SWITCHED_IN 等)
FreeRTOS 在关键内核事件处埋了一批空的宏(任务切换、队列收发、阻塞/唤醒、malloc 等)。可视化 Trace 工具正是通过重定义这些宏,把事件带时间戳记录下来。你也能自定义它们,比如在任务切换时翻转一个 GPIO,用逻辑分析仪观察调度时序——零成本的土办法。
可视化 Trace 工具(Tracealyzer / SystemView)
Percepio Tracealyzer 和 SEGGER SystemView 把内核事件流渲染成时间线:谁在何时运行、被谁抢占、卡在哪个信号量、中断多久一次。对"偶发卡顿、响应超时、优先级翻转"这类靠单步根本看不出的时序问题,它是降维打击。可离线记录到缓冲区事后分析,也可通过 J-Link RTT 实时流式查看。
死锁与优先级问题排查
RTOS 特有 bug:死锁(两任务互等对方的锁)、饿死(低优先级任务永远抢不到 CPU)、优先级翻转(第 5 章)、忘了 portYIELD_FROM_ISR 导致响应延迟。排查思路:先看 vTaskList 谁卡在 Blocked、看 Trace 时间线定位阻塞点、检查中断优先级与 configMAX_SYSCALL_INTERRUPT_PRIORITY、给所有 Take 设超时暴露死锁。

调试配置一次到位

/* FreeRTOSConfig.h —— 开发期强烈建议全部打开 */

/* 断言:把 bug 停在现场,最重要的一项 */
#define configASSERT(x)  if((x)==0){ taskDISABLE_INTERRUPTS(); for(;;); }

/* 栈溢出检测,方法 2 最可靠 */
#define configCHECK_FOR_STACK_OVERFLOW   2

/* 任务列表 / 状态可视化 */
#define configUSE_TRACE_FACILITY             1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1

/* 运行时统计:需要一个远快于 tick 的计时源 */
#define configGENERATE_RUN_TIME_STATS        1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()  timer_stats_init()
#define portGET_RUN_TIME_COUNTER_VALUE()          timer_stats_get()

/* 捕获堆耗尽 */
#define configUSE_MALLOC_FAILED_HOOK         1
不开 configASSERT 等于蒙眼开发

FreeRTOS 里最常见、最难查的坑——中断优先级配错(第 9 章)——恰恰会被 configASSERT 在启动或首次中断时直接抓到。很多"跑几小时才偶发死机"的问题,开了断言后启动瞬间就报出来了。开发阶段务必定义它并让它停机(而不是空实现),联调稳定后再评估是否在发布版关闭。

逐任务测栈,按需收敛栈大小

/* 打印所有任务的栈最小剩余(字),据此调栈大小 */
void report_stacks(void) {
  TaskHandle_t tasks[] = { g_sensorTask, g_procTask, g_commTask };
  for (int i = 0; i < 3; i++) {
    UBaseType_t hw = uxTaskGetStackHighWaterMark(tasks[i]);
    printf("task[%d] min free stack = %lu words (%lu bytes)\r\n",
           i, (unsigned long)hw, (unsigned long)hw * 4);
  }
}
/* 经验:跑遍最坏路径后,剩余至少留 20%~30% 余量。
   高水位长期 < 32 字 → 该任务栈偏小,尽快加大 */

栈溢出钩子:抓到是哪个任务爆了

void vApplicationStackOverflowHook(TaskHandle_t xTask,
                                   char *pcTaskName) {
  (void) xTask;
  taskDISABLE_INTERRUPTS();
  printf("!!! STACK OVERFLOW in: %s\r\n", pcTaskName);
  for (;;) { }   /* 停在此,用调试器看调用栈与 pcTaskName */
}

运行时统计:看清 CPU 被谁吃了

/* 需 configGENERATE_RUN_TIME_STATS + 一个高频计时器 */
void dump_cpu_usage(void) {
  static char buf[512];
  vTaskGetRunTimeStats(buf);   /* 各任务绝对时间 + 百分比 */
  printf("Task            Time        %%\r\n%s\r\n", buf);
}

/* 典型输出:
   Task            Time        %
   IDLE            9803214     78%   ← 空闲占比高=负载轻,健康
   CommTask         1502211    12%
   ProcTask          876543     7%
   SensorTask        375012     3%
   若 IDLE 长期接近 0% → CPU 快跑满,需优化或降负载 */

任务状态快照:一眼看出谁卡住了

void dump_task_list(void) {
  static char buf[512];
  vTaskList(buf);
  printf("Name        State  Prio  Stack  Num\r\n%s\r\n", buf);
}
/* State: X=运行 R=就绪 B=阻塞 S=挂起 D=已删除
   若某任务长期 B 且本该被唤醒 → 排查它等的队列/信号量
   若优先级(Prio)与设计不符 → 检查 xTaskCreate 传参 */

零成本土办法:GPIO + 逻辑分析仪看调度

/* 在 FreeRTOSConfig.h 重定义切换钩子,任务切入时打 GPIO */
#define traceTASK_SWITCHED_IN()  \
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET)
#define traceTASK_SWITCHED_OUT() \
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET)
/* 用逻辑分析仪抓 PB0,就能看到某任务实际何时运行、
   被抢占多久——不用买 Trace 工具也能观测调度时序 */
SystemView / Tracealyzer 值得投入

文本统计能告诉你"结果"(谁占 CPU、谁栈紧张),但说不清"过程"(在第几微秒被谁抢占、卡在哪个信号量多久)。对偶发时序 bug,接上 SEGGER SystemView(配 J-Link RTT 免费用)或 Percepio Tracealyzer,把内核事件渲染成时间线,往往几分钟就能定位单步调试几天都找不到的问题。这是嵌入式 RTOS 调试的分水岭工具。

常见 RTOS 故障速查表

症状可能原因排查手段
启动即 HardFault / 偶发死机中断优先级配错、栈溢出开 configASSERT、栈溢出钩子
某任务再也不运行卡在 Blocked、被更高优先级饿死、死锁vTaskList 看状态、Trace 时间线
数据莫名被改写栈溢出覆盖相邻内存uxTaskGetStackHighWaterMark、方法 2 检测
中断事件响应有延迟忘了 portYIELD_FROM_ISR、优先级翻转检查 ISR 末尾、用互斥量优先级继承
系统卡顿、CPU 跑满某任务忙等/不阻塞、轮询过密vTaskGetRunTimeStats 看占用
创建对象返回失败堆不足、碎片xPortGetFreeHeapSize、malloc 失败钩子
两任务同时卡死死锁(互等锁)给 Take 设超时暴露、统一加锁顺序
运行时统计的计时器要"远快于 tick"

configGENERATE_RUN_TIME_STATS 依赖 portGET_RUN_TIME_COUNTER_VALUE() 返回一个高分辨率计数。若图省事直接返回 tick 计数,分辨率太粗(1ms),短任务全被记成 0%,统计失真。正确做法是配一个独立硬件定时器,频率约为 tick 的 10~100 倍(如 10~100kHz),才能准确反映各任务真实占用。

本章小结

RTOS 的 bug 多为偶发时序问题,靠工具而非蒙。configASSERT 是头号武器,能把中断优先级配错、ISR 误用 API 等停在现场,开发期务必开启且让它停机。是重灾区:用 configCHECK_FOR_STACK_OVERFLOW=2 + vApplicationStackOverflowHook 抓溢出(知道是哪个任务),用 uxTaskGetStackHighWaterMark 实测余量按需收敛栈大小。vTaskList(需 configUSE_TRACE_FACILITY)打印任务状态/优先级/栈,一眼看出谁卡在 Blocked、优先级是否配反;vTaskGetRunTimeStats(需 configGENERATE_RUN_TIME_STATS + 高频计时器)看清 CPU 被谁吃、空闲占比反映负载余量。Trace 钩子宏可自定义(如切换时翻 GPIO 用逻辑分析仪观测),也可接 SystemView / Tracealyzer 把内核事件渲染成时间线,降维打击偶发时序 bug。常见故障:优先级配错、栈溢出、死锁、饿死、忘 portYIELD_FROM_ISR、堆碎片——对照速查表逐项排除。