从裸机到 RTOS:用 SEGGER SystemView 量化任务切换开销与栈使用峰值

1. 为什么需要量化?

裸机程序只有一个主循环,没有任务切换和独立栈,开销几乎为零。引入 RTOS 后,每次任务切换都要保存/恢复上下文,每个任务需要独立栈空间。如果切换开销过大,实时性会下降;如果栈分配不足,会引发 HardFault。因此,用数据说话比凭感觉配置更可靠。

SEGGER SystemView 是一款实时记录工具,通过 J-Link 的 RTT 通道以极低开销记录 RTOS 事件(任务切换、中断、API 调用),并可在 PC 端图形化显示时间线和统计信息。

2. 原理:SystemView 如何工作

  • 事件记录:FreeRTOS 的 traceTASK_SWITCHED_IN 等宏被 SystemView 的 SEGGER_SYSVIEW_RecordEnterISR 等函数替换,在切换点插入时间戳。
  • 时间基准:使用 Cortex-M 的 DWT CYCCNT 或 SysTick 作为时基,精度可达 CPU 周期级。
  • 数据传输:通过 RTT 上行通道,几乎不影响目标系统运行。
  • 栈峰值检测:FreeRTOS 提供 uxTaskGetStackHighWaterMark(),返回任务运行过程中栈剩余的最小值,从而推算峰值使用量。

3. 配置步骤(STM32 + FreeRTOS + SystemView)

3.1 硬件与软件准备

  • STM32F4/F7/H7 等 Cortex-M3/M4/M7 开发板
  • J-Link 调试器
  • STM32CubeMX、Keil MDK 或 STM32CubeIDE
  • SEGGER SystemView 软件(免费)
  • FreeRTOS 源码(CubeMX 可自动生成)

3.2 添加 SystemView 源码

  1. 从 SEGGER 官网下载 SystemView Target Sources。
  2. 将 SEGGER_SYSVIEW.c、SEGGER_SYSVIEW_Config_FreeRTOS.c、SEGGER_RTT.c 等加入工程。
  3. 在 FreeRTOSConfig.h 中启用 trace 宏:
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1

3.3 初始化 SystemView

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

#include "SEGGER_SYSVIEW.h"

int main(void) {
    HAL_Init();
    SystemClock_Config();
    SEGGER_SYSVIEW_Conf();  // 初始化 SystemView
    SEGGER_SYSVIEW_Start(); // 开始记录
    // 创建任务...
    vTaskStartScheduler();
}

3.4 配置 FreeRTOS 钩子

在 FreeRTOSConfig.h 中确保以下宏指向 SystemView 函数:

#define traceTASK_SWITCHED_IN()  SEGGER_SYSVIEW_RecordEnterISR()
#define traceTASK_SWITCHED_OUT() SEGGER_SYSVIEW_RecordExitISR()
#define traceISR_ENTER()         SEGGER_SYSVIEW_RecordEnterISR()
#define traceISR_EXIT()          SEGGER_SYSVIEW_RecordExitISR()

注意:不同 FreeRTOS 版本宏名可能不同,请参考 SystemView 的 Config_FreeRTOS.c。

4. 完整代码示例:测量切换开销与栈峰值

以下代码创建两个任务,分别记录切换次数和栈高水位,并通过 RTT 打印。

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

static TaskHandle_t xTask1 = NULL;
static TaskHandle_t xTask2 = NULL;

// 任务1:高优先级,频繁触发切换
void vTask1(void *pvParameters) {
    volatile uint32_t i = 0;
    while (1) {
        i++;
        if (i % 100000 == 0) {
            taskYIELD();  // 主动切换
        }
    }
}

// 任务2:低优先级,定期打印栈峰值
void vTask2(void *pvParameters) {
    UBaseType_t uxHighWaterMark;
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
        uxHighWaterMark = uxTaskGetStackHighWaterMark(xTask1);
        SEGGER_RTT_printf(0, "Task1 stack free: %u words\n", uxHighWaterMark);
        uxHighWaterMark = uxTaskGetStackHighWaterMark(xTask2);
        SEGGER_RTT_printf(0, "Task2 stack free: %u words\n", uxHighWaterMark);
    }
}

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

    xTaskCreate(vTask1, "Task1", 256, NULL, 3, &xTask1);
    xTaskCreate(vTask2, "Task2", 256, NULL, 2, &xTask2);

    vTaskStartScheduler();
    while (1);
}

在 SystemView PC 软件中,连接目标板后即可看到任务切换的时间线。放大时间轴,可读取两次切换之间的时间差,即切换开销。对于 Cortex-M4 @ 168MHz,典型切换开销约 1~3 µs。

5. 注意事项

  • 栈高水位单位:uxTaskGetStackHighWaterMark 返回的是剩余栈深度(单位:字),不是字节。实际峰值 = 分配栈大小 - 剩余值。
  • 中断优先级:SystemView 使用 RTT,需确保 RTT 中断优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY,否则可能死锁。
  • 时间基准:若使用 DWT,需在 SEGGER_SYSVIEW_Conf() 中启用 CYCCNT。
  • 开销影响:SystemView 本身有微小开销(约 1~2% CPU),测量切换开销时建议关闭其他无关记录。
  • 栈溢出检测:可同时启用 configCHECK_FOR_STACK_OVERFLOW,但会略微增加开销。
  • 多任务统计:若任务较多,建议用 vTaskList() 和 vTaskGetRunTimeStats() 辅助分析。

6. 总结

通过 SEGGER SystemView,我们可以直观地看到任务切换的耗时,并用高水位标记法精确掌握每个任务的栈峰值。这些数据能帮助你合理分配栈大小、优化任务优先级,从而在裸机与 RTOS 之间做出有依据的决策。记住:没有测量,就没有优化。