ARTICLE DETAIL

资讯详情

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

GD32调试新思路:J-Link RTT替代串口打印实战

GD32调试新思路:J-Link RTT替代串口打印实战 GD32 的调试手段很多人第一反应就是接个串口用串口调试助手看打印。这招确实简单但一旦板子跑起来、串口被业务逻辑占满或者你压根没引出串口线调试就变得很别扭。我最近在几个 GD32 项目里换了个思路用 SEGGER Embedded Studio 配合 J-Link 的 RTT 功能来做调试体验下来比传统串口打印舒服太多。RTT 全称 Real Time Transfer是 J-Link 自带的一种双向通信机制它不需要占用 MCU 的串口外设直接通过调试接口在目标内存里开一块缓冲区上位机去读写这块内存速度比串口快得多而且不打断程序运行。这篇文章就围绕 GD32 平台把 SEGGER Embedded Studio 和 J-Link RTT 这套组合的实战细节讲透包括环境搭建、RTT 移植、性能调优、常见坑点以及和 Keil、EIDE 这些环境的对比取舍。不管你是刚接触 GD32 的新手还是想从串口打印升级到更高效调试方式的老手都能从里面找到能直接抄作业的东西。1. 为什么 GD32 项目值得从串口打印切换到 RTT1.1 串口打印在 GD32 实战中的三个硬伤先说清楚为什么要换。串口打印不是不能用而是在 GD32 这类 Cortex-M 芯片上它有几个绕不开的限制。第一个是占用外设资源。GD32 的 USART 数量有限小容量型号可能就两三个。你把一个 USART 分给调试打印业务上就少一个可用串口。更麻烦的是很多项目里串口要接 485 总线、接上位机、接模组调试口和业务口抢资源的情况非常常见。我见过一个 GD32F303 的项目三个 USART 全被业务占满最后调试只能靠点灯效率低得离谱。第二个是打印本身拖慢实时性。串口打印是阻塞式的你用printf重定向到 USART每打印一行都要等波特率把数据一位一位送出去。115200 波特率下打印 100 个字符大概要 8.7 毫秒。如果这段代码在中断里或者在一个对时序敏感的循环里打印直接把实时性打崩。我实测过一个 1kHz 的控制循环里面加一句串口打印循环周期直接从 1ms 抖到 3ms 以上。第三个是接线和供电的麻烦。串口调试要接 TX、RX、GND 三根线还得单独供电板子跑在现场设备里的时候你不可能随时把串口线接出来。而 J-Link 的调试接口本来就在RTT 复用这个接口不用额外接线。1.2 RTT 到底快在哪里内存缓冲区机制拆解RTT 的原理其实不复杂但理解了它才能用好它。J-Link 在目标芯片的 RAM 里划出一块区域作为缓冲区这块区域由一段叫SEGGER_RTT的控制块管理。控制块里维护了若干个上行通道Target 到 Host和下行通道Host 到 Target每个通道有自己的环形缓冲区。目标代码调用SEGGER_RTT_Write的时候其实是把数据 memcpy 到上行缓冲区里然后更新写指针。这个过程是纯内存操作纳秒级完成不涉及任何外设等待。上位机那边J-Link 通过调试接口周期性去读这块内存把新数据取走显示。因为是内存操作RTT 的写入速度可以做到比串口快几十倍官方数据是最高能到 1MB/s 级别实际在 GD32 上跑几百 KB/s 是很轻松的。这里有个关键点RTT 不依赖中断。它默认工作在轮询模式目标代码写缓冲区上位机轮询读取。这意味着你甚至可以在中断服务函数里安全地调用 RTT 打印只要缓冲区够大不会因为等待外设而阻塞。这一点是串口打印做不到的。1.3 什么场景下 RTT 比串口更合适不是所有场景都无脑上 RTT得看情况。我总结了几类特别适合 RTT 的场景实时性敏感的控制循环电机控制、PID 调节、传感器融合这些地方打印不能拖慢周期RTT 的内存写入几乎无感。中断里需要输出调试信息串口在中断里打印容易出问题RTT 可以放心用。串口资源紧张的项目USART 全被业务占用时RTT 是救命的。需要双向交互的调试RTT 支持下行通道上位机可以发命令给 MCU实现简单的命令行交互比串口还方便。量产前的日志抓取RTT 可以配合 J-Link 把日志直接存文件不用外接任何设备。反过来如果板子已经量产、没有调试接口或者你只是偶尔打印一句那串口也够用。RTT 的价值在调试密集、实时性要求高的开发阶段体现得最明显。2. SEGGER Embedded Studio 环境搭建与 GD32 工程配置2.1 安装 SES 与 J-Link 软件包的顺序讲究SEGGER Embedded Studio 简称 SES是 SEGGER 自家的 IDE对 J-Link 和 RTT 的支持是原生的不用额外装插件。安装顺序上有个小细节先装 J-Link Software and Documentation Pack再装 SES。因为 SES 在安装时会去检测系统里的 J-Link 驱动如果先装 SES 后装 J-Link有时候 SES 里的调试配置识别不到设备得手动指定路径。J-Link 软件包去 SEGGER 官网下载选对应操作系统的版本。装完之后设备管理器里能看到 J-Link 设备命令行敲JLinkExe能进交互界面就说明驱动没问题。SES 下载的时候注意选对版本免费版对非商业用途够用商业项目要买 License。装好之后打开 SES新建工程时选 Create a new project然后选 C/C 下的 Executable芯片选 GD32 对应的型号。SES 内置了 GD32 的器件支持包如果列表里没有你的具体型号选同系列相近的即可比如 GD32F303 系列选 GD32F303xE。2.2 从零建立 GD32 标准工程模板SES 新建的工程是空的需要把 GD32 的启动文件、链接脚本、外设库加进去。这里我建议直接建一个标准工程模板以后新项目复制一份改改就行。需要的文件清单启动文件startup_gd32f30x_hd.s根据具体型号选hd 是大容量md 是中容量链接脚本gd32f30x_flash.ld这个要自己根据芯片的 Flash 和 RAM 大小改外设库GD32 官方固件库里的GD32F30x_standard_peripheral目录系统文件system_gd32f30x.c主文件main.c链接脚本是重点很多人在这里踩坑。GD32F303xE 的 Flash 是 512KBRAM 是 64KB链接脚本里要写清楚MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 512K RAM (rwx) : ORIGIN 0x20000000, LENGTH 64K }栈顶地址设成 RAM 末尾也就是0x20010000。这些参数写错程序要么跑不起来要么一进中断就 HardFault。工程配置里还要注意编译优化等级。调试阶段建议用-O0或-Og优化等级太高会把变量优化掉RTT 打印出来的值可能不对断点也会乱跳。等调试完了再切到-O2发布。2.3 调试器配置里那几个容易填错的参数SES 的调试配置在 Project - Options - Debug 里。几个关键项Target Interface选 SWDGD32 支持 SWD 和 JTAGSWD 占引脚少速度也够。Target Device选具体型号选错会导致 Flash 烧写算法不匹配。SpeedSWD 时钟默认 4000kHz 一般没问题如果线长或者干扰大降到 1000kHz 更稳。Reset选 Reset and Halt 或者 Connect under Reset。GD32 有时候程序跑飞了普通连接连不上得用 Connect under Reset这个在调试锁死的芯片时特别有用。还有一个隐藏坑J-Link 固件版本和 SES 版本的兼容性。老版本的 J-Link 固件配新 SES可能出现 No J-Link found 或者连接超时。解决办法是用 J-Link 自带的 J-Link Configurator 升级固件或者降级 SES 版本。我遇到过 J-Link V9 固件太老SES 6.x 连不上升级固件后就好了。3. RTT 在 GD32 上的移植与代码落地3.1 RTT 源码文件的最小集与放置位置RTT 的源码在 J-Link 软件包的Samples/RTT目录下核心就几个文件SEGGER_RTT.cSEGGER_RTT.hSEGGER_RTT_Conf.hSEGGER_RTT_printf.c可选提供格式化打印把这几个文件加到 SES 工程里SEGGER_RTT_Conf.h是配置文件主要改缓冲区大小和通道数量。默认配置是上行通道 0 用 1KB 缓冲区下行通道 0 用 16 字节。对于大多数调试场景上行缓冲区建议加大到 2KB 或 4KB因为打印密集的时候 1KB 容易溢出。放置位置上SEGGER_RTT.c和SEGGER_RTT_printf.c加到源文件组头文件路径加到 include 路径里。SEGGER_RTT_Conf.h可以放在工程根目录方便修改。3.2 缓冲区大小与通道配置的取舍SEGGER_RTT_Conf.h里几个关键宏#define BUFFER_SIZE_UP (1024) // 上行缓冲区Target 到 Host #define BUFFER_SIZE_DOWN (16) // 下行缓冲区Host 到 Target #define SEGGER_RTT_MAX_NUM_UP_BUFFERS (3) #define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (3) #define SEGGER_RTT_MODE_DEFAULT SEGGER_RTT_MODE_NO_BLOCK_SKIPSEGGER_RTT_MODE_DEFAULT这个模式很关键有三个选项SEGGER_RTT_MODE_NO_BLOCK_SKIP缓冲区满了直接丢弃新数据不阻塞。调试实时系统首选这个保证不拖慢程序。SEGGER_RTT_MODE_NO_BLOCK_TRIM缓冲区满了能塞多少塞多少剩下的丢掉。SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL缓冲区满了就阻塞等待。千万别在中断里用这个会死锁。我一般上行缓冲区设 2KB模式用 SKIP。如果打印量特别大比如要抓一段高频日志可以临时把缓冲区加到 8KB但注意 GD32 的 RAM 本来就紧张别把业务内存挤没了。3.3 封装一套顺手的 RTT 打印宏直接用SEGGER_RTT_printf有点啰嗦我习惯封装一层宏用起来跟printf一样#include SEGGER_RTT.h #define LOG_INFO(fmt, ...) SEGGER_RTT_printf(0, [INFO] fmt \r\n, ##__VA_ARGS__) #define LOG_WARN(fmt, ...) SEGGER_RTT_printf(0, [WARN] fmt \r\n, ##__VA_ARGS__) #define LOG_ERR(fmt, ...) SEGGER_RTT_printf(0, [ERR ] fmt \r\n, ##__VA_ARGS__)这样代码里写LOG_INFO(adc value %d, adc_val);就行输出带级别前缀方便在 RTT Viewer 里过滤。注意SEGGER_RTT_printf的第一个参数是通道号0 是默认上行通道。有个细节SEGGER_RTT_printf内部用的是自己的格式化实现不支持浮点。如果要打印浮点得用SEGGER_RTT_printf的浮点版本或者在配置里打开浮点支持。我一般把浮点转成整数打印比如(int)(value * 1000)省事又省空间。3.4 在中断里安全使用 RTT 的注意事项前面说了 RTT 可以在中断里用但有几个前提第一模式必须是 NO_BLOCK_SKIP 或 NO_BLOCK_TRIM不能用 BLOCK。中断里阻塞等于系统挂掉。第二缓冲区要够大。中断频率高的时候上位机轮询速度可能跟不上缓冲区小了会丢数据。我一般给中断里的打印单独开一个上行通道比如通道 1缓冲区设 1KB和主循环的通道 0 分开避免互相干扰。第三不要在中断里做复杂格式化。SEGGER_RTT_printf的格式化本身要花时间中断里尽量只打印原始数据格式化留给上位机或者主循环。比如中断里只SEGGER_RTT_Write(1, raw_data, sizeof(raw_data))主循环再解析。4. RTT Viewer 与命令行工具的实战用法4.1 RTT Viewer 的连接与多通道显示RTT Viewer 是 J-Link 软件包里自带的图形化工具打开后选好芯片型号和接口点 OK 就能连上。连接成功后默认显示通道 0 的数据。多通道的用法在 RTT Viewer 里可以开多个终端窗口每个窗口绑定不同的通道。比如通道 0 看主日志通道 1 看中断数据通道 2 看下行命令回显。这样信息分类清晰不会混在一起。RTT Viewer 还有个实用功能是日志存文件。菜单里选 Log to File可以把所有 RTT 输出存成文本方便事后分析。抓偶发 bug 的时候特别有用让板子跑一晚上第二天看日志找线索。4.2 用 JLinkRTTClient 做纯命令行日志抓取图形界面有时候不方便比如在远程服务器上或者自动化测试里。这时候用JLinkRTTClient命令行工具。它连上 J-Link 后把 RTT 数据直接打到标准输出可以重定向到文件JLinkRTTClient rtt_log.txt配合JLinkExe的脚本模式还能实现自动烧写加日志抓取。我做过一个自动化测试脚本流程是JLinkExe 烧写固件 - 复位运行 - JLinkRTTClient 抓 30 秒日志 - 分析日志判断测试通过。整套下来不用人工干预。4.3 下行通道实现简易命令行交互RTT 的下行通道可以让上位机发数据给 MCU。用法是在代码里调SEGGER_RTT_Readchar cmd_buf[64]; unsigned num_read SEGGER_RTT_Read(0, cmd_buf, sizeof(cmd_buf)); if (num_read 0) { // 解析命令 }在 RTT Viewer 的输入框里敲命令回车MCU 就能收到。我一般用它做几个调试命令查变量、改参数、触发测试。比重新烧写固件快多了。比如调 PID 参数的时候通过下行通道实时改 Kp、Ki看着波形调效率翻倍。注意下行通道默认缓冲区只有 16 字节要发长命令得改BUFFER_SIZE_DOWN。我一般设成 64 或 128 字节。5. 性能调优与常见问题排查5.1 RTT 时延的正确理解与测量很多人问 RTT 的时延是多少。这里要分清楚两个概念写入时延和显示时延。写入时延是目标代码调SEGGER_RTT_Write到数据进缓冲区的时间这个基本就是 memcpy 的时间几微秒级别可以忽略。显示时延是数据进缓冲区到上位机显示出来的时间这个取决于 J-Link 的轮询周期默认大概几毫秒到几十毫秒。所以 RTT 适合看日志不适合做精确的时序测量。要测时序还是得用示波器或者 GPIO 翻转。如果觉得显示时延大可以在 RTT Viewer 里调 Polling Period调小一点刷新更快但会占用更多调试接口带宽。5.2 缓冲区溢出丢数据的定位方法RTT 丢数据最常见的原因就是缓冲区溢出。怎么判断SEGGER_RTT_Write的返回值是实际写入的字节数如果小于你要写的长度说明溢出了。可以在封装宏里加个计数static uint32_t rtt_drop_cnt 0; int written SEGGER_RTT_Write(0, buf, len); if (written len) { rtt_drop_cnt; }定期打印rtt_drop_cnt如果一直在涨就得加大缓冲区或者降低打印频率。我遇到过一个案例主循环里每毫秒打印一次每次 200 字节1KB 缓冲区几秒就溢出。后来改成每 100 毫秒打印一次问题解决。5.3 连接失败、找不到设备的排查链路No J-Link found 或者连接超时排查顺序是这样的物理连接SWD 的 SWCLK、SWDIO、GND 三根线是否接好有没有虚焊。VCC 要不要接看情况J-Link 一般能检测目标电压。目标供电板子有没有上电电压是否正常。J-Link 连不上很多时候是目标没供电。复位状态程序跑飞或者进了低功耗模式普通连接连不上。用 Connect under Reset让 J-Link 在复位期间接管。芯片锁死GD32 如果读保护开了J-Link 连不上。这时候要用 J-Link 的解锁功能或者用 GD32 的 DFU 模式擦除。GD32 的 DFU 驱动装好进 Boot 模式用官方工具擦全片。驱动和固件J-Link 驱动版本、固件版本、SES 版本三者兼容性。升级或降级试试。这个链路我踩过好几次最坑的是芯片锁死那次折腾了半天才发现是读保护。后来养成习惯新板子第一次连接前先确认读保护状态。5.4 和 Keil、EIDE 环境的取舍对比SES 不是唯一选择Keil 和 VSCode 的 EIDE 插件也能配 RTT。对比一下环境RTT 支持配置难度适合场景SEGGER Embedded Studio原生最顺低新项目追求调试体验Keil MDK需手动加 RTT 源码中已有 Keil 工程不想换VSCode EIDE需手动加 RTT 源码中高喜欢 VSCode多语言开发Keil 里用 RTT把 RTT 源码加进工程然后在 Debug 设置里选 J-LinkRTT Viewer 照样能用。EIDE 类似只是工程配置在 JSON 里。如果团队已经用 Keil 多年没必要为了 RTT 换 IDE加几个文件的事。但如果是新项目SES 的原生支持确实省心调试配置、RTT 集成都是一步到位。我个人现在的做法是新项目用 SES老项目继续 Keil 加 RTT 源码。两套并行互不影响。6. 把 RTT 用出花几个进阶玩法6.1 用 RTT 做简易示波器RTT 的高速特性可以用来传波形数据。思路是定时器触发 ADC 采样采样值通过 RTT 上行通道发出去上位机用 Python 脚本接收并实时画图。J-Link 软件包里有个JLinkRTTLogger工具可以把 RTT 数据导成文件Python 读文件画图。我做过一个电机电流环的调试采样率 10kHz每次采 100 个点打包发RTT 完全扛得住。上位机用 matplotlib 实时刷新能看到电流波形调 PID 的时候直观多了。这比串口传波形快得多串口 115200 波特率传 10kHz 采样根本不可能。6.2 结合日志分级做运行时过滤调试信息太多的时候全打出来会淹没关键信息。我在 RTT 封装里加了日志级别#define LOG_LEVEL_ERR 1 #define LOG_LEVEL_WARN 2 #define LOG_LEVEL_INFO 3 #define LOG_LEVEL_DEBUG 4 #ifndef LOG_LEVEL #define LOG_LEVEL LOG_LEVEL_INFO #endif #define LOG_DEBUG(fmt, ...) do { if (LOG_LEVEL LOG_LEVEL_DEBUG) \ SEGGER_RTT_printf(0, [DBG ] fmt \r\n, ##__VA_ARGS__); } while(0)编译时通过LOG_LEVEL宏控制输出级别发布版本设成 ERR只打错误。调试版本设成 DEBUG全打。这样一套代码不同版本自动切换日志量不用改代码。6.3 多通道分离业务日志与调试日志前面提过多通道这里展开说。我一般这样分配通道 0业务日志正常运行时也保留用于现场问题追溯。通道 1调试日志只在开发阶段用发布时关掉。通道 2下行命令通道接收上位机指令。每个通道在 RTT Viewer 里开独立窗口互不干扰。业务日志可以一直开着存文件调试日志随时开关。这种分离让日志管理清晰很多不会因为调试信息把业务日志冲掉。6.4 量产阶段的 RTT 保留策略量产固件里要不要保留 RTT我的建议是保留代码但关闭输出。RTT 代码本身占的 Flash 和 RAM 很小几 KB 而已。保留着万一现场出问题接上 J-Link 就能抓日志不用重新烧调试固件。关闭输出通过日志级别宏控制发布版本设成 ERR 级别平时不输出需要时改个宏重新编译。如果 Flash 实在紧张可以用条件编译把 RTT 代码整个排除#ifdef ENABLE_RTT #include SEGGER_RTT.h #define LOG_INFO(...) SEGGER_RTT_printf(0, __VA_ARGS__) #else #define LOG_INFO(...) #endif发布版本不定义ENABLE_RTTRTT 代码完全不参与编译零开销。这套 RTT 调试方案我在几个 GD32 项目里跑下来开发效率提升很明显。最直观的感受是以前调一个偶发 bug 要反复烧写加串口打印现在 RTT 常开日志自动存文件问题复现的时候直接翻日志就行。而且 RTT 不占串口业务调试两不误。如果你还在用串口打印调 GD32真心建议花半天时间把 RTT 搭起来后面省下的时间远不止这半天。
返回列表