ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于QT的J-Link RTT上位机开发:从原理到二次开发

基于QT的J-Link RTT上位机开发:从原理到二次开发 简介这是一套面向ARM单片机嵌入式开发者的RTTReal-Time Terminal调试上位机完整工程基于Qt框架开发专为配合RT-Thread实时操作系统进行串口通信、日志查看与设备交互调试而设计适用于电子信息、自动化、物联网等专业学生的课程设计、毕业设计及企业原型验证。资源包共40个文件涵盖Qt核心源码7个cpp、8个h、界面定义.ui、编译配置.pro、Makefile、可执行程序exe、图标资源ico、png、jpg及配置文件ini结构清晰便于理解Qt信号槽机制、串口通信实现与UI逻辑分离设计。压缩包仅190KB轻量易部署所有代码经实测运行稳定含详细中文文档与README说明支持快速上手或二次开发。目前已有76人下载学习适合具备C和嵌入式基础的学习者进阶实践亦可直接用于毕设答辩演示。1. 为什么我会用QT重写一版RTT上位机先说说我做这个东西的动机。过去调试ARM单片机最常见的方式就是串口printf硬件上接一个USB转TTL模块代码里初始化UART然后通过串口助手看输出。这套方案本身没毛病但用的时间越长越觉得有几个痛点绕不过去一是波特率高了容易丢数据115200往上走特别是打印浮点数、数组这类信息稍不注意就乱码、丢帧二是串口占用了宝贵的UART外设有些项目里串口要给传感器、蓝牙模块、RS485总线用还得额外做引脚复用和中断优先级分配三是打印本身对实时性有影响如果调试信息量大单片机在printf上阻塞太久直接影响中断响应和时序控制。后来接触到了SEGGER J-Link的RTTReal-Time Transfer技术相当于用调试器的SWD接口在芯片运行时直接读写内存里的缓冲区完全不占用额外的硬件引脚传输速度可以到几MB/s比串口快了不止一个数量级。配合J-Link调试器在Keil、IAR里也能直接开RTT Viewer窗口看打印信息。但J-Link官方自带的RTT Viewer是Windows桌面程序界面偏老字体、配色、日志管理、波形显示都谈不上好用而且不能用脚本自动化处理数据。我当时的需求是调试电机驱动和传感器融合算法时需要看高频的曲线数据还要能把日志按时间戳存下来官方工具不能满足我自己就用QT写了一个RTT上位机。这个上位机做出来之后在几个基于STM32和国产ARM内核芯片的项目里都实际用了效果相当稳定。这套源码加上详细的文档适合正在做ARM单片机开发、对调试效率和工具定制有要求的工程师参考也适合想在QT里做串口、J-Link等调试工具二次开发的初学者。需要特别说明的是这个项目依赖J-Link官方提供的DLL和RTT源码它们在MCU端是一套开源的在PC端通过JLinkARM.dll与调试器通信。下面的内容会从RTT工作机制、上位机模块设计、编译环境、踩坑记录到二次开发方向逐一展开。任何一部分单独拿出来都能写很深我尽量把实际用到的重点和容易翻车的地方讲透。2. RTT能快在哪控制块、缓冲区与内存搜索机制2.1 和串口调试的本质差别串口调试的本质是数据从单片机UART外设发出来经过电平转换到USB在PC端识别成虚拟串口整个过程是“数据搬运”单片机主动往外发不关心PC端是否已经准备好接收。而RTT不是数据传输链路它的本质是“共享内存”单片机上定义一块固定的内存区域作为缓冲区调试器通过SWD接口在后台轮询这块内存一旦发现新数据就通过USB传到PC端显示。这意味着单片机只需要把数据写入内存中的环形缓冲区几个周期就完成了完全不用等待数据真正发出去。对实时系统来说这个开销比串口小得多对中断嵌套和时序的影响可以忽略不计。这也是RTT能跑出超高吞吐量的原因——理论上SWD接口在高速模式下达几十Mbps即使实际打折也比普通的UART方案快很多。2.2 控制块单片机与上位机之间的约定要实现RTT单片机固件里必须包含两个东西一个是SEGGER提供的RTT实现源码另一个是定义在内存中的控制块结构体。控制块Control Block是整个RTT机制的核心它存储了RTT的配置信息和各个通道的状态一般定义在RAM里并且建议做四字节对齐。控制块的头部包含一个标识符就是ASCII码“SEGGER RTT”固定16字节紧接着是上行缓冲区描述最多3个默认只用通道0和下行缓冲区描述最多3个。这里需要理解一个概念上行Up方向指从单片机到PC也就是日志、调试数据输出的方向下行Down方向指从PC到单片机也就是命令下发、参数设置的方向。每个缓冲区描述又包含起始地址、大小、写位置和读位置。单片机端往缓冲区写入数据后更新写指针PC端读取并更新读指针双方通过指针的差来判断有没有新数据。控制块和缓冲区的地址在编译后是确定的但并不是固定值——它取决于芯片型号和链接脚本。J-Link固件和PC端DLL并不知道你的控制块在哪里所以它们用了一个策略在芯片RAM范围内搜索“SEGGER RTT”这16个字节的特征码。这个机制在J-Link连接时执行搜索范围默认是整个RAM区如果RAM比较大搜索可能需要一些时间通常几百毫秒到几秒不等。这种内存搜索机制保证了即使每次编译后控制块地址变化只要RAM范围和搜索选项正确调试器都能找到它。2.3 缓冲区容量与丢数据的关系RTT上行缓冲区默认大小是1024字节在SEGGER_RTT_Conf.h里可以调整通过BUFFER_SIZE_UP宏定义。这个值直接决定了在高吞吐量下是否丢数据。缓冲区本质是一个环形结构如果单片机写入速度持续快于PC端读取速度缓冲区会被写满。此时SEGGER官方默认的策略是丢弃新数据并返回错误保证旧数据不被覆盖。也就是说如果你的调试信息量非常大单次打印的内容超过缓冲区剩余空间就会发生数据丢失。这一点和串口不同串口是流式的只要波特率足够数据会持续发完不会因为缓冲区满而丢弃新数据代价是可能阻塞或者后台中断频繁触发。RTT的缓冲区如果太小日志打印内容又长打印调用就会因为无法写入全部内容而丢掉一部分而且没有任何直观的提示这是很多人第一次用RTT时觉得“数据怎么少了”的根源。我在实际项目里把BUFFER_SIZE_UP调到过4096日志频率高的时候依然不够最后改了PC端的读取周期并增加了PC端处理数据的效率问题才解决。所以调这个缓冲区时不要只看单片机的RAM够不够还要看PC端读取速度和你打印频率的匹配度。2.4 J-Link DLL如何与QT上位机交互J-Link官方提供了一套PC端的SDK核心是JLinkARM.dll可以理解为J-Link调试器的驱动层。通过DLL导出的函数可以完成打开调试器、连接芯片、读写内存等操作。对于RTT功能核心操作是先连接目标芯片然后通过读取内存来查找控制块找到后根据控制块内缓冲区的地址和读写指针周期性地读取缓冲区中的数据。这个过程的底层是SWD或者JTAG协议但QT上位机不需要关心这些底层时序只需要调用DLL函数即可。需要注意的是PC端使用RTT依赖于目标芯片已经通过J-Link连接而且芯片必须处于运行状态如果芯片休眠或者被调试器暂停RTT传输也会跟着停止。这里有一个常见误区以为J-Link的RTT和版本无关其实J-Link驱动也就是JLinkARM.dll版本和MCU端的RTT源代码版本之间最好保持兼容尤其是SEGGER更新了RTT控制块结构后旧DLL可能无法正确识别新控制块。不然就会出现连接正常、内存也能读取但RTT窗口始终没有数据或者显示乱码、数据错位。3. 上位机模块设计与关键实现3.1 整体需求拆解和界面规划一个好用的调试上位机不只是把数据打印出来那么简单。在动笔写代码之前我列了一下这个RTT上位机必须具备的能力自动搜索并连接J-Link调试器选择已连接的芯片型号通过RTT读取目标板的上行数据同时支持下行命令发送支持多通道显示、时间戳、字符/十六进制切换日志自动保存按日期或按会话分文件界面操作流畅长时间运行时数据刷新不卡顿、不泄漏内存。界面分成了四个区域顶部是连接配置区和功能按钮左侧是通道列表和连接状态中间是多页签的显示区每个通道一个页签可以独立控制显示、暂停、清空底部是命令输入框和发送按钮。这么设计主要是从实际调试习惯出发的。调试时常常需要同时看普通日志和波形数据如果混在一个通道里两类数据会互相干扰。所以我习惯性地把通道0当作普通日志通道1当作数据曲线输出。QT中的QTabWidget天然适合这种多通道分页显示需求。3.2 为什么选择QT而不是其他框架PC端的上位机框架可选很多C#的WinForms/WPF、Python的Tkinter/PyQt、Electron等。我之前也用C#写过串口上位机但这次选择QT是基于几个具体原因跨平台我平时在Windows上调试但偶尔需要用Linux主机跑测试脚本QT的跨平台能力让我只需要维护一套代码信号槽机制QT的信号槽非常适合串口、网络、调试器数据这类异步事件驱动的场景收到一帧数据后发送信号、界面槽函数更新显示不用手动管理线程锁丰富的控件和生态QPlainTextEdit做日志显示、QTableView做数据表格、QCustomPlot做波形图都成熟QSerialPort、QTcpSocket等模块也让后续扩展很轻松代码可维护性QT的QMainWindow多个QDockWidget的布局方式让界面结构清晰二次开发时改某个模块不容易影响其他模块。当然用QT的代价是学习曲线相对陡峭尤其是新手对信号槽连接、事件循环、对象父子关系的理解如果不到位程序编译过了但跑起来会莫名崩溃。这篇文章后面会专门讲这几个坑。3.3 核心模块J-Link DLL的封装与调用整个上位机最深的一层是J-Link DLL的封装。JLinkARM.dll提供的API有上百个但做RTT上位机真正用到的其实不多核心是这组函数函数名用途JLINK_Open打开J-Link DLL初始化环境JLINK_Close关闭DLL释放资源JLINK_ExecCommand执行命令如设置日志文件、清空等JLINK_GetNumDevices获取已连接的J-Link数量JLINK_GetDeviceList获取可用J-Link设备列表JLINK_Connect连接目标芯片JLINK_GetHardwareVersion获取硬件版本JLINK_TargetGetID获取目标芯片IDJLINK_ReadMem读取目标内存数据JLINK_WriteMem写入目标内存数据JLINK_ReadMemU32读取32位整型数据用于读取指针在QT中调用DLL我用了QLibrary动态加载的方式而不是在编译时链接JLinkARM.lib。原因是JLinkARM.dll分x86和x64版本而且用户的环境可能安装在不同目录静态链接反而绑定死路径动态加载可以在启动时让用户手动选择DLL路径灵活性更高。封装思路是这样的写一个JLinkManager类用QLibrary加载DLLtypedef定义函数指针然后包装成类成员函数。这个类只干一件事管理J-Link的连接和底层内存读写。上层RttWorker类负责查找控制块、解析缓冲区、周期性读取数据。显示层完全不接触DLL只和RttWorker通信。这个分层很关键。因为DLL操作是阻塞型的如果直接在UI线程里调用读取内存的函数界面会卡死。正确做法是RttWorker跑在QThread里通过信号把读取到的字符串发回主线程。以下是DLL加载的一个简化示例// JLinkManager.cpp bool JLinkManager::loadDll(const QString dllPath) { m_lib.setFileName(dllPath); if (!m_lib.load()) { m_errorMessage QString(加载JLinkARM.dll失败: %1).arg(m_lib.errorString()); return false; } m_funcOpen (Func_Open)m_lib.resolve(JLINK_Open); m_funcClose (Func_Close)m_lib.resolve(JLINK_Close); m_funcConnect (Func_Connect)m_lib.resolve(JLINK_Connect); // ... 其余函数解析 return true; }3.4 核心模块RTT控制块的查找与数据读取目标芯片连上之后第一步是找到控制块的地址。我的实现里提供了两种模式自动搜索和手动指定。自动搜索的逻辑是从RAM起始地址开始按4字节对齐以一定步长读取内存每块数据检查前16字节是否等于“SEGGER RTT”。搜索范围可以通过界面配置默认是0x20000000到0x20010000针对STM32这类RAM在0x20000000起始的芯片这个范围可以根据具体芯片型号调整。搜索找到控制块后立即读取控制块中描述的各个通道的缓冲区参数包括缓冲区地址、大小、写指针、读指针。数据读取是放在一个QTimer里定时执行的周期默认10ms。每次读取的流程是先读取读指针和写指针计算未读数据长度然后从缓冲区中读取出这些字节再做字符切分按\n拆分成一行行的字符串通过信号丢给界面显示。这里有个细节要注意环形缓冲区的读位置指向上一次读到的位置写位置由单片机更新。如果写位置小于读位置说明写指针绕回了缓冲区头部需要分两次读取第一次从读位置读到缓冲区末尾第二次从头部读到写位置。这是环形缓冲区处理的经典问题漏掉这个处理数据会错乱或丢失。// RttWorker.cpp 中读取Uplink数据的伪代码 QByteArray RttWorker::readUpBuffer(int ch) { ChannelInfo info m_channels[ch]; quint32 rd readMemU32(info.bufferAddr 8); // 读位置偏移 quint32 wr readMemU32(info.bufferAddr 12); // 写位置偏移 quint32 size info.bufferSize; QByteArray data; if (wr rd) { data readMem(info.bufferStart rd, wr - rd); } else { data readMem(info.bufferStart rd, size - rd); data readMem(info.bufferStart, wr); } updateReadPointer(ch, wr); return data; }3.5 显示模块日志区和波形区日志显示用的是QPlainTextEdit的只读模式每次收到新数据后追加文本并自动滚动到底部。但如果日志量很大每次都直接append会导致界面卡顿。我的做法是先追加到一个QString缓存里用定时器每200ms刷新一次界面。这样即使数据量是几万行的频率界面也能保持流畅。波形显示是基于QCustomPlot的。单片机端只需要按固定格式输出数据例如CH1:温度,25.3;CH2:电流,1.2\n这样的文本上位机按行解析按通道分组存到QVector里再更新曲线。为了让曲线实时滚动我用了QCustomPlot的setRange功能把x轴的范围整体偏移。在UI层面还有几个实用小功能右键复制选中内容、字体大小调节、自动换行、颜色区分不同通道。这些功能虽然简单但实际调试时高频使用没有的话会感觉很难受。3.6 下行通道与命令面板下行通道的实现和上行类似只是方向相反上位机把命令字符串写到下行缓冲区单片机端的SEGGER_RTT代码会在某个时机读到这些数据。实际项目中我一般用下行通道来调整PID参数、切换运行模式、给执行机构发指令。命令面板就是一个QLineEdit加一个“发送”按钮支持发送历史记录支持按回车快速发送。值得注意的是下行缓冲区默认很小我记得默认是16字节如果命令超过这个长度会写不进去。所以如果要做命令下发记得在单片机端把BUFFER_SIZE_DOWN改大至少128字节起步。4. 环境准备与编译最容易翻车的一环4.1 QT版本与编译器的选型我开发时用的是QT 5.15.2版本编译套件是MinGW 64位。选择这个组合的原因是5.15是最后一代同时支持Win7和Win10的老版本6.x不支持Win7而且MinGW的部署简单不需要依赖VC运行库。如果你用MSVC编译器发布程序时还要注意带上对应的vc_redist否则客户机上可能会因为缺运行库而启动不起来。如果你用的是QT 6.x需要注意一个差异QT 6里把一些模块合并或移除了比如QRegExp换成了QRegularExpression如果你的代码是从5.x移植过来的这些地方可能要改。另外QT 6不再默认支持Win7对旧开发环境的兼容性要求高的话还是用5.15更稳。4.2 JLink SDK的获取与目录结构J-Link的官方SDK不是单独下载的而是包含在J-Link Software Pack里。安装完成后在安装目录下你能找到这些关键文件JLinkARM.dll32位版本在根目录JLink_x64.dll64位版本在/x64子目录/Samples/RTT包含了MCU端的RTT源码其中SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h是要移植到单片机工程里的核心文件/Samples/JLinkJLink SDK示例代码。需要额外说的一点这个DLL的位数必须和你的QT工程位数一致。如果你用32位的MinGW编译QT程序就要加载32位的JLinkARM.dll如果你用64位的QT程序就要加载JLink_x64.dll。这个不匹配会导致函数调用崩溃而且错误提示不一定明显有时候是非法访问有时候是直接闪退。4.3 外部依赖除了QT和J-Link DLL还需要什么如果你要在界面上做波形显示需要引入QCustomPlot一个头文件和源文件的第三方绘图库。如果你用QT自带的Qt Charts模块也可以但QCustomPlot更轻量不需要额外安装模块直接添加到工程文件即可。此外还建议引入一个轻量级的JSON解析库比如Qt自带的QJsonDocument就够了不用折腾第三方库。在保存配置、解析协议时JSON格式比自定义文本格式更省心。QT的QSettings也能保存配置但它生成的ini文件结构比较简单适合存窗口位置、串口号、最近打开文件这类信息不适合存复杂的协议配置。4.4 工程配置文件里的几个关键项QMake工程文件里有几点需要注意CONFIG c11低版本QT默认的C标准不够用如果不加上某些写法会编译失败QT widgets serialport network如果你的工程还会用到串口和网络提前加上对应模块DEFINES QT_DEPRECATED_WARNINGS启用API过时警告有利于后续维护时发现旧函数调用CONFIG(debug, debug|release): 分别配置Debug和Release版本的输出目录避免两个版本互相覆盖还需要注意Release版本下不要忘了把JLink DLL和QCustomPlot源文件一起打包发布。这里还想提醒一个关于QT单实例运行的坑。RTT调试工具每次只能连接一个J-Link如果用户开了两个实例第二个实例会连接失败或者和第一个打架。我在程序启动时用QLockFile加了一个锁如果检测到已经有一个实例在运行直接弹窗提示并退出。这个功能虽小但很提升使用体验。4.5 静态编译与动态发布的选择如果要发布给其他同事用或者部署到实验室多台电脑上可以编译一个静态版的QT把所需库全部编进exe这样目标机器上不用安装QT运行时拷贝单个exe就能跑。但静态编译QT需要自己重新编译QT源码耗时较长而且JLinkARM.dll本身不能静态链接还是需要把DLL放到exe同目录下。如果不做静态编译最稳妥的发布方式是用QT自带的风河工具windeployqt把所有需要的DLL、插件、字体资源收集到exe所在目录然后整个目录压缩分发。注意windeployqt默认收集的是platforms/qwindows.dll这类关键插件如果你的程序用了svg、imageformats等插件也要一并确认。5. 实际调试中踩过的坑和排查过程5.1 现象一RTT窗口连上后没有任何数据这是最让人抓狂的问题。硬件连接正确、J-Link能识别芯片、程序在跑但RTT视图空无一物。我第一次遇到时排查了一下午。先排除最不可能的确认单片机端的RTT代码确实被调用了。在main函数初始化时调用SEGGER_RTT_Init()然后立即SEGGER_RTT_printf(0, RTT OK\n)。如果连这一行都出不来说明问题在底层。然后检查J-Link连接是否正常。在J-Link软件里能看到连接的芯片型号和固件版本如果J-Link提示无法识别设备那可能是接触不良、芯片供电不够、SWD引脚被复用。接下来要用J-Link工具自带的RTT Viewer做对照测试打开官方RTT Viewer看看有没有数据。如果官方工具也没数据说明问题在MCU端重点检查SEGGER_RTT_Conf.h里的配置参数、控制块是否被链接脚本放到了特定section、有没有开启编译器优化导致代码被裁剪。特别是如果你把RTT控制块定义在某个自定义section里某些链接脚本可能把它放到不连续的内存区域导致J-Link搜不到。如果官方工具有数据但我的上位机没有问题就出在控制块搜索上。我用JLINK_ReadMem搜索RAM时第一步就搜索范围给错了。STM32F103的RAM在0x20000000但我当时用的是LPC1768RAM起始在0x10000000搜索范围完全错开当然找不到。所以一定要根据目标芯片的RAM地址范围正确配置搜索范围。5.2 现象二数据偶尔丢失或乱码数据丢失的原因前面提过大概率是缓冲区满了。我当时排查过程是用一个计数变量反复自增并打印发现输出的数值不连续——每次跳变没有规律。检查缓冲区的大小时发现上位机用了默认的1024但我打印的频率太高一帧数据还没读完下一帧又覆盖了。解决的思路是双向的一是把单片机端上行缓冲区从1024调到2048或4096二是把上位机读取周期从默认的50ms改成10ms并根据缓冲区剩余空间动态调整读取频率。但还有一个隐藏问题J-Link DLL的读取操作本身有耗时频繁读取反而会拖慢总吞吐量。更优的方案是调整成“数据驱动”每次读取当前缓冲区内的所有数据读取完立即更新读指针然后根据上次读取的数据量动态调整下一次读取的间隔。数据量大时就读得快些数据量小时读得慢些避免无谓的USB通信压力。5.3 现象三浮点数据显示为乱码RTT打印浮点数时如果使用类似SEGGER_RTT_printf(0, V%f\n, voltage)的代码在MCU端默认情况下是打印不出来的因为SEGGER RTT的printf实现默认不支持浮点格式为了减小代码体积。很多人在这个坑里卡了很久以为是数据输出有问题其实是格式化函数根本就没实现浮点输出。解决方法是配置SEGGER_RTT_Conf.h里与printf格式相关的宏定义或者直接用字符数组预格式化数据。我的做法是在MCU端用标准snprintf把浮点转成字符串再通过SEGGER_RTT_WriteString输出。这样虽然多了一些处理开销但格式完全可控也不会被SEGGER的轻量化printf限制。5.4 现象四高DPI显示下界面模糊在Windows上如果未做高DPI适配QT程序在4K屏幕或缩放了125%、150%的显示器上会显示模糊。这个问题在QT 5.14及更早版本中需要手动处理在main函数里设置QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);但对于我自己写的这个版本我在main函数中提前设置了这两个属性然后结合QSettings保存窗口尺寸和状态在需要时记住了界面元素在缩放前后的相对位置。需要特别提醒的是这些属性必须在创建QApplication对象之前设置否则不生效。5.5 现象五断开连接后重新连接失败J-Link断开后如果不彻底释放DLL资源第二次连接经常会报“无法打开设备”或“设备忙”。这是因为J-Link DLL在第一次连接时占用了USB资源如果未完全释放第二次调用JLINK_Open时无法重新初始化。解决这个方法的关键是断开时不只是调用JLINK_Close还要把DLL卸载。因为QLibrary对象在重新加载时会自动释放之前的资源所以我的实现是断开时调用JLINK_Close然后调用m_lib.unload()同时把所有函数指针置为nullptr重新连接时再重新加载DLL。这样看似多了一些开销但确保第二次连接的成功率是100%。5.6 现象六界面长时间运行后内存增长如果长时间采集数据不释放内存程序的内存占用会持续走高。原因主要有两类一是日志显示区中QPlainTextEdit的document对象持续累积字符不进行上限清理二是波形显示区中数据点数量无上限地append。解决方案分别是在日志追加时检查行数超过一定数量比如5000行时删除最前面的部分行在波形显示时限制存储点数比如最多2000点超过后移除旧点。这个思路不仅适用于RTT上位机其他类似的数据采集工具也都适用。6. 源码结构与二次开发方向6.1 源码目录划分整个工程源码的目录结构清晰便于阅读和修改RttUpperMachine/ ├── RttUpperMachine.pro # 工程文件 ├── main.cpp # 程序入口 ├── mainwindow.h/mainwindow.cpp # 主窗口 ├── jlinkmanager.h/jlinkmanager.cpp # J-Link DLL封装层 ├── rttworker.h/rttworker.cpp # RTT读取工作线程 ├── datadisplayer.h/displayer.cpp # 日志显示控件封装 ├── waveformwidget.h/waveformwidget.cpp # QCustomPlot波形封装 ├── configdialog.h/configdialog.cpp # 连接参数配置对话框 ├── qcustomplot/ # QCustomPlot第三方库 └── resources/ # 图标、样式表等资源文件这个划分遵循了一个简单原则界面逻辑和数据逻辑分离。JLinkManager只负责DLL调用RttWorker只负责连接状态下的控制块搜索与数据读取MainWindow只负责把数据显示出来以及把用户的命令通过RttWorker转发到下行通道。这样如果在某个环节出问题定位会非常快。6.2 核心类之间的关系JLinkManager是底层基础它封装了DLL调用。RttWorker继承QThread内部有一个JLinkManager实例每个定时读取周期内都会调用它来读取内存。RttWorker通过信号把获取到的字节流发送出来MainWindow连接这些信号将字节流解析后分发到不同的显示控件。这样的设计让模块单元测试变得容易。例如你可以把JLinkManager替换成一个模拟对象模拟一组内存数据来测试RttWorker的解析逻辑是否正常。这在没有物理硬件时也能调试PC端代码非常方便。6.3 二次开发方向一增加协议解析层目前RTT输出的原始字节流直接当文本显示。如果需要更清晰的数据展示可以在RttWorker和显示层之间增加一个协议解析层。例如定义一种简单的带帧头帧尾的数据格式帧头存放通道号、数据长度、数据类型温度、电压、电流等解析层拆帧后把数据更新到对应的QTableWidget单元格或者波形图。我个人的建议是单片机端定义好结构化输出的格式上位机端按格式解析。这样即使以后有几百个数据项需要展示也不需要修改上位机逻辑只需增加对应的配置项即可。6.4 二次开发方向二支持多J-Link并行调试多J-Link并行调试在同时调试多个从机或者多机通信时很实用。实现的思路是把RttWorker改成可多实例化的每个实例对应一个J-Link连接每个实例里配置不同的设备序号、芯片型号、RAM搜索范围。主界面用标签页把多个连接隔离开。这个扩展点需要特别注意的就是DLL对多实例的支持。JLinkARM.dll官方支持同时打开多个J-Link设备只要每次打开时传入不同的设备序号即可。但要注意很多底层函数在打开设备之前一般会做一次枚举如果设备列表有变化比如一个J-Link拔掉了重新枚举时索引可能会发生偏移。稳妥起见最好用序列号J-Link的Serial Number来绑定设备而不是用枚举序号。6.5 二次开发方向三集成脚本自动化调试过程中有很多重复性操作连接设备、设置目标芯片、配置缓冲区大小、执行控制块搜索、开启日志记录、采集一段时间后导出数据。如果每次都手动点击一遍耗时而且容易出错。可以在上位机里加一个简单的脚本引擎用QTscript或者解析JSON命令序列的方式来批量执行这些操作。我在实际项目里就是这么做的把“连接-采集-保存-断开”整个流程做成了一个一键执行的脚本配合自定义命令模板测试时可以自动化运行。这样做不仅提高了调试效率还能确保每次采集数据的起始条件一致方便对比多组结果。6.6 二次开发方向四波形数据的离线回放在线采集时可以实时显示波形但有时候采集到的问题需要事后分析。这个回放功能就是把保存的日志文件解析出来把数据重新填充到相同的波形控件中。实现上很简单加载文件、按行解析、调用追加数据接口。但这个功能却意外的很实用——遇到偶发性故障时可以把长时间的采集数据保存下来回放时逐步拖拽查看异常发生前后的数据变化。7. 从实际项目中沉淀的几条建议如果你打算在自己的项目里用这套方案有几点体会值得提前说单片机端建议给RTT缓冲区单独安排一段内存区域特别是在内存紧张的单片机上。把SEGGER_RTT_Conf.h里的缓冲区大小调成适合你应用的值尽量减少动态内存分配因为调试工具本身不应该成为系统稳定性的短板。PC端解析和显示的性能很重要。千万不要在UI线程里去做耗时操作像DLL调用、文件写入、大数组处理都要放到工作线程。一旦UI线程被阻塞整个程序就像死了一样会在调试关键时刻让人抓狂。日志文件的命名和保存目录也有讲究。我一般按“日期_时间_工程名_备注.log”的命名方式保存目录按“工程/日期”自动创建。这样时间久了之后回看历史调试记录时非常清晰。如果没有这样的习惯几个月后面对一堆无意义文件名的日志会浪费大量时间。最后一点关于使用习惯RTT虽然快但也不是万能。在需要长时间记录大量数据的场合我会优先考虑把单片机端的数据存到SD卡或Flash再用上位机通过文件同步的方式读取RTT更多用于实时观察和交互式调试。工具选型还是要根据场景来不是越快就一定越好。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进