☰
GD32调试利器:RTT实时传输替代串口打印实战指南
2026/9/28 1:12:30 网站建设 项目流程

1. 为什么我放弃了串口打印,转投RTT

搞GD32开发的朋友大概率都经历过这个场景:板子已经装进外壳,或者电机一转起来,串口助手就开始疯狂丢包,printf出来的波形数据全是乱码。更难受的是,有些项目对实时性要求高,你往串口里塞几行调试信息,主循环的时序直接崩了。我早期做无刷电机FOC调试的时候就吃过这个亏,用UART打角度和电流,波特率拉到921600还是不够用,最后只能把调试代码全删了才勉强跑通。

后来接触到SEGGER的RTT(Real Time Transfer)技术,配合J-Link调试器,才算是找到了一个真正能打的方案。RTT的本质是在MCU的内存里开一块环形缓冲区,J-Link通过调试接口直接读写这块内存,不占用任何外设资源,也不需要额外的引脚。理论上速度能到兆字节每秒级别,实际用下来,在GD32F303上跑到几百KB/s毫无压力,比串口快了不止一个数量级。

这篇文章主要面向已经有一定GD32开发基础、正在被调试效率困扰的工程师。我会从环境搭建讲起,把SEGGER Embedded Studio的配置、J-Link RTT的移植、实际项目中的调试技巧,以及我踩过的那些坑,全部掰开揉碎讲清楚。不管你是用Keil还是SES,不管你是调试GD32F103还是GD32F450,这套方法都能直接套用。

2. 环境准备与工具链选型

2.1 为什么选SEGGER Embedded Studio而不是Keil

先说结论:如果你手头有J-Link,SES是调试GD32最省心的选择。Keil当然也能用,但RTT的集成度差很多。SES是SEGGER自家出的IDE,对J-Link的支持是原生级别的,RTT Viewer、RTT Client这些工具直接内置,不需要额外配置。

我实测过Keil MDK + J-Link RTT的方案,需要手动把SEGGER_RTT.c和SEGGER_RTT_printf.c加到工程里,然后在Keil的Debug设置里勾选"Enable Real-Time Terminal",步骤不算复杂,但有个致命问题:Keil的调试会话一旦暂停,RTT输出就断了。SES没这个问题,调试器暂停时RTT缓冲区还在,恢复后数据继续往外吐。

SES的下载和安装这里不展开,官网直接下就行。需要注意的是,SES有商业版和免费版,免费版对GD32这种Cortex-M芯片完全够用,没有代码大小限制。安装完之后,第一次启动会让你选J-Link的安装路径,如果你电脑上已经装了J-Link Software Pack,它会自动识别。

2.2 J-Link驱动与固件版本匹配

这里有个坑我必须提前说:J-Link的驱动版本和固件版本不匹配,会导致RTT控制块找不到。我遇到过好几次"RTT Viewer连不上"的情况,最后发现是J-Link固件太老,不支持新版RTT协议。

检查方法很简单,打开J-Link Commander,输入ShowEmuList,看固件版本。2023年之后的J-Link OB固件基本都支持RTT,但如果你用的是山寨J-Link,固件可能停留在2018年,那就得先升级。升级有风险,山寨J-Link升级后可能变砖,这个自己权衡。

驱动方面,建议用J-Link Software and Documentation Pack V7.88以上版本。安装的时候注意,如果你同时装了Keil和SES,安装程序会问你要不要替换Keil的J-Link DLL,选"是"就行,不影响Keil正常使用。

2.3 GD32的Pack支持与工程模板

SES本身不包含GD32的器件支持包,需要手动添加。GD32的Pack文件在GigaDevice官网能下到,文件名类似GigaDevice.GD32F30x_DFP.x.x.x.pack。下载后直接双击安装,SES会自动识别。

如果你用的是GD32E23x或者GD32L233这些比较新的系列,Pack可能更新不及时。我的做法是直接拿STM32的对应型号Pack来用,比如GD32F103就用STM32F103的Pack,寄存器地址基本兼容,调试功能不受影响。但要注意,Flash算法得换成GD32专用的,不然烧录会失败。

工程模板我建议自己建一个,不要用SES自带的示例。步骤是:新建工程 -> 选芯片型号 -> 添加启动文件 -> 添加GD32标准外设库 -> 配置头文件路径 -> 设置J-Link调试。这套流程走下来大概十分钟,建好之后存成模板,以后新项目直接复制。

3. RTT核心原理与移植实操

3.1 RTT到底是怎么工作的

RTT的机制其实不复杂,我画个简单的类比你就明白了。想象MCU的内存里有一块公告栏,MCU往上贴纸条(写数据),J-Link在旁边看(读数据),双方约定好公告栏的位置和格式。这个"公告栏"就是RTT控制块,里面包含了读写指针、缓冲区大小、通道数量等信息。

