用 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 为例)
- 新建工程,启用 SWD 调试。
- 在
System Core > SYS中 Debug 选Serial Wire,务必勾选Trace Asynchronous Sw(否则 SWO 无输出)。 - 配置时钟:SWO 波特率通常设为 CPU 主频的 1/2 或 1/4(如 72MHz 主频,SWO 设 36MHz 或 18MHz)。
- 在调试配置中启用 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:支持二进制数据解析,需自定义脚本。
-
开源工具:如
orbuculum、swo-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 + 示波器”的双重功能。建议在开发阶段默认启用,量产时通过条件编译关闭。