调试与性能分析
RTOS 出问题往往"偶发、难复现"。本章教你用 configASSERT、栈高水位、运行时统计和可视化 Trace 工具,把死锁、栈溢出、优先级配置错误、CPU 占用异常一网打尽。
RTOS 出问题往往"偶发、难复现"。本章教你用 configASSERT、栈高水位、运行时统计和可视化 Trace 工具,把死锁、栈溢出、优先级配置错误、CPU 占用异常一网打尽。
configASSERT(x) 后,内核会在中断优先级配错、在 ISR 里误用非 FromISR API、句柄为空、栈损坏等情况下立刻触发断言,把你停在出错现场。开发期必须开启;它能在问题"变成偶发崩溃"之前就抓住根因。发布版可关闭以省空间。uxTaskGetStackHighWaterMark(handle) 返回某任务从创建至今栈的最小剩余空间(单位:字)。它靠创建时给栈填充特定字节、运行时看有多少没被覆盖来测量。返回值越接近 0 越危险,说明该任务栈快爆了。是给每个任务"量体裁衣"设置栈大小的唯一可靠依据——先给大,实测后按最坏余量收敛。configCHECK_FOR_STACK_OVERFLOW 设 1(切换时查栈指针越界)或 2(查栈末尾填充字节是否被改),触发时回调 vApplicationStackOverflowHook(xTask, pcTaskName),告诉你是哪个任务栈爆了。方法 2 更可靠。这是定位"莫名其妙硬 fault / 数据被改"的首选,因为栈溢出正是这类诡异 bug 的高频来源。configGENERATE_RUN_TIME_STATS=1 后,内核用一个高频计时器(远快于 tick,如硬件 TIM)累计每个任务占用 CPU 的时间。vTaskGetRunTimeStats() 打印各任务的绝对时间和百分比,让你看清"CPU 到底被谁吃了"、有没有任务饿死、空闲任务占比多少(反映系统负载余量)。是性能调优的核心数据。configUSE_TRACE_FACILITY=1 + configUSE_STATS_FORMATTING_FUNCTIONS=1 后,vTaskList() 打印所有任务的状态(Running/Ready/Blocked/Suspended/Deleted)、优先级、栈剩余、任务号。一眼看出"某任务是不是卡在 Blocked 再没醒"、"优先级是不是配反了"。配合运行时统计是调试的两大文本利器。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
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 */ }
/* 需 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 传参 */
/* 在 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 工具也能观测调度时序 */
文本统计能告诉你"结果"(谁占 CPU、谁栈紧张),但说不清"过程"(在第几微秒被谁抢占、卡在哪个信号量多久)。对偶发时序 bug,接上 SEGGER SystemView(配 J-Link RTT 免费用)或 Percepio Tracealyzer,把内核事件渲染成时间线,往往几分钟就能定位单步调试几天都找不到的问题。这是嵌入式 RTOS 调试的分水岭工具。
| 症状 | 可能原因 | 排查手段 |
|---|---|---|
| 启动即 HardFault / 偶发死机 | 中断优先级配错、栈溢出 | 开 configASSERT、栈溢出钩子 |
| 某任务再也不运行 | 卡在 Blocked、被更高优先级饿死、死锁 | vTaskList 看状态、Trace 时间线 |
| 数据莫名被改写 | 栈溢出覆盖相邻内存 | uxTaskGetStackHighWaterMark、方法 2 检测 |
| 中断事件响应有延迟 | 忘了 portYIELD_FROM_ISR、优先级翻转 | 检查 ISR 末尾、用互斥量优先级继承 |
| 系统卡顿、CPU 跑满 | 某任务忙等/不阻塞、轮询过密 | vTaskGetRunTimeStats 看占用 |
| 创建对象返回失败 | 堆不足、碎片 | xPortGetFreeHeapSize、malloc 失败钩子 |
| 两任务同时卡死 | 死锁(互等锁) | 给 Take 设超时暴露、统一加锁顺序 |
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、堆碎片——对照速查表逐项排除。