3. 任务调度跟踪实战

任务调度跟踪的目标不是“看到任务在跑”,而是把调度器内部的决策过程还原成一条可验证的时间线:谁在什么时候被唤醒、谁抢占了谁、切换花了多久、有没有任务被高优先级任务间接阻塞。TRACE32 提供了三类互补手段:基于调试口的任务上下文跟踪(Task Context Tracking)、基于 RTOS 感知的调度器事件记录(Scheduler Event Log / Trace),以及基于片上跟踪(ETM/ETB、Nexus、AURORA 等)的指令级时间戳跟踪。本节以常见的 Cortex-M + FreeRTOS 场景为例,说明如何把这三类手段组合成一次完整的调度分析。

一、先建立可观测的调度对象

在开始跟踪前,需要让 TRACE32 认识 RTOS 的对象。以 FreeRTOS 为例,加载符号后执行 SYStem.Option.OS RTOS 或使用 TASK.ORTI 相关命令,让调试器读取 pxCurrentTCB、就绪链表 pxReadyTasksLists[]、阻塞链表 xDelayedTaskList 等内核结构。随后用 TASK.List 查看任务清单,确认每个任务的名称、优先级、状态和栈指针都能被正确解析。若任务名显示为 ?,通常是 configMAX_TASK_NAME_LEN 或 TCB 偏移配置不匹配,需要在内核感知配置中修正。

关键前提是时间基准。调度延迟的测量精度取决于时间戳来源:如果使用 SYStem.MemAccess 轮询,分辨率受调试口速度限制,通常只有毫秒级;如果使用 ETM/ETB 或 DWT 周期计数器,可以做到微秒甚至纳秒级。建议在跟踪前先执行 SYStem.CONFIG.Trace 检查跟踪通路,并用 Trace.STATistic 确认能采到数据。

二、任务切换的捕获与判读

最直接的观察方式是设置任务切换断点。TRACE32 支持在 vTaskSwitchContext、xTaskIncrementTick、prvPortYIELD 等内核函数上设置断点,也可以使用 Break.Set /Program 配合条件表达式,只在切换目标变化时停下。例如在 vTaskSwitchContext 返回处设置断点,读取 pxCurrentTCB 的前后值,就能得到一次切换的“从谁到谁”。

但断点会破坏实时性,频繁切换时几乎无法继续运行。更实用的做法是使用 TRACE32 的 Trace 功能记录程序流,再通过 Trace.List 或 Trace.Chart 回放。下图给出了一次典型跟踪中,从 SysTick 中断到任务切换完成的事件链与状态迁移关系。

SysTick 触发到任务切换完成的事件链与状态迁移 t0 t1 t2 t3 SysTick 中断 xTaskIncrementTick 就绪链表更新 pxReadyTasksLists PendSV 挂起 SCB->ICSR |= PENDSVSET 上下文切换 PendSV_Handler 任务状态迁移(以 Task_High 抢占 Task_Low 为例) Task_Low Running Task_Low Ready(被抢占) Task_High Running 被抢占 恢复运行 调度延迟测量点 调度延迟 = t3 − t0(含中断响应、链表操作、PendSV 切换) 判读要点: ① t0→t1 过长说明中断入口或 Tick 处理被阻塞;② t1→t2 过长说明就绪链表操作或临界区过久; ③ t2→t3 过长说明 PendSV 被更高优先级异常延迟,或上下文保存/恢复开销异常。

三、调度器事件记录与状态转换分析

TRACE32 的 RTOS 感知可以输出调度器事件记录,典型字段包括时间戳、事件类型(TaskSwitch、TaskReady、TaskBlock、TaskSuspend)、任务名、优先级和 CPU 号。在 Trace.List 中按时间排序后,可以清楚看到“Task_Low 从 Running 变为 Ready,Task_High 从 Ready 变为 Running”这一对事件。若使用 Trace.Chart,还能以甘特图形式显示每个任务的时间片占用。

