为什么用 SWO + ITM 替代串口打印?

在 Cortex-M 开发中,printf 重定向到 UART 是最常见的调试手段。但它有几个痛点:

  • 开销大:一次 printf 可能阻塞数百微秒,破坏实时性。
  • 占用外设:UART 引脚、DMA 通道被调试占用。
  • 时间戳不准:日志输出延迟导致无法精确还原事件顺序。

SWO(Single Wire Output)是 Cortex-M 内核调试组件的一部分,配合 ITM(Instrumentation Trace Macrocell)可以在不占用任何外设的情况下,通过调试器的一根线输出日志。开销极低(通常几十个周期),并且可以结合 DWT 周期计数器打上高精度时间戳。

原理:ITM、SWO 与 DWT

ITM

ITM 是 Cortex-M 内核中的一个硬件模块,提供 32 个刺激端口(Stimulus Port)。向某个端口写入数据,数据会通过 SWO 引脚输出。常用端口 0 用于普通日志。

SWO

SWO 是物理输出引脚(通常与 JTAG 的 TDO 复用),通过异步串行协议将 ITM 数据发送给调试器。需要配置正确的时钟频率(通常为 CPU 主频的 1/2 或 1/4)。

DWT

DWT(Data Watchpoint and Trace)中的 CYCCNT 是一个 32 位周期计数器,每个 CPU 周期加 1。用它可以在日志中嵌入精确的时间戳,分辨率达纳秒级(取决于主频)。

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

  1. 启用调试引脚:在 CubeMX 的 SYS 中,Debug 选择 Trace Asynchronous SW。
  2. 配置时钟:确保 SWO 时钟与调试器设置一致。通常 SWO 频率 = HCLK / 2。
  3. 启用 DWT:在代码中使能 DWT->CTRL 的 CYCCNTENA 位。
  4. 调试器设置:在 ST-Link 配置中,启用 SWO 并设置正确的频率(如 2 MHz)。

完整代码示例

初始化 ITM 与 DWT

#include "stm32f4xx.h"

// 初始化 DWT 周期计数器
void DWT_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能跟踪
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 启动周期计数器
}

// ITM 发送一个字符
void ITM_SendChar(uint8_t ch) {
    if ((ITM->TCR & ITM_TCR_ITMENA_Msk) && (ITM->TER & 1)) {
        while (ITM->PORT[0].u32 == 0);
        ITM->PORT[0].u8 = ch;
    }
}

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

带时间戳的日志宏

#define LOG(fmt, ...) do { \
    uint32_t ts = DWT->CYCCNT; \
    printf("[%lu] " fmt "\r\n", ts, ##__VA_ARGS__); \
} while(0)

使用示例

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

    LOG("System started, clock=%lu Hz", SystemCoreClock);

    while (1) {
        LOG("Tick");
        HAL_Delay(1000);
    }
}

时间戳对齐与可视化

在调试器中,你可以将 SWO 输出保存为文本,然后用脚本解析时间戳。由于 CYCCNT 是周期数,除以主频即可得到秒数。例如:

# 假设主频 168 MHz
freq = 168_000_000
with open("swo.log") as f:
    for line in f:
        if line.startswith("["):
            ts = int(line[1:line.index("]")])
            print(f"{ts/freq:.6f}s: {line}")

这样就能精确还原事件顺序,甚至分析中断延迟。

注意事项

  • SWO 引脚:确保硬件连接了 SWO(通常为 PB3),且调试器支持。
  • 时钟匹配:SWO 频率必须与调试器设置一致,否则输出乱码。
  • 缓冲区:ITM 端口有 FIFO,写入前检查 u32 == 0 避免阻塞。
  • DWT 溢出:32 位计数器在 168 MHz 下约 25 秒溢出,长时间运行需处理溢出。
  • 低功耗模式:进入睡眠后 DWT 可能停止,需重新初始化。
  • 多任务:在 RTOS 中,建议每个任务使用不同 ITM 端口,便于过滤。

总结

SWO + ITM 是 Cortex-M 上替代串口打印的利器,开销低、不占外设,配合 DWT 时间戳可实现微秒级对齐。虽然配置稍复杂,但一旦搭建完成,调试体验将大幅提升。建议在性能敏感或引脚紧张的项目中优先采用。