基于Qt的J-Link RTT调试上位机:从原理到实现
发布时间:2026/9/1 2:29:18来源:尧图网络
简介这是一套面向嵌入式开发初学者与进阶者的RTTReal-Time-Thread调试辅助工具专为ARM架构单片机调试场景设计适用于电子信息、自动化、物联网等专业学生的课程设计、毕业设计及工程实践。资源基于Qt 5.x框架开发包含完整可运行的上位机源码、编译配置文件.pro、Makefile、界面资源.ui、.ico、.png、串口通信核心模块comfunc、seggerop及详细使用文档支持实时日志接收、命令下发与基础参数配置。压缩包共40个文件涵盖7个.cpp、8个.h、2个.ini、1个.ui及可执行exe等关键类型总大小仅190KB结构清晰、模块解耦便于理解Qt信号槽机制与嵌入式通信协议对接逻辑。已有76人下载学习代码经实测运行稳定附带README.md说明与高分项目答辩背景可直接用于毕设演示或在此基础上扩展多设备管理、数据可视化等功能。 做单片机调试的人应该都遇到过这个场景程序烧进去没反应想看看日志串口 printf 倒是能打但为了插一个 USB 转 TTL 模块得翻箱倒柜找杜邦线有时候板子都封装好了连调试串口都没引出来。后来换用 J-Link 的 RTT 功能这个问题算是彻底解决了——不占串口、不占单片机外设资源直接通过 SWD 调试口把日志和数据抠出来速度还快。不过官方 RTT Viewer 有一个挺尴尬的问题功能太过基础。界面简陋不说想加个波形显示、数据自动记录、自定义命令下发基本全靠自己动手。项目组里之前有人用 Python 写过一个替代工具能用是能用但每次换电脑都要重新配环境一堆依赖装下来用户体验一言难尽。所以我干脆用 QT 完整写了一个 RTT 上位机包含全部源码、使用文档和配套的 ARM 单片机端移植资料全部打包整理好了。这篇文章就把这个项目的核心设计、实现细节、踩过的坑一次讲清楚给正准备自己搞一个调试工具的同行做个参考。这个上位机主要解决三件事第一替代官方 RTT Viewer 的日常日志显示功能把 J-Link 的 RTT 数据实时展示在电脑上第二支持数据波形实时绘制调试 PID、采集传感器数据的时候不用再把数据导出到 Excel 里画第三提供一个双向命令通道在电脑上直接给单片机发指令比如改参数、切换模式。适合正在做 ARM 单片机开发、觉得串口调试不够高效、或者对现有调试工具有一定洁癖想自己掌控的开发者。1. 项目整体设计与需求拆解1.1 传统调试方式为什么不够用在讲实现之前先说说我为什么非要做这个项目。ARM 单片机调试最传统的方式就是 UART 串口打印用 printf 重定向到串口再接一个 USB 转 TTL 线到电脑上看日志。这个方法本身没问题但有几个很难受的痛点。首先串口是物理外设很多封装好的产品板或者四层板上面根本没有引调试串口只有 SWD 四根线。其次串口打印速度受波特率限制115200 波特率下实际有效数据吞吐量大概只有 11KB/s 左右传输大量调试数据的时候会很吃力。再者串口本身是单片机的一个外设资源如果项目里要用多个 UART 进行通信调试占用一个通道就会很麻烦特别是在量产前的稳定性测试阶段外设资源本来就紧张。还有一个容易被忽略的问题串口打印本身是阻塞式的如果调用 printf 时发送缓冲区满了整个程序就被卡住这在实时控制类项目里面是非常致命的事情。RTT 技术恰恰解决了这几个问题传输走的是调试接口不占目标外设资源速度又快而且是通过内存直接交换数据不会阻塞 MCU 主流程。1.2 为什么选 RTT 而不是串口或半主机这里先给不太熟悉 RTT 的读者解释一下原理。Segger RTTReal-Time Transfer是一种基于 J-Link 调试器的实时数据传输技术它的本质是在目标单片机 RAM 中维护一个控制块Control Block里面记录了上行缓冲区和下行缓冲区的地址、读写指针、缓冲区大小等信息。J-Link 通过 SWD 或 JTAG 调试接口直接读写这块 RAM从而实现主机和单片机之间的双向数据交换。对比一下几种常见调试方式就能看出为什么 RTT 更合适调试方式占用外设传输速度是否阻塞MCU是否需要额外引线双向通信UART串口UART外设慢一般115200bps可能阻塞需要支持SWO跟踪仅CoreSight中否需引SWO引脚只支持输出RAM调试RTT无快取决于SWD速率否不需要走SWD支持SWO 虽然也是一个不错的方案但很多兼容 J-Link 的调试器不支持 SWO而且 SWO 多半只能单向输出。RTT 在这种场景下几乎是天然的最优解这也是为什么后来 STM32 圈子里用 J-Link RTT 的人越来越多。但官方工具的可定制性实在太弱这就引出第三个问题为什么用 QT 来做这个上位机。1.3 为什么是 QT 而不是 C# 或 Python选择什么框架做上位机本质上是在开发效率、运行性能和跨平台能力之间做权衡。C# 的 WinForms/WPF 在 Windows 下开发效率很高但如果项目组里有人用 Linux 做开发或者部署那个 .NET 环境的问题就非常让人头疼。Python 加 PyQt 的搭配也算行但打包体积大运行效率偏低而且用 ctypes 调 J-Link 的 DLL 时排查问题会特别痛苦因为 Python 的调用栈和 C 的动态库混合在一起出了问题很难定位。QT 在这几个维度上最均衡。首先它本身是 C直接通过 dylib 加载和函数指针调用 J-Link 的 SDK性能和原生 C 程序没有区别其次 QT 的信号槽机制天然适合处理设备数据流比如从读取线程发数据到 UI 线程刷新界面这个模型用起来非常顺手比 C# 的事件模型和 Python 的队列方案都要简洁最后QT 的跨平台特性让这个工具在 Windows 和 Linux 上都能跑实测代码几乎不需要改动只是动态库加载部分要做个小分支。另外还有一个现实考量。J-Link 官方 SDK 在 Windows 下以 DLL 形式提供在 Linux 下是 .so 文件QT 的 QLibrary 类把这两种动态库的加载统一封装了用同一套代码就能兼容两个平台。如果当初选 C# 做Linux 这边就得重新写一个入口维护成本直接翻倍。2. RTT 上位机的工作原理与整体架构2.1 RTT 数据的完整通路要写一个稳定好用的 RTT 上位机光会调 API 是不够的必须先把数据通路想清楚。RTT 的数据链路从单片机内部开始到上位机界面显示一共经过四个环节。第一个环节是单片机端也就是 MCU 中运行的程序。程序调用 RTT 库的函数比如SEGGER_RTT_printf(0, ...)把数据写入 RAM 中对应的上行缓冲区同时更新控制块中的写指针。第二个环节是 J-Link 调试器它通过 SWD 接口周期性地读取目标 RAM拿到控制块和缓冲区的内容。第三个环节是 J-Link 的 PC 端驱动也就是 DLL 或 .so 库它以 API 的形式向上层提供读取 RTT 数据的接口。第四个环节就是我们写的上位机调用驱动提供的 API把数据拿回来解析并显示在界面上。这个连接关系理清楚之后代码该往哪里写就很明确上位机的核心工作其实就是两个一是管理 J-Link 设备连接二是通过 RTT API 读写目标缓冲区。QT 在这里的角色是提供一个窗口来展示数据并提供用户交互界面。2.2 J-Link 驱动层 API 与调用方式J-Link 的 PC 端驱动是 SEGGER 官方提供的 JLinkARM.dllWindows或 libjlinkarm.soLinux在安装 J-Link 软件时就会带上。使用这个 DLL 有两种方式一种是把官方头文件加进工程直接链接 JLinkARM.lib另一种是用动态加载的方式通过 QLibrary 或系统 API 解析函数指针来调用。实际开发中推荐动态加载理由是版本兼容性最好。J-Link 驱动更新很频繁不同版本的 DLL 在内部实现上略有差异如果用静态链接的方式换一台电脑就要重新编译动态加载则只需要在运行时检查函数是否存在不存在就弹个提示代码层面完全不用改。动态加载的核心代码很简单就是一个函数指针的解析过程#include QLibrary #include QDebug typedef void* (*JLINK_Open_Func)(const void* pParam); typedef int (*JLINK_Connect_Func)(void); typedef void (*JLINKARM_SetSpeed_Func)(unsigned long speedHz); typedef int (*JLINKARM_ReadMem_Func)(unsigned long addr, unsigned long numBytes, unsigned char* pData); typedef int (*JLINKARM_WriteMem_Func)(unsigned long addr, unsigned long numBytes, const unsigned char* pData); QLibrary jlinkLib(JLinkARM.dll); // Linux下改为libjlinkarm.so JLINK_Open_Func pJLINK_Open; JLINK_Connect_Func pJLINK_Connect; JLINKARM_SetSpeed_Func pJLINKARM_SetSpeed; JLINKARM_ReadMem_Func pJLINKARM_ReadMem; JLINKARM_WriteMem_Func pJLINKARM_WriteMem; bool loadJLinkSDK() { if (!jlinkLib.load()) { qDebug() 加载 JLinkARM.dll 失败: jlinkLib.errorString(); return false; } pJLINK_Open (JLINK_Open_Func)jlinkLib.resolve(JLINK_Open); pJLINK_Connect (JLINK_Connect_Func)jlinkLib.resolve(JLINK_Connect); pJLINKARM_SetSpeed (JLINKARM_SetSpeed_Func)jlinkLib.resolve(JLINKARM_SetSpeed); pJLINKARM_ReadMem (JLINKARM_ReadMem_Func)jlinkLib.resolve(JLINKARM_ReadMem); pJLINKARM_WriteMem (JLINKARM_WriteMem_Func)jlinkLib.resolve(JLINKARM_WriteMem); if (!pJLINK_Open || !pJLINK_Connect || !pJLINKARM_ReadMem || !pJLINKARM_WriteMem) { qDebug() JLinkARM.dll 中的函数解析失败请检查驱动版本; return false; } return true; }这里有个容易踩的坑DLL 的位数必须和编译环境匹配。如果 QT 用的是 64 位编译器那么必须加载 64 位版本的 JLinkARM.dll如果用的是 32 位编译器必须加载 32 位版本。装了 J-Link 软件之后系统里通常会有两个目录64 位版本在 JLink 安装目录下32 位版本在 JLink 安装目录的JLinkARM.dll旁边可能会被覆盖或者需要去C:\Windows\SysWOW64找实际使用时要确认清楚。2.3 源码包的组成与整体使用流程做这个项目时我特意把资料整理得很完整因为自己以前也经常下载到只有代码没有文档的开源项目看半天不知道从哪里开始。全套资料分成四块上位机源码目录QT 工程完整源码Qt 5.12 以上版本直接打开就能编译UI 布局、RTT 读取逻辑、波形显示模块全部分开。MCU 端 RTT 移植包包含 SEGGER_RTT 全部源码以及针对 STM32F1/F4 系列配置好的示例工程MDK 和 IAR 两份。详细文档一份 30 多页的 Markdown 文档从 J-Link 驱动安装到上位机编译、MCU 工程移植、联调步骤都有图文说明。发布包Windows 下编译好的可执行程序拷贝到其他电脑上不需要装 QT 环境。使用这个项目只需要三步。第一步在电脑上装好 J-Link 驱动版本 6.80 以上后面会说原因第二步把 RTT 移植包里的文件加到自己的单片机工程调用 RTT 打印函数输出数据第三步编译上位机连接上调试器点“连接”就能看到实时数据。整个流程不需要编写任何额外的通信协议因为数据通路由 RTT 本身承担上位机只需要负责展示和交互。3. 上位机核心代码拆解与实现3.1 连接管理模块打开设备与配置参数RTT 上位机启动之后第一步是和 J-Link 建立连接。连接之前需要确定几个参数接口类型是 SWD 还是 JTAG、目标芯片型号、SWD 通讯速率。这几个参数里最影响调试体验的是 SWD 速率设置低了数据吞吐量上不去设置太高又可能出现断连。考虑到实际项目中使用的主控芯片频率范围一般把 SWD 速率默认设置成 4000kHz4MHz。这个速度在大多数 ARM Cortex-M 系列芯片上都很稳定实测 STM32F103 和 STM32F407 上都没有问题。如果目标板布线质量一般或者线缆比较长可以降速到 1000kHz。连接代码大致如下void MainWindow::onConnectClicked() { if (!loadJLinkSDK()) { QMessageBox::critical(this, 错误, JLinkARM.dll 加载失败); return; } // 打开 J-Link 设备参数传空表示自动识别第一个 J-Link m_jlinkHandle pJLINK_Open(nullptr); if (m_jlinkHandle nullptr) { QMessageBox::critical(this, 错误, 打开 J-Link 失败请检查设备连接); return; } // 设置 SWD 通信速率 pJLINKARM_SetSpeed(4000000); // 连接目标芯片此处以 Cortex-M4 型号为例做判断 int connectResult pJLINK_Connect(); if (connectResult ! 0) { QMessageBox::critical(this, 错误, QString(连接目标芯片失败错误码: %1).arg(connectResult)); return; } statusBar()-showMessage(J-Link 连接成功正在进行 RTT 初始化...); startRttReadThread(); }连接成功后还需要等待目标芯片的 RTT 控制块准备就绪。这里要注意时序问题MCU 上电后要运行到调用 RTT 初始化函数的地方控制块才会被正确填充。如果上位机连接速度太快可能会在 MCU 初始化 RTT 之前就开始搜索控制块导致搜索失败。解决办法是在连接之后延时几百毫秒再开始读取或者做自动重试机制。3.2 RTT 控制块搜索与数据读取J-Link 的 RTT API 在设计上比较友好高版本的 DLL 提供了JLINK_RTT_Read这样的封装函数直接传入缓冲区索引就能读取数据不需要关心控制块的具体位置。但有个问题这套封装函数是 J-Link 驱动较晚才加入的如果读者的电脑上装的驱动版本比较老这些函数可能不存在。为了保证代码在不同驱动版本下都能正常工作我实现了两套读取机制优先使用官方封装 API检测到不存在时自动退回手动搜索控制块的方案。手动方案其实就是仿照 RTT Viewer 的做法在目标芯片 RAM 范围内搜索“SEGGER RTT”这个魔法字符串找到后根据控制块结构体中的偏移量解析出缓冲区指针和读写位置。控制块的搜索范围需要根据芯片 RAM 地址空间来设定。以 STM32F103 为例RAM 地址范围是 0x20000000 到 0x20010000一共 64KB。直接用 JLINKARM_ReadMem 分块读取这段内存然后在每一块中查找“SEGGER RTT”字符串命中之后就可以确定控制块地址。代码实现#include cstring #define RTT_MAGIC SEGGER RTT bool RttReader::searchRttBlock(unsigned long ramStart, unsigned long ramSize) { const unsigned long chunkSize 1024; char buffer[chunkSize]; for (unsigned long offset 0; offset ramSize; offset chunkSize) { unsigned long readLen qMin(chunkSize, ramSize - offset); pJLINKARM_ReadMem(ramStart offset, readLen, (unsigned char*)buffer); // 在读取到的数据块中查找魔法字符串 for (unsigned long i 0; i readLen - strlen(RTT_MAGIC); i) { if (memcmp(buffer i, RTT_MAGIC, strlen(RTT_MAGIC)) 0) { m_rttCtrlBlockAddr ramStart offset i; return true; } } } return false; }不过在实际项目里我基本都是用官方封装的那套 API因为现在 J-Link 驱动更新频率很快几乎不太可能遇到 6.80 以下的版本。直接调用的语法更简洁少了一层内存地址解析的复杂度。读取数据的核心代码如下bool RttReader::readUpBuffer(int bufferIndex, char* outData, int bufSize, int bytesRead) { typedef int (*JLINK_RTT_Read_Func)(unsigned int, void*, unsigned int); static JLINK_RTT_Read_Func pRead (JLINK_RTT_Read_Func)jlinkLib.resolve(JLINK_RTT_Read); if (pRead nullptr) { // 老版本驱动没有 RTT 封装接口返回 false 让上位机走手动方案 return false; } bytesRead pRead(bufferIndex, outData, bufSize); return true; }这段代码在读取时要注意一个细节RTT 上行缓冲区是环形缓冲区J-Link 读取时返回的是当前可读的数据量并不保证一次读走全部数据。如果日志输出很频繁上位机需要以较短的循环间隔反复调用读取函数把数据尽可能及时拿走。我的实现中读取线程的轮询间隔设置为 5 毫秒大多数场景下足够。3.3 读取线程与界面刷新的协作RTT 数据读取是一个持续循环的过程如果直接放在 UI 线程里做界面会卡死鼠标拖动窗口都会不流畅。QT 的解决方案是工作线程加信号槽通知这是非常经典的 Qt 并发模型。我定义了一个RttReadWorker类继承自QThread在run()方法里做轮询读取每读到数据就通过标准信号发送给主窗口接收。主窗口拿到信号后把数据追加到日志文本框或曲线控件中。线程和界面的关系非常干净工作线程不碰任何 UI 对象UI 线程不碰任何 J-Link API两边通过信号槽解耦既避免了界面卡顿也规避了跨线程访问 UI 对象崩溃的问题。class RttReadWorker : public QThread { Q_OBJECT public: explicit RttReadWorker(QObject* parent nullptr) : QThread(parent), m_running(false) {} void stop() { m_running false; } signals: void dataReady(const QByteArray data); void errorOccurred(const QString message); protected: void run() override { m_running true; char buffer[2048]; while (m_running) { // 尝试读取上行通道 0 的数据 int bytesRead 0; if (readUpBuffer(0, buffer, sizeof(buffer) - 1, bytesRead) bytesRead 0) { buffer[bytesRead] \0; emit dataReady(QByteArray(buffer, bytesRead)); } // 休眠 5ms避免无数据时空转导致 CPU 占用过高 msleep(5); } } };这里的 5ms 轮询间隔是经验值不是拍脑袋定的。J-Link 的 USB 通信本身有开销如果轮询间隔太短比如 1msCPU 占用率会明显上升而且 USB 包处理不过来如果间隔太长比如 50ms日志的实时性就会变差波形显示会出现明显的锯齿。5ms 是一个平衡值实测下 CPU 占用率在 3% 以内波形显示也足够平滑。主窗口接收数据后做两步处理第一步是追加到日志显示区如果开了时间戳就在每行前面加上毫秒级系统时间第二步是把数据中符合格式的行解析成数值交给波形控件更新曲线。日志文本用QPlainTextEdit的appendPlainText方法追加大量日志进来时要注意控制显示行数否则界面内存会越吃越多一般保留最近 5000 行就足够了超出就删除最旧的部分。3.4 命令下行通道从电脑控制单片机RTT 数据读取做出来之后已经能满足大部分调试需求了但能不能反向给单片机发命令才是这个上位机拉开差距的地方。官方 RTT Viewer 也支持 ch0 下行通道输入但它的交互方式比较朴素只能往终端窗口里输入文本没有做结构化指令处理。我的实现是在界面下方放一个指令输入框支持两种输入模式普通文本模式和带参数的命令模式。普通文本模式就是把输入框里所有内容原样通过 RTT 下行缓冲区发送给 MCU命令模式则是在输入框里输入形如set_pid 1.5 0.1 0.02这样的内容上位机按空格拆分后组装成结构化数据包发送。MCU 端收到后按自己的协议解析执行。发送代码如下void MainWindow::onSendCommand() { QString cmd ui-cmdEdit-text().trimmed(); if (cmd.isEmpty()) { return; } // 按空格拆分命令便于 MCU 端解析 QByteArray payload cmd.toUtf8(); typedef int (*JLINK_RTT_Write_Func)(unsigned int, const void*, unsigned int); static JLINK_RTT_Write_Func pWrite (JLINK_RTT_Write_Func)jlinkLib.resolve(JLINK_RTT_Write); if (pWrite ! nullptr) { int written pWrite(0, payload.constData(), payload.size()); if (written 0) { appendLog([发送] cmd); } else { appendLog([错误] 命令发送失败); } } ui-cmdEdit-clear(); }MCU 端的任务就是周期性检查下行缓冲区是否有新数据。以 STM32 为例在主循环里调用SEGGER_RTT_GetKey()或者批量读取下行缓冲区的函数把收到的字节放入一个命令解析器按预设格式处理。比如 PID 参数调整、模式切换、传感器采样率修改这些都可以通过 RTT 命令通道远程完成省去了反复重新烧录程序的麻烦。3.5 数据可视化波形显示与日志着色波形显示是我做这个上位机最开始的主要诉求。之前用官方 RTT Viewer 看传感器数据只能看到一行行数值数据变化趋势要盯着屏幕盯半天才能看出来用 Excel 画图就得手动导出数据效率低到让人想砸键盘。波形绘制我用了 QCustomPlot 这个第三方库它虽然不算大牌但胜在轻量级两个文件加进工程就能编译不需要额外安装庞大的绘图框架。QCustomPlot 的功能足够应付嵌入式调试场景实时曲线、网格、坐标轴缩放这些都有。曲线数据显示有一个关键设计不直接把原始文本解析成数值而是定义一个轻量级的数据点格式MCU 端通过 RTT 输出形如data:x:y:z的紧凑行。上位机在解析日志时如果发现这行以data:开头就把它当作普通日志不显示而是拆分成三个数值追加到三个曲线通道中。这样日志区不会刷屏曲线区又能保持干净两不耽误。日志区还做了一个简单的按关键字着色功能。比如日志行里出现ERROR就标红出现WARN就标黄出现OK就标绿。这个功能实现起来不难用QTextEdit的富文本格式给段落设置颜色就行但实际使用的体验提升非常明显几千行日志里扫一眼就能看到报错位置。4. MCU 端配置要点与联调细节4.1 在 STM32 工程中快速移植 RTT搞上位机的同时不能忘了目标端RTT 是双向的单片机工程里没有正确的 RTT 实现上位机写得再漂亮也没用。RTT 的 MCU 端移植其实非常简单因为 SEGGER 已经把代码抽象得很好不依赖某个特定平台只要封装了底层的内存读取和调试器断点相关功能即可。具体步骤是先找到 RTT 源码。装了 J-Link 软件后在安装目录的Samples\RTT子目录下有一个SEGGER_RTT_V*.zip压缩包解压后会有RTT目录。把里面的SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h、SEGGER_RTT_printf.c、SEGGER_RTT_printf.h这几个文件复制到自己的工程里然后在需要输出日志的源文件中包含头文件#include SEGGER_RTT.h使用的时候直接调用uint16_t adc_value 4095; SEGGER_RTT_printf(0, ADC value: %d\n, adc_value);需要注意的是SEGGER_RTT_printf是 SEGGER 自己实现的一个轻量级格式化函数只支持有限的格式说明符不支持浮点数%f。如果单片机端想输出 float 类型的数据需要先把浮点数拆解成整数部分和小数部分或者直接通过联合体转为十六进制输出在上位机再解析。这个限制在文档里有明确说明但很多初次使用的人都会踩一下。4.2 缓冲区大小配置与数据丢失问题RTT 缓冲区大小是影响调试体验的一个重要参数。SEGGER_RTT_Conf.h里有三个关键配置BUFFER_SIZE_UP控制上行缓冲区大小、BUFFER_SIZE_DOWN控制下行缓冲区大小、SEGGER_RTT_MAX_NUM_UP_BUFFERS控制上行通道数量。默认配置下上行缓冲区是 1024 字节下行缓冲区是 16 字节。这个配置在日志输出量不大的时候够用但如果你在中断里高频打印一些数据1KB 的缓冲区很容易被打满。RTT 的设计是当缓冲区满时直接丢弃新数据因此你会看到日志中间突然缺了一段没有任何提示。场景BUFFER_SIZE_UP 推荐值BUFFER_SIZE_DOWN 推荐值备注普通日志打印102416默认值中断中高频输出409632防止中断打印丢数据大量传感器数据采集819264提升数据吞吐量远程命令交互频繁2048128下行通道也需要空间要注意的是缓冲区放在目标芯片 RAM 里每增加 1KB 就多占 1KB 的 RAM。对 STM32F103C8T6 这种内存只有 20KB 的芯片来说缓冲区不能无限制加大。合理的做法是按需配置尽量在缓冲区满之前让上位机把数据取走。4.3 多通道使用与中断安全考量RTT 默认只有 0 号通道但通过配置SEGGER_RTT_MAX_NUM_UP_BUFFERS和SEGGER_RTT_MAX_NUM_DOWN_BUFFERS可以启用多通道。多通道的价值在于逻辑隔离比如通道 0 用来输出普通日志通道 1 专门输出传感器数据通道 2 用于命令应答。这样上位机可以根据通道索引决定数据显示在哪个区域日志和波形不会互相干扰。另一个非常重要的实践点是在中断服务函数中调用 RTT 打印。RTT 本身是设计为中断安全的但要注意如果在中断里调用SEGGER_RTT_printf时主循环同时在写 RTT 缓冲区可能出现数据交叉写入的情况。SEGGER 官方手册里说 RTT 支持中断嵌套但前提是不要从两个不同优先级的中断同时往同一个通道写数据。我的建议是专门分配一个通道给中断日志并且在中断里只打印最关键的信息格式尽量最简单避免格式化和内存操作占用太多中断时间。此外RTT 缓冲区采用环形结构在 MCU 端写入和上位机端读取是并发进行的因此存在读写指针竞争。SEGGER 使用临界区来保护缓冲区访问默认是通过关中断实现的。如果你的程序里有对实时性要求极高的中断关中断时间不能太长那么可以把SEGGER_RTT_Conf.h中SEGGER_RTT_LOCK的实现改写成基于调度器锁或其他方式这部分改动需要一定的 OS 移植经验。5. 开发过程中遇到的坑与排查速查表5.1 最常见的四类问题这个项目从零开始写到现在前后折腾了一两个月的业余时间踩过的坑不少。整理成表格方便大家直接对照排查。问题现象可能原因解决方案连接时提示 DLL 加载失败QT 编译器位数和 JLinkARM.dll 不一致确认 32/64 位一致或者改用动态加载方式上位机连接成功但一直没有数据MCU 端没有正确初始化 RTT或者 RTT 控制块搜索失败检查 MCU 工程中是否有 RTT 源码并调用初始化使用手动搜索模式日志显示缺失、不连续上行缓冲区太小数据被丢弃调大 BUFFER_SIZE_UP降低轮询间隔界面卡顿、CPU 占用率高读取线程轮询间隔太短或者日志刷新频率太高设置 5ms 轮询间隔限制日志显示行数使用批量刷新发送命令没有响应下行缓冲区太小或 MCU 端没有主动读取下行通道调大 BUFFER_SIZE_DOWN确认 MCU 端主循环调用了 RTT 下行读取函数连接后 J-Link 无法识别目标芯片SWD 速率过高降低速率到 1000kHz 试一下波形显示出现毛刺或断点数据解析格式错误或采样间隔不均匀检查 MCU 端输出的数据格式是否规范统一 CRC 校验5.2 几个容易忽略的细节除了上面这些表面问题还有一些更深层的坑值得单独拿出来说。第一个是 RTT 控制块地址的稳定性问题。如果你用我前面提到的手动搜索方案搜索到的地址在每次上电时可能是不同的因为 RAM 中的数据不一定清零了老的“SEGGER RTT”字符串可能残留。为了避免搜到错误的地址搜索时不仅匹配魔法字符串还要校验控制块中缓冲区的地址是否落在 RAM 地址范围内这样能过滤掉大部分残留数据。第二个是 JLINK_RTT_Read 函数在未连接目标时直接调用会崩溃。我在开发过程中就踩过界面已经关了连接按钮但读取线程还在跑结果程序直接崩掉。后来在读取线程退出之前加了一个连接状态的判断标志线程内部循环每跑一次都要检查这个标志断开连接时先停止线程再关闭 J-Link顺序不能反。第三个是多个 J-Link 同时插在电脑上的场景。JLINK_Open(nullptr)这个调用在只有一个 J-Link 的时候没有问题但如果有多个它默认选第一个不一定是你想用的那个。解决方法是先调用JLINK_GetEmuList系列 API 枚举所有 J-Link 设备在界面上做一个下拉框让用户选择选完再打开。这部分代码在源码包里已经有实现这里不再贴出。5.3 实测效果与性能表现拿 STM32F407 开发板实测了一下整体效果。MCU 端用 RTT 通道 0 输出 PID 调试日志每 10ms 输出三个浮点数经过编码后的数据通道 1 输出传感器原始值。上位机开启后波形区可以同时显示两条曲线日志区能实时刷新数据吞吐量大致估算下来约 3KB/sSWD 速率设为 4MHz 时一切正常CPU 占用率在 2% 左右。如果做大量数据采集比如 ADC 以 1kHz 采样率输出原始值每个采样点 4 字节一秒钟就是 4KB 数据量。这种情况下 RTT 依然能稳定工作但上位机的日志显示和曲线绘制要分开处理不要让波形和日志共用同一个文本控件否则界面刷新会成为瓶颈。我还特意对比了官方 RTT Viewer 和这个自研上位机的启动和连接速度。官方工具启动时需要加载一堆插件连接过程也比较慢大概需要两三秒这个自研上位机因为没有多余模块启动和连接基本在一秒以内。虽然这不是决定性优势但日常使用中体感好很多。5.4 后续扩展思路这个项目目前已经能满足我日常调试 90% 以上的需求但还有几个扩展方向值得继续做。第一个是增加数据记录功能把每次调试会话的数据完整保存成 CSV 或二进制文件回放和分析就有了依据。现在数据只是实时显示没做落盘存储调试完一个问题之后想复盘还得重新跑一遍效率不高。这个功能技术上不大加一个文件写入模块就行难点在于文件写入不能阻塞 UI 线程最好用异步队列的方式。第二个是增加自定义协议解析插件机制。现在上位机解析的是简单data:x:y:z格式如果换一个项目数据格式不同就得改代码重新编译。理想的做法是提供一个脚本接口让用户用脚本语言定义解析规则这样换项目时只需要改配置文件而不是改程序。第三个是支持多设备连接。现在已经能枚举多个 J-Link但界面还只支持同时连接一个。如果两个调试器分别接着两个板子理论上可以同时查看两块板的实时日志这种场景在做主从机联调时非常实用。我不打算把这些功能全部做完因为工具类软件的演进逻辑就是需求驱动用到哪个加哪个。如果你也在做类似的上位机开发建议先把自己最痛的那几个点解决掉再做锦上添花的功能。6. 实际使用中的一些个人心得做完这个项目之后最大的感受是调试工具这种东西不亲手做一遍永远不知道自己的需求边界在哪里。以前用 RTT Viewer 觉得勉强够用但真正自己写了上位机之后才发现可以把日志过滤、波形显示、远程命令、数据记录全部整合到一个界面里调试效率的提升是非常直观的。一些具体的使用技巧也值得说一下。比如在做 PID 调试时我会在单片机端把目标值、实际值、输出值同时通过三个通道输出上位机同一张图上画三条曲线这样调参数的时候一眼就能看出响应快慢和超调量。又比如做低功耗调试在休眠模式下不能频繁唤醒 MCU 打印日志我就会用 RTT 的下行通道发一个“唤醒并打印状态”的指令比自己等它醒来再摸黑盲调要高效得多。还有一个小技巧分享给大家调试结束后千万不要直接拔掉 USB先在上位机界面点断开让线程安全退出再拔线。否则在下一次打开连接时某些 J-Link 驱动可能会出现异常状态需要重新插拔 USB 才能恢复。这个问题不是每次都会出现但一旦出现就要断电重启设备很浪费时间。最后说一下这个项目源码的管理方式。整个项目在一个 Git 仓库里上位机源码、MCU 端移植包、文档、发布包分目录存放每个发布版本打一个 tag这样回溯历史版本非常方便。如果你准备长期维护自己的调试工具建议从一开始就保持这个习惯。本文还有配套的精品资源点击获取
网站建设高端定制企业官网