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

1. 为什么裸机没问题,上了 RTOS 反而“抖”了?

裸机程序是单任务顺序执行,中断响应时间可预测。引入 FreeRTOS 后,任务切换、中断嵌套、临界区保护都会引入额外延迟。常见症状:

  • 高优先级任务偶尔延迟几十微秒才运行
  • 低优先级任务长期占用互斥锁,导致中优先级任务“饿死”
  • 系统响应忽快忽慢,难以复现

要定位这类问题,必须看到任务切换的真实时间线。SEGGER SystemView 正是为此而生:它通过 SEGGER RTT 实时上传事件,在 PC 端绘制任务状态、中断、API 调用,精度可达微秒级。

2. 原理速览:任务切换抖动与优先级反转

2.1 任务切换抖动

FreeRTOS 的 taskYIELD() 或 vTaskDelay() 会触发 PendSV 异常。PendSV 优先级最低,因此会等待所有 ISR 完成。如果 ISR 过长,高优先级任务就会被推迟。抖动 = 实际切换时刻 - 期望切换时刻。

2.2 优先级反转

经典场景:

  • 任务 L(低优先级)持有互斥锁
  • 任务 H(高优先级)请求同一锁,被阻塞
  • 任务 M(中优先级)就绪,抢占 L
  • 结果:H 等待 M 执行完,仿佛优先级比 M 还低

解决方案:使用互斥量(Mutex)并开启优先级继承。当 H 阻塞时,L 临时提升到 H 的优先级,快速执行完释放锁。

3. 配置 SEGGER SystemView(STM32F407 + FreeRTOS)

3.1 硬件与软件准备

  • STM32F407 开发板,J-Link 调试器
  • FreeRTOS 内核(本文用 V10.4.3)
  • SEGGER SystemView 目标端源码(SEGGER_SYSVIEW_FreeRTOS.c 等)

3.2 关键配置步骤

  1. 将 SEGGER_SYSVIEW_FreeRTOS.c、SEGGER_RTT.c 加入工程。
  2. 在 FreeRTOSConfig.h 中启用跟踪:
#define configUSE_TRACE_FACILITY          1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define INCLUDE_xTaskGetCurrentTaskHandle  1
#define configGENERATE_RUN_TIME_STATS      0
  1. 在 main.c 中初始化 SystemView:
#include "SEGGER_SYSVIEW.h"

int main(void) {
    HAL_Init();
    SystemClock_Config();
    SEGGER_SYSVIEW_Conf();          // 初始化 SystemView
    SEGGER_SYSVIEW_Start();         // 开始记录
    // 创建任务...
    vTaskStartScheduler();
}
  1. 在 PC 端打开 SystemView,选择 J-Link,连接目标板,即可看到实时任务图。

4. 完整代码:复现优先级反转

以下代码故意制造反转,便于观察。

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

SemaphoreHandle_t xMutex;

void vTaskLow(void *pv) {
    while (1) {
        xSemaphoreTake(xMutex, portMAX_DELAY);
        SEGGER_SYSVIEW_Print("Low: got mutex");
        // 模拟长时间处理,但未释放锁
        for (volatile int i = 0; i < 5000000; i++);
        xSemaphoreGive(xMutex);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vTaskMid(void *pv) {
    while (1) {
        SEGGER_SYSVIEW_Print("Mid: running");
        for (volatile int i = 0; i < 3000000; i++);
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

void vTaskHigh(void *pv) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(200));
        SEGGER_SYSVIEW_Print("High: try mutex");
        xSemaphoreTake(xMutex, portMAX_DELAY);
        SEGGER_SYSVIEW_Print("High: got mutex");
        xSemaphoreGive(xMutex);
    }
}

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

    xMutex = xSemaphoreCreateMutex();   // 注意:用 Mutex 而非 Binary

    xTaskCreate(vTaskLow,  "Low",  256, NULL, 1, NULL);
    xTaskCreate(vTaskMid,  "Mid",  256, NULL, 2, NULL);
    xTaskCreate(vTaskHigh, "High", 256, NULL, 3, NULL);

    vTaskStartScheduler();
    while (1);
}

在 SystemView 中你会看到:High 请求锁后被阻塞,Mid 却持续运行,Low 迟迟不释放锁——典型的优先级反转。

5. 用 SystemView 定位抖动

打开 SystemView 的 Timeline 视图,关注:

  • 任务切换点之间的间隔是否均匀
  • 是否有长中断(如 SysTick、串口)打断切换
  • vTaskDelay 实际唤醒时间与预期偏差

若发现抖动大,检查:

  • 中断优先级分组:NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)
  • PendSV 和 SysTick 优先级设为最低:NVIC_SetPriority(PendSV_IRQn, 15)
  • 临界区是否过长(taskENTER_CRITICAL 内不要放耗时操作)

6. 解决优先级反转:优先级继承

将 xSemaphoreCreateMutex() 创建的互斥量自动支持优先级继承。当 High 阻塞时,Low 的优先级临时提升到 3,Mid 无法抢占,Low 快速执行完释放锁,High 立即获得锁。

在 SystemView 中可观察到:Low 的优先级在持有锁期间变为 3,释放后恢复为 1。

注意:优先级继承只对互斥量有效,二值信号量不提供此机制。

7. 注意事项与最佳实践

  • 互斥量只能在任务中使用,不可在 ISR 中获取
  • 避免嵌套获取同一互斥量(FreeRTOS 不支持递归锁,需用递归互斥量)
  • SystemView 会占用少量 CPU 和 RTT 带宽,量产固件应关闭
  • 任务优先级设计遵循“Rate Monotonic”,周期越短优先级越高
  • 使用 configASSERT() 捕获 API 误用

通过 SystemView 的可视化时间线,优先级反转和切换抖动不再是“玄学”,而是可测量、可验证的工程问题。