状态转换分析要抓住三个关键点。第一,就绪但未运行的时间:如果 Task_High 在 t1 已经进入就绪链表,但直到 t3 才真正运行,这段差值就是调度延迟。第二,阻塞与唤醒的配对:TaskBlock 和 TaskReady 必须成对出现,若只有阻塞没有唤醒,说明任务可能永久挂起或等待超时。第三,优先级与就绪链表的对应关系:FreeRTOS 使用 uxTopReadyPriority 位图加速查找,若该变量与 pxReadyTasksLists 不一致,说明内核感知的偏移配置有误。

四、调度延迟的量化测量

调度延迟定义为“任务变为就绪”到“任务真正获得 CPU”之间的时间。在 TRACE32 中可以用两种方式测量。第一种是事件时间戳法:在 vTaskIncrementTick 中记录唤醒时刻,在 vTaskSwitchContext 中记录切换时刻,两者相减。第二种是 DWT 周期计数器法:在任务入口读取 DWT->CYCCNT,与中断触发时刻的计数值相减,再除以主频得到时间。

下面给出一个可参考的测量配置示例,假设主频 168 MHz、SysTick 周期 1 ms:

; 使能 DWT 周期计数器
SYStem.MemAccess Enable
PER.Set DWT_CTRL %Long 0x00000001

; 在 SysTick 中断入口和任务切换点设置跟踪
Break.Set xTaskIncrementTick /Program
Break.Set vTaskSwitchContext /Program

; 记录时间戳并导出
Trace.List
Trace.EXPORT.CSV sched_trace.csv

假设某次跟踪得到:t0 = 0 μs(SysTick 触发),t1 = 12 μs(就绪链表更新完成),t2 = 18 μs(PendSV 挂起),t3 = 31 μs(Task_High 开始运行)。则调度延迟约为 31 μs,其中中断与 Tick 处理占 12 μs,链表操作占 6 μs,PendSV 切换占 13 μs。这些数值属于示例性假设,实际值取决于主频、编译优化和临界区长度。若延迟明显偏大,应优先检查是否有长临界区、是否在中断中调用了非 FromISR 版本 API、以及 PendSV 优先级是否被错误配置。

五、优先级反转的初步观察

优先级反转的典型现象是:高优先级任务 H 等待一个被低优先级任务 L 持有的互斥量,而中优先级任务 M 抢占了 L,导致 H 被 M 间接阻塞。在 TRACE32 中,可以通过事件记录观察到这种“H 就绪但长时间不运行,同时 M 在运行”的模式。

具体观察方法是:在 xQueueSemaphoreTake 和 xQueueGenericSend 上设置断点或跟踪点,记录互斥量的持有者与等待者。若发现 H 在等待 L 持有的互斥量,而 M 在 H 等待期间持续运行,就说明发生了优先级反转。此时可以进一步检查是否启用了优先级继承:FreeRTOS 的互斥量在 xSemaphoreTake 时会临时提升持有者的优先级,若未启用或配置错误,反转会持续更久。

优先级反转的初步判断不能只看一次事件,需要结合多次跟踪确认“H 等待、M 运行、L 被抢占”三者同时出现。单次采样可能只是正常的调度抖动。

六、一次完整的跟踪分析流程

把上述手段串起来,一次完整的调度跟踪分析可以按以下顺序进行:先确认 RTOS 感知配置正确,用 TASK.List 验证任务对象;再开启跟踪通路,用 Trace.STATistic 确认采样正常;然后设置关键内核函数断点或跟踪点,采集一段包含至少一次抢占和一次阻塞唤醒的事件记录;接着导出 CSV,按时间戳排序,计算调度延迟;最后检查是否存在优先级反转模式。整个过程的关键不是工具命令本身,而是把“事件—状态—时间”三者对应起来,让调度器的决策过程变得可读、可量化、可复现。

建议在跟踪前先固定一个可复现的测试场景,例如让 Task_High 周期 10 ms 唤醒、Task_Low 持有互斥量 5 ms、Task_Mid 持续计算。这样每次采集到的事件序列具有可比性,便于判断配置修改是否真正改善了调度延迟。