ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GD32调试利器:RTT实时传输替代串口打印实战指南

GD32调试利器:RTT实时传输替代串口打印实战指南 1. 为什么我放弃了串口打印转投RTT搞GD32开发的朋友大概率都经历过这个场景板子已经装进外壳或者电机一转起来串口助手就开始疯狂丢包printf出来的波形数据全是乱码。更难受的是有些项目对实时性要求高你往串口里塞几行调试信息主循环的时序直接崩了。我早期做无刷电机FOC调试的时候就吃过这个亏用UART打角度和电流波特率拉到921600还是不够用最后只能把调试代码全删了才勉强跑通。后来接触到SEGGER的RTTReal 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-LinkSES是调试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侧只做内存拷贝不涉及任何外设操作所以速度极快。我实测在GD32F303120MHz上连续调用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_SKIPBUFFER_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 InterfaceGD32用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本身的耗时所以更精确的做法是用DWTData 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内核都支持DWTM23内核比如GD32E23x不支持这个要注意。4.4 在SES里配置RTT Viewer自动启动每次调试都要手动打开RTT Viewer很麻烦SES支持在调试会话启动时自动拉起RTT Viewer。配置方法Project - Options - Debug - J-Link勾选Start RTT Viewer然后设置RTT Viewer的路径。这样每次点DebugRTT 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_WriteRTT连上但无输出缓冲区地址冲突检查链接脚本确保RTT缓冲区不被其他变量覆盖输出乱码时钟配置错误检查SystemCoreClock是否正确RTT不依赖时钟但printf依赖输出一段时间后停止缓冲区溢出增大BUFFER_SIZE_UP或降低输出频率我遇到最多的是第三种情况。RTT控制块默认放在.bss段如果你用了自定义链接脚本把.bss段放到了特殊位置比如ITCMJ-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, errorsignore), 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, errorsignore)) f.flush()flush()很重要不然程序崩溃的时候日志会丢。6.3 与VSCode EIDE插件配合如果你习惯用VSCode开发GD32EIDE插件也支持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环境搭起来。一旦用习惯了就再也回不去串口打印了。
返回列表