ARTICLE DETAIL

资讯详情

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

rttsh:J-Link RTT调试的脚本化利器,赋能CI与AI在板调试

rttsh:J-Link RTT调试的脚本化利器,赋能CI与AI在板调试 搞嵌入式的人谁没被日志折磨过板子一上电printf 到底吐了什么、系统卡在哪个模块、中断有没有按时触发——都靠 J-Link 的 RTT 通道把数据从调试接口里捞出来。我前阵子写了个命令行小工具 rttsh简单说就是给 RTT 加了一层脚本化外壳能跑脚本、能导数据、能接 CI甚至能充当 AI 在板调试时的“手和眼”。这篇就聊聊我为什么做它、怎么设计、四种典型玩法以及我在真实环境里踩出来的一些坑。如果你还在用 RTT Viewer 手工复制日志或者正想把固件验证塞进 CI 流水线又或者想给 AI 工具接一块真实开发板的实时数据那这个工具的思路应该能给你一些参考。项目已经开源在 GitHub搜 rttsh 就能找到代码量不大核心也就一千行出头。1. 为什么要做 rttsh传统 RTT 调试的痛点1.1 RTT 到底是个啥为什么嵌入式离不开它很多刚入行的同学会把 RTT 当成一种“串口替代方案”这么理解不算错但容易低估它的价值。RTT 的全称是 Real Time TransferSEGGER 搞的一套基于调试接口的数据传输协议。探针通过 SWD 或者 JTAG 这两根线去访问目标芯片内存里的环形缓冲区把固件主动写进去的数据拉出来同时也能把调试器侧的命令塞回去。这里有个关键点数据搬运不经过 UART 外设不占 GPIO不需要额外接线。只要调试器连着目标芯片哪怕活在水深火热的硬故障里只要内核还能被调试接口访问最后那几个字的日志就有机会拖出来。这在现场排查“死机到底死在哪儿”的时候是 UART 给不了的。我自己常用的场景包括主循环日志、shell 命令行菜单、自动化测试框架、断言输出。RTT 比 UART 方便太多了我把对比表贴在下面对比项UART printfSEGGER RTT引脚占用至少 1 个 TX双向还得加 RX复用 SWD/JTAG 接口不额外占引脚速度115200bps 很常见实际几十 KB/s跟调试接口时钟挂钩轻松到 MB/s 量级外设依赖需要 UART 外设、GPIO、中断/轮询只需要目标固件里有 RTT 控制块和缓冲区系统卡死时卡在打印函数里日志也没了调试器主动拉取内核能访问就还能看最后数据双向性麻烦要额外接 RX天然上/下行通道读日志、发命令一把梭1.2 官方工具在自动化面前有多被动SEGGER 官方给了两个 RTT 前端RTT Viewer 和 RTT Client。RTT Viewer 是图形界面日常调试很好用信号量、终端模拟、颜色标记都有。但只要我想让它跑批处理、进 CI、自动化验证就特别别扭。第一个硬伤是 GUI 没法脚本化。CI 跑在 Linux 服务器上没有显示器界面上那些按钮全成了摆设。第二个硬伤是数据不落盘或者落盘格式难解析。确实可以手动保存日志但每跑一次要人为点保存脚本根本没法“等数据到齐”。第三个硬伤是没有命令通道的编程接口。我想让测试程序自动往下行通道发一条mem test等它回一个PASS这在 RTT Viewer 里没法写代码。RTT Client 虽然是命令行程序但它的定位只是“把 RTT 数据流映射到终端”没有我们真正需要的东西没有 expect 超时机制没有返回码约定没有对外部脚本友好的输入输出协议。我试过在 shell 里包一层timeout去模拟超时再用 grep 去匹配关键字拼凑起来非常脆弱改一个板子型号就要重写一遍。1.3 AI 在板调试到底缺什么rttsh 补的是什么口子这两年大家都在聊 AI 辅助写代码AI 看代码、看编译日志、看 issue 都能干但它看不见实时状态。我在调试一个调度器问题的时候深有体会代码逻辑翩然合理编译日志干干净净可板子上的任务 A 就是不动。这种问题必须看实时日志才能定位到是信号量没释放、还是中断优先级改坏了。要让 AI 在板调试成为可能它需要一条可编程、可解析的 IO 通道能读日志能发命令能拿命令的输出继续推理。rttsh 的核心设计就是围绕这个需求展开的。我把 stdout 设计成 JSON Lines每次输出都带时间戳、通道号、方向同时提供write命令向下行通道灌数据。这样 AI 工具通过 subprocess 甚至 MCP 就能直接控制一块真实开发板。这也解释了为什么我做工具的第一优先级是“脚本化”而不是“做得像个终端”。脚本化本质上是把人的操作降维成机器可读、可控制、可复现的协议AI 只是其中的一个消费方。2. rttsh 整体设计把 RTT 变成一门能跑的“小语言”2.1 实现语言与驱动库选型为什么是 Python pylink写这种工具最稳的组合就是 Python 加 pylink。pylink 是 SEGGER 官方 J-Link SDK 的 Python 封装接口做得还算干净跨平台表现一致。Windows 上开发、Linux 上跑 CI、macOS 上应急看一眼无一例外装个 Python 环境就行。有人可能会问这工具要追求性能吧怎么不用 C我计算过一遍RTT 的上行数据流以 SWD 4000kHz 跑大概就是每秒几万行日志封顶。Python 处理这个量级加上 JSON 序列化CPU 占用根本是洒洒水。真正耗时的点不在解析而在 expect 超时等待那本来就是空转。所以开发效率完胜。另外 pylink 对多探针的支持也不错初始化时可以指定序列号。我后面会讲到多 J-Link 并行作业的时候这个--serial参数是救命的。2.2 脚本语法设计面向流程不搞复杂 DSL我给 rttsh 设计了一套非常朴素的命令式脚本语法。每一行一个动词参数用--key value的格式从头到尾顺序执行。名字就叫.rttsh文件。没有 if/else、没有循环、没有变量因为我觉得那些东西让工程师去背是作恶。脚本的核心动作是这几个命令作用关键参数connect建立 J-Link 会话连接目标芯片--serial, --device, --speeddisconnect断开会话释放探针无reset触发目标芯片复位--delaywrite往下行通道写命令或数据--channel, --dataread读上行通道数据--channel, --once, --watchexpect等待某关键字出现在上行数据里--match, --timeoutwait单纯等待一段时间常用于上电延时--msrecord开始或停止记录数据--start, --stopsave把缓冲区数据导出到文件--output, --formatexit设置进程返回码并退出--code真正让这套脚本有“验证”能力的是expect。它会不停读取上行通道数据用正则去匹配你给的关键字匹配成功立即返回 0超时还没匹配到就返回 1并且把最后 1KB 缓冲打印出来。这个设计借鉴了网络设备自动化的expect思想但比传统 expect 简单得多我不做复杂的 spawn 交互只管 RTT 这一件事。我极力避免把 rttsh 做成一个“内嵌 Lua 解释器的 RTT 工具”。嵌入式工程师不是来学新语言的他们需要的是一眼能看懂、写完能上 CI 的脚本。想要复杂逻辑外面套 Shell 或 Python 就好。2.3 模块划分与技术要点项目分四个模块连接管理层、RTT 控制层、脚本引擎层、数据导出层。连接管理层负责探针搜索、序列号匹配、J-Link 固件版本检查、SWD 速度设置。RTT 控制层负责扫描 RTT 控制块、枚举通道、读写环形缓冲区。脚本引擎层按行解析脚本管理超时和返回码。数据导出层把带时间戳的数据序列化成 TXT、JSON 或 CSV。有两个技术点值得单独说一下。第一是 RTT 控制块的扫描地址。默认目标固件的 RTT 控制块在 RAM常见裸机工程会落在 0x20000000 附近但具体地址和链接脚本有关。pylink 提供了按范围搜索控制块的机制我让 rttsh 支持--rtt-address手动指定防止某些工程把 RAM 起点改得很偏。第二是缓冲区的读写策略。RTT 的环形缓冲区不是传统队列读指针和写指针都在目标芯片内存里调试器端千万不要在没读完数据时直接改读指针要用“备份位置读取一次性取走”的方式。pylink 底层已经处理好大部分逻辑但我在封装时额外加了一个“堆积数据量”统计用于判断是不是缓冲太小导致丢日志。2.4 和官方工具的关系不是替代是互补有朋友问你都写了 rttsh是不是 RTT Viewer 可以扔了。我的答案是日常快速看日志RTT Viewer 依然香。带颜色标记、终端仿真、多窗口这些不是命令行工具能轻易替代的。rttsh 的定位是自动化前端。底层仍然是 SEGGER 官方固件库和 J-Link 探针没有自己发明协议。也就是说你的目标固件里原来怎么调SEGGER_RTT_Write就怎么调想切换工具零改动。这才是它敢进 CI 的前提稳定、可预测、不挑环境。3. 安装与 5 分钟上手3.1 环境准备先说依赖。Python 3.9 以上就行推荐 3.11。装好后pip install rttsh pylink-squareWindows 用户还需要装 J-Link Software Pack从 SEGGER 官网下载即可。安装之后pylink 会自动找到 J-Link DLL。如果你用的是 J-Link v9 且系统是 Win11驱动兼容性会有一些坑后面第 5 章专门讲。Linux 下要注意非 root 访问 USB 设备的权限。默认 udev 规则会让普通用户拿不到 J-Link我是这样处理的把当前用户加入plugdev组并确保安装了 J-Link 官方包附带 udev 规则。否则运行时会报No J-Link found但lsusb里明明能看到设备。3.2 一条命令连上目标板连接命令rttsh connect --serial 000123456 --device STM32F407ZG --speed 4000--serial是探针的序列号同时插多根 J-Link 时一定要指定不然工具会随机挑一根CI 里随机性是灾难。--device是目标芯片型号要写你跟 J-Link Commander 里一致的名称。--speed是 SWD 时钟频率单位 kHz。选速度有个经验值线长在 10 厘米内、供电稳定、环境噪声小用 4000kHz 没问题。线拉长了或者有大功率设备在旁边降到 1000kHz 更稳。速度太快导致的是连接失败或读出来的数据有 CRC 错误没必要硬扛。连接成功后rttsh 会自动扫描 RTT 控制块并枚举所有通道打印类似下面的信息[INFO] J-Link serial 000123456, firmware V1.4 [INFO] Target: STM32F407ZG, SWD at 4000kHz [INFO] RTT found at 0x20000000, 3 up channels [INFO] Channel 0: up buffer 4096 bytes, down buffer 2048 bytes3.3 读日志、写命令先跑顺这两件事看日志的姿势rttsh read --watch --timestamp这个命令会持续打印上行通道 0 的数据每行前缀是毫秒级时间戳。要退出就按 CtrlC数据不丢退出前会提示是否保存到文件。往下行发命令的姿势rttsh write --channel 0 --data help\r\n注意一定要带上\r\n。很多固件的 shell 按行解析只给\n可能不理你。我一开始写脚本时漏了\r坑了自己一晚上。工具里没有隐式补全因为不同固件的行结束符不一样显式传是唯一可靠的方式。也可以一条命令搞定“读完再写再读”rttsh exec --cmd write --channel 0 --data task list\r\n; read --once --channel 03.4 第一个能进库的脚本我在仓库的 examples 里放了一个最小示例boot_test.rttsh内容如下# 连接目标板 connect --serial 000123456 --device STM32F407ZG --speed 4000 # 复位并等待 shell 出现 reset expect --channel 0 --match Shell --timeout 5000 # 执行内存检测 write --channel 0 --data mem test full\r\n expect --channel 0 --match PASS --timeout 30000 # 保存日志 save --output boot_test.log --format txt exit 0执行方式rttsh run boot_test.rttsh脚本任意一步返回非零进程立刻停止并以同样的返回码退出这样 CI 能直接感知失败。如果没匹配到Shell工具会把它捕获到的最后 1KB 打印出来这就是你排查“为什么上电起不来”的第一手资料。4. 四个核心使用场景的完整实战4.1 场景一AI 在板调试先说结论AI 在板调试不是玄学本质是给 LLM 开一条可编程的硬件 IO 通道。我把 rttsh 跑成 daemon 模式持续输出 JSON LinesAI 任务通过标准输入输出交互既干净又安全。启动 daemonrttsh daemon --json --channel 0输出类似{ts: 1760010000123, ch: 0, dir: up, data: task list} {ts: 1760010000345, ch: 0, dir: down, data: help}AI 侧我用 Python 子进程去调一个最小闭环是这样的import json import subprocess def rtt(args): proc subprocess.run( [rttsh, *args], capture_outputTrue, textTrue, timeout10 ) return [json.loads(line) for line in proc.stdout.strip().splitlines() if line] # 1. 抓一段最新日志 latest rtt([daemon, --json, --once, --channel, 0]) # 2. 把最近 20 行交给 LLM让它判断下一步动作 prompt 你是嵌入式调试助手。以下是日志\n \n.join(x[data] for x in latest) # 3. LLM 返回建议命令例如 task list cmd llm_suggest_command(prompt) # 4. 执行该命令并观察结果 rtt([write, --channel, 0, --data, cmd \r\n])我这里把llm_suggest_command留成了占位具体接入 OpenAI 还是 Claude 还是本地模型都行接口是通用的。我自己跑通的一个真实案例是让 AI 分析“任务 A 为什么没运行”。它读完 RTT 日志发现日志里一直有task A waiting on semaphore建议我先查是谁占着信号量没释放再顺着task list输出发现是另一个任务超时没跑完。这个结论靠人工也能推出来但 AI 只花了十秒而且不用打断我的编码思路。有个安全建议必须单独说给 AI 的命令要走白名单只允许它执行task list、mem status、show config这类只读命令。别让它一上来就erase flash板子真能被它玩死。我实现里有个--allow-cmd参数把合法命令列表传进去write会在执行前校验前缀。4.2 场景二批量脚本验证产测和回归一把抓老化测试和生产测试是我最早做 rttsh 的动机。以前的流程是测试员拿 RTT Viewer 看日志看到PASS就在纸上打个勾。12 小时老化测试下来人累不累先不说漏看、误判太常见。现在我把验证流程整个搬进脚本。老化测试循环脚本长这样# 产测脚本100 次循环 connect --serial 000123456 --device STM32F407ZG --speed 4000 set --var cycle 0 # 我支持的 repeat 语法其实就一行等价展开 repeat --times 100 --script per_cycle.rttshper_cycle.rttsh里做这些事复位、等待 shell、读版本号、写配置、跑自检、比对结果。如果某次循环失败脚本在expect那里就返回非零外层汇总脚本立刻拉红。这里有一个容易忽略的点一条 J-Link 同一时刻只能被一个进程打开。所谓“批量”其实是严格串行的一次连一块板、跑一轮脚本、断掉、再下一块板。想要并行得给每块板插一根独立的 J-Link再用--serial区分。我在 repo 里提供了一个parallel_example.py用进程池同时跑 4 个探针的脚本产测时间直接除以 4。4.3 场景三数据导出文件从“看日志”变成“分析日志”rttsh 的记录功能我很早就做了因为 RTT Viewer 的保存功能每次要点鼠标实在不适合长期抓取。用法很简单rttsh record --output /data/rtt_20250101.json --format json --watch导出的 JSON 每一行都是结构化事件{ts_ms: 1760010000123, channel: 0, direction: up, data: mem test full}字段固定了后面接 pandas 或者其他分析工具都方便。比如统计系统每分钟上报的错误数、找两个日志之间的时间间隔、把多次测试的PASS/FAIL拉成表格基本就是几行 Python 的事。我还拿这些数据干过一件有意思的事把一段 RTT 日志喂给 AI 做大模型微调样本让模型学习“什么样的日志意味着什么样的故障”。这个想法不见得能落地大效果但至少在 RTT 日志这一亩三分地上数据格式统一了处理起来轻松很多。4.4 场景四CI 流水线里跑真板验证这是 rttsh 最有价值的场景也最需要小心设计。CI 不能用 GitHub 官方托管的 runner因为 J-Link 是物理硬件必须插在某一台固定机器上。我用的是自托管 runner标签加了jlink让硬件测试 job 只投递到这台机器。一个可用的 GitHub Actions 流水线片段name: firmware-ci on: [push, pull_request] jobs: hw-test: runs-on: [self-hosted, jlink] steps: - name: 拉取代码 uses: actions/checkoutv4 - name: 安装工具链 run: pip install rttsh pylink-square - name: 烧录固件 run: JLinkExe -device STM32F407ZG -if swd -speed 4000 -CommanderScript flash.jlink - name: 跑硬件验证 run: rttsh run verify.rttsh --workspace /opt/ci-workspace - name: 失败时上传日志 if: failure() uses: actions/upload-artifactv4 with: name: rtt-logs path: /opt/ci-workspace/logs/**有几件事必须盯紧。第一runner 机器上不要同时跑两个操作 J-Link 的 job否则探针占用冲突会随机失败。我在 runner 上做了锁目录进程获取不到锁就自杀重试。第二flash.jlink里要写exit不然 JLinkExe 会挂在那里等人按回车CI 卡死。第三--workspace目录每次跑之前清空避免旧日志污染新结果。这套流水线跑起来之后固件改动合入前先过一遍真板验证编译不过的、该初始化没初始化的、shell 命令行为变化的全在合并前暴露。我自己维护的一个固件仓库已经跑了三个多月曾经把一次“某个驱动初始化顺序错误导致任务 A 阻塞”抓在 PR 阶段而不是让测试同事到现场才发现。5. 踩坑记录与排查技巧5.1 J-Link 连不上、识别异常先按顺序排查我把实际遇到过的连接问题整理成一个速查表症状可能原因处理建议The connected J-Link is defective驱动不匹配、USB 线不良、探针固件过期换 USB 口、换线、升级官方 Software Pack、重刷探针固件No J-Link found探针没枚举成功、Linux 权限不够设备管理器/usb 里看枚举Linux 加 udev 规则连接后读写全超时SWD 速度过高、线过长降速到 1000kHz缩短杜邦线J-Link v9 在 Win11 不识别旧驱动与新系统不兼容用最新版 J-Link Software Pack重插后重装驱动特别说一下 J-Link v9 在 Win11 的问题。这个组合在 2024 年之后越来越多见官方兼容性更新解决了一部分但还会偶发“设备描述符请求失败”。我的经验是换个 USB 口大概率能恢复优先试 USB 2.0 口再不行把探针固件升到新版。如果公司有预算v11 稳很多毕竟是新硬件驱动支持更勤快。5.2 RTT 通道找不到或者日志丢行问题大半在固件侧如果你连上板子后rttsh 报告找不到 RTT 控制块先别怀疑工具九成是固件没调SEGGER_RTT_Init()或者链接脚本把 RTT 控制块放到了不可扫描的区域。在代码里搜索一下有没有调用没有就补上。日志跑起来之后出现丢行第一反应是SEGGER_RTT_Conf.h里的上行缓冲太小。默认值经常是 1024 字节如果某个中断服务程序一次性写 2KB 日志RTT 必然丢。我把BUFFER_SIZE_UP调到 4096再配合 rttsh 的持续读取实测丢行明显改善。还有一个易错点系统进入了低功耗模式会冻结时钟RTT 缓冲区的写入也停了调试器读到的数据会出现“断层”这不代表 RTT 坏了是目标芯片在睡觉。如果你需要多个通道默认只开了 3 个。要更多得改SEGGER_RTT_MAX_NUM_UP_BUFFERS并重新编译固件。动态创建的通道需要应用层调用SEGGER_RTT_ConfigUpBuffer不然 rttsh 枚举不到。5.3 脚本执行挂死与超时给你一个可复现的排查路径脚本挂死的 80% 场景都出在expect上。某次写产测脚本目标板在报完PASS之前又多打了一行版本信息我的正则只匹配了“PASS”但等待窗口被额外输出拖长了实际也没问题。真正坑人的是目标板在expect等待期间复位了RTT 控制块地址失效读取函数一直拿不到新数据整个进程像死了一样。现在我在脚本引擎里加了两个保险每个expect都必须显式给--timeout不给就默认 3000ms超时后打印最近 1KB 缓冲并返回非零。另外遇到目标板需要中途复位的场景脚本里先disconnect再connect保证 J-Link 重新枚举 RTT 控制块。之前直接在一个会话里reset诡异问题层出不穷。还有个小经验如果 CI 里明明脚本正常退出了整个 job 还是卡住去查是不是后台还挂着一个rttsh record --watch在无限读。我遇到过这种CI 回收 runner 时把所有子进程都留着下一轮 job 就抢不到探针。后来统一在脚本末尾加disconnect并给所有后台读进程加--max-duration自动退出。5.4 多进程、多探针并发冲突的解决方式pylink 打开探针失败时报错一般是The J-Link is opened by another process。这种情况最常发生在本地正在用 RTT ViewerCI 同时想连同一根探针。RTT Viewer 只要开着探针就被独占哪怕你只是打开没连接也占着。我的解法是“串行化 序列号隔离”。本地调试和 CI 之间靠人工约定CI 跑的时候别开 RTT Viewer多块板子要靠--serial明确指定探针序列号。工具里还加了一个list命令列出当前机器上所有 J-Link 及其序列号、固件版本方便大家在 CI 配置里写清楚用哪根探针。对于大规模产测可以在 runner 上用文件锁配合超时重试。伪代码大概是加锁失败就 sleep 3 秒再试试 5 次还不行就报失败。这个逻辑不需要多复杂但能挡住并发随机挂。5.5 Windows 与 Shell 环境的闪退、路径、编码热搜词里飘着一堆 Windows 脚本命令闪退、PowerShell 无法识别命令的问题我写 rttsh 的时候也被这些东西教育过。Windows 下最常见的坑是 PATH 里没有 Python 的 Scripts 目录直接敲rttsh提示“无法将“rttsh”项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这时用py -m rttsh能绕开 PATH 问题同时建议把%APPDATA%\Python\Scripts加进 PATH。另一个坑是换行符。从 Windows 上编辑的.rttsh脚本如果带 CRLFLinux CI 解析器会遇到一个\r残留的坑。我在解析器里先统一转成\n但如果你自己写 shell 包装脚本记住dos2unix一条命令的事。还有日志路径别带中文和空格Windows 下 PowerShell 转发参数偶尔会把路径切片烦得很。我把这些经验全写在仓库的docs/TROUBLESHOOTING.md里了新用户踩坑少一点是一点。6. 一些个人体会和对下一步的想法rttsh 写了几个月回头看最大的变化不是多了一个工具而是我的调试习惯彻底变了。以前是“改代码、看日志、靠猜”现在是“先写脚本、跑验证、看结论”。固件改动合入前本地先跑一遍rttsh run verify.rttsh该暴露的问题早暴露不用等测试同事拿着截图来找我。我觉得这种“脚本化的硬件 IO”思路还能延伸很多方向。比如给 rttsh 加一个 RPC server让远端机器通过网络访问某台服务器上的 J-Link这样私有云的 CI 就能共享插在办公室工位上的探针再比如跟 MCP 协议打通让 Claude 这类工具以标准方式调用AI 在板调试的接入成本会更低。仓库我已经开源了欢迎对硬件自动化有兴趣的朋友来提 issue 或者 PR。我自己接下来打算先做的是让 rttsh 支持多目标板级的会话编排也就是真正意义上的并行产测而不是靠进程池硬怼。
返回列表