从裸机到 RTOS:用 SEGGER SystemView 量化任务切换与中断延迟的实操方法

一、为什么需要量化任务切换与中断延迟?

裸机开发时,我们通常用 GPIO 翻转 + 示波器测量中断延迟,但任务切换时间难以直接观测。引入 RTOS 后,调度器行为变得复杂,若仅凭感觉调优,很容易出现优先级反转、中断响应超时等问题。

SEGGER SystemView 是一款实时记录与分析工具,通过 J-Link 或普通 SWD 接口,以极低开销记录 RTOS 事件(任务切换、中断进入/退出、API 调用等),并在 PC 端图形化展示时间轴。它可以帮助我们:

  • 精确测量任务切换耗时(从就绪到运行)
  • 量化中断延迟(从中断触发到 ISR 第一条指令)
  • 发现调度瓶颈、优先级配置不合理等问题

二、SystemView 工作原理与移植准备

SystemView 依赖目标端的 SEGGER_RTT 进行数据上传,并需要 RTOS 适配层记录事件。对于 FreeRTOS,SEGGER 官方提供了 SEGGER_SYSVIEW_FreeRTOS.c 适配文件。

移植步骤:

  1. 从 SEGGER 官网下载 SystemView 目标端源码包,解压后得到 SEGGER 文件夹。
  2. 将以下文件加入工程:
    • SEGGER_RTT.cSEGGER_RTT_ASM_ARMv7M.S(RTT 通信)
    • SEGGER_SYSVIEW.cSEGGER_SYSVIEW_Config_FreeRTOS.c(SystemView 核心)
    • SEGGER_SYSVIEW_FreeRTOS.c(FreeRTOS 适配)
  3. FreeRTOSConfig.h 中启用跟踪宏:
#define configUSE_TRACE_FACILITY                1
#define configUSE_STATS_FORMATTING_FUNCTIONS    1
#define INCLUDE_xTaskGetCurrentTaskHandle       1
#define INCLUDE_xTaskGetSchedulerState          1
#define INCLUDE_uxTaskGetStackHighWaterMark     1
  1. main.c 中初始化 SystemView:
#include "SEGGER_SYSVIEW.h"

int main(void) {
    HAL_Init();
    SystemClock_Config();
    SEGGER_SYSVIEW_Conf();   // 配置 SystemView
    SEGGER_SYSVIEW_Start();  // 开始记录
    // 创建任务、启动调度器...
    vTaskStartScheduler();
    while (1);
}
  1. 在中断服务程序中,需调用 SEGGER_SYSVIEW_RecordEnterISR()SEGGER_SYSVIEW_RecordExitISR() 来记录中断时间。

三、测量任务切换时间

任务切换时间是指从调度器决定切换,到新任务第一条指令执行的时间。SystemView 会自动记录 TaskStartReadyTaskStopReady 等事件,我们只需在时间轴上观察两个事件之间的间隔。

实操示例: 创建两个同优先级任务,通过信号量同步,观察切换耗时。

SemaphoreHandle_t xSem1, xSem2;

void vTask1(void *pv) {
    while (1) {
        xSemaphoreTake(xSem1, portMAX_DELAY);
        // 触发切换:释放信号量给任务2
        xSemaphoreGive(xSem2);
    }
}

void vTask2(void *pv) {
    while (1) {
        xSemaphoreTake(xSem2, portMAX_DELAY);
        xSemaphoreGive(xSem1);
    }
}

在 SystemView 中,你会看到任务1释放信号量后,任务2立即进入就绪态,随后发生上下文切换。测量 TaskStopReadyTaskStartReady 的时间差,即为任务切换时间。在 STM32F407 @168MHz 下,通常为 1~3 µs。

四、测量中断延迟

中断延迟 = 中断触发到 ISR 第一条指令的时间。SystemView 通过记录 EnterISR 事件来标记 ISR 开始。

方法: 使用一个 GPIO 在中断触发时拉高,在 ISR 入口拉低,同时 SystemView 记录中断事件。但更简单的方式是直接利用 SystemView 的 RecordEnterISR

void EXTI0_IRQHandler(void) {
    SEGGER_SYSVIEW_RecordEnterISR();
    // 用户代码
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    SEGGER_SYSVIEW_RecordExitISR();
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

在 SystemView 时间轴上,中断事件会以红色标记显示。从中断触发(外部信号)到 EnterISR 的间隔即为中断延迟。注意:SystemView 本身有约 1~2 µs 开销,测量时需扣除。

五、完整代码示例(基于 STM32CubeIDE + FreeRTOS)

/* main.c 片段 */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "SEGGER_SYSVIEW.h"

SemaphoreHandle_t xSem1, xSem2;

void vTask1(void *pv) {
    while (1) {
        xSemaphoreTake(xSem1, portMAX_DELAY);
        SEGGER_SYSVIEW_PrintfTarget("Task1 running");
        xSemaphoreGive(xSem2);
    }
}

void vTask2(void *pv) {
    while (1) {
        xSemaphoreTake(xSem2, portMAX_DELAY);
        SEGGER_SYSVIEW_PrintfTarget("Task2 running");
        xSemaphoreGive(xSem1);
    }
}

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

    xSem1 = xSemaphoreCreateBinary();
    xSem2 = xSemaphoreCreateBinary();
    xTaskCreate(vTask1, "Task1", 128, NULL, 2, NULL);
    xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
    xSemaphoreGive(xSem1);
    vTaskStartScheduler();
    while (1);
}

六、注意事项与优化建议

  • RTT 缓冲区大小:默认 1KB,高频率事件下可能溢出,建议增大到 4KB 以上。
  • 中断优先级:SystemView 记录中断时,若中断优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY,可能无法记录,需调整。
  • 测量开销:SystemView 本身占用 CPU 约 1~5%,对时间敏感场景需评估。
  • 时间戳精度:使用 Cortex-M 的 DWT CYCCNT 可获得纳秒级精度,需在 SEGGER_SYSVIEW_Conf() 中配置。
  • 任务切换时间:受编译器优化、Flash 等待周期影响,建议在最终发布配置下测量。

通过 SystemView,你可以将原本“黑盒”的 RTOS 行为可视化,用数据驱动优化,让嵌入式系统更稳定、更高效。