玩嵌入式调试的,谁还没被日志逼疯过?板子上跑着关键算法,想实时看状态,只能打开 J-Link RTT Viewer 手动操作;想跑个批量测试,还得人肉盯屏幕记录数据;等想把产线验收脚本接入 CI,更是无从下手。我前段时间写了个小工具 rttsh,专门解决这套场景。它把 J-Link RTT 调试封装成命令行接口,支持脚本文件、数据导出和 CI 集成,真正让板卡日志的采集和验证变得像跑 shell 脚本一样顺手。这篇文章聊聊它的设计思路、核心功能和实际踩坑记录,适合搞嵌入式、做板级验证、以及在自动化测试里折腾 RTT 的朋友。
1. 背景与设计思路:从RTT调试痛点谈起
1.1 为什么需要脚本化的RTT调试
先说说 RTT 本身。SEGGER J-Link 的 RTT(Real-Time Transfer)和串口完全不是一回事。串口必须依赖 MCU 里的 UART 外设,波特率、DMA 一堆配置,哪怕 921600 波特率在高速打印场景下也会拖慢系统。RTT 则直接走调试接口,通过目标内存中的 RTT Control Block 交换数据,打印一条日志可能只消耗几个时钟周期,几乎不影响实时性。这个特性让 RTT 在高频日志、时间敏感调试里几乎是不可替代的。
可问题也出在这里。日常调试我用 RTT Viewer,连接、打印、保存都正常,但一旦遇到下面这些场景就特别难受:
- 验证脚本没法自动化:RTT Viewer 能看能存,但没法做断言,也没法在特定时刻向设备注入命令。想跑二十个用例,只能一个个手动开日志、看结果。
- 数据导出不友好:Viewer 的保存格式是固定的纯文本,时间戳、二进制数据混在一起。我要统计某个传感器数值的平均值,还得自己写解析脚本去扣。
- 没法进 CI:持续集成里跑板级测试,需要命令行能调用,能挂到流水线上,能根据退出码判断成功失败。RTT Viewer 是个 GUI 程序,这一步就卡死了。
还有一个隐性痛点,就是调试经验的沉淀。老工程师调一个问题,往往是“连接板子、启动 RTT、敲个命令、看现象”这一套组合拳。但这套组合拳如果只能靠人手去敲,就永远无法复用。如果能把调试动作固化成一个脚本文件,存在仓库里,不管是同事还是机器都能一键重跑,效率完全是两个量级。
1.2 命令行工具的核心设计目标
想通了上面的问题,我给自己提了几个设计目标。首先,工具必须是一个纯命令行的交互式 shell,像 redis-cli 那样,既能单条命令交互,也能从脚本文件里批量读命令。其次,脚本语言要足够简单。嵌入式工程师大多不是专业脚本玩家,搞太复杂的语法反而劝退。第三,退出码要有意义。CI 判断成败全看退出码,如果工具内部断言失败还返回 0,那流水线就等于摆设。最后,数据导出能力要强。至少支持 CSV、JSON、原始二进制三种格式,方便下游用 Python 或 Excel 处理。
技术选型也比较直接。Python 生态里有 pylink 这类封装库,底层走 SEGGER 的 DLL,跨平台也方便。我用 Python 写 CLI,把连接管理、RTT 读写、脚本解析、断言执行拆成几个独立模块。这么说吧,rttsh 的骨架就是一个“命令分发器 + 设备抽象层”,上层用cmd模块处理交互,底层用 pylink 控制 J-Link,中间夹一个简单的脚本解释器。这样结构也方便以后加命令扩展。
2. rttsh的核心功能拆解
2.1 脚本化支持:调试脚本的语法与执行
脚本化是 rttsh 的立身之本。我的设计原则是“一行一条命令,不带括号,不用缩进”。每条命令由动词和参数组成,比如connect --device STM32F407 --if swd --speed 4000、expect "Ready" timeout 5、assert match "BP=([0-9.]+)" --group 1 --cmp gt 3.14。这种风格对人和机器都非常友好,人一眼能看懂,语言模型也很容易生成。脚本文件用.rttsh后缀,支持注释行,以#开头。
脚本的解释执行是顺序的,但为了处理循环和条件分支,我加了一组控制命令:
# 连接与启动 connect --device STM32F407 --if swd --speed 4000 rtt start # 等待设备输出 BOOT_OK expect "BOOT_OK" timeout 10 # 若匹配成功则进入压力测试循环 if match "BOOT_OK": repeat 100 rtt write "start_pressure" sleep 50 expect "CYCLE_DONE" timeout 5 log append /tmp/pressure.csv endrepeat else fail "NO_BOOT" endif核心逻辑是expect命令。它不阻塞整条脚本,而是读取 RTT 输出直到匹配某个字符串或超时,然后把匹配结果存入内部变量。assert则更严格,匹配失败立即中止脚本并返回非零退出码。这里我想强调的是:脚本语言的核心语义只有“等待、匹配、断言”三件事,所以它足够简单,几乎不存在学习曲线,又完全覆盖板级验证的需求。
一个小技巧是支持-c参数直接执行单条命令。比如在 CI 里不想写整个脚本文件,就想快速启动 RTT 并确认发射,可以用rttsh -c "connect --device STM32F407; rtt start; expect BOOT_OK timeout 5"。这对临时诊断特别顺手。
2.2 对接AI辅助调试:让模型也能玩转板卡
标题里提到的“AI在板调试”,我的落地方式并不是让 AI 直接操作调试器,而是让它生成和调试 rttsh 脚本。有一次我把自己板子的源码、串口日志格式和 rttsh 帮助文档一起丢给在线语言模型,让它写一个“检查四次传感器采样并计算平均值”的验证脚本。它几秒钟就输出了repeat 4和assert组合。我在板子上跑了一遍,第一次断言阈值写错了,模型根据报错信息自我修正,第二次就过了。
这件事让我意识到 rttsh 天然是 AI 友好的工具。原因是命令行接口简单、输出结构化(有明确的退出码和匹配变量),语言模型容易理解。为了让这个流程更顺滑,我在 rttsh 里增加了--json输出模式。脚本执行过程中,每次expect和assert的结果都会以 JSON 流写到 stdout,包括匹配值、耗时、行号、动作类型。这样 AI 或者后续的解析器只需要读一行 JSON,就能判断板子状态。
当然,让 AI 直接操作板卡要加保险。我加了两个实用机制:一是dryrun模式,rttsh -f script.rttsh --dryrun只解析脚本、打印执行计划,不真正连设备;二是全局超时,--timeout 60,防止脚本卡住生产线。实际项目里我都是先 dryrun 检查脚本结构,再真连板子,AI 生成脚本后也建议先跑 dryrun。
2.3 批量验证与数据导出:从杂乱的日志中提炼数据
批量脚本验证是我用得最多的功能。以前做固件验收,测试项有十几条:上电打印、外设初始化、内存读写、电源管理切换。每条都对应一个.rttsh脚本文件,我用一个总控脚本把全部用例串起来。
for file in $(ls tests/*.rttsh); do echo "Running $file..." rttsh -f $file --json >> results.jsonl echo "exit code: $?" done数据导出这部分,log export命令支持多个格式。比如log export /tmp/adc_data.csv --format csv --timestamp会把 RTT 收到的原始 ASCII 按 CSV 写出,每行前加毫秒时间戳。--format json则保留二进制数据和通道信息,适合后续做频谱分析。--format raw是对二进制 RTT 通道的精确保真,适合协议抓包。
说一个处理二进制数据的教训。早期 rttsh 把 RTT 上游通道当纯文本处理,结果有一个模块会通过 RTT 发送结构体,日志直接被decode成乱码。后来我改成按字节流分块,并支持--encoding hex参数,需要二进制时用 hex 字符串打印,这样既能看到原始字节,又不会把非文本数据搞坏。如果你的设备也有类似结构体打印的需求,建议从一开始就用 hex 模式,别等数据坏了一堆才回头改。
2.4 打通CI:让板卡验证成为流水线的一环
板级测试进 CI,最大的障碍就是“没有图形界面也能控制 J-Link”。rttsh 在这方面没什么包袱,它本来就是命令行工具,跑在 Windows/Linux 的 runner 上都不需要桌面。具体集成方式我把完整示例放到 3.4 节,这里先说说架构。
CI 里最关键的一点是失败传递。rttsh 所有错误都走统一退出码:0 表示脚本执行且所有断言通过;1 表示断言失败;2 表示连接错误;3 表示参数错误;4 表示超时。这样流水线里只需要检查$?就能准确判断是板子问题还是环境问题。我甚至会在 CI 阶段里用continue-on-error来分别收集“失败但想看的日志”和“环境准备失败”的情况。
另外,为了不让 J-Link 被多个并发 job 抢用,我在 CI 里对跑板卡的 job 只允许一个并发,用 GitHub Actions 的concurrency控制。这是很常见的一个坑:两个流水线同时抓同一个 J-Link,轻则报错,重则把缓冲区搞乱。下面实操章节会详细给一套能直接抄的 CI 配置。
3. 实操过程:从零构建一个可用的调试流程
3.1 环境准备:驱动、固件与工具安装
rttsh 是 Python 包,直接pip install rttsh或者从源码装都可以。但最关键的先决条件是 J-Link 驱动和 SEGGER 软件包。这里多啰嗦一句:安装顺序很重要。先装 SEGGER J-Link 软件包,再安装 pylink,最后装 rttsh。因为 pylink 在初始化时要自动找 SEGGER 的 DLL,如果软件包不在默认路径,连接会失败。
如果你用的是老一代 J-Link v9,Windows 11 系统请务必升级驱动到最新版。我遇到过 v9 在 Win11 下系统识别不了,设备管理器里一直黄色感叹号。解决方案是去 SEGGER 官网下载最新版 J-Link Software Pack,安装完成后用 J-Link Updater 刷一遍固件。注意刷固件时别拔 USB,v9 在断电半更新状态下变砖的概率不低。
J-Link 固件这块还有一个容易卡住的点。连接时会弹 "The firmware of the connected J-Link does not support ..." 这样的错误,本质是 J-Link 固件版本太旧,跟不上驱动软件的新命令。处理方式很简单,打开 J-Link Updater,把固件更新到与软件包匹配的最新版。现场遇到这个问题,我通常直接下载官网最新的 Software Pack,然后执行 Updater 一键升级,不用额外操作。
3.2 连接配置:RTT控制块与连接参数
rttsh 的连接参数和 J-Link Commander 类似。最常用的参数是设备型号、接口类型、目标速率和 RTT 控制块地址。例如:
rttsh --device STM32F407 --if swd --speed 4000 --rtt-address 0x20000000--device必须写 SEGGER 支持的型号名,比如STM32F407、nRF52840、ATSAMD51。如果型号不对,连接会报 “Cannot connect to target”。接口默认是 SWD,速度建议先保守用 1000 kHz,等确认稳定再往上调。4 MHz 在短杜邦线的情况下比较稳,排线一长就容易乱。
RTT 控制块地址是很多新人最懵的地方。RTT 输出不需要像传统串口一样初始化外设,但需要在目标内存里找到控制块。rttsh 提供了两种方式:一种是让工具自动搜索,--auto-search;另一种是手动指定地址。自动搜索适合快速验证,但有时会因为搜索范围有限而找不到。手动指定最适合发布固件:在链接脚本里固定一个地址,比如0x20000000,然后在脚本里写死。这里有个经验:建议把 RTT 控制块放到一个固定 RAM 地址段,并在.map文件里查一下地址,这样以后调试不用每次做内存扫描。
3.3 脚本编写与运行示例
拿一个最简单的启动验证脚本做例子。设备是一只 STM32F407 板,固件上电后打印一行APP_VERSION=1.2.3,随后进入低功耗模式,RTT 不再有输出。我们的目标是验证版本号正确,并导出日志。
# verify_boot.rttsh connect --device STM32F407 --if swd --speed 4000 rtt start expect "APP_VERSION=([0-9]+\.[0-9]+\.[0-9]+)" timeout 5 assert match "APP_VERSION=1\.2\.3" log export /tmp/boot_log.csv --format csv --timestamp运行命令:
rttsh -f verify_boot.rttsh --json我叠加--json,看到类似这样的输出:
{"event": "expect", "status": "ok", "match": "1.2.3", "time_ms": 122} {"event": "assert", "status": "pass", "line": 5}对于循环压力测试,脚本适合这样组织:
connect --device STM32F407 --if swd --speed 4000 rtt start repeat 5 rtt write "UT:1" # 触发一次单元测试 sleep 100 expect "UT_PASS" timeout 3 log append /tmp/results.txt "cycle $i ok" endrepeat这里的$i是循环计数器,rttsh 的脚本解释器会自动展开。实际执行时板子 100 毫秒跑完一个单元测试,脚本总共 10 秒跑完 5 轮,退出码 0。整个流程没有 GUI,没有人工介入,纯命令行搞定。
3.4 集成到CI流水线
现在给一个 GitHub Actions 的实操配置。我假设你的 runner 是windows-latest,因为 Windows 环境对 J-Link v9 驱动最友好,Linux 上 v9 老固件坑更多。步骤包括安装驱动、安装 rttsh、跑脚本、上传日志。
name: board-test on: [push] jobs: rtt-test: runs-on: windows-latest concurrency: board-rtt steps: - uses: actions/checkout@v4 - name: Download J-Link Software Pack run: | curl -L -O https://www.segger.com/downloads/jlink/JLink_Windows_V796_x86_64.exe ./JLink_Windows_V796_x86_64.exe /S - name: Install rttsh run: | pip install rttsh - name: Run RTT script run: | rttsh -f tests/verify_boot.rttsh --json --timeout 60 env: JLINK_SERIAL: "20090928" - name: Upload logs if: always() uses: actions/upload-artifact@v4 with: name: rtt-logs path: /tmp/boot_log.csv几个注意点:
concurrency: board-rtt控制同一时刻只能跑一个板卡任务,避免两个 runner 抢 J-Link。JLINK_SERIAL用来指定具体是哪只 J-Link,如果机器上插着多只调试器,这个参数能避免连错。if: always()确保即使断言失败也能上传日志,这个对排查问题特别重要。
跑完流水线,你会看到:脚本执行、断言失败、日志上传一条龙。板子没插好时 rttsh 会报连接错误,退出码 2,流水线直接红,一眼就知道问题在哪。
4. 常见问题与排查技巧实录
4.1 J-Link v9在Win11下的驱动问题
这个我遇到太多次了。Win11 对老的 J-Link v9 驱动兼容性并不好,症状是插上 J-Link 后系统提示“设备无法启动”,或者设备管理器里出现带感叹号的未知设备。最直接的解决方案是安装新版驱动。有时候旧驱动残留也会冲突,建议用卸载工具把旧 SEGGER 驱动清干净,重启后再装。
另外,J-Link v9 的 EEPROM 固件分区特别小,新版驱动在固件升级时偶尔会卡在 50%。别慌,重新运行 J-Link Updater,多试几次。如果还不行,把 USB 线换成带屏蔽的短一点,干扰少一些成功率高很多。这个坑我至少折腾过两次,后面只要见到 v9,第一件事就是检查驱动版本和固件版本。
4.2 “The firmware of the connected J-Link does not support...”报错
这条报错原文一般类似 “The firmware of the connected J-Link (s/n:20090928) does not support the following features...”。我遇到过 s/n 20090928 的 J-Link v9,在升级驱动后连接 STM32 时蹦出这行字。大意是连接时执行了某个新的调试特性,固件不支持。处理思路很简单:用 J-Link Updater 把固件刷到最新。如果已经在最新还不行,就要退一步想,是不是你在 rttsh 里开了太新的接口参数。
我一般按顺序排查:
- 检查 J-Link 固件版本:rttsh 加
--info参数看固件版本。 - 对比 SEGGER 软件包版本,固件和驱动版本要匹配。
- 如果固件实在升不动,换用较老的 SEGGER DLL 兼容版本,比如 v6.80。
- 最后手段是换一台新一点的 J-Link,但一般用不到。
这里提醒一句:升级 J-Link 固件是有风险的,跨版本大升级前建议先导出当前固件备份。SEGGER 的 Updater 里有固件保存功能,别嫌麻烦。
4.3 RTT控制块找不到或输出不显示
连接正常,RTT 启动也提示成功,但读不到任何输出。这是最气人的。大部分原因是程序还没运行到初始化 RTT 的代码,或者控制块地址扫错了。我遇到过一个情况:固件里 RTT 控制块在.data段,但上电后那段 RAM 被 startup 代码初始化前是随机值,自动搜索时找到了假的控制块,导致后面全读错。
解决办法归纳成几点:
- 确认目标板程序真的跑起来了,最简单的办法是先用 JLink Commander 读一下内存,看目标是否响应。
- 手动指定 RTT 控制块地址,别依赖自动。用
--rtt-address 0x20000000这种明确地址。 - 调大自动搜索范围,比如
--auto-search --search-start 0x20000000 --search-end 0x20001000。 - 使用最新版 SEGGER DLL,早期版本对 Cortex-M7 的 RTT 自动搜索兼容不好。
还有个不起眼的细节,就是 J-Link RTT 的 Memory Setup。如果你用的是AT91SAM之类芯片,RTT 默认扫描区可能覆盖不到实际 RAM。这时候要手动把内存区间填进去。rttsh 提供了--rtt-range参数,可以多个区间逗号分隔,比如0x20000000-0x20008000,0x30000000-0x30005000。
4.4 脚本卡死与超时处理
脚本跑着跑着就卡住,是最影响 CI 心情的事。我之前写过一个脚本,里面expect "DONE"没有设置 timeout,结果板子某个用例进入死循环,CI 直接挂半小时。后来我给 rttsh 加了两层保护:单条命令的--timeout和全局的--timeout。
实用建议:
- 每条
expect尽量写明确的timeout,这个超时值根据板子真实响应时间放大 50% 到 100%。 - 不要用 0 或非常大的 timeout,除非你能保证板子绝对不死锁。
- 如果脚本里有什么长任务,比如 Flash 擦除,可以分段:先
expect "ERASE_START",再expect "ERASE_FINISH" timeout 30。
RTT 缓冲区满导致的卡死也要特别注意。默认 RTT 上行缓冲区只有 1024 字节,如果板子一口气打日志,而你脚本没及时读,新日志就会被覆盖。rttsh 的rtt read会持续把数据从 buffer 搬走,但如果脚本停在一个sleep命令里,buffer 很容易爆。所以在脚本里减少长sleep,改用基于expect的等待更稳妥。
最后分享一个小技巧
调试 RTT 脚本卡住时,别只盯输出。我经常用两个命令排查:一个是rttsh -c "rtt channel",看各个通道的当前 buffer 水线;另一个是rttsh -c "rtt bank",看 J-Link 协议层的缓冲状态。很多“卡死”其实只是上层没读,底层 buffer 已经满了。这个观察经验几乎能解决一半的脚本超时问题。
还有一条,脚本化的调试流程强烈建议先放在本地手工跑通,再推 CI。别指望 CI 环境能自动解决驱动和固件问题,你的流水线红一半都是环境差异带来的。rttsh 的好处是脚本本身可以随便重跑,环境问题排查一次以后就再也不会踩,这份投入很值得。