做嵌入式调试这些年,我最早也是一个不折不扣的串口党。每个板子到手,先留一路USART,焊好排针,翻出USB转TTL,打开串口调试助手,然后祈祷波特率没选错、线序没接反。直到有一次项目里UART资源被业务占满,临时又找不到一块CH340,同事甩了一句:“你手上不是有J-Link吗,用RTT Viewer啊。”我才发现,原来调试手段还有一条更干净的路。从那以后,我大部分项目的调试信息都迁移到了J-Link RTT Viewer上,配合Keil开发环境,不管是STM32、GD32还是国产CW32L010这类芯片,只要SWD能连上,这套玩法都能让你省掉一屁股串口接线的麻烦,还能顺手把中文乱码问题解决掉。
1. 为什么要“告别串口”:RTT的原理与优势
1.1 传统串口调试的三个老大难
先说痛点。串口调试不是不能用,而是用起来总有几个绕不开的问题。
第一是硬件资源占用。串口调试必须占用一颗MCU的UART外设,哪怕你用再简单的调试板,也得给它留出TX、RX两个引脚。很多项目做到后面引脚紧张得要死,还要为了调试硬挤一路串口出来,这本身就是一种浪费。有的板子甚至要专门飞线接到排针,丑得没法看。
第二是链路脆弱。串口是异步通信,双方必须约定好波特率。115200、921600这些数字我闭着眼都能背出来,但实际用的时候总有翻车的时候——波特率没对上出来一堆乱码,RX/TX接反没有数据,GND没共地偶尔飘数据。排查这些问题本身就是时间杀手。
第三是带宽瓶颈。串口的速度天然受限于波特率。拿最常见的115200bps来说,刨掉起始位停止位,有效数据率也就11.5KB/s左右。只要你的日志稍微打多一点,整个输出就开始卡、丢、截断。你要在高速信号采集代码里实时打印几十个变量,串口根本扛不住。
1.2 RTT的工作原理:J-Link替你偷看内存
RTT的全称是Real Time Transfer,是SEGGER提出的一种调试信息传输方案。它的原理说起来特别简单:目标芯片的SRAM里划出一块环形缓冲区,你自己的代码往缓冲区里写数据,J-Link通过SWD或JTAG调试接口周期性地去读取这块内存,再把数据搬到USB另一头的PC上,RTT Viewer收到后显示出来。
整个过程完全不需要UART外设,不需要额外引脚,不需要波特率匹配。它实际上就是让J-Link充当了一个“内存扒手”,不断把调试数据从目标芯片的SRAM里掏出来。用个生活化的类比:串口调试就像两个人隔着窗户用对讲机喊话,得先约定好频道、音量,还得保证有人在窗边值守;RTT则像是J-Link这个“探长”直接打开你房间的窗户,递出来的纸条随拿随走,不需要屋里的人专门守着。
1.3 RTT与串口的硬核对比
我把两种方式的差异整理成一张表,大家感受更直观:
| 对比项 | 传统串口调试 | J-Link RTT |
|---|---|---|
| 硬件占用 | 占用UART外设+2个引脚 | 不占用任何外设,只用SWD的SWDIO/SWCLK |
| 接线 | 需要USB转TTL、杜邦线,注意TX/RX交叉 | J-Link调试线直接连4个SWD引脚 |
| 速率 | 115200bps下约11.5KB/s | 实测几十KB/s到几MB/s,取决于缓冲与J-Link型号 |
| 配置复杂度 | 需要约定波特率、数据位、校验位 | 无需波特率,连上即可看 |
| 附加依赖 | 需要串口驱动如CH340驱动 | 需要SEGGER J-Link软件包与RTT源码 |
| 收发双向 | 支持,但下行需要额外逻辑 | RTT支持多通道双向,天然适合做命令行交互 |
从这张表能看出来,RTT并不是简单地“换了个输出管道”,它在资源占用、吞吐量、双向交互能力上都比传统串口高一个量级。当然,它有一个天然前提:必须有一只J-Link仿真器,且目标芯片处在调试状态。好在做嵌入式开发的人手里基本都会有一只J-Link,这个门槛并不高。
2. 开搞之前:环境准备与组件添加
2.1 工具链清单与版本确认
先把需要的工具列清楚,避免很多人卡在开局:
- Keil MDK,版本5.x或6.x都能跑,项目用AC5或AC6编译器均可,但建议新工程直接用AC6;
- SEGGER J-Link软件包,也就是J-Link Software and Documentation Pack,建议V7.x以上,安装后会自动带上J-Link RTT Viewer;
- 目标板与J-Link仿真器,J-Link通过SWD接口连接,只需VCC、GND、SWDIO、SWCLK四根线;
- SEGGER RTT源码,这个通常在J-Link软件包安装目录下,路径一般是“C:\Program Files (x86)\SEGGER\SEGGER J-Link\Samples\RTT”,也可以从SEGGER官网单独下载。
顺便说一句,不少国产芯片对J-Link版本有要求,比如CW32L010,用太老的J-Link软件包可能无法识别目标。遇到这种情况,先打开J-Link Commander确认一下能不能连上目标芯片,再决定是否升级软件包。这套RTT玩法在Cortex-M全系列上都是通用的。
2.2 把SEGGER RTT组件加进Keil工程
打开Keil工程后,在项目树里新建一个“RTT”分组,然后把RTT源码文件夹里的四个文件拖进去:
- SEGGER_RTT.c
- SEGGER_RTT.h
- SEGGER_RTT_printf.c 以及配套的SEGGER_RTT_printf.h
- SEGGER_RTT_Conf.h
如果你的芯片是Cortex-M7这类带指令缓存的高端货,或者你明确知道需要汇编级优化,可以把SEGGER_RTT_ASM_ARMv7M.S也加进去。绝大多数情况下,上面四个文件就够用了。
加完文件之后,在main函数开头调用一次初始化函数,然后直接打印:
#include "SEGGER_RTT.h" int main(void) { SEGGER_RTT_Init(); SEGGER_RTT_WriteString(0, "RTT is ready\r\n"); while (1) { // your code } }很多人以为还要配置一大堆东西才能看到输出,其实没有,RTT的设计初衷就是开箱即用。编译下载后打开RTT Viewer,如果一切正常,你会在终端窗口里立刻看到“RTT is ready”。
2.3 SEGGER_RTT_Conf.h按需调整
SEGGER_RTT_Conf.h是RTT的核心配置文件,里面有几个关键参数值得解释一下:
- BUFFER_SIZE_UP:上行缓冲大小,默认1024字节,也就是目标芯片往PC传数据的缓冲。日志多的场景建议改到4096或8192。
- BUFFER_SIZE_DOWN:下行缓冲大小,默认16字节,是PC往目标芯片发数据的缓冲。做简单命令行交互的话,保留默认就行。
- SEGGER_RTT_PRINTF_BUFFER_SIZE:SEGGER_RTT_printf内部格式化时的临时缓冲,默认64字节。如果你的日志单条比较长,比如要打印一个结构体,建议改到128或256。
- SEGGER_RTT_MAX_NUM_UP_BUFFERS / SEGGER_RTT_MAX_NUM_DOWN_BUFFERS:上行和下行通道的最大数量,默认都是2。要做多通道日志分级,建议把上行改成4。
修改这些参数后记得重新编译整个工程。另外,不要把上行缓冲改成所谓的阻塞模式,也就是不要轻易让RTT在缓冲区满时死等,否则日志一大,CPU全耗在写缓冲上,你的实时逻辑就全卡死了。保持默认的不阻塞丢数据模式,配合调大缓冲才是正道。
3. 基础玩法:让printf从串口换成RTT
3.1 重定向printf到RTT:MicroLIB快捷方式
很多人的代码里用的是printf格式化输出,直接改成SEGGER_RTT_printf也不是不行,但老代码里printf散布得到处都是,一个个改太费劲。更优雅的做法是重定向fputc,让标准printf的输出底层走RTT。
在Keil工程里先勾上MicroLIB,然后写一个fputc:
#include <stdio.h> #include "SEGGER_RTT.h" int fputc(int ch, FILE *f) { SEGGER_RTT_Write(0, (const char *)&ch, 1); return ch; }这样你代码里所有printf,底层都会通过RTT通道0输出到RTT Viewer。实测下来这个方案最省心,不用动老代码,也不影响代码可读性。
注意一个坑:工程里如果以前为了串口重定向写过类似的fputc函数,要么注释掉,要么用条件编译切换。否则两个fputc定义直接编译冲突。另外,MicroLIB的printf不支持浮点格式化,这条限制在串口时代也存在,大家知道就好。
3.2 SEGGER_RTT_printf与标准printf的差异
如果你需要更精细地控制输出,也可以直接用SEGGER_RTT_printf。它和标准printf有区别,我的经验是:
- 支持%d、%u、%x、%p、%c、%s等常用格式,基础调试完全够用;
- 不支持%f浮点打印,打印float变量会得到不可预期的结果;
- 默认实现里精度、宽度支持有限,不要拿它当完整的标准C格式化函数用。
要打浮点数的时候,我的写法是先借标准库格式化到临时数组,再通过RTT输出:
char tmp[128]; float voltage = 3.24f; snprintf(tmp, sizeof(tmp), "voltage = %.3f V\r\n", voltage); SEGGER_RTT_WriteString(0, tmp);这个方式虽然多了一步,但在调试电源管理、传感器采集这类场景时很好用。你只需要确保临时数组够长,别把栈搞爆就行。
3.3 RTT Viewer常用操作速查
RTT Viewer是SEGGER软件包里那个独立的显示工具,安装完J-Link软件包后,在开始菜单里就能找到。
打开RTT Viewer,第一屏会弹连接设置,通常选择USB,然后在下拉框里选目标芯片型号,接口选SWD,速度可以先用4MHz。如果你用的是带CDC虚拟串口的J-Link clone,记得在USB设备列表里选J-Link而不是那个串口设备。
连接成功后,RTT Viewer的主界面就是一个终端窗口。窗口下面有个输入框,往里面敲字符并回车,数据会通过下行通道发到目标芯片。想做命令行交互的朋友,这一条后面会细讲。
RTT Viewer还支持多终端窗口,后续做多通道调试时,可以通过菜单里的Terminal选项添加新的终端,并为每个窗口指定不同的上行通道。把调试日志和业务数据分开显示,排查问题会轻松很多。
3.4 缓冲区多大才不丢数据
缓冲区大小的选择,我直接给经验值:
- 嵌入式调试场景,上行缓冲给4096字节起步,如果日志量大而且主机端显示跟不上,直接给8192;
- 下行缓冲给512字节足够,除非你要通过RTT传输比较大的配置数据;
- SEGGER_RTT_PRINTF_BUFFER_SIZE保持默认即可,如果发现长日志被截断,再改成256。
很多人以为缓冲越大越好,其实不至于。缓冲太大浪费SRAM,嵌入式芯片的RAM本来就很宝贵。判断标准很简单:运行你的满负载日志场景,如果RTT Viewer显示没有明显丢行、丢内容,那这个缓冲就是够用的。
4. 高阶玩法:多通道、日志分级与双向交互
4.1 多通道设计:把调试日志和业务数据分开
RTT一个很重要的特性是支持多通道。默认的通道0叫Terminal,用来跑printf是最直观的。但实际调试中,我们往往不想让普通日志和关键调试信息混在一起。这时可以让RTT同时开多个上行通道:通道0放常规日志、通道1放传感器原始数据、通道2放错误告警。
初始化多通道时,可以用SEGGER_RTT_ConfigUpBuffer。例如:
SEGGER_RTT_ConfigUpBuffer(1, "SENSOR", sensor_buf, sizeof(sensor_buf), SEGGER_RTT_MODE_DEFAULT); SEGGER_RTT_ConfigUpBuffer(2, "ERROR", NULL, 0, SEGGER_RTT_MODE_DEFAULT);当第二个参数指定名称、第三个参数传NULL、第四个参数传0时,RTT会自己从静态内存里分配缓冲。然后打印时指定通道号:
SEGGER_RTT_printf(1, "sensor0=%d, sensor1=%d\r\n", s0, s1); SEGGER_RTT_printf(2, "fault=%d\r\n", fault_code);RTT Viewer里,菜单Terminal选择Add Terminal,新建窗口后把该窗口的Up Channel设成对应的通道号。我习惯开三个终端窗口叠在一起:一个看业务日志、一个看原始数据、一个专门盯错误。排查bug时这种“分开看”的方式省下来的时间,用过的都知道有多爽。
4.2 用颜色和前缀给日志分级
多通道很好,但有时候我们想在同一个通道里一眼分清ERROR、WARN、INFO。RTT Viewer的终端窗口支持常见的ANSI颜色控制码,虽然在串口助手里不支持,但在RTT里可以直接用。
做一个简单的日志宏:
#define LOG_ERROR(...) SEGGER_RTT_printf(0, "\x1B[31m[ERR] " __VA_ARGS__ "\x1B[0m\r\n") #define LOG_WARN(...) SEGGER_RTT_printf(0, "\x1B[33m[WRN] " __VA_ARGS__ "\x1B[0m\r\n") #define LOG_INFO(...) SEGGER_RTT_printf(0, "\x1B[32m[INF] " __VA_ARGS__ "\x1B[0m\r\n")红色错误、黄色警告、绿色信息,一看就知道当前程序状态。注意别把样式控制码和普通日志混在一起,多个线程同时打印时容易乱。如果日志量和颜色码一起涌出来,终端可能偶尔抽搐一下,这是正常现象,只要数据不丢就行。
4.3 用RTT做一个小型命令行
RTT的双向通信能力非常适合做调试命令行。配合RTT Viewer下方的输入框,可以给目标芯片发送字符串。下位机代码用SEGGER_RTT_Read读取:
#define CMD_BUF_SIZE 64 static char rx_buf[CMD_BUF_SIZE]; void rtt_command_task(void) { int n = SEGGER_RTT_Read(0, rx_buf, CMD_BUF_SIZE - 1); if (n > 0) { rx_buf[n] = '\0'; /* 解析并执行命令 */ if (strstr(rx_buf, "help") != NULL) { SEGGER_RTT_WriteString(0, "commands: help, version, led_on, led_off\r\n"); } else if (strstr(rx_buf, "version") != NULL) { SEGGER_RTT_WriteString(0, "app v1.0.3\r\n"); } else if (strstr(rx_buf, "led_on") != NULL) { LED_ON(); } else if (strstr(rx_buf, "led_off") != NULL) { LED_OFF(); } } }这个思路扩展性很强,像寄存器读写、任务栈使用量查询,都能通过这种命令行完成。调试过程中不用反复下载程序,输个命令就能改参数,工作效率提升非常明显。
4.4 在RTOS多任务下的注意事项
如果你的工程跑的是FreeRTOS这类RTOS,那么多任务同时打印时会有日志交错问题。RTT的环形缓冲区写入并不是原子的,两个任务同时调SEGGER_RTT_Write,可能出现半串日志被打断的情况。
我的做法是给关键打印加临界区保护:
taskENTER_CRITICAL(); SEGGER_RTT_WriteString(0, "task_a start\r\n"); SEGGER_RTT_WriteString(0, "task_b done\r\n"); taskEXIT_CRITICAL();注意,临界区不能一直占着不放,否则影响实时性。另外在中断服务程序里打印时要特别谨慎,最好不要在ISR里用SEGGER_RTT_printf这种带格式化的函数,既耗时又容易和主循环的打印冲突。如果一定要ISR打印,只做最简单的SEGGER_RTT_Write,把格式化工作留给主循环。
5. 中文打印解决方案:从乱码到正常显示
5.1 乱码的根源:GB2312源码 vs UTF-8终端
RTT调试中最大的坑,恐怕就是中文乱码。这个乱码的根源,说白了就是编码不一致。
Keil在中文Windows系统下,默认的源文件编码是ANSI,也就是GB2312/GBK编码。你在Keil里写下中文字符串,编译后存进Flash的实际就是GB2312的字节。而RTT Viewer在PC端默认按UTF-8解码,UTF-8和GB2312的编码规则完全不同,相同的字节流被当成UTF-8去解释,自然出来的就是一堆天书。
这就像你用中文写了一封信,对方却用法语字母表来拆解笔画,当然读不出原来的意思。要解决这个问题,路径其实就两条:要么让源码里存的字节变成UTF-8,要么让RTT Viewer在显示前把GB2312转成UTF-8。
5.2 方案A:源码统一转成UTF-8
最直接的思路是把源码文件都转成UTF-8编码保存。操作路径是Keil的Edit -> Configuration -> Editor,在Encoding里选择UTF-8。如果是老工程,也可以先用VS Code或者Notepad++批量转码保存后再回到Keil。
这个方案适合新工程,但有几个坑要提醒:
- Keil对UTF-8源码的编辑器显示在部分版本上依然有兼容问题,尤其是中文注释,转成UTF-8之后可能显示成乱码,不过编译时注释会被忽略,代码本身没问题;
- 如果工程里还有其他工具链参与,比如git diff、脚本、代码生成器,所有环节都必须是UTF-8,否则某一步再引入GB2312的中文字符串,乱码又会死灰复燃;
- AC5编译器对UTF-8源文件的支持不如AC6稳定,旧工程想整体转码建议先把编译器换成AC6。
我的建议是:新工程、团队协作标准统一的场景,源码整体UTF-8是可行方案。但老工程、多人维护的代码,动源码编码风险较大,不推荐。
5.3 方案B:运行时GB2312到UTF-8映射表(推荐)
老工程或者只想局部解决中文的情况,推荐在代码里做一个轻量级转换:发送前把中文字符串从GB2312转成UTF-8,再交给RTT输出。
这里不推荐网上那些“高位低位直接移位”的简化算法,因为GB2312区码对应的Unicode码点并不是线性排布,直接用位运算算出来的字符有一半是错的。真正稳妥的做法是维护一张映射表,只收录你日志中用到的汉字。调试日志来来去去就那么几个词,比如“初始化成功”“温度异常”“电压过高”,汉字量很小,映射表完全可控。
先定义映射结构:
typedef struct { unsigned short gb; /* GB2312两字节编码,如 0xB3C9 表示“成” */ unsigned char utf[3]; /* 对应的UTF-8三字节 */ } GB2UTF8_ITEM;再放一张示例表:
static const GB2UTF8_ITEM g_gb2utf8_tbl[] = { {0xB3C9, {0xE6, 0x88, 0x90}}, /* 成 */ {0xB9A6, {0xE5, 0x8A, 0x9F}}, /* 功 */ {0xB4ED, {0xE9, 0x94, 0x99}}, /* 错 */ {0xCEE5, {0xE8, 0xAF, 0xAF}}, /* 误 */ {0xB5E7, {0xE7, 0x94, 0xB5}}, /* 电 */ {0xD1B9, {0xE5, 0x8E, 0x8B}}, /* 压 */ {0xCEC2, {0xE6, 0xB8, 0xA9}}, /* 温 */ {0xB6C8, {0xE5, 0xBA, 0xA6}}, /* 度 */ {0xB5F7, {0xE8, 0xB0, 0x83}}, /* 调 */ {0xCAD4, {0xE8, 0xAF, 0x95}}, /* 试 */ {0xB3F5, {0xE5, 0x88, 0x9D}}, /* 初 */ {0xCABC, {0xE5, 0xA7, 0x8B}}, /* 始 */ {0xBBAF, {0xE5, 0x8C, 0x96}}, /* 化 */ {0xB7A2, {0xE5, 0x8F, 0x91}}, /* 发 */ {0xCB CD, {0xE9, 0x80, 0x81}}, /* 送 */ {0xCAFD, {0xE6, 0xAD, 0xA3}}, /* 正 */ {0xB3A3, {0xE5, 0xB8, 0xB8}}, /* 常 */ };然后再写一个通用的发送函数:
static void rtt_write_cn_string(const char *str) { while (*str) { unsigned char c1 = (unsigned char)*str; if (c1 < 0x80) { /* ASCII字符原样输出 */ SEGGER_RTT_Write(0, str, 1); str++; } else if (c1 >= 0xB0 && c1 <= 0xF7 && str[1] != '\0') { /* GB2312汉字:两字节 */ unsigned char c2 = (unsigned char)str[1]; unsigned short gb = (unsigned short)((c1 << 8) | c2); unsigned char utf[3]; int found = 0; for (unsigned int i = 0; i < sizeof(g_gb2utf8_tbl) / sizeof(g_gb2utf8_tbl[0]); i++) { if (g_gb2utf8_tbl[i].gb == gb) { utf[0] = g_gb2utf8_tbl[i].utf[0]; utf[1] = g_gb2utf8_tbl[i].utf[1]; utf[2] = g_gb2utf8_tbl[i].utf[2]; found = 1; break; } } if (found) { SEGGER_RTT_Write(0, utf, 3); } else { /* 表里没有的字,原样输出,至少不崩溃 */ SEGGER_RTT_Write(0, str, 2); } str += 2; } else { SEGGER_RTT_Write(0, str, 1); str++; } } }用的时候先格式化到临时数组,再调这个函数输出:
char tmp[128]; snprintf(tmp, sizeof(tmp), "初始化成功,电压=%.3fV\r\n", voltage); rtt_write_cn_string(tmp);这个方案的好处是:不用动源文件编码,不会影响老代码,编译出来的体积只有一点点,映射表里放多少字就占多少空间。坏处也明显:映射表需要维护,用到的新汉字要手动加。
5.4 如何快速生成映射表
手动查编码表太痛苦。我的习惯是用Python写个小脚本,把目标汉字批量转成UTF-8字节。在PC上跑一段类似这样的代码,输出直接粘贴进C文件即可:
import sys for ch in "初始化成功电压温度调试发送正常": gb = ch.encode("gb2312") utf8 = ch.encode("utf-8") gb_hex = "0x%02X%02X" % (gb[0], gb[1]) utf_hex = ", ".join("0x%02X" % b for b in utf8) print("{ " + gb_hex + ", {" + utf_hex + "} }, /* " + ch + " */")输出的每一行直接对应映射表的条目,根本不用自己算字节。我每次遇到新汉字,就跑一下脚本,把新条目追加到表里,十分钟搞定一套日志用字库。
这里再提供一个我个人很喜欢的思路:日志文字尽量复用同一批高频汉字,比如“正常、异常、成功、失败、开始、结束、温度、电压、电流、速度、错误、警告”,这样映射表会非常小,查表也快,代码维护负担几乎为零。
5.5 方案之间怎么选
针对中文打印,我把三种常用方案摆在一起对比:
| 方案 | 推荐场景 | 优势 | 劣势 |
|---|---|---|---|
| 源码转UTF-8 | 新工程、编译链统一 | 改一次一劳永逸 | Keil对UTF-8支持不完美,旧工程风险大 |
| 运行时映射表 | 老工程、中文词少、想要可控 | 不动源码编码,增量维护简单 | 映射表需要自维护 |
| 硬编码UTF-8数组 | 极少数临时中文 | 最直白,零依赖 | 手写字节痛苦,不适合长文本 |
在实际项目中,我现在默认的姿势是:新工程一开始就把源文件设为UTF-8,从根上避开乱码;老工程用运行时映射表做增量改造,只在需要中文输出的函数里调用转换输出,其他地方不动。如果你的J-Link软件包版本较新,RTT Viewer的终端设置里提供了字符集选项,也可以尝试直接选择GB2312编码,如果能正常显示,那当然最省事。
6. 常见问题与排查技巧实录
6.1 “no J-Link found”与连接失败
这个报错是J-Link用户最常见的噩梦。我通常按下面这个顺序排查:
| 现象 | 可能的原因 | 解决办法 |
|---|---|---|
| 提示no J-Link found | J-Link驱动没装好或软件被其它工具卸载过 | 重新安装SEGGER J-Link Software Pack |
| USB口插入无反应 | USB线只供电不传输数据 | 换一根支持数据传输的USB线,台式机插后置USB口 |
| J-Link识别到但无法连接目标 | 目标板没上电,或SWD接线错误 | 确认VCC连接到目标板3.3V,GND共地 |
| Keil里能下载但RTT Viewer连不上 | J-Link被Keil调试会话占用 | 先退出Keil的调试状态,再打开RTT Viewer |
| 老款J-Link连接国产新芯片失败 | J-Link软件包版本太老,芯片ID无法识别 | 升级J-Link软件包,或先用J-Link Commander测试连接 |
如果J-Link在Keil里能正常下载程序,只是RTT Viewer连不上,绝大多数情况是软件占用问题,不是硬件问题。把Keil的调试窗口先关掉,甚至拔插一次USB,再打开RTT Viewer连接,基本都能解决。
6.2 日志丢失、卡顿和假死的排查
RTT日志丢数据,排查思路要从三个方向走:
第一,上行缓冲太小。日志输出频率高、数据量大时,缓冲很快被写满,新日志就会按配置被丢弃。解决办法是调大BUFFER_SIZE_UP,从4096再加到8192。这招九成情况下都能解决。
第二,SWD时钟速度太低。J-Link和目标的SWD速度如果只有几百kHz,数据搬运速度根本跟不上打印速度。把SWD速度调到4MHz以上,如果目标板稳定,甚至可以用更高的速度。速度调高后日志刷新会明显变快。
第三,打印调用太频繁。有些代码把调试打印放在了高频中断或者主循环里,每一轮都打印一遍,那数据必然刷屏。我的习惯是给这类日志加一个节流逻辑,例如每100ms只输出一次:
uint32_t next_log_time = 0; uint32_t now = get_tick_ms(); if (now >= next_log_time) { SEGGER_RTT_printf(0, "sensor:%d\r\n", sensor_val); next_log_time = now + 100; }有一个特别容易困惑的点:当Keil里触发断点、程序暂停时,RTT Viewer也会一起“停摆”。这不是RTT的bug,而是因为J-Link暂停了CPU,目标芯片不再运行,环形缓冲区自然也不再更新。你点在Keil里单步调试时看到RTT Viewer不动了,这是正常现象,全速运行后马上恢复。
6.3 中文在某些版本显示异常
如果你已经用了上面的某个方案,但还是发现个别字符显示不对,先确认一下目标芯片的Flash里存的字节是什么。用调试器直接在内存视图里查看字符串常量区,可以直观看到是字节没转对,还是转对了但RTT Viewer显示有问题。
另外,不要在SEGGER_RTT_printf里直接传中文字符串,因为SEGGER_RTT_printf内部在处理字符串时只拷贝原始字节,不会做任何编码转换。最稳妥的中文输出姿势就是我在5.3节写的:先格式化,再用自定义输出函数发送。
6.4 RTT Viewer和Keil调试同时开的兼容性
很多人习惯边用Keil调试边开着RTT Viewer看日志。这里的关键点是J-Link本身是否支持多进程共享。较新版本的SEGGER软件支持多个程序同时访问同一个J-Link,但老版本或者兼容性一般的时候,很容易出现“RTT Viewer连接成功但数据不刷新”或者Keil下载时报错“Cannot access target”的情况。
我的实际经验是:先让Keil进入调试模式,再手动打开RTT Viewer连接,通常可以共存;反过来先开RTT Viewer再开Keil,共存的成功率低一些。如果遇到冲突,不用纠结谁对谁错,先把RTT Viewer关了,让Keil把程序下载好进入调试状态,再重新打开RTT Viewer,按这个顺序走基本不打架。
6.5 实战中的几个独家小技巧
最后分享几个文档里看不到的习惯:
第一,给每条日志加上“房间号”。也就是在printf的开头固定打一个模块前缀,比如[ADC]、[MOTOR]、[RF],这样日志一多,扫一眼就能定位是哪个模块打的。
第二,日志里带时间戳。直接用芯片的systick或者任意毫秒计时器,打印时带上tick值,排查时序问题时方便到飞起。
第三,不要把敏感操作放在RTT打印前面。有时候RTT打印会影响时序,尤其是你在ISR里打印过多、格式化耗时太长,整个系统的实时性会被拖崩。调试阶段没问题,但定位时序问题时记得先把打印功能关掉,用真实的时序数据说话。
我个人现在的新板子调试流程是:板子到手先接好J-Link,SWD四根线搞定,第一件事就是跑通RTT,然后才轮到串口。串口只有在和外部设备通信、跑量产测试、或需要脱机记录日志的时候才会专门去接。RTT也不是万能的,它没法脱离J-Link独立工作,上电自检日志这类场景依然要依赖串口,但对于日常开发来说,它已经是我最顺手的调试手段。希望这篇文章能帮你把RTT的潜力都用起来,也能少踩几个我踩过的坑。