用 SWO + ITM 替代串口打印:在 Cortex-M 上实现零阻塞、带时间戳的调试日志

1. 为什么需要 SWO + ITM?

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

  • 占用外设:UART 引脚、DMA 通道被调试占用,无法用于正常通信。
  • 阻塞或中断开销:printf 重定向后,若使用阻塞发送,会打乱实时任务;若用中断/DMA,则增加代码复杂度。
  • 无时间戳:难以定位事件发生的精确时刻。

Cortex-M 内核内置 ITM(Instrumentation Trace Macrocell) 和 SWO(Single Wire Output),通过 SWD 调试接口的 SWO 引脚输出跟踪数据,不占用任何外设,且写入 ITM 寄存器是非阻塞的(硬件 FIFO 缓冲)。配合 DWT(Data Watchpoint and Trace)的周期计数器,还能为每条日志打上微秒级时间戳。

2. 原理简析

  • ITM:内核中的硬件模块,提供 32 个刺激端口(Stimulus Port)。向 ITM_STIM[0] 写入字符,数据即进入跟踪 FIFO,由调试器(如 ST-Link、J-Link)通过 SWO 引脚读出。
  • SWO:单线异步输出,波特率可配置(通常与 CPU 主频成比例)。
  • DWT:包含 CYCCNT 周期计数器,可精确测量时间。
  • 零阻塞:写 ITM 寄存器仅需几个周期,若 FIFO 满则丢弃数据(可配置),不会阻塞 CPU。

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

3.1 硬件连接

  • 确保调试器 SWO 引脚(通常为 PB3 或专用引脚)连接到 MCU 的 SWO 引脚。
  • ST-Link V2 需连接 SWO 线(部分廉价版无此线,需确认)。

3.2 软件配置

  1. 启用 ITM 和 DWT:在 Core/Src/main.c 初始化中添加:
// 启用DWT周期计数器
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

// 解锁ITM(部分MCU需要)
ITM->LAR = 0xC5ACCE55;
ITM->TER = 0x1; // 使能刺激端口0
ITM->TCR = ITM_TCR_ITMENA_Msk | ITM_TCR_SYNCENA_Msk | ITM_TCR_TraceBusID_Msk;
  1. 配置SWO波特率:在调试配置中设置(如 STM32CubeIDE:Run -> Debug Configurations -> Debugger -> SWV -> Core Clock 和 SWO Clock)。通常 SWO 时钟 = CPU 主频 / (分频系数),需与调试器设置一致。

3.3 调试器设置

  • 在 IDE 中打开 SWV ITM Data Console,勾选端口 0。
  • 设置正确的 Core Clock 和 SWO Clock。

4. 完整代码示例

以下代码实现带时间戳的 ITM_Printf,支持格式化字符串,且非阻塞。

#include "stm32f4xx.h" // 根据实际型号修改
#include <stdarg.h>
#include <stdio.h>

// 初始化ITM和DWT
void ITM_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    ITM->LAR = 0xC5ACCE55;
    ITM->TER = 0x1;
    ITM->TCR = ITM_TCR_ITMENA_Msk | ITM_TCR_SYNCENA_Msk | ITM_TCR_TraceBusID_Msk;
}

// 发送单个字符到ITM端口0
static inline void ITM_SendChar(uint8_t ch) {
    if (ITM->TCR & ITM_TCR_ITMENA_Msk) {
        while (ITM->PORT[0].u32 == 0); // 等待FIFO有空位(可改为非阻塞,但会丢数据)
        ITM->PORT[0].u8 = ch;
    }
}

// 带时间戳的printf
void ITM_Printf(const char *fmt, ...) {
    char buf[128];
    va_list args;
    uint32_t timestamp = DWT->CYCCNT; // 获取周期计数
    
    // 格式化时间戳(假设系统时钟168MHz,转换为微秒)
    uint32_t us = timestamp / (SystemCoreClock / 1000000);
    int len = snprintf(buf, sizeof(buf), "[%lu us] ", us);
    
    va_start(args, fmt);
    len += vsnprintf(buf + len, sizeof(buf) - len, fmt, args);
    va_end(args);
    
    for (int i = 0; i < len; i++) {
        ITM_SendChar(buf[i]);
    }
}

// 使用示例
int main(void) {
    HAL_Init();
    SystemClock_Config();
    ITM_Init();
    
    while (1) {
        ITM_Printf("Hello, SWO! Counter: %d\n", 42);
        HAL_Delay(1000);
    }
}

说明:

  • ITM_SendChar 中 while 等待 FIFO 空位,若希望完全非阻塞,可改为 if (ITM->PORT[0].u32 != 0) 直接写入,但可能丢失数据。
  • 时间戳基于 DWT->CYCCNT,需确保 SystemCoreClock 正确。

5. 注意事项

  • SWO 引脚:并非所有调试器都引出 SWO,如 ST-Link V2 克隆版常省略。J-Link 和 ST-Link V3 均支持。
  • 波特率匹配:SWO 时钟必须与调试器设置一致,否则乱码。
  • FIFO 溢出:高频日志可能导致 FIFO 满,建议降低日志频率或增大缓冲区。
  • 多任务环境:ITM 写入是原子操作,但 snprintf 可能重入,建议加锁或使用静态缓冲区。
  • 低功耗模式:进入睡眠后 DWT 可能停止,需重新初始化。
  • 替代方案:若无需时间戳,可直接使用 ITM_SendChar 或 printf 重定向到 ITM。

6. 总结

SWO + ITM 为 Cortex-M 开发者提供了一种高效、非阻塞的调试手段。通过 DWT 时间戳,还能精确分析代码执行时序。释放 UART 资源,让调试不再干扰正常功能。建议在项目初期就集成 ITM 日志,提升开发效率。