用 SWO + ITM 替代串口打印:Cortex-M 无阻塞调试输出与性能实测

为什么需要替代串口打印?

在嵌入式开发中,printf 重定向到 UART 是最常见的调试手段。但它存在明显痛点:

  • 占用硬件资源:必须占用一个 UART 外设及对应引脚。
  • 阻塞 CPU:发送一个字节需等待 TXE 标志,高波特率下仍会拖慢实时任务。
  • 速率瓶颈:常用 115200bps 下,打印 100 字节需约 8.7ms,严重影响控制周期。
  • 额外功耗:UART 收发器持续工作。

Cortex-M 内核自带 SWO(Serial Wire Output) 与 ITM(Instrumentation Trace Macrocell),可通过 SWD 调试接口输出调试信息,不占用任何外设、不阻塞 CPU,速率可达数 Mbps。

原理:SWO 与 ITM 如何工作

  • ITM:内核中的硬件模块,包含 32 个刺激端口(Stimulus Port),其中端口 0 用于 printf 类输出。CPU 只需向 ITM_STIM 寄存器写入数据,硬件自动打包成 ITM 数据包。
  • SWO:Cortex-M 的调试引脚(通常与 JTDO 复用),以 NRZ 或曼彻斯特编码 串行输出 ITM 数据包。
  • DWT:数据观察点单元,可提供时间戳,用于测量代码执行时间。

数据流向:CPU 写 ITM_STIM → ITM 打包 → SWO 引脚 → 调试器(ST-Link/J-Link)→ IDE 控制台。

配置步骤(以 STM32CubeIDE + ST-Link 为例)

1. 硬件连接

  • 确保 SWD 接口的 SWO 引脚(PA13/SWDIO、PA14/SWCLK、PB3/SWO)已连接调试器。
  • 若使用 ST-Link V2,需将 SWO 引脚引出。

2. 使能 ITM 与 SWO 时钟

在 main() 初始化中调用:

// 使能 DWT 和 ITM
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
ITM->LAR = 0xC5ACCE55; // 解锁 ITM
ITM->TCR |= ITM_TCR_ITMENA_Msk; // 使能 ITM
ITM->TER |= 1UL; // 使能刺激端口 0

3. 重定向 printf 到 ITM

#include <stdio.h>

int _write(int file, char *ptr, int len) {
    (void)file;
    for (int i = 0; i < len; i++) {
        ITM_SendChar(*ptr++);
    }
    return len;
}

ITM_SendChar 是 CMSIS 提供的函数,内部检查 ITM 使能状态,若未使能则直接返回,不会阻塞。

4. IDE 端配置

  • STM32CubeIDE:Debug Configuration → Debugger → 勾选 Enable SWO,设置 Core Clock 为系统主频(如 72MHz)。
  • Keil MDK:Debug → Settings → Trace → 勾选 Trace Enable,设置 Core Clock 与 SWO 时钟。
  • IAR:Project → Options → Debugger → Setup → 勾选 SWO。

完整代码示例

#include "stm32f1xx.h"
#include <stdio.h>

// 初始化 SWO/ITM
void ITM_Init(void) {
    // 使能跟踪与调试
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    // 解锁 ITM
    ITM->LAR = 0xC5ACCE55;
    // 使能 ITM 与端口 0
    ITM->TCR = ITM_TCR_ITMENA_Msk | ITM_TCR_SYNCENA_Msk;
    ITM->TER = 1UL;
    // 使能 DWT 周期计数器(用于时间戳)
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    DWT->CYCCNT = 0;
}

// 重定向 _write
int _write(int file, char *ptr, int len) {
    (void)file;
    for (int i = 0; i < len; i++) {
        ITM_SendChar(*ptr++);
    }
    return len;
}

// 测量代码执行周期
uint32_t start, end, cycles;

int main(void) {
    SystemInit();
    ITM_Init();

    printf("ITM ready!\n");

    // 性能测试:测量一段空循环
    start = DWT->CYCCNT;
    for (volatile int i = 0; i < 1000; i++);
    end = DWT->CYCCNT;
    cycles = end - start;
    printf("1000 loops: %lu cycles\n", cycles);

    while (1) {
        // 主循环
    }
}

性能实测对比

在 STM32F103(72MHz)上,分别用 UART(115200bps)和 SWO(2MHz)打印 100 字节字符串,测量 CPU 阻塞时间:

| 方式 | 波特率/速率 | 打印 100 字节耗时 | CPU 阻塞 | |------|-------------|-------------------|----------| | UART | 115200bps | 约 8.7ms | 是 | | SWO | 2MHz | 约 0.5ms | 否(仅写寄存器) |

结论:SWO 速度提升约 17 倍,且 CPU 几乎不阻塞,适合实时性要求高的场景。

注意事项

  • SWO 引脚复用:SWO 与 JTDO 共用,若使用 JTAG 需改为 SWD 模式。
  • 时钟配置:IDE 中 Core Clock 必须与实际系统时钟一致,否则输出乱码。
  • 调试器支持:ST-Link V2 需固件支持 SWO;J-Link 需在配置中使能 SWO。
  • ITM 端口:默认使用端口 0,若需多通道可启用其他端口。
  • 低功耗模式:进入 Sleep/Stop 模式后 SWO 可能停止,需唤醒后重新初始化。
  • 缓冲区:ITM 硬件 FIFO 较小,高速打印大量数据时可能丢失,建议控制输出频率。

总结

SWO + ITM 是 Cortex-M 调试的利器,无需额外外设、不阻塞 CPU、速率高。通过简单的寄存器配置和 _write 重定向,即可替代传统串口打印。在实时性要求高的项目中,强烈推荐使用。