为什么需要替代 UART 打印

在嵌入式开发中,printf 重定向到 UART 是最常见的调试手段。但它有几个致命缺点:

  • 阻塞 CPU:HAL_UART_Transmit 默认阻塞发送,一条日志可能占用几十微秒到几毫秒,实时性被严重破坏。
  • 占用引脚:至少需要 TX/RX 两根线,PCB 紧张时很奢侈。
  • 速率受限:115200 bps 下打印 100 字节需要约 8.7 ms,高频日志根本来不及。
  • 中断中不可用:在中断里调用阻塞 UART 打印,极易造成时序错乱甚至死锁。

而 Cortex-M 内核自带的 ITM + SWO 机制,可以做到:

  • 零阻塞:写入 ITM 寄存器即返回,硬件 FIFO 自动搬运。
  • 不占额外引脚:复用调试用的 SWD 接口中的 SWO 引脚(PB3 或专用引脚)。
  • 高带宽:SWO 时钟可达内核频率的 1/2,轻松上 Mbps。
  • 可在中断中使用:只要 FIFO 不满,写入几乎无开销。

ITM 与 SWO 原理

ITM 是什么

ITM(Instrumentation Trace Macrocell)是 ARM CoreSight 调试组件的一部分,位于 Cortex-M 内核内部。它提供 32 个刺激端口(Stimulus Port),软件通过写 ITM_STIMULUS_PORTn 寄存器把数据送入 ITM,硬件再通过 SWO 引脚 以异步串行方式输出到调试器。

常用端口:

  • Port 0:printf 类文本日志(最常用)。
  • Port 1:RTOS 事件跟踪。
  • Port 31:保留给调试器。

SWO 物理层

SWO 使用 UART 异步串行协议(NRZ,1 起始位 + 8 数据位 + 1 停止位),但时钟由内核提供,不需要双方约定波特率,只需在调试器中设置 SWO 时钟频率(通常等于 CPU 主频或 HCLK)。

数据流路径:

CPU 写 ITM_STIM → ITM FIFO → SWO 编码器 → SWO 引脚 → 调试探针 → IDE 控制台

配置步骤(以 STM32 + STM32CubeIDE 为例)

1. 硬件连接

  • 使用 SWD 接口:SWCLK、SWDIO、GND,外加 SWO(STM32 上通常是 PB3,复用为 TRACESWO)。
  • 确认调试器支持 SWO:ST-Link V2/V3、J-Link 均支持。

2. 使能 SWO 引脚

在 main.c 初始化中,把 PB3 配置为复用推挽输出(AF0):

__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_3;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF0_TRACE;
HAL_GPIO_Init(GPIOB, &gpio);

3. 使能 ITM 与调试时钟

在 Core/Src/main.c 开头加入:

/* 使能 TRACE 和调试时钟 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
ITM->LAR = 0xC5ACCE55;      /* 解锁 ITM */
ITM->TCR = ITM_TCR_ITMENA_Msk; /* 使能 ITM */
ITM->TER = 0xFFFFFFFF;      /* 使能所有刺激端口 */

4. 实现零阻塞日志函数

#include <stdarg.h>
#include <stdio.h>

/* 单字符发送:检查 FIFO 是否就绪 */
static inline void itm_send_char(char c)
{
    if ((ITM->TCR & ITM_TCR_ITMENA_Msk) &&
        (ITM->TER & (1UL << 0))) {
        while (ITM->PORT[0].u32 == 0) { /* 等待 FIFO 空位 */ }
        ITM->PORT[0].u8 = (uint8_t)c;
    }
}

/* 零阻塞日志:格式化到缓冲区后逐字节写入 ITM */
void log_printf(const char *fmt, ...)
{
    char buf[128];
    va_list args;
    va_start(args, fmt);
    int len = vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    if (len < 0) return;
    if (len > (int)sizeof(buf)) len = sizeof(buf);
    for (int i = 0; i < len; i++) {
        itm_send_char(buf[i]);
    }
}

注意:while (ITM->PORT[0].u32 == 0) 在 FIFO 满时会短暂等待,但 FIFO 深度通常为 4~8 字节,等待时间极短,远优于 UART。若追求绝对零等待,可改为“FIFO 满则丢弃”策略。

5. 在 STM32CubeIDE 中查看输出

  1. 进入 Run → Debug Configurations → Debugger。
  2. 勾选 Enable SWO,设置 Core Clock 为实际主频(如 72 MHz)。
  3. 启动调试,打开 Window → Show View → SWV → SWV ITM Data Console。
  4. 点击 Configure Trace,使能 ITM Stimulus Port 0。
  5. 点击红色录制按钮,即可看到日志。

完整示例:带时间戳的实时日志

#include "stm32f1xx_hal.h"
#include <stdarg.h>
#include <stdio.h>

static uint32_t log_tick = 0;

void log_init(void)
{
    /* 使能 GPIOB 与 TRACE 时钟 */
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_AFIO_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_3;
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &gpio);

    /* 解锁并配置 ITM */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    ITM->LAR = 0xC5ACCE55;
    ITM->TCR = ITM_TCR_ITMENA_Msk;
    ITM->TER = 0xFFFFFFFF;
}

void log_printf(const char *fmt, ...)
{
    char buf[128];
    int n = snprintf(buf, sizeof(buf), "[%lu] ", (unsigned long)HAL_GetTick());
    va_list args;
    va_start(args, fmt);
    n += vsnprintf(buf + n, sizeof(buf) - n, fmt, args);
    va_end(args);
    if (n > (int)sizeof(buf)) n = sizeof(buf);
    for (int i = 0; i < n; i++) {
        while (ITM->PORT[0].u32 == 0) { }
        ITM->PORT[0].u8 = (uint8_t)buf[i];
    }
}

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

    log_printf("System started, SYSCLK=%lu Hz\r\n",
               (unsigned long)HAL_RCC_GetHCLKFreq());

    uint32_t cnt = 0;
    while (1) {
        log_printf("loop count = %lu\r\n", (unsigned long)cnt++);
        HAL_Delay(500);
    }
}

注意事项与常见问题

  • SWO 引脚复用:PB3 默认是 JTDO,需确保调试器使用 SWD 而非 JTAG,否则引脚被占用。
  • 时钟频率必须匹配:IDE 中设置的 Core Clock 必须与实际主频一致,否则输出乱码。
  • FIFO 溢出:高频日志下 FIFO 可能溢出,导致字符丢失。可降低日志频率或增大 SWO 时钟。
  • 中断中使用:log_printf 中的 vsnprintf 会占用较多栈空间,中断里建议只调用 itm_send_char 发送简单字符。
  • Keil 环境:使用 Debug → Settings → Trace 勾选 Trace Enable,设置 Core Clock,并在 ITM Stimulus Ports 中使能 Port 0。
  • J-Link:需在 J-Link 设置中使能 SWO,并设置正确的 CPU 频率。
  • 低功耗模式:进入 Sleep/Stop 模式后 SWO 可能停止,调试时注意。

总结

SWO + ITM 是 Cortex-M 调试体系中被低估的利器。相比 UART,它零阻塞、高带宽、不占额外引脚,特别适合实时性要求高的场景。只需几行初始化代码,就能把 printf 从“性能杀手”变成“透明观察者”。建议在项目初期就接入 ITM 日志,让调试效率提升一个量级。