为什么需要替代 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 中查看输出
- 进入 Run → Debug Configurations → Debugger。
- 勾选 Enable SWO,设置 Core Clock 为实际主频(如 72 MHz)。
- 启动调试,打开 Window → Show View → SWV → SWV ITM Data Console。
- 点击 Configure Trace,使能 ITM Stimulus Port 0。
- 点击红色录制按钮,即可看到日志。
完整示例:带时间戳的实时日志
#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 日志,让调试效率提升一个量级。