4. 日志与追踪技术:UART 打印日志、SEGGER RTT 高速日志、Tracealyzer 与 SystemView 追踪
调试嵌入式系统,说白了就是跟“看不见摸不着”的代码行为做斗争。你没法像调试桌面程序那样打断点看变量,因为很多实时场景一停就崩。这时候,日志和追踪就成了我们的眼睛。
我个人习惯把调试手段分成三个层次:最基础的日志打印、高速日志输出、以及系统级行为追踪。这一章我们就把这三种手段挨个捋一遍。
4.1 UART 打印日志:最朴素的调试手段
UART 打印日志,几乎是每个嵌入式工程师入门的第一个调试技能。简单、直接、依赖少,一根串口线就能搞定。
核心思路: 在关键代码路径插入 printf 或自定义打印函数,通过串口输出到 PC 端的串口助手(如 SecureCRT、Putty、MobaXterm)。
但这里有个坑——阻塞问题。我记得刚做项目时,在一个 1ms 的中断服务函数里直接调了 printf,结果系统直接卡死。为什么?因为 printf 默认是阻塞的,要等串口发送完才返回。1ms 的中断里发几十个字节,时间根本不够。
避坑指南: 中断服务函数里绝对不要直接调用 printf 或阻塞式串口发送。我曾经因为这个 bug 排查了整整两天。
正确的做法是:
- 使用环形缓冲区(Ring Buffer):中断里只把日志数据压入缓冲区,由后台任务或 DMA 负责发送。
- 分级日志:定义 LOG_LEVEL,生产环境只输出 ERROR/WARNING,调试时才输出 DEBUG 信息。
- 时间戳:每条日志带上系统滴答计数,方便定位时序问题。
// 简易环形缓冲区日志示例
#define LOG_BUF_SIZE 256
static char log_buf[LOG_BUF_SIZE];
static uint16_t head = 0, tail = 0;
void log_putchar(char c) {
uint16_t next = (head + 1) % LOG_BUF_SIZE;
if (next != tail) { // 缓冲区未满
log_buf[head] = c;
head = next;
}
}
// 在后台任务或空闲循环中调用
void log_flush(UART_HandleTypeDef *huart) {
while (tail != head) {
HAL_UART_Transmit(huart, (uint8_t *)&log_buf[tail], 1, 10);
tail = (tail + 1) % LOG_BUF_SIZE;
}
}
4.2 SEGGER RTT:高速日志的正确姿势
UART 虽然简单,但速度是硬伤。115200 波特率下,每秒也就传 11KB 左右。如果你要输出大量调试数据,比如传感器原始波形、协议栈内部状态,UART 根本扛不住。
这时候就该 SEGGER RTT(Real Time Transfer) 上场了。RTT 利用调试接口(JTAG/SWD)的存储器访问能力,直接在目标 RAM 和调试器之间交换数据,速度能到几 MB/s。
我的经验: 在调试一个音频算法时,需要实时查看 16kHz 采样的 PCM 数据。用 UART 只能看降采样后的数据,换成 RTT 后直接看原始波形,问题定位效率翻倍。
RTT 的使用非常简单:
- 在工程中加入
SEGGER_RTT.c和SEGGER_RTT.h。 - 调用
SEGGER_RTT_printf()或SEGGER_RTT_Write()输出数据。 - PC 端用 J-Link RTT Viewer 或 RTT Client 接收。
// RTT 使用示例
#include "SEGGER_RTT.h"
void debug_printf(const char *fmt, ...) {
char buf[128];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
SEGGER_RTT_Write(0, buf, strlen(buf));
}
// 在代码中调用
debug_printf("ADC value: %d, timestamp: %lu\n", adc_val, HAL_GetTick());
RTT 还有一个杀手级功能——上行通道。你可以在 RTT Viewer 里输入命令,通过调试接口直接控制目标板上的变量或函数。比如输入 set_gain 2.5,就能实时调整算法参数,不用重新烧录固件。
| 特性 | UART 打印 | SEGGER RTT |
|---|---|---|
| 典型速度 | 115200 bps (~11 KB/s) | 1-10 MB/s |
| 硬件依赖 | 串口引脚 + USB 转串口 | J-Link 调试器 + SWD 接口 |
| 对目标影响 | 阻塞或中断占用 | 极低,仅少量 RAM 和 CPU |
| 双向交互 | 需额外实现 | 原生支持 |
| 适用场景 | 简单状态输出、错误报告 | 高频数据流、实时参数调优 |
4.3 Tracealyzer 与 SystemView:系统级追踪
日志打印能告诉你“发生了什么”,但很难回答“为什么会发生”。比如系统偶尔卡顿 10ms,你翻遍日志也找不到原因,因为问题出在任务调度、中断抢占或资源竞争上。
这时候就需要 系统级追踪工具。它们通过记录操作系统的调度事件、中断触发、API 调用等,生成可视化的时间轴,让你一眼看出系统的真实行为。
4.3.1 SEGGER SystemView
SystemView 是 SEGGER 推出的免费追踪工具,配合 J-Link 使用。它通过 RTT 通道实时上传事件记录,PC 端以图形化方式展示。
使用步骤:
- 在 RTOS 中集成 SystemView 的
SEGGER_SYSVIEW.c和配置文件。 - 在任务切换、中断进出、API 调用处插入记录点(大部分 RTOS 已有现成移植)。
- PC 端打开 SystemView,连接目标板,开始录制。
你能看到什么?
- 每个任务的运行时间、切换次数、阻塞原因。
- 中断的触发频率、执行时长、嵌套情况。
- CPU 利用率随时间的变化曲线。
- 系统调用(如信号量、队列)的等待和释放时序。
我记得有一次排查一个周期性抖动的 bug。用 SystemView 一看,发现一个低优先级任务每隔 100ms 会抢占高优先级任务,原因是某个信号量释放时触发了优先级反转。这种问题靠打日志根本查不出来。
4.3.2 Tracealyzer
Tracealyzer 是 Percepio 公司的商业产品,功能比 SystemView 更强大。它支持 FreeRTOS、ThreadX、Zephyr 等多种 RTOS,并且能进行离线记录(把追踪数据存到 Flash 或 SD 卡里,事后分析)。
Tracealyzer 的独特优势:
- 高级分析:自动检测死锁、优先级反转、饥饿、CPU 过载等 20 多种异常模式。
- 响应时间分析:精确测量中断到任务唤醒的延迟。
- 用户事件:可以在代码中插入自定义事件,把应用层状态也纳入追踪。
// Tracealyzer 用户事件示例
#include "trcUser.h"
void process_sensor_data(void) {
xTracePrint(1, "Sensor reading: %d", sensor_val);
xTracePrint(2, "Filter type: %s", filter_name);
// 标记一段代码的执行时间
traceLOG(TRACE_DEBUG, "Start FFT processing");
fft_process(sensor_buf);
traceLOG(TRACE_DEBUG, "End FFT processing");
}
| 特性 | SystemView | Tracealyzer |
|---|---|---|
| 价格 | 免费 | 商业授权(有免费试用) |
| 硬件依赖 | J-Link | J-Link / 串口 / 离线存储 |
| RTOS 支持 | FreeRTOS、embOS 等 | FreeRTOS、ThreadX、Zephyr 等 10+ |
| 异常检测 | 手动分析 | 自动检测 20+ 种模式 |
| 离线记录 | 不支持 | 支持(Flash/SD 卡) |
| 适用阶段 | 日常开发、快速定位 | 复杂问题、性能调优、回归测试 |
4.4 三种手段的选型策略
你可能会问:我到底该用哪个?我的建议是:
- 日常开发、简单调试:UART 打印日志就够了。成本低,上手快。
- 高频数据输出、实时参数调优:上 RTT。速度优势太明显,而且双向交互功能很实用。
- 系统级问题、性能瓶颈分析:用 SystemView 或 Tracealyzer。尤其是那些偶发性的卡顿、死锁、优先级问题,追踪工具是唯一靠谱的手段。
我的建议: 项目中最好同时保留 UART 日志(用于生产环境的最低限度输出)和 RTT/追踪接口(用于开发调试阶段)。通过编译开关切换,既保证现场问题可追溯,又不影响开发效率。
下面这张图总结了三种调试手段的定位和关系:
嗯,这一章的内容就到这里。日志和追踪是调试的基石,但光有工具还不够,下一层我们要聊的是如何用这些工具真正定位问题——也就是 性能分析的方法论。不过那是后面章节的事了,先把这些工具用熟再说。