从裸机到 RTOS:用 SEGGER SystemView 定位任务切换抖动的真实案例

背景:裸机很稳,RTOS 却抖了

某工业采集设备原裸机程序稳定运行,为支持多任务升级到 FreeRTOS。迁移后出现:ADC 任务周期从 1ms 抖动到 3~8ms,偶尔丢数据。裸机时用定时器触发,抖动 <10us。问题显然出在任务调度上。

为什么需要 SystemView?

传统调试手段(串口打印、GPIO 翻转)会干扰实时性,且难以捕捉微秒级事件。SEGGER SystemView 通过 RTT(Real Time Transfer) 以极低开销记录任务切换、中断、API 调用,并在 PC 端图形化展示时间线。关键优势:

  • 无需额外引脚,仅需 J-Link 调试器
  • 时间戳精度达 1us(Cortex-M DWT 周期计数器)
  • 可直观看到任务就绪、运行、阻塞的完整时序

配置 SystemView(基于 FreeRTOS + STM32F4)

1. 添加源码

从 SEGGER 官网下载 SystemView 包,将以下文件加入工程:

  • SEGGER_SYSVIEW.c/.h
  • SEGGER_SYSVIEW_Config_FreeRTOS.c
  • SEGGER_RTT.c/.h

2. 修改 FreeRTOSConfig.h

#define configUSE_TRACE_FACILITY                1
#define configUSE_STATS_FORMATTING_FUNCTIONS    1
#define INCLUDE_xTaskGetCurrentTaskHandle       1
#define INCLUDE_xTaskGetSchedulerState          1

3. 初始化 SystemView

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

#include "SEGGER_SYSVIEW.h"
#include "SEGGER_SYSVIEW_Config_FreeRTOS.h"

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

4. PC 端连接

打开 SystemView 软件,选择 J-Link,目标芯片 STM32F407,即可看到实时任务时间线。

定位抖动:时间线暴露真相

录制 10 秒数据后,观察到:

  • ADC 任务 本应每 1ms 运行一次,但实际间隔在 1ms~8ms 间跳动
  • 每次抖动前,串口中断 持续执行了约 200us
  • 串口中断期间,调度器被挂起(时间线显示为灰色)

进一步查看中断详情,发现串口中断优先级配置为 5,而 FreeRTOS 的 configMAX_SYSCALL_INTERRUPT_PRIORITY 为 5。这意味着串口中断等于系统可管理最高优先级,它不会屏蔽其他中断,但会延迟任务切换——因为中断服务程序(ISR)中调用了 xQueueSendFromISR,而该函数内部有临界区,导致调度器被挂起。

根因分析:两个典型错误

错误 1:中断优先级设置不当

FreeRTOS 要求:调用 FreeRTOS API 的中断,优先级必须 >= configMAX_SYSCALL_INTERRUPT_PRIORITY(数值越大优先级越低)。原代码将串口中断设为 5,恰好等于该值,导致 ISR 中临界区过长时,其他中断被阻塞,任务切换被推迟。

错误 2:临界区过长

串口 ISR 中直接处理了 128 字节数据包,并调用 xQueueSendFromISR。这导致:

  • 中断执行时间长达 200us
  • 期间调度器被挂起,高优先级任务无法抢占

修复方案与代码

1. 调整中断优先级

将串口中断优先级设为 6(低于 configMAX_SYSCALL_INTERRUPT_PRIORITY),确保它可被 FreeRTOS 管理,且不会阻塞其他中断。

// 在 MX_USART1_UART_Init() 中
HAL_NVIC_SetPriority(USART1_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

2. 缩短 ISR:仅做数据搬运

// 全局缓冲区
uint8_t uart_dma_buf[128];
QueueHandle_t xUartQueue;

void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (LL_USART_IsActiveFlag_RXNE(USART1)) {
        uint8_t data = LL_USART_ReceiveData8(USART1);
        // 仅将数据放入队列,不做解析
        xQueueSendFromISR(xUartQueue, &data, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

3. 任务中处理数据

void UartTask(void *arg) {
    uint8_t data;
    while (1) {
        if (xQueueReceive(xUartQueue, &data, portMAX_DELAY) == pdPASS) {
            // 解析数据,耗时操作放这里
            process_byte(data);
        }
    }
}

修复效果

重新用 SystemView 录制:

  • 串口中断执行时间从 200us 降至 <5us
  • ADC 任务周期稳定在 1ms ± 20us
  • 任务切换抖动消失

注意事项

  • 优先级分组:STM32 默认 NVIC 分组可能影响优先级数值,建议统一设为 NVIC_PRIORITYGROUP_4(4 位抢占优先级)。
  • SystemView 时间戳:确保 SEGGER_SYSVIEW_Config_FreeRTOS.c 中 SYSVIEW_TIMESTAMP_FREQ 与 CPU 主频一致,否则时间轴失真。
  • RTT 缓冲区:默认 1KB,高频率记录时可增大至 4KB,避免数据丢失。
  • 中断中禁用 API:configASSERT() 可帮助检查非法优先级,建议在调试阶段开启。

总结

从裸机到 RTOS,任务切换抖动往往源于中断优先级配置和临界区设计。SystemView 提供了“上帝视角”,让隐藏的时序问题一目了然。记住:ISR 要短,优先级要低,临界区要小。