从裸机到 RTOS:用 SEGGER SystemView 量化任务切换开销与栈水位

1. 为什么需要量化任务切换与栈水位?

裸机程序通常只有一个主循环和中断,逻辑简单、栈使用固定。引入 RTOS 后,任务切换、中断嵌套、动态内存分配等都会带来额外开销。若不加测量,你可能会遇到:

  • 高优先级任务频繁抢占,CPU 大量时间耗在上下文切换上,有效算力下降。
  • 某个任务栈溢出,破坏相邻内存,导致随机死机,难以复现。
  • 中断响应延迟超出预期,实时性不达标。

SEGGER SystemView 是一款免费(非商业用途)的实时系统分析工具,通过 J-Link 或板载调试器,以极低开销记录 RTOS 事件(任务切换、中断、API 调用等),并在 PC 端图形化展示时间线、CPU 占用率和栈水位。

2. 原理:SystemView 如何工作?

SystemView 由三部分组成:

  • 目标端记录库SEGGER_SYSVIEW.c/h 及 RTOS 适配层(如 SEGGER_SYSVIEW_FreeRTOS.c)。
  • 传输通道:通过 RTT(Real Time Transfer)或串口将事件数据发送到主机。RTT 速度远高于串口,推荐使用。
  • PC 端分析软件:解析事件流,绘制任务状态、中断、栈水位等。

关键机制:

  • 每个事件带时间戳(通常使用 Cortex-M 的 DWT CYCCNT 或 RTOS 的 tick)。
  • 记录库使用环形缓冲区,避免阻塞。
  • 任务切换时,FreeRTOS 的 traceTASK_SWITCHED_IN/OUT 宏被调用,SystemView 记录任务 ID 和时间。
  • 栈水位通过 uxTaskGetStackHighWaterMark() 获取,SystemView 周期性记录。

3. 配置步骤(以 STM32 + FreeRTOS + J-Link 为例)

3.1 硬件与软件准备

  • STM32 开发板(如 STM32F407 Discovery)。
  • J-Link 调试器(或板载 ST-Link,但 SystemView 对 J-Link 支持最好)。
  • SEGGER SystemView 软件(官网下载)。
  • FreeRTOS 源码(本文以 V10.4.3 为例)。

3.2 添加 SystemView 源码

从 SystemView 安装目录拷贝以下文件到工程:

  • Config/SEGGER_SYSVIEW_Config_FreeRTOS.c
  • SEGGER/SEGGER_SYSVIEW.c
  • SEGGER/SEGGER_SYSVIEW.h
  • SEGGER/SEGGER_SYSVIEW_ConfDefaults.h
  • SEGGER/SEGGER_SYSVIEW_Int.h
  • Sample/FreeRTOSV10/SEGGER_SYSVIEW_FreeRTOS.c
  • Sample/FreeRTOSV10/SEGGER_SYSVIEW_FreeRTOS.h

3.3 修改 FreeRTOSConfig.h

启用跟踪宏:

#define configUSE_TRACE_FACILITY                1
#define configUSE_STATS_FORMATTING_FUNCTIONS    1
#define INCLUDE_uxTaskGetStackHighWaterMark     1
#define configGENERATE_RUN_TIME_STATS           0  // SystemView 自带计时
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 0

在文件末尾包含 SystemView 的 FreeRTOS 适配头:

#include "SEGGER_SYSVIEW_FreeRTOS.h"

3.4 初始化 SystemView

main() 中,创建任务前调用:

#include "SEGGER_SYSVIEW.h"

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    SEGGER_SYSVIEW_Conf();          // 初始化 SystemView
    SEGGER_SYSVIEW_Start();         // 开始记录

    xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);
    xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);
    vTaskStartScheduler();

    while (1);
}

3.5 配置 RTT 与时间戳

SEGGER_SYSVIEW_Config_FreeRTOS.c 中:

  • 设置 SYSVIEW_CPU_FREQ 为实际主频(如 168000000)。
  • 确保 SEGGER_RTT_Init() 被调用(SEGGER_SYSVIEW_Conf() 内部会调用)。
  • 若使用 DWT 计时,在 SEGGER_SYSVIEW_Config_FreeRTOS.c 中实现 SEGGER_SYSVIEW_GET_TIMESTAMP() 返回 DWT->CYCCNT

4. 完整代码示例:测量任务切换与栈水位

以下代码创建两个任务,Task1 以 10ms 周期运行,Task2 以 5ms 周期运行,并周期性打印栈水位。

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

static TaskHandle_t xTask1Handle = NULL;
static TaskHandle_t xTask2Handle = NULL;

void Task1(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(10);
    UBaseType_t uxHighWaterMark;

    for (;;) {
        // 模拟工作:翻转 GPIO 或计算
        volatile uint32_t i;
        for (i = 0; i < 1000; i++);

        uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
        SEGGER_RTT_printf(0, "Task1 stack free: %u words\n", uxHighWaterMark);

        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

void Task2(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(5);
    UBaseType_t uxHighWaterMark;

    for (;;) {
        volatile uint32_t i;
        for (i = 0; i < 500; i++);

        uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
        SEGGER_RTT_printf(0, "Task2 stack free: %u words\n", uxHighWaterMark);

        vTaskDelayUntil(&xLastWakeTime, xPeriod);
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    SEGGER_SYSVIEW_Conf();
    SEGGER_SYSVIEW_Start();

    xTaskCreate(Task1, "Task1", 128, NULL, 2, &xTask1Handle);
    xTaskCreate(Task2, "Task2", 128, NULL, 1, &xTask2Handle);

    vTaskStartScheduler();

    while (1);
}

在 SystemView PC 软件中:

  • 点击“Start Recording”,选择 J-Link 连接。
  • 查看“Timeline”视图,每个任务的颜色块表示运行区间,切换点清晰可见。
  • 查看“Task Info”视图,显示每个任务的栈水位(Stack Usage)。
  • 使用“Measure”工具测量两个切换点之间的时间,即为上下文切换开销(通常 1-5 μs,取决于 MCU 和 RTOS 配置)。

5. 注意事项

  • 时间戳精度:使用 DWT CYCCNT 可获得 CPU 周期级精度,但需确保 DWT 已使能(CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;)。
  • RTT 缓冲区大小:默认 1KB,高频率事件可能溢出。可在 SEGGER_RTT_Conf.h 中增大 BUFFER_SIZE_UP
  • 中断优先级:SystemView 记录中断时,需确保 configMAX_SYSCALL_INTERRUPT_PRIORITY 配置正确,否则可能丢失事件。
  • 栈水位解读uxTaskGetStackHighWaterMark() 返回的是历史最小剩余栈空间(单位:字)。若接近 0,需增大任务栈。
  • 性能影响:SystemView 本身有开销(约 1-2% CPU),但在调优阶段可接受。量产固件应关闭记录。
  • FreeRTOS 版本兼容:不同版本的 SEGGER_SYSVIEW_FreeRTOS.c 可能需微调,请参考 SystemView 安装目录下的示例。

6. 总结

通过 SEGGER SystemView,你可以直观地看到 RTOS 任务切换的时序、每个任务的栈使用情况,从而量化切换开销、优化任务优先级和栈大小。本文提供的配置步骤和代码示例可直接用于 STM32 + FreeRTOS 项目。建议在开发早期就集成 SystemView,把性能问题扼杀在摇篮里。