为什么需要 SWO + ITM?

传统调试常用 UART 打印,但存在明显痛点:

  • 占用额外引脚和串口外设,增加硬件成本。
  • 打印本身耗时,尤其在中断中调用会破坏实时性。
  • 高波特率下仍可能丢数据,且 CPU 需轮询或频繁进中断。

Cortex-M 内核内置 ITM(Instrumentation Trace Macrocell) 和 SWO(Single Wire Output),通过调试器(如 ST-Link、J-Link)的 SWD 接口即可输出 trace 数据。它几乎不占用 CPU 时间(写寄存器即可),且支持硬件时间戳,非常适合实时系统。

原理速览

  • ITM:内核中的一个硬件模块,提供 32 个 stimulus 端口(0~31)。向 ITM_STIM 寄存器写入数据即可发送。
  • SWO:物理输出引脚(通常与 JTDI 复用),以异步方式串行输出 ITM 数据。
  • DWT:数据观察点与跟踪单元,可提供 CPU 周期计数器,用于时间戳。
  • TPIU:跟踪端口接口单元,负责将 ITM/DWT 数据打包并通过 SWO 输出。

关键寄存器:

  • ITM_STIM0~31:写数据到对应端口。
  • ITM_TER:使能对应端口。
  • ITM_TCR:使能 ITM 和同步。
  • TPIU_ACPR:设置 SWO 波特率分频。
  • TPIU_SPPR:选择 SWO 协议(NRZ 等)。
  • DEMCR:使能跟踪。

硬件与工具准备

  • 任意 Cortex-M3/M4/M7 开发板(M0/M0+ 无 ITM,不适用)。
  • 调试器:ST-Link V2/V3、J-Link、CMSIS-DAP 等,需支持 SWO。
  • 软件:Keil MDK、IAR 或 STM32CubeIDE,配合相应调试插件。
  • 一根 SWD 排线,确保 SWO 引脚连接(通常为 4 线 SWD + SWO)。

配置步骤

1. 初始化 ITM 与 SWO

以下代码以 STM32F4(Cortex-M4)为例,直接操作寄存器。

#include "stm32f4xx.h"

#define ITM_PORT0 0

void ITM_Init(uint32_t cpu_freq, uint32_t swo_freq) {
    // 使能跟踪与调试
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;

    // 解锁 ITM 寄存器
    ITM->LAR = 0xC5ACCE55;

    // 使能 ITM 和端口 0
    ITM->TER = (1 << ITM_PORT0);
    ITM->TCR = ITM_TCR_ITMENA_Msk | ITM_TCR_SYNCENA_Msk;

    // 设置 SWO 波特率(TPIU 异步时钟分频)
    // SWO_baud = cpu_freq / (ACPR + 1)
    TPI->ACPR = (cpu_freq / swo_freq) - 1;

    // 选择 NRZ 编码
    TPI->SPPR = 0x02;

    // 使能 TPIU
    TPI->FFCR = 0x100; // 使能格式化
}

2. 实现 printf 重定向

通过 ITM_SendChar 发送字符,并重定向 fputc。

int fputc(int ch, FILE *f) {
    if (ITM->TCR & ITM_TCR_ITMENA_Msk) {
        while (ITM->PORT[ITM_PORT0].u32 == 0);
        ITM->PORT[ITM_PORT0].u8 = (uint8_t)ch;
    }
    return ch;
}

若使用 ARM Compiler,需在工程中勾选“Use MicroLIB”或自行实现 _write。

3. 添加时间戳(可选)

使能 DWT 周期计数器,并在输出前发送时间戳。

void DWT_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

void ITM_SendTimestamp(void) {
    uint32_t ts = DWT->CYCCNT;
    ITM->PORT[1].u32 = ts; // 端口 1 发送时间戳
}

在调试器中可配置解析端口 1 为时间戳。

4. 调试器端配置

  • Keil:Debug -> Settings -> Trace,勾选“Enable”,设置 Core Clock 与 SWO Clock,选择 ITM 端口 0 和 1。
  • STM32CubeIDE:Run -> Debug Configurations -> SWV,使能 ITM 并设置时钟。
  • J-Link:使用 J-Link SWO Viewer 或 Ozone。

完整示例:带时间戳的 trace 输出

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

void ITM_Init(uint32_t cpu_freq, uint32_t swo_freq);
void DWT_Init(void);

int main(void) {
    SystemInit();
    ITM_Init(168000000, 2000000); // CPU 168MHz, SWO 2MHz
    DWT_Init();

    printf("System started.\n");
    while (1) {
        ITM_SendTimestamp();
        printf("Tick: %lu\n", DWT->CYCCNT);
        for (volatile int i = 0; i < 1000000; i++);
    }
}

注意事项与常见问题

  • 仅 Cortex-M3/M4/M7 支持 ITM,M0/M0+ 无此功能。
  • SWO 引脚需在硬件上连接,且调试器支持。
  • 波特率不宜过高,通常 1~4 Mbps 足够,过高易受干扰。
  • 在低功耗模式下 ITM 可能停止工作,需退出低功耗后再使用。
  • 多任务环境中,ITM 写入是原子的,但 printf 本身可能非线程安全,建议加锁。
  • 若使用 RTOS,可在空闲任务中刷新,避免阻塞。
  • 时间戳端口需在调试软件中单独配置解析。
  • 确保 ITM->TCR 的 ITMENA 置位,否则数据不会输出。

总结

SWO + ITM 是 Cortex-M 上替代串口打印的优雅方案,开销极低,不占用外设,且支持时间戳。只需简单寄存器配置和 printf 重定向,即可获得实时 trace 能力。对于追求高性能调试的嵌入式开发者,这是必备技能。