用 SWO/ITM 替代串口打印:在 Cortex-M 上做低开销实时日志与变量示波

1. 为什么需要替代串口打印?

  • 占用硬件资源:UART 需要引脚、波特率匹配,高速时易受干扰。
  • 中断开销大:每字节中断或 DMA 配置消耗 CPU 周期,影响控制环路。
  • 实时性差:115200 波特率下打印 100 字节需约 8.7ms,对 1kHz 控制是灾难。
  • 无法同步观察变量:串口打印是异步文本,难以做波形示波。

Cortex-M 内核内置 ITM(Instrumentation Trace Macrocell)DWT(Data Watchpoint and Trace),配合 SWO(Single Wire Output) 引脚,可在不占用 UART 的情况下实现纳秒级时间戳的日志与变量输出。

2. ITM/SWO 原理速览

  • SWO:Cortex-M 调试接口中的单线异步输出引脚(通常与 JTDI 复用),仅需 SWDIO、SWCLK、SWO 三线即可调试+跟踪。
  • ITM:产生软件跟踪信息,包含 32 个刺激端口(Stimulus Port)。向端口 0 写字符即输出日志;端口 1 可输出二进制变量。
  • DWT:提供 CYCCNT 周期计数器,可做微秒级时间戳;还可设置数据观察点。
  • 开销:ITM 写入是单周期存储指令(若 FIFO 满则阻塞),无中断,CPU 占用极低。

3. 硬件与工具配置

3.1 硬件连接

  • STM32 的 PB3 作为 SWO(需确认复用功能)。
  • 调试器:ST-Link V2/V3、J-Link 均支持 SWO。

3.2 软件配置(以 STM32CubeIDE 为例)

  1. 新建工程,启用 SWD 调试。
  2. System Core > SYS 中 Debug 选 Serial Wire务必勾选 Trace Asynchronous Sw(否则 SWO 无输出)。
  3. 配置时钟:SWO 波特率通常设为 CPU 主频的 1/2 或 1/4(如 72MHz 主频,SWO 设 36MHz 或 18MHz)。
  4. 在调试配置中启用 SWV(Serial Wire Viewer),设置 Core Clock 与 SWO 频率一致。

4. 重定向 printf 到 ITM

标准库的 printf 默认走 UART,需重写 _write(GCC)或 fputc(Keil)。以下是 GCC 版本:

#include <stdio.h>
#include <stdint.h>
#include "stm32f1xx.h"  // 根据型号修改

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

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

注意:需在工程中启用 ITM 时钟(调试器自动开启),且 ITM->TER 使能端口 0。若使用 Keil,重写 fputc 并调用 ITM_SendChar 即可。

5. 变量示波:DWT 时间戳 + ITM 二进制输出

5.1 初始化 DWT 周期计数器

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

static inline uint32_t DWT_GetCycles(void) {
    return DWT->CYCCNT;
}

5.2 发送带时间戳的变量

定义端口 1 用于二进制变量,每包 8 字节:4 字节时间戳(周期数)+ 4 字节变量值(float 或 int)。

typedef struct {
    uint32_t timestamp;
    float value;
} TracePacket;

void ITM_SendTrace(float val) {
    TracePacket pkt;
    pkt.timestamp = DWT_GetCycles();
    pkt.value = val;
    // 以 32 位方式写入端口 1,共 2 次
    while (ITM->PORT[1].u32 == 0);
    ITM->PORT[1].u32 = pkt.timestamp;
    while (ITM->PORT[1].u32 == 0);
    ITM->PORT[1].u32 = *(uint32_t*)&pkt.value;
}

5.3 在调试器中实时绘图

  • STM32CubeIDE:打开 SWV 视图,添加 ITM port 1 的变量监视,可实时绘制波形。
  • J-Link SWO Viewer:支持二进制数据解析,需自定义脚本。
  • 开源工具:如 orbuculumswo-parse 可解析 ITM 流并绘图。

6. 完整示例:1kHz 正弦波实时输出

#include "stm32f1xx.h"
#include <math.h>

// 省略 ITM_SendChar、_write、DWT_Init 等函数

int main(void) {
    HAL_Init();
    SystemClock_Config();  // 72MHz
    DWT_Init();

    float phase = 0.0f;
    const float step = 2 * 3.1415926f * 10 / 1000; // 10Hz 正弦,1kHz 采样

    while (1) {
        phase += step;
        if (phase > 2 * 3.1415926f) phase -= 2 * 3.1415926f;
        float val = sinf(phase);
        ITM_SendTrace(val);
        // 简单延时 1ms(实际应用用定时器)
        for (volatile int i = 0; i < 72000; i++);
    }
}

在 SWV 中即可看到 10Hz 正弦波形,时间戳精确到 CPU 周期。

7. 注意事项与避坑指南

  • SWO 引脚复用:PB3 默认是 JTDO,需在 GPIO 初始化中配置为复用推挽,或直接由调试器控制。
  • 波特率匹配:SWO 时钟必须与调试器设置一致,否则乱码。
  • FIFO 溢出:ITM 写入前检查 PORT[x].u32 == 0,否则会阻塞或丢数据。
  • 中断中打印:ITM 写入是阻塞的,高优先级中断中慎用,可先存入环形缓冲区,主循环输出。
  • DWT 周期计数器:需先使能 TRCENA,否则 CYCCNT 不计数。
  • 编译器优化ITM_SendChar 建议用 static inline 并加 volatile 访问 ITM 寄存器。
  • 多端口使用:端口 0 用于文本,端口 1 用于二进制,避免混用。
  • 功耗影响:调试时 ITM 会持续输出,增加少量功耗,量产应关闭。

8. 总结

SWO/ITM 是 Cortex-M 调试的利器,以极低开销实现实时日志与变量示波,特别适合电机控制、音频处理等对实时性敏感的场景。掌握 ITM 重定向、DWT 时间戳和二进制打包,即可在单根 SWD 线上完成“printf + 示波器”的双重功能。建议在开发阶段默认启用,量产时通过条件编译关闭。