从裸机到 RTOS:用 SEGGER SystemView 定位任务切换抖动与优先级反转

裸机时代,一个 while(1) 加中断,时序基本可预测。引入 RTOS 后,调度器、临界区、阻塞唤醒都会带来任务切换抖动(jitter),而共享资源竞争又可能引发优先级反转。这两个问题用 printf 或 GPIO 翻转很难看清,SEGGER SystemView 能把内核事件画成时间轴,是定位利器。

一、原理:抖动与优先级反转从哪来

  • 切换抖动:任务本应每 10ms 运行一次,实际唤醒时刻却在 9.7~10.4ms 间波动。来源包括:SysTick 与高优先级中断抢占、临界区关中断过长、taskENTER_CRITICAL() 嵌套、内存分配(pvPortMalloc)耗时、以及同优先级任务时间片轮转。
  • 优先级反转:低优先级任务 L 持有互斥量,高优先级任务 H 等待该互斥量而阻塞,中优先级任务 M 就绪后抢占 L,导致 H 被 M 间接拖延。若用二值信号量而非互斥量,FreeRTOS 不会启用优先级继承,反转会非常明显。

SystemView 通过 SEGGER RTT 把 FreeRTOStraceTASK_SWITCHED_IN/OUTtraceISR_ENTER/EXIT 等钩子事件实时上传到 PC,时间精度达微秒级,抖动一目了然。

二、配置步骤(STM32 + FreeRTOS + SystemView)

  1. 在工程中加入 SEGGER 的 SEGGER_RTT.c/.hSEGGER_SYSVIEW.c/.hSEGGER_SYSVIEW_Config_FreeRTOS.cSEGGER_SYSVIEW_FreeRTOS.c
  2. FreeRTOSConfig.h 中开启跟踪:
#define configUSE_TRACE_FACILITY            1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define INCLUDE_xTaskGetCurrentTaskHandle    1
#define INCLUDE_pxTaskGetStackStart          1
  1. main() 初始化硬件后、创建任务前调用:
#include "SEGGER_SYSVIEW.h"
SEGGER_SYSVIEW_Conf();
SEGGER_SYSVIEW_Start();
  1. PC 端打开 SystemView,Target 选 RTT,连接后即可看到时间轴。

三、复现优先级反转的完整代码

下面代码故意用二值信号量保护共享资源,制造反转。

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "SEGGER_SYSVIEW.h"

SemaphoreHandle_t xSem;   /* 二值信号量,无优先级继承 */

static void LowTask(void *p){
    for(;;){
        xSemaphoreTake(xSem, portMAX_DELAY);
        /* 模拟长临界区:占用 30ms */
        vTaskDelay(pdMS_TO_TICKS(30));
        xSemaphoreGive(xSem);
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

static void MidTask(void *p){
    for(;;){
        /* 纯计算,抢占 Low */
        volatile uint32_t i; for(i=0;i<2000000;i++);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

static void HighTask(void *p){
    for(;;){
        xSemaphoreTake(xSem, portMAX_DELAY); /* 等待 Low 释放 */
        xSemaphoreGive(xSem);
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

int main(void){
    HAL_Init(); SystemClock_Config();
    SEGGER_SYSVIEW_Conf();
    SEGGER_SYSVIEW_Start();

    xSem = xSemaphoreCreateBinary();
    xSemaphoreGive(xSem);

    xTaskCreate(LowTask,  "Low",  256, NULL, 1, NULL);
    xTaskCreate(MidTask,  "Mid",  256, NULL, 2, NULL);
    xTaskCreate(HighTask, "High", 256, NULL, 3, NULL);
    vTaskStartScheduler();
    for(;;);
}

在 SystemView 时间轴上会看到:High 就绪后长时间处于 Ready 而非 Running,Mid 却持续运行——这就是反转。

四、用 SystemView 定位抖动

  • 打开 Events 视图,按 Task switched in 统计各任务唤醒间隔,观察最大/最小偏差。
  • 若发现某任务唤醒被推迟,检查其前面是否有长临界区:在时间轴上找 ISR entertaskENTER_CRITICAL 区间。
  • 常见修复:缩短临界区、把耗时操作移出临界区、提高 SysTick 与 PendSV 优先级一致性、避免在中断里调用非 FromISR 的 API。

五、修复优先级反转

把二值信号量换成互斥量,FreeRTOS 自动启用优先级继承:

xSem = xSemaphoreCreateMutex();

此时 Low 持有互斥量期间会被临时提升到 High 的优先级,Mid 无法抢占,High 得以尽快获得资源。SystemView 上可看到 Low 的优先级被动态提升的事件。

六、注意事项

  • SystemView 依赖 RTT,需保证调试器连接且 RTT 缓冲区足够大,否则丢事件。
  • 跟踪本身有开销,量产固件应关闭 configUSE_TRACE_FACILITY
  • 互斥量只能在任务中使用,中断里用 xSemaphoreGiveFromISR 配二值信号量。
  • 优先级继承只解决反转,不解决死锁;仍要避免嵌套持锁。
  • 抖动分析要结合示波器测 GPIO,交叉验证 SystemView 时间轴。

掌握 SystemView 后,RTOS 的“黑盒”调度变得可见,抖动与反转不再是玄学。