具体来说,RTT在内存里维护一个SEGGER_RTT_CB结构体,这个结构体里有个数组acUpBuffer,默认是1KB,用于MCU向PC发送数据。还有个acDownBuffer,也是1KB,用于PC向MCU发送数据。MCU调用SEGGER_RTT_Write的时候,数据被拷贝到acUpBuffer,然后更新写指针。J-Link通过调试接口轮询这个写指针,发现有新数据就通过USB传给PC端的RTT Viewer。

整个过程MCU侧只做内存拷贝,不涉及任何外设操作,所以速度极快。我实测在GD32F303(120MHz)上,连续调用SEGGER_RTT_printf输出浮点数,吞吐量能稳定在400KB/s以上。作为对比,115200波特率的串口理论最大也就11.5KB/s,差了三十多倍。

3.2 把RTT源码加入GD32工程

RTT的源码是开源的,在J-Link安装目录下能找到,路径大概是C:\Program Files\SEGGER\JLink\Samples\RTT。核心文件就四个:SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h、SEGGER_RTT_printf.c。把这四个文件复制到你的工程目录,然后在IDE里添加到工程。

SEGGER_RTT_Conf.h里有几个关键配置需要改:

#define BUFFER_SIZE_UP (1024) // 上行缓冲区大小 #define BUFFER_SIZE_DOWN (16) // 下行缓冲区大小 #define SEGGER_RTT_MAX_NUM_UP_BUFFERS (2) // 上行通道数 #define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (2) // 下行通道数 #define SEGGER_RTT_MODE_DEFAULT SEGGER_RTT_MODE_NO_BLOCK_SKIP

BUFFER_SIZE_UP我一般设成1024或者2048,太小了容易丢数据,太大了浪费RAM。SEGGER_RTT_MODE_DEFAULT建议用NO_BLOCK_SKIP,意思是缓冲区满了就丢弃新数据,不阻塞MCU。如果你调试的是实时性要求极高的场景,千万别用BLOCK_IF_FIFO_FULL,否则MCU会卡在RTT写入里出不来。

移植的时候有个细节要注意:RTT源码默认用SEGGER_RTT_LOCK()和SEGGER_RTT_UNLOCK()做临界区保护,这两个宏在SEGGER_RTT_Conf.h里定义。如果你的工程里已经有关中断的宏,直接替换过去就行。比如GD32的标准库里有__disable_irq()和__enable_irq(),直接填进去。

3.3 初始化与第一个RTT输出

RTT不需要显式初始化,第一次调用SEGGER_RTT_Write的时候会自动初始化控制块。但为了保险,我习惯在main函数开头加一句SEGGER_RTT_Init(),确保控制块在内存里的位置固定。

最简单的测试代码:

#include "SEGGER_RTT.h" int main(void) { SEGGER_RTT_Init(); while(1) { SEGGER_RTT_printf(0, "Hello GD32! Tick: %d\n", GetTickCount()); Delay_ms(500); } }

烧录进去之后,打开J-Link RTT Viewer,选择你的J-Link设备,连接目标芯片,就能看到输出了。如果连不上,先检查J-Link Commander能不能识别到芯片,再检查RTT控制块地址是否正确。

注意:RTT Viewer连接的时候会让你选"Target Interface",GD32用SWD就行,别选JTAG,除非你板子上真的引出了JTAG引脚。

4. 高效调试实战技巧

4.1 多通道分离:把日志和波形分开

RTT支持多个上行通道,默认通道0是给终端用的,通道1可以拿来传波形数据。这个功能在调试PID参数的时候特别好用,你可以把设定值、实际值、误差分别往不同通道发,然后在RTT Viewer里开多个窗口同时看。

配置方法是在SEGGER_RTT_Conf.h里把SEGGER_RTT_MAX_NUM_UP_BUFFERS改成3或者更多,然后调用的时候指定通道号:

SEGGER_RTT_printf(0, "PID Debug Start\n"); // 通道0,文本日志 SEGGER_RTT_Write(1, &setpoint, sizeof(float)); // 通道1,波形数据 SEGGER_RTT_Write(2, &actual, sizeof(float)); // 通道2,波形数据

RTT Viewer里可以开多个Terminal窗口,分别绑定不同通道。我一般通道0看printf日志,通道1和2用"Data"模式显示,能直接看到数值变化曲线。

4.2 用RTT替代串口做命令行交互

RTT的下行通道可以用来接收PC发来的命令,实现一个轻量级的命令行界面。这个在调试参数的时候比重新烧录固件方便多了。实现思路是:在main循环里轮询SEGGER_RTT_HasKey(),有按键就读取,然后解析命令。

