ARTICLE DETAIL

资讯详情

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

rttsh:把J-Link RTT变成可编程命令行工具,嵌入式调试自动化新解

rttsh:把J-Link RTT变成可编程命令行工具,嵌入式调试自动化新解 做嵌入式调试这几年我最常用也最头疼的调试手段就是 J-Link 的 RTT。说它常用是因为 RTT 在目标芯片上只需要消耗几十字节 RAM一条 SWD 线就能把日志、变量、甚至交互式命令通道全部搞定调试体验远好过老式的半双工串口。说它头疼是因为官方配套的 JLinkRTTViewer 始终是一个“给人用”的图形工具要么手动开窗口要么截图粘贴想在脚本里读一行日志、在 CI 里跑一轮回归、或者把日志喂给大模型做 AI 辅助调试官方工具一律帮不上忙。于是我动手写了 rttsh——一个把 RTT 会话变成可编程命令行的工具。它既能当交互式 shell 用也能执行 .rttsh 脚本文件能自动打时间戳、导出 CSV/JSONL、把结果回传给 CI配合复位和固件加载命令还能实现从“烧录固件→启动运行→抓取日志→断言结果”的全自动闭环。这篇文章就把整个设计思路、核心实现选型和我在实际操作中踩过的坑完整写出来给同样被 RTT 自动化折磨的嵌入式工程师参考。1. rttsh 要解决的三件事AI 调试喂数据、批量跑用例、CI 里抓日志1.1 先把痛点说清楚RTT 好用但官方工具是“给人用的”RTTReal Time Transfer的原理简单说就是调试器通过 J-Link 的 SWD/JTAG 接口直接去读目标芯片 RAM 里的一个环形缓冲区目标固件把日志写进缓冲区主机侧就能以极低成本拿到数据。它不占 UART不需要额外接 TX/RX 线在 Cortex-M 上可以达到几百 KB/s 以上的吞吐对实时性影响也远小于串口阻塞打印。这些优点让 RTT 几乎成了嵌入式调试的标配。但 RTT 真正做到“好用”其实卡在主机侧。官方提供的 JLinkRTTViewer 是个 Windows GUI 程序连接、探测控制块、显示数据都是人肉操作。想在自动化环境里用它基本不可能它没有稳定的命令行接口没有可编程的断言能力也不会把数据按文件导出。更麻烦的是它在无桌面环境的 CI 机器上根本跑不起来。JLink.exe 倒是命令行工具但它面向的是交互式调试场景。你可以在提示符下输入命令控制目标输出也有一定结构化但要把 RTT 数据流完整接出来、按时间戳记录、做条件等待和回放还差一个自动化外壳。最典型的场景就是我想在一条命令里完成“连接目标、开启 RTT、等关键字出现、把日志存文件”官方工具链做不到rttsh 就是补上这一层的。1.2 AI 在板调试让大模型自己看日志、跑命令最近这两年用 LLM 辅助调试嵌入式固件越来越普遍。常规做法是人肉把 RTT 日志复制粘贴给大模型让它分析崩溃点、猜测原因、给出补丁然后人再编译烧录重新抓一轮日志再喂给模型。这个循环很有效但效率瓶颈全在人工搬运上。rttsh 在这个链路里扮演的是“传感器和执行器”。传感器部分rttsh exec debug.rttsh --json可以直接抓取带时间戳的日志并用 JSONL 格式输出LLM 解析起来非常舒服。执行器部分脚本里可以写reset、loadfile、go相当于把复位和固件加载也变成模型可调用的原子操作。配合外部 Agent 框架一个 session 里就能完成“读日志→分析→改代码→重烧→再验证”的闭环。我在实际使用时有个体会喂给 LLM 的日志不能是原始全量流token 限制很快会爆。rttsh 要提供--max-lines、--grep、--tail这类过滤参数让工具“带着筛选结果返回”效果比直接灌原始数据好得多。这也是 rttsh 值得花功夫做 CLI 参数语义化的原因。1.3 批量脚本验证与 CI没有显示器也要能把日志抓回来嵌入式工程的持续集成一直比纯软件难做因为很多时候代码逻辑正确与否必须跑在真实硬件上才能判断。如果 CI 基础设施里有一块板卡长期连着 J-Link那么批量验证完全可以自动化编译出不同配置的固件逐个烧录每个烧录后跑一段预设的测试序列从 RTT 里断言输出是否符合预期。没写 rttsh 之前这种批量验证我只能在工位上手动操作一晚上盯几十个构建产物既枯燥又容易漏。写了之后回归流程变成一条命令行遍历固件文件、烧录、复位、等待预期日志、判定结果、存 RFC 和 CSV 报告。脚本跑完后人只需要看汇总表。CI 环境更是如此。GitHub Actions 和 GitLab CI 的 runner 都是无头服务器JLinkRTTViewer 根本装不了也起不来。rttsh 不依赖图形环境只要系统能识别 USB 里的 J-Link就能在 Docker 容器里跑。这一步是它能进入 CI 流水线的根本前提。2. rttsh 的脚本模型怎么设计一次 RTT 会话变成一段可回放的流程2.1 先定目标REPL 交互 脚本批处理一个都不能少在设计 rttsh 的第一天我就明确了两件事它必须既适合人工交互也适合程序调用。交互式场景里用户连上板子后想即时看 RTT 输出甚至往目标设备里敲命令这需要 REPL 风格体验。自动化场景里用户要把一系列操作写成可重复执行的脚本这需要批处理模式和明确的退出码。这两种模式共用一套内部状态机连接、配置、运行、读取、写入、断开。交互模式只是把每次键盘输入当作一条脚本命令执行脚本模式则是逐行解释执行文件内容遇到等待条件或断言失败就退出并返回非零状态码。这样实现上的重复代码最少两种模式下的行为也完全一致。退出码的语义很重要。我的定义是0 表示整个流程成功1 表示断言失败即数据读到了但结果不符合预期2 表示连接或设备初始化失败通常是 USB、驱动或序列号问题3 表示超时wait/expect 没有等来想要的输出。CI 里判断测试失败只需要看退出码非常直接。2.2 脚本语法里的几个关键原语连接、等待、断言、导出rttsh 没有发明复杂的通用语言而是做了一组针对调试场景的领域原语。下面这个例子基本覆盖了大部分使用场景# demo.rttsh target stm32f407vg swd 4000 connect rtt on log open rtt.log --timestamp loadfile build/app.hex reset go wait system_ready 5000 write test_case_01 expect test_case_01_pass 10000 export csv result.csv assert $last_line test_case_01_pass log close disconnectwait和expect是我用的最多的两个命令。wait表示“阻塞直到从 RTT 读到指定关键字或超时”expect类似但超时后直接判定失败。区别在于wait常用于同步点比如等待系统启动完成、等待某个单元测试实例开始expect则直接代表一个验收条件。write命令负责往 RTT 的 down buffer 写数据也就是把主机侧输入送到目标设备相当于给固件发命令或切换测试用例。方向和通道必须可配置默认走通道 0但脚本里可以channel 1切换到辅助通道。export与assert的配合是批处理的核心export把当前会话累计的数据按指定格式落盘assert对变量或最近读到的数据做比较。有了它们批量验证就不是“抓日志给人看”而是“自动判定并通过退出码告诉 CI 结果”。2.3 时间戳和数据结构RTT 没有时间概念时间必须由主机打这是很多初次做 RTT 自动化的人没意识到的问题RTT 协议本身不带任何时间信息。目标芯片往环形缓冲里写数据时不会记录“我现在是第几毫秒”因为那会让实现复杂化也占用额外带宽。时间戳必须由主机侧在数据被读出时打上。但这里有个误差要理解主机打的时间戳 数据“被读取”的时间不等于数据“被写入缓冲”的时间。RTT 缓冲区本来就有一点延迟数据量越大、主机轮询间隔越长时间戳和真实事件时间的偏差越大。我的处理方式是让 rttsh 的读取线程保持高优先级轮询同时在轮询间隔和性能之间做平衡默认每毫秒采样一次足以覆盖绝大多数调试和测试场景。数据结构上rttsh 内部维护的是“行”级别的事件流每读到一个完整换行符就把这一行连同时间戳封装成一个事件对象。这样无论导出成纯文本、CSV 还是 JSONL 都统一。也正因如此grep过滤、tail截取、按正则匹配的expect都能在事件流上直接做不需要重新解析原始文本。3. 核心实现怎么落地JLink 命令行封装、DLL 直调与对外接口3.1 方案 A用 PTY 封装 JLink.exe 的交互式终端最直接的想法是JLink.exe 本身就是一个交互式命令行程序在提示符下执行rtt就能进入 RTT 终端模式。我的第一个原型就是用伪终端PTY拉起 JLink.exe然后往终端里写入命令解析终端输出的文本流把 RTT 数据从里面剥离出来。优点很明显不依赖特定编译器不需要额外安装开发包只要机器上有 JLink 软件包就能跑。但这个方案很快就暴露了问题。JLink.exe 的输出里混着大量交互控制字符、菜单边框、进度百分比甚至还有彩色转义序列不同版本的 JLink.exe 输出格式还会变化。解析这种“给人看”的文本极其脆弱一次软件包升级就可能让解析器失灵。另外PTY 方案无法直接获得底层的高效读取接口RTT 数据量大时会因为终端渲染导致吞吐降低。所以我的结论是PTY 方案适合做快速验证不适合做生产级工具。如果你只是临时想要一个能脚本化的 RTT 抓取器可以试试但如果你想把它长期用在 CI 和批量回归上还是考虑方案 B。3.2 方案 B绕过命令行直接读写 J-Link DLL更专业的做法是绕过 JLink.exe 这个壳直接调用 J-Link 官方软件包里的动态库。在 Windows 上通常是 JLinkARM.dllLinux 上是 libjlinkarm.so。J-Link 软件安装包里附带丰富的 API 头文件我能拿到读内存、写内存、控制目标执行、枚举设备列表等底层能力。RTT 数据在这个方案里的获取路径是扫描目标 RAM定位SEGGER RTT控制块读取出 up buffer / down buffer 的结构描述然后轮询读取缓冲区内容。这套协议是 SEGGER 公开的并没有黑魔法。实现时大致是这样一个流程# rttsh 核心逻辑简写示意具体以实际头文件为准 lib load_jlink_dll() lib.JLINK_Open(None, None) lib.JLINK_ExecCommand(CONNECT, ...) lib.JLINK_ExecCommand(SETTARGETPOWER, ...) # 扫描 RAM 中的 RTT 控制块 cb_addr rtt_scan_ctrl_block(lib, start, size) # 读取 up buffer 数据 data lib.JLINK_RTTERM_Read(0, buffer, size)DLL 方案的好处是稳定、高效、不受 JLink.exe 终端渲染影响还支持在 Linux Docker 容器里工作。缺点是 API 数量多光设备枚举和连接参数就有几十种头文件的注释又写得比较简略上手有门槛。而且要注意不同版本软件包里 API 签名可能有细微变化做封装时要留意版本兼容。3.3 我的选择与实践中的取舍rttsh 最终选的是 DLL 方案但并没有完全抛弃命令行工具。原因是我还需要 JLink.exe 里的loadfile、savebin、flash download等成熟的烧录能力这些功能自己用 DLL 重写工作量不小而且容易在细节上翻车。所以现在的实现是双引擎RTT 数据通路走 DLL姿态烧录和复位操作走 JLink.exe 命令行调用两个进程之间通过状态文件同步。这种“双轨制”听起来复杂实际效果很好。RTT 数据流对实时性敏感DLL 直读是最优选择固件烧录这类一次性操作对速度不敏感命令行调用更稳妥也更方便在出问题时看到官方工具的原始输出。调试时经常遇到“固件下载失败”这类问题能看到 JLink.exe 的报错文本比给一长串 DLL 错误码人性化得多。另外跨平台问题上 Windows 和 Linux 我都做了支持。Linux 下 JLink 软件包提供了 libjlinkarm.soCI 镜像里装好后rttsh 在 Docker 里跑和物理机上跑没有差别。Windows 上则要注意把 DLL 路径找到尽量使用安装目录里的固定路径避免 PATH 混乱导致加载失败。3.4 rttsh 对外接口给人类用、给机器用、给 AI 用接口设计直接影响工具能被多少人用起来。rttsh 提供三种使用方式交互式 shell、单条命令、脚本文件。单条命令是rttsh run connect; wait ready 3000这种直接传命令串的形态适合在 Makefile 和 CI 快速调用脚本文件是推荐方式可读性和可维护性更好。为了给 LLM 和 CI 提供结构化输出rttsh 支持--json参数。带这个参数时工具不再输出人类可读的彩色日志而是把所有事件、断言结果、退出码打包成 JSON 写到 stdout日志文件只保留原始 RTT 内容。这个设计让外部程序可以安全地解析结果而不会被日志内容干扰。设备选择和序列号也必须可配置。多块板卡并联在同一台机器时J-Link 的序列号是唯一标识。rttsh 的--serial参数直接传入设备编号避免“连错板子”这种 CI 里最尴尬的故障。4. 照着做就能跑通环境准备、最小脚本与 CI 接入4.1 环境准备软件包、驱动、固件版本三件套第一步是安装 SEGGER 官方的 J-Link Software Pack。这个名字有点绕口其实就是 J-Link 的完整软件包里面包含了 JLink.exe、JLinkRTTViewer、DLL 以及各种配置文件Windows 和 Linux 都有对应版本。安装时注意选对操作系统架构x86 和 x64 的 DLL 不通用。驱动方面Windows 上安装软件包时一般会自动装上 SEGGER 的 USB 驱动但 Win11 的驱动签名策略比较严格某些老版本 J-Link V9 设备插上去会显示感叹号。这时要么更新到支持 Win11 的配套驱动版本要么换新一点的 J-Link。Linux 上则是 udev 规则问题需要把当前用户加入plugdev组或者给 J-Link 设备写一条 udev 规则否则普通用户没有 USB 访问权限。固件版本是最容易埋雷的一项。J-Link 设备本身有固件软件包会尝试给设备升级或降级以匹配当前软件版本。如果你的 J-Link 是很多年前的旧设备新版软件包可能不再支持它或者升级到一半报错。建议在开发机上先跑一次 J-Link Commander 确认固件状态正常再开始用 rttsh。4.2 最小示例连接开发板读一行日志我先给一个最简脚本让刚接触 rttsh 的人 10 分钟内跑通。假设目标板是 STM32F407VG接口 SWD速度 4000 kHz# minimal.rttsh target stm32f407vg swd 4000 connect rtt on wait Hello 3000 log open hello.log --timestamp disconnect执行方式rttsh exec minimal.rttsh如果固件启动后通过 RTT 打印了包含 “Hello” 的行脚本会正常退出并在退出前把所有行写入hello.log。如果 3 秒内没等到退出码为 3CI 会立刻判断失败。这个脚本已经具备 CI 测试的基本雏形连接、运行、等待、结果收集。一个小细节这里的wait Hello 3000是通过正则表达式匹配的不是简单的字符串包含。所以你可以写wait test_0[12]_pass来做更灵活的判定。但提示一句正则写复杂后容易误匹配测试场景里尽量让固件打印的日志带上唯一标识。4.3 数据导出文件日志、CSV、JSONL 三种形态日志输出最容易踩的坑是“数据量大时写文件卡住”。我建议 rttsh 内部用一个独立的写入线程主读取线程只往阻塞队列里放数据写线程负责消费和落盘。这样即使日志文件很大也不会拖慢 RTT 读取速度。交互式查看时文件写入比终端显示更可靠因为终端刷新本身会占用不少 CPU。导出 CSV 时我会把每一行拆成“时间戳、通道号、内容”三列时间戳精度默认毫秒可以用参数切换到微秒。给 AI 和 CI 用的 JSONL 则是每行一个 JSON 对象字段包含完整的元信息解析成本最低。纯文本日志用于人眼排查三种格式可以在同一个脚本里同时导出。实际测试中我发现JSONL 虽然功能最全但体积比纯文本大 3 到 5 倍。如果固件每秒打印几千行日志文件轻易上百 MB。所以 CI 里建议默认只导纯文本只有在 AI 调试场景才开 JSONL而且配合--tail或--grep把数据量压下来。4.4 接进 GitHub Actions / GitLab CI 的模板思路仓库里我放了一个可直接复制的 GitHub Actions 模板。关键点是 runner 必须是自托管并且物理连接着 J-Link 和开发板否则没有 USB 设备可用。模板里的任务大致是checkout、编译固件、执行 rttsh 回归脚本、上传日志作为 artifactrtt-test: runs-on: [self-hosted, emb] steps: - uses: actions/checkoutv4 - name: build firmware run: make -C firmware - name: flash and verify run: rttsh exec test/regression.rttsh --timeout 120 - name: upload rtt logs uses: actions/upload-artifactv4 with: name: rtt-logs path: *.logGitLab CI 的思路完全一致只是语法换成 GitLab 的 job 描述。唯一的额外建议是把 rttsh 二进制放进 CI 镜像而不是每次运行时现场安装能省下不少流水线时间另外不要让多个 job 同时访问同一个 J-Link务必用--serial区分设备避免两个 job 抢设备导致随机挂死。5. 现场排障实录固件报错、Win11 驱动与 RTT 丢数据的坑5.1 S/N:20090928 的固件报错 “does not support the following command”这条报错几乎每个用过老 J-Link 的人都见过典型输出是“The firmware of the connected J-Link (S/N: 20090928) does not support the following command: ...”。意思是当前 J-Link 的固件版本太老或者设备本身不是官方最新固件导致新版软件包发出的某个命令它无法响应。遇到这个报错时我会先做一件事打开 JLink Commander执行firmware update看能否把固件升到当前软件包兼容的版本。如果升级成功问题通常消失。如果设备是早年流传的第三方改造版固件更新经常会失败这时能做的就是固定使用旧版 J-Link 软件包让工具和固件保持在同一个年代。在 rttsh 里我也加了一层保护启动时先用 DLL 枚举设备并读取固件版本如果发现固件过旧直接给出可读的提示而不是在连接中途才报莫名其妙的错误。自动化的价值就是让问题尽早暴露而不是让你在 CI 日志里翻半天。5.2 J-Link V9 在 Win11 下的驱动识别迷局老一批 J-Link V9 设备在 Win11 上可以说是“重灾区”。最典型的现象是设备管理器里能看到一个带黄色感叹号的 USB 设备怎么装驱动都无效。原因多半是新版 J-Link 驱动已经不再支持 V9 的旧接口或者驱动签名策略把设备拦下了。解决办法我建议按这个顺序试先确认安装了最新版的 J-Link 软件包很多情况下驱动库里就带着新签名驱动如果无效查看设备管理器中的硬件 ID手动指定驱动路径到 J-Link 安装目录还不行就检查 Win11 的“内核隔离”和驱动签名设置部分设备需要关闭内存完整性才能正常加载驱动。但这属于系统级改动公司电脑上要谨慎。我的个人建议如果项目里有充足预算调试设备尽量用 J-Link V10 以上的版本。V9 在 Win11 下的兼容性投入产出比太低了用 rttsh 是为了省时间不要在驱动的泥潭里浪费一整天。5.3 CI 无头环境里的 USB 权限与稳定性问题Linux 自建 runner 是 CI 里跑 rttsh 最常见的环境但 USB 权限问题能把人卡很久。rttsh 访问 J-Link 的提示通常是“Cannot connect to J-Link”或 “USB device not found”实际上权限根本不够。解决办法是把运行用户加进plugdev组或者写自定义 udev 规则SUBSYSTEMusb, ATTR{idVendor}1366, MODE0666, GROUPplugdevJ-Link 的 USB vendor ID 是 1366这条规则对所有 J-Link 设备有效。如果跑在 Docker 里容器启动时要挂载 USB 设备目录比如-v /dev/bus/usb:/dev/bus/usb --privileged。不过加--privileged会降低容器隔离性只建议在自建 runner 上用。稳定性方面我的经验是每次 CI 任务结束时让 rttsh 主动执行disconnect并关闭会话而不是直接杀进程。J-Link 设备在非正常断开后有时需要几秒钟才能被系统再次识别连续多个 job 之间如果没留间隙下一个任务就可能撞上“设备被占用”的错误。rttsh 有--attach-retry参数默认重试三次基本能把这个竞争窗口抹平。5.4 RTT 丢数据环形缓冲溢出与主机侧消费速度RTT 环形缓冲区大小由固件里的SEGGER_RTT_CONFIG_BUFFER_SIZE_UP决定。如果固件打印量超过了主机侧读取速度缓冲会被覆盖日志里会看到中间缺了一块最直观的现象是“昨天还能看到这行日志今天跑了同样流程却不见了”。碰到这种问题除了加大缓冲区更重要的是检查 rttsh 的读取线程是否有足够的调度优先级。主机侧我在 Linux 上遇到过一个隐藏瓶颈Python 的 GIL 和垃圾回收偶尔会把读取线程卡住几毫秒对高频 RTT 流来说这几毫秒可能丢掉几百字节。后来我把读取部分拆成单独的子进程并使用共享内存传递数据核心进程保持极高的轮询频率丢包率明显下降。如果你的固件日志量大建议关注工具实现是否把读取路径暴露给了慢速语言运行时。另一个容易忽略的点是wait/expect的超时时间设置不能太短。如果固件启动流程需要 2 秒才打印第一行你设置 1 秒必然超时而这不代表设备有问题。先用交互模式观察一次真实时间线再根据实际数值设定脚本超时才是合理做法。5.5 几个提高成功率的实用建议第一日志文件不要放在 CI 工作目录的临时路径里很多 runner 会在任务结束清理工作区结果 artifact 上传前日志就被删了。rttsh 支持通过--log-dir指定独立产物目录比如/tmp/rtt-logs然后 CI 任务里显式上传这个目录。第二让脚本具备幂等性。同一个回归脚本应该可以反复执行而不依赖上一次的状态。比如脚本开始前固定rtt off再加rtt on避免上一次残留的 RTT 控制块干扰本次连接。rttsh 的rtt reset命令就是干这个的规范里建议每个脚本第一条就要重置状态。第三在团队推广前先把 rttsh 的退出码约定写进 README。这不是文档洁癖而是因为 CI 排障时大家第一反应就是看 job 状态码。我的约定是 0/1/2/3 对应成功/断言失败/连接失败/超时也有文档明确说明。这套约定被团队成员记住后看流水线失败原因的时间从几分钟缩短到几秒。最后说一个我个人的实操体会rttsh 最大的价值不是省掉了打开 JLinkRTTViewer 的那几秒而是让调试过程可以被记录、回放和比较。以前我遇到偶现 bug只能盯着窗口等复现现在我会直接跑一个带时间戳和完整日志的脚本把复现现场完整保留下来再慢慢分析。这个习惯改变之后很多曾经难以定位的时序问题都有了可追溯的路径。如果你也被 RTT 自动化折腾过不妨照着上面的思路做一个属于你自己的 rttsh或者直接拿这个工具改造一番它会让你重新认识“调试”这两个字。
返回列表