
调试嵌入式和搞自动化有两个问题一直绕不开一是怎么实时看板子内部的状态二是怎么在流水线里跑真机验证。串口日志太脆终端模拟器动不动乱码波特率还会影响整个程序的时序想接入 CI 又压根没法判断“到底成没成”。我前阵子写了个命令行工具叫 rttsh本质上是把 J-Link RTT 的能力封装成了可脚本化的 shell 接口让板卡调试、AI 在板诊断、批量脚本验证和数据导出都变成了可调用、可返回码、可交付的状态。这篇文章就把它的设计思路、核心使用方法和踩过的坑完整拆一遍给同样被硬件调试自动化困扰的朋友一个可以直接上手的参考。1. 为什么选 RTT传统的串口调试到底差在哪1.1 串口的各种“不体面”串口日志基本是嵌入式调试的默认方案但用的人心里都清楚它有一堆毛病。先说最常见的波特率要匹配。你用 115200 发上位机就得用 115200 收一旦两边晶振都有偏差就会出现偶尔丢字节、偶尔整行乱码的情况。串口打印还会在中断里占用 CPU 时间在时间敏感型程序里printf 一进去本来 10us 能完成的中断直接拖到 100us 开外整个时序都乱了。更麻烦的是并不是所有板子上都预留了串口引脚功能板和量产板往往会因为引出 UART 引脚而多布线、多占用。这些痛点不是我一个人在项目中遇到几乎每个嵌入式工程师都在这些破事上浪费过时间。当然串口也不是完全没得救。比如加 DMA、用环形缓冲区、降低日志频率都能缓解一部分问题。但本质上串口是一条独立于调试工具的通信链路它需要的是整个系统里单独分配外设资源。换句话说串口是有“税收”的每用一次都要付出引脚和 CPU 代价而且这个代价随日志频繁程度线性上升。1.2 RTT 的工作原理与真正优势RTTReal-Time Transfer是 SEGGER 官方提出的调试通道技术它走的是 J-Link 和 SWD/JTAG 这条本来就存在的调试链路。SWD 只需要两根线SWDIO、SWCLK加上地线和复位线就同时承担下载和 RTT 双向传输的职责。RTT 本身不需要额外占用任何 UART 外设也不需要为了调日志去裁剪代码里的中断优先级对于稀引脚资源的产品板来说这是非常大的解脱。RTT 之所以快是因为它的本质不是串口通信而是“内存共享探测轮询”。目标机的 RTT 控制块Control Block定义在 RAM 中里面有上行缓冲、下行缓冲的写/读索引J-Link 通过 SWD 每隔几十微秒读取这些索引和缓冲区的变化就可以把数据搬运到主机端。这个机制的传输速度可以轻松到几 MB/s比常规串口的 11.5 KB/s115200 8N1高了两个数量级。最让我觉得舒服的是哪怕目标机卡死了RTT 里已经发出来的数据往往还留在 RAM 中通过 J-Link 照样能读出来这种“死后验尸”能力是串口不具备的。用生活化一点的比喻串口调试像快递柜每个柜子都需要单独租用、单独交付包裹多了就得排队RTT 更像是直接在你面前开了一扇窗你想看屋里什么状态不需要等屋里人开门直接从窗外往里瞟一眼就行全程只需要一根 SWD 线。2. rttsh 的设计目标给调试装上一个可编程的“闸门”2.1 命令行化的核心让每一条操作都有退出码单独用 SEGGER 的 RTT Viewer 做交互调试日常非常顺手但一旦遇到“批量验证”和“CI 集成”的场景图形界面就成了最大的瓶颈。脚本没法点击 RTT Viewer 的按钮也没法稳定地判断某一个输出是不是符合预期。rttsh 的目标很简单把 RTT 的收发能力封装成一个个命令每个命令的最终结果用一个退出码exit code表示。命令成功退出码 0命令失败或超时退出码非 0。这就让上层脚本和 CI 系统有了统一的判断依据。比如我想验证板子某个固件版本号的输出传统做法是打开 RTT Viewer、看输出、手动比对。rttsh 脚本里只需要写 rtt_run.sh 里的一条命令把期望内容作为断言参数传入如果板子输出不匹配脚本立刻以非 0 退出流水线直接 Fail。从“人肉观察”变成“机器判断”这一点是整个自动化链路能够跑起来的根基。2.2 场景覆盖从手工调试到无人值守的过渡在设计 rttsh 这套东西时我梳理了所有实际会用到的情况发现不外乎四种手工交互调试像用 shell 一样敲命令查看变量、寄存器、调用函数。AI 在板诊断把调试入口开放给大模型让 AI 能自己发指令、读回复、判断运行状态。批量脚本验证跑设备测试用例、压力测试、回归测试。数据导出与 CI把 RTT 数据流落盘成文件在 GitHub Actions 或 GitLab CI 里跑真机冒烟测试。这四种场景对应四种形态rttsh 从一开始就按这种划分来设计。手工交互靠终端模式AI 在板诊断靠无交互的一次性命令模式批量验证靠内建断言与流程控制语法CI 则靠纯命令行参数和退出码。后面的章节我会逐个拆开详细讲。2.3 对比 RTT Viewer、OpenOCD 等方案的取舍做方案选型的时候我认真比过 RTT Viewer、OpenOCDSemihosting 和 rttsh 这类命令行封装。RTT Viewer 的图形界面交互体验很好但它不具备脚本化能力OpenOCD 的 semihosting 需要编译器额外支持而且输出走的是调试通道模拟性能和稳定性都一般。rttsh 的最大差异化在于它不跟 SEGGER 官方工具抢交互体验而是补上“可编程、可断言、可对接外部系统”这块拼图。这不是替代关系而是继承关系——J-Link DLL 提供的能力仍然是底层基础rttsh 只是把操作变得更可组合。3. rttsh 核心细节与实操要点3.1 命令行语法与内建函数的设计rttsh 的命令行语法设计得很像 shell 和脚本语言的混合体目的只有一个让自动化表达尽量简洁。基础形式是一条命令后跟若干参数。比如说从板卡读取某个变量的值可以写成rttsh read32 0x20000010底层实现是用 J-Link DLL 里的 J-Link_RTT_Read 和 J-Link_RTT_Write 接口来搬运数据再配合实际请求地址处的四字节数据拼接返回结果。而处理更复杂的交互逻辑时我提供了一组内建函数这些函数是脚本化的刹车与油门具体包括wait_for pattern [timeout_ms]等待 RTT 上行数据中出现指定文本出现即成功。expect pattern检查当前 RTT 缓冲区中是否已经存在目标文本不能等待常用于快速断言。assert_silent timeout_ms在指定时间内没有新的 RTT 输出常用于确认系统稳定。delay ms单纯延时给板卡留出处理时间。global name address type声明一个全局符号后续脚本可直接用名字访问。sprintf fmt args...格式化一条 UART 控制命令发送到下行缓冲。这些函数本质上都是对 RTT 通道的读写封装但它们提供的是“语义”不是“裸读写”这才是脚本化工具真正比裸调用 J-Link DLL 高级的地方。拿 wait_for 举例它内部其实做了轮询和超时计数外部脚本只需要关心结果——命中还是超时不用自己去管理缓冲区和时间循环。3.2 超时与中断异步事件是的常见调试痛点做嵌入式交互时有个很现实的问题你发了一条命令下去板卡可能要几百毫秒甚至几秒才会回应期间还可能有其他日志输出混进来。rttsh 里统一用“等待-匹配-超时”的模型处理这种情况。wait_for 命令会不断从上行缓冲读入数据直到找到目标模式或超过设定的毫秒数。在超时之后退出码为 1同时把已经读到的完整内容在 STDERR 里打印出来这样后续脚本和用户都能明确看到“板子当时到底吐了什么”。这个设计也考虑到了半包和粘包问题。RTT 的数据是以流的形式出现的目标模式可能被拆在两段传输里也可能和别的输出挤在同一个缓冲批次里。wait_for 的做法很简单但有效把每次读到的数据追加到内部匹配缓冲区然后对整段缓冲做子串匹配而不是逐包匹配。只要是文本流就总能拼出完整目标不会因为恰好分在两次读而误判。3.3 多板卡连接与实例隔离很多测试环境不止一块板子rttsh 支持用 --serial 参数指定 J-Link 的序列号来选中目标。这个参数非常关键因为它让多板卡并发测试成为可能。你可以同时跑三个 rttsh 进程分别连三块板子各自做独立的验证流程每块板子的输出都只会路由到自己的进程里。我在实验室搭过四路并发冒烟测试实测下来,只要 USB Hub 供电充足、每块板子的 SWD 线足够短小于 20cm四路并跑没有任何互相干扰。多实例隔离还涉及一个细节每个进程都会读取 RTT 控制块的内容但只读不写不会破坏缓冲区状态。因此多实例本身不会导致数据竞争真正需要注意的是不要让两个实例同时对同一块板子发下行命令否则板卡收到的是指令穿插测试结果就不可信了。我在项目文档里明确了一条约定并发测试只读、互斥写。写操作必须通过一个信号量文件来控制或者干脆把写操作集中在单一的“命令板”实例中。3.4 数据导出把 RTT 输出变成结构化的文件流数据导出是 rttsh 的另一个重要能力。配合 --save-file 参数所有从 RTT 上行读到的数据会同时追加写入一个文件。这个文件说白了就是一个原始时间戳文本流每一行格式是[epoch_ms] [channel] [payload内容]没有花哨的二进制格式就是为了让后续不管是人看还是程序解析都足够简单。实测下来连续跑 8 小时的日志采集文件大小约 1.4 GB完全没有内存泄漏和句柄耗尽的问题。如果数据量更大可以用 --rotate-mb 参数指定每写满多少 MB 自动切换文件这个在长时间老化测试里非常实用。数据导出的价值不只在存档更重要的是能给后续的数据分析提供一条干净、不丢行的真实数据链这在调试疑难问题时能少走很多弯路。4. 实操落地从环境搭建到 AI 在板调试验证4.1 准备工作J-Link 驱动与硬件连接在跑 rttsh 之前有几样东西必须备齐一块支持 SWD 的板卡、一个 J-Link 调试器正版或者兼容版都行但建议用正版以免踩坑、SEGGER 官方 J-Link Software Pack。软件包的安装是个容易遗漏的步骤因为只装上 rttsh 可执行文件是不够的它调用的 J-Link DLL 和驱动必须存在。官网下载安装后可以在设备管理器里看到 J-Link 的 USB 设备说明驱动没问题。硬件连接方面我强烈建议 SWDIO、SWCLK、GND 三根线是最低要求如果板卡支持把复位线也连上。虽然 RTT 本身不依赖复位线但 J-Link 在初始化时如果发现目标芯片状态不对可以通过复位线把芯片拉回正常模式。连接好之后用命令行验证jlink -Commander -AutoConnect 1 -Device STM32F407VG -If SWD -Speed 4000能看到目标芯片 ID 就说明链路是通的。这里特别注意设备名称必须写对比如 STM32F407 系列有的具体型号是 VG有的引脚不同名称不匹配会直接报错。在 vscode 里搭建 STM32 开发环境时也是同样的流程装好 Cortex-Debug 插件、选好 J-Link、配好 svd 文件和 rttsh 并不冲突反而互补。IDE 负责下载和断点调试rttsh 负责自动化和数据流。4.2 AI 在板调试给大模型配一双会“摸”硬件的手现在的大模型写代码能力很强但有个天然短板它看不见真实的硬件现象。让它写一个定点数库它能写出看起来合理的代码但跑在板子上是不是真的符合寄存器时序它不知道。rttsh 做的一件事就是把这个反馈闭环打开AI 可以通过 rttsh 向板卡发送命令、读取 RTT 日志、验证变量值。这样一来AI 不再是凭空写代码而是像一个有手有眼的工程师一样能主动探测硬件状态。实际操作中我通常在 AI 对话上下文中附上 rttsh 的使用摘要让它知道有哪些命令、怎么读变量、怎么等待输出。然后给一个具体需求描述例如“读取 0x20000010 地址处的 32 位数据判断是否在 1000 到 2000 之间并导出当前系统时间戳”。AI 会自己生成形如rttsh --serial 12345678 read32 0x20000010 rttsh --serial 12345678 wait_for RTT_TEST_OK 2000这样的命令组合来完成任务。这里有一个很重要的注意点不要让 AI 一次性执行太多写操作。写操作一定要有明确的节奏和防护最好让 AI 在写之前先读一次状态、写后立刻读回验证而不是盲目连续 write。我在一次测试里让 AI 连续把一个寄存器写成不同值结果中间有一次写太快板卡还没接受完就又收到了下一条导致状态机直接崩了。这种问题根源不是命令错而是缺少节拍控制。所以我现在给 AI 的 RTT 交互规范里明确写入每次写操作后必须等待至少 20ms 或等待板上回显确认。在安全性方面我还会额外做一个保护机制给写操作加一个“白名单寄存器列表”。比如只允许写 0x20001000~0x200010FF 这一段其他地址一律拒绝。这样即使 AI 生成了意外地址的写命令rttsh 的内建校验也会直接拦截避免把配置寄存器写乱。4.3 批量脚本验证一条命令跑完 N 个用例批量验证是 rttsh 的主场。拿一个常见的 LCD 驱动板卡测试举例假设固件启动后进入一个简单的命令菜单通过 RTT 下行通道可以输入指令切换显示模式。传统手工测试需要按无数次按键、观察屏幕。用 rttsh我可以写一个简单的验证脚本先等固件打印“menu ready”然后依次下发“mode_1”“mode_2”“mode_3”每次下发后等待一个确认字符串最后再读一个状态变量确认当前模式值。如果直接用命令行来完成大概长这样rttsh wait_for menu ready 5000 \ rttsh sendCmd mode_1 \ rttsh wait_for ACK mode_1 1000 \ rttsh sendCmd mode_2 \ rttsh wait_for ACK mode_2 1000 \ rttsh read32 0x200000C0注意第二条命令 sendCmd本质上就是把参数写入 RTT 下行缓冲区板卡固件里的 RTT 接收逻辑会解析并执行。整个验证流程本质上是一条命令链任何一个环节失败退出码就非 0后续的用例就不再执行。这种形式对 CI 特别友好因为不用写一堆解析脚本只看退出码就好。如果你有更多用例写成脚本文件更合理。rttsh 支持从文件加载命令序列执行到第一条失败处停止并打印失败原因。我在实际项目里习惯把用例组织成 3 层第一层是基础连接检查连得上、能读 ID第二层是功能冒烟启动、显示、通信第三层是压力/回归跑满时长、大流量数据。每层独立脚本、独立超时流水线里逐层调用。4.4 CI 集成GitHub Actions 和 GitLab CI 的真机冒烟CI 集成的痛点一直很明确编译完代码之后怎么知道它在真实硬件上能跑如果没接板卡那只能靠人肉烧录验证等于 CI 的意义只完成了一半。rttsh 让“真机冒烟测试”可以钻进 CI 脚本里。实现思路是在本地准备一台接好板卡的“硬件测试机”CI 系统用自托管 runnerGitHub Actions或专用 RunnerGitLab CI来执行包含 rttsh 命令的任务。一个典型 GitHub Actions 工作流片段是这样的- name: Flash firmware run: JLinkExe -device STM32F407VG -if SWD -speed 4000 -CommanderScript flash.jlink - name: RTT smoke test run: rttsh --serial 12345678 wait_for Boot OK 5000flash 步骤用官方 JLinkExe 烧录固件smoke 步骤用 rttsh 等待固件启动完成。这两步串在一起之后每次代码推送都能自动完成“编译-烧录-通电自检”的闭环。GitLab CI 的 runners 也大同小异核心就是把板卡所在的机器注册成 runner然后让 job 打到这台 runner 上。这里有一个重要细节CI runner 通常会并行跑多个 job如果你只有一个 runner、一块板子就必须禁止并发否则两个 job 同时抢同一块板卡会互相踩。我的做法是在 CI 配置里给 job 加concurrency: 1或使用锁文件机制保证同一时间只有一个 job 访问板卡。这可能让流水线慢一些但换来的是结果的确定性这一点比任何优化都重要。4.5 数据导出文件日志也能结构化rttsh 的单次运行日志和多次运行日志可以合并形成一份按时间轴排列的完整现场数据。假设我要做连续 7 天的设备老化测试可以每天执行一次rttsh --serial 12345678 --save-file aging_$(date %Y%m%d).log --run 10h导出文件里每一行都带时间戳脚本后面可以用 Python 做离线分析比如看看每天的温度采集值是否在合理区间、哪些时间点出现了超过阈值的异常事件。这也是我推荐“时间戳文本流”格式而不是二进制格式的原因下游分析工具链几乎没有门槛谁都能处理。更妙的是这些导出文件本身就是回归测试的输入——把当天导出的数据文件和历史文件做对比能自动发现长期的性能劣化趋势这个用途是我当初设计时没想到的但实际价值极大。5. 常见问题与排查技巧实录5.1 J-Link 没有被识别或报 defective我遇到最多的一个报错是 “No J-Link found” 或者 “The connected J-Link is defective. Proper termination might be missing”。这两种情况多半不是硬件真坏了而是供电不稳或者 SWD 线太长。J-Link 对 VCC 的监测比较灵敏如果目标板供电不稳它就直接拒绝工作。排查方法是先换一根短一点的 USB 线把板卡和 J-Link 的 GND 用粗线连好然后去设备管理器里确认设备状态。如果还不行就把 J-Link 拔下来等 5 秒重插很多时候就恢复。defective 的报错还有一个原因是目标芯片已经进入低功耗模式或调试接口被复位可能需要手动按住板卡复位键再连接。在 rttsh 连接前先跑一次jlink -Commander手动探针一下能规避掉绝大多数偶发连接故障。这是我最推荐的习惯。5.2 RTT 控制块找不到启动后 RTT 输出一片空白是最典型的现象。原因通常有两种第一固件里没有初始化 RTT第二J-Link 扫描控制块位置的方式不对。SEGGER 的默认行为是在 RAM 中搜索特定签名标识如果固件优化级别太高控制块可能没按预期布局搜索就失效了。解决办法是拿到控制块的精确地址.bss 段里的地址然后通过 rttsh 的--rtt-address参数显式指定rttsh --device STM32F407VG --rtt-address 0x200000A0这个地址怎么拿通常在工程编译后的 map 文件里搜_SEGGER_RTT看到的全局地址就是。显式指定之后RTT 连接速度会更快也更稳。5.3 缓冲区溢出导致丢日志如果日志量特别大上行缓冲可能在 J-Link 轮询前就被写满。SEGGER RTT 的默认行为是覆盖旧数据这对于实时调试没问题但对“完整日志导出”来说是灾难因为你会看到数据中间突然断了一截然后跳到另一段。对策有两个方向一是加大上行缓冲把SEGGER_RTT_PRINTF_BUFFER_SIZE提到 2KB 或 4KB二是调高 J-Link 的轮询频率也就是提高 SWD 时钟速度通常 4000kHz 就是一个稳定的选择。双向优化之后我在 115200 波特率级别的数据流场景下没有遇到掉包。5.4 CI 环境下的 USB 权限和设备串占Linux 的 CI runner 上跑 rttsh 还有一个大坑USB 设备权限。默认情况下普通用户访问不了/dev/bus/usb/*需要写 udev 规则或把 runner 用户加入 plugdev 组。不然你会在 CI 日志里看到 J-Link 识别失败但本地手动跑却正常。另一个坑是长时间运行时 J-Link 的 USB 句柄泄漏Windows 上尤其明显。我们曾经连续跑了一周脚本发现第 3 天开始报连接失败后来排查是 J-Link USB 驱动句柄没有完全释放。解决办法是把长任务拆成多个短任务每个任务结束时通过系统调用释放 USB 设备句柄或者每隔固定周期用脚本重启 rttsh 进程来刷新连接实测非常有效。5.5 常见问题速查表现象可能原因处理方式No J-Link foundUSB 未识别重插、换线、确认驱动J-Link defective供电不稳或线过长检查 VCC/GND短接复位RTT 无输出控制块地址错误用 map 文件找到_SEGGER_RTT并显式指定日志跳变上行缓冲过小增大 BUFFER_SIZE提高 SWD 频率CI 里识别失败USB 权限不足配置 udev 规则或加入 plugdev 组长时间运行失败驱动句柄泄漏定期重启 rttsh 进程、拆分长任务串口乱码非 RTT波特率不匹配换用 RTT 通道即可根治6. 这个工具的未来和可扩展方向rttsh 目前已经稳定支撑了我好几个项目的日常调试、AI 诊断和 CI 真机验证但这套思路还有不少可以往外延展的地方。比如我把 RTT 数据流和 Traces 的解析结合起来做一个自动判定信号异常的监控系统也可以把 rttsh 的导出文件直接接入时序数据库做长周期设备趋势分析。再有就是把 RTT 通道直接当一种“设备调试总线”用通过脚本动态配置板卡上的测试探针而不只是读日志、发文字命令。只要 J-Link 和 RTT 这两条基础设施不变rttsh 这类脚本化工具的上限就很高。如果后续时间允许我计划把 rttsh 的 wait_for 语法升级成支持正则表达式让断言能力更强再加一个 --debug 模式可以打印 RTT 收发的原始字节流方便排查自定义协议的编解码问题。这些方向都是被实际需求推着走的而不是凭空想功能。写这个工具最大的体会是别怕去封装那些别人看起来很成熟的工具真正成熟的往往只是“成熟于单独使用”一旦你需要组合它、自动化它、让它在无人值守的场景里稳定工作新工具的生存空间一下就出来了。rttsh 就是站在 J-Link RTT 的肩膀上把“实时查看”变成“可断言、可调度、可留存”的工程能力。如果你也正在被硬件调试自动化困扰强烈建议试一下这条路子哪怕不用我的工具至少要把“RTT 命令行 退出码 CI”这套组合思路放进自己的工具箱里。