char cmd_buffer[64]; int cmd_index = 0; void Poll_RTT_Command(void) { if(SEGGER_RTT_HasKey()) { char ch = SEGGER_RTT_GetKey(); if(ch == '\r' || ch == '\n') { cmd_buffer[cmd_index] = '\0'; Execute_Command(cmd_buffer); cmd_index = 0; } else if(cmd_index < sizeof(cmd_buffer) - 1) { cmd_buffer[cmd_index++] = ch; } } }

Execute_Command里可以解析"set kp 1.5"这种命令,直接修改全局变量。我调试电机的时候,用这套机制在线调PID参数,省了无数次烧录的时间。

4.3 性能分析:用RTT做代码执行时间测量

RTT还能用来做性能分析。原理很简单:在函数入口和出口分别调用SEGGER_RTT_Write记录时间戳,然后在PC端算差值。但这样会引入RTT本身的耗时,所以更精确的做法是用DWT(Data Watchpoint and Trace)计数器。

不过DWT需要额外配置,我一般用RTT做粗粒度分析就够了。比如测量一个PID计算函数的耗时:

uint32_t t1 = DWT->CYCCNT; PID_Calculate(); uint32_t t2 = DWT->CYCCNT; SEGGER_RTT_printf(0, "PID cost: %d cycles\n", t2 - t1);

DWT的初始化代码:

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

GD32的Cortex-M3/M4内核都支持DWT,M23内核(比如GD32E23x)不支持,这个要注意。

4.4 在SES里配置RTT Viewer自动启动

每次调试都要手动打开RTT Viewer很麻烦,SES支持在调试会话启动时自动拉起RTT Viewer。配置方法:Project -> Options -> Debug -> J-Link,勾选"Start RTT Viewer",然后设置RTT Viewer的路径。这样每次点Debug,RTT Viewer会自动打开并连接。

还有个更省事的做法:直接用SES内置的"Debug Terminal"。在View -> Debug Terminal里打开,它会自动连接RTT通道0,不需要额外开RTT Viewer。但内置终端的功能比较弱,不支持多通道和波形显示,看纯文本日志够用。

5. 常见问题与排查实录

5.1 RTT Viewer连不上目标芯片

这是最常见的问题,排查顺序如下:

现象可能原因解决方法
J-Link Commander识别不到芯片SWD接线错误或芯片未供电检查SWDIO/SWCLK/GND接线,测量芯片供电
能识别芯片但RTT连不上RTT控制块未初始化确认代码里调用了SEGGER_RTT_Init或SEGGER_RTT_Write
RTT连上但无输出缓冲区地址冲突检查链接脚本,确保RTT缓冲区不被其他变量覆盖
输出乱码时钟配置错误检查SystemCoreClock是否正确,RTT不依赖时钟但printf依赖
输出一段时间后停止缓冲区溢出增大BUFFER_SIZE_UP或降低输出频率

我遇到最多的是第三种情况。RTT控制块默认放在.bss段,如果你用了自定义链接脚本,把.bss段放到了特殊位置(比如ITCM),J-Link可能读不到。解决办法是在SEGGER_RTT_Conf.h里把SEGGER_RTT_SECTION定义成.data段,或者直接指定一个固定地址。

5.2 GD32芯片锁住后的解锁方法

调试过程中难免会遇到芯片被锁的情况,表现是J-Link连不上,报"Could not find core in Coresight setup"。GD32的解锁方法和STM32类似,但有个细节不一样。

用J-Link Commander解锁的步骤:

J-Link> connect J-Link> unlock J-Link> erase

如果unlock命令无效,试试WriteDP 1 0x50000000,然后erase。GD32F103系列有时候需要先拉低BOOT0引脚再上电,进入系统存储器启动模式,然后用J-Link擦除。

还有个更彻底的方法:用GD32的ISP工具,通过串口擦除。但这个方法需要板子上有串口引出,而且得把BOOT0拉高。

注意:解锁会擦除整个Flash,如果芯片里有未备份的代码,先想办法读出来再解锁。J-Link的savebin命令可以读Flash内容。

5.3 RTT输出影响中断实时性

RTT虽然快,但如果在中断里调用SEGGER_RTT_printf,还是会引入微秒级的延迟。我实测在GD32F303上,一次SEGGER_RTT_printf输出20个字符大约耗时3-5微秒。对于大多数中断来说这个延迟可以接受,但如果你在做高频PWM控制(比如100kHz以上),中断里调RTT就会导致波形畸变。

我的做法是在中断里只做数据采集,把数据存到全局数组,在主循环里统一用RTT输出。如果数据量太大,就用DMA把数据搬到RAM,主循环慢慢发。

5.4 SES调试时变量显示不正确

SES的调试器有时候会显示错误的变量值,尤其是局部变量和结构体。这个问题通常是因为编译器优化导致的。解决办法是在Project -> Options -> Code -> Optimization里把优化等级降到-O0或者-Og。-Og是调试优化,既保证调试信息准确,又不会让代码慢太多。

如果必须用-O2,可以在变量定义前加volatile关键字,强制编译器每次都从内存读取。但volatile会影响性能,只建议在调试关键变量时临时加。

还有个技巧:在SES的Watch窗口里,右键变量 -> "Add to Watch",然后勾选"Show as Array"或者"Show as Struct",能展开看结构体成员。这个在调试通信协议的时候特别有用。

6. 进阶玩法:RTT与自动化测试结合

6.1 用RTT Client做自动化脚本

RTT Client是命令行版的RTT工具,支持通过Telnet接口读取RTT数据。这意味着你可以用Python脚本连接RTT Client,自动解析MCU输出的数据,做自动化测试。

基本流程是:启动JLinkRTTClient.exe,它会监听本地的Telnet端口(默认19021),然后用Python的socket库连接这个端口,读取数据。

import socket import time sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', 19021)) sock.settimeout(1.0) while True: try: data = sock.recv(1024) if data: print(data.decode('utf-8', errors='ignore'), end='') except socket.timeout: continue

这套方案我用来做电机参数的批量测试,MCU每完成一次测试就通过RTT输出结果,Python脚本自动记录到CSV文件。比人工看串口助手效率高太多了。

6.2 RTT日志保存到文件

RTT Viewer本身支持日志保存,在File -> Log to File里设置路径就行。但RTT Viewer的日志是带时间戳和通道标记的,格式比较乱。如果你想要干净的日志,用RTT Client + Python脚本自己写文件更灵活。

我一般会在Python脚本里加个时间戳,然后按日期分文件保存:

import datetime filename = datetime.datetime.now().strftime('rtt_log_%Y%m%d_%H%M%S.txt') with open(filename, 'w') as f: while True: data = sock.recv(1024) if data: f.write(data.decode('utf-8', errors='ignore')) f.flush()

flush()很重要,不然程序崩溃的时候日志会丢。

6.3 与VSCode EIDE插件配合

如果你习惯用VSCode开发GD32,EIDE插件也支持RTT调试。配置方法是在.eide/eide.json里添加RTT配置:

{ "debug": { "rtt": { "enabled": true, "address": "auto", "channel": 0 } } }

EIDE会自动调用J-Link的RTT功能,在VSCode的终端里显示输出。但EIDE的RTT支持不如SES完善,多通道和波形显示都不行,适合简单的日志输出。

7. 我踩过的那些坑

第一个坑是RTT缓冲区地址冲突。有次我把RTT缓冲区定义在了一个自定义段里,结果和堆栈重叠了,MCU一跑就HardFault。排查了半天才发现是链接脚本的问题。后来我学乖了,RTT缓冲区就用默认的.bss段,不折腾。

第二个坑是J-Link固件版本太老。有块山寨J-Link,固件是2016年的,RTT Viewer死活连不上。升级固件之后好了,但升级过程中差点变砖,心跳都漏了一拍。所以如果你用的是山寨J-Link,升级前先做好心理准备。

第三个坑是RTT输出太多导致缓冲区溢出。有次我在主循环里每毫秒输出一次IMU数据,每次输出100多字节,结果RTT Viewer显示的数据断断续续。后来把BUFFER_SIZE_UP从1KB加到4KB,问题解决。但4KB对RAM只有20KB的GD32F103来说有点奢侈,最后改成每10毫秒输出一次,缓冲区保持1KB。

第四个坑是SES的调试终端和RTT Viewer同时打开会冲突。两个工具抢同一个RTT控制块,导致数据错乱。解决办法是只用其中一个,我一般用RTT Viewer,功能更全。

最后一个坑是关于RTT时延的误解。很多人以为RTT是"零延迟",其实不是。RTT的延迟主要来自J-Link的轮询周期,默认是1ms左右。如果你需要微秒级的实时性,RTT做不到。但对于大多数调试场景,1ms的延迟完全可以接受。

提示:RTT Viewer里可以设置"Polling Period",最小能到100微秒,但会增加J-Link的USB负载。如果不是特别需要,保持默认的1ms就行。

这套RTT调试方案我在GD32F103、GD32F303、GD32F450上都验证过,稳定性没问题。唯一需要注意的是RAM占用,RTT缓冲区加上控制块大概占2KB左右,对于RAM紧张的型号(比如GD32E230只有4KB RAM),得把缓冲区调小到256字节。

如果你也在用GD32做开发,强烈建议花半个小时把RTT环境搭起来。一旦用习惯了,就再也回不去串口打印了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询