rttsh:让J-Link RTT调试链路支持脚本化与CI集成

发布时间:2026/11/4 6:00:42
rttsh:让J-Link RTT调试链路支持脚本化与CI集成 做了多年嵌入式开发J-Link RTT 算是我用得最多的调试手段之一。但说实话官方工具一直让我不太舒服——RTT Client 是图形界面没法脚本化JLinkExe 又只暴露了几个固定命令想写自动化验证脚本、接 CI、或者让 AI 直接操作板子的时候就非常别扭。所以我自己写了 rttsh一个支持脚本化的 J-Link RTT 命令行工具。这篇文章就把它的设计思路、核心用法、以及和 AI 在板调试、CI 集成的实践经验完整拆一遍希望能给同样踩过这些坑的人一些参考。1. rttsh 究竟解决了什么问题1.1 在用 rttsh 之前我每天是怎么调试的以前项目中需要和板子交互路径基本是打开 J-Link RTT Viewer手动在输入框里敲命令盯着输出窗口看日志。单次调试没问题但一旦涉及“验证某个行为是否正常”、“连续跑 200 次压力测试”、“在 CI 流水线里自动检查固件状态”这套手工操作就完全抓瞎。更麻烦的是RTT Viewer 依赖图形界面跑在无头服务器上根本没戏。我也试过用 Python 包 pyswd 或者直接解析 JLinkExe 的 stdout但这些方案要不就是重要不就是解析不稳定——JLinkExe 的提示信息在不同版本里会变脚本写起来又丑又脆。J-Link RTT 本身的原理并不复杂。它本质上是在目标 MCU 的 RAM 里放了一个控制块Control Block里面定义了若干个环形缓冲区上行通道用来传 MCU 到 PC 的数据下行通道反过来。J-Link 调试器通过 SWD 或 JTAG 接口周期性地轮询这块内存把数据搬出来或者送进去。相比传统的 UART 调试RTT 的传输不需要 MCU 额外配置串口引脚也不用跑一个中断驱动的协议栈对目标 CPU 几乎零开销而且速度可以跑到几十 KB/s 甚至更高。既然 RTT 的底层机制已经这么方便了缺的就是一个“把 RTT 能力暴露给脚本和自动化系统”的中间层。这就是 rttsh 出生的原因。1.2 现有 RTT 工具链到底缺了哪块拼图官方工具链的分布是这样的JLinkExe 负责烧录、连接、基本内存操作RTT Viewer 负责实时日志查看和手动输入RTT Logger 负责记录数据到文件RTT Client老版本里集成在 JLinkExe 里负责简单的终端交互。看起来功能齐全但实际上每块都是独立软件没有统一的编程接口也没有“脚本运行完自动退出并返回状态码”这种 CI 友好设计。我需要的其实非常简单一条命令能连接目标板发一串文本然后读回结果一个脚本文件能按顺序跑多条交互逻辑支持条件判断和超时整个工具能无界面运行退出时给出明确的 0 / 非 0 状态码输出数据结构化方便 JSON / CSV 直接落盘rttsh 的定位就是把这些需求统一到一个 CLI 工具里。它底层通过 J-Link SDK 的 DLL 接口直接访问 RTT 控制块不依赖 JLinkExe 的文本解析因此在无图形界面的 Linux CI 服务器上也可以稳定跑。2. rttsh 的整体设计与架构拆解2.1 命令行交互模式与批量脚本模式并行rttsh 从设计上讲有两个核心运行模式。第一个是交互式 REPL 模式这个模式主要是给人工调试用的——连接板子后进入一个类似 shell 的界面直接输入命令比如$ rttsh --device STM32F407VG --interface swd --speed 4000 rttsh init [OK] RTT control block detected at 0x20000000 rttsh w help\n rttsh r --timeout 1000 [app] max31875 init ok [app] task scheduler started命令设计参照了日常终端习惯w是 write 写入r是 read 读取init是初始化 RTT 通道quit退出。每个命令都有对应的长写法脚本里用长写法更语义化手敲的时候用短命令省事。第二个是批处理模式即-s参数指定脚本文件rttsh --device STM32F407VG --script test/stress_test.rttsh这个模式下的核心特征是确定性。脚本按顺序执行每一条指令都有明确的成功/失败判定遇到失败可以选择立即中断默认行为也可以配置成“记录错误继续执行”。脚本跑完后进程返回对应的 shell 状态码0 表示全部通过非 0 表示存在失败项。这个设计为 CI 集成交了底。2.2 脚本引擎的取舍为什么不用 Python 或 Lua写脚本化工具时最初的方案选择其实是个分岔路口。我认真考虑过直接嵌入 Python 解释器、用 Lua 做脚本层或者像 autotest 那样搞一套自定义 DSL。但最终选了自定义 DSL 方案就是 rttsh 现在用的这套.rttsh语法。放弃 Python 的理由很实际。第一嵌入式现场环境里不一定有完整的 Python 运行时特别是 Windows 主机上经常是一台裸的操作系统你总不能到客户现场先装个解释器第二Python 脚本很难做静态的超时控制如果一个read()调用在嵌入式环境下因为目标断连而阻塞整个进程就挂死了而 DSL 的解释器可以严格约束每条指令的执行时间。自定义 DSL 的语法设计也尽量往“可读”和“好写”两个方向靠。比如下面的脚本# verify_adc_read.rttsh init write adc read ch0\n expect adc3.29v --timeout 2000 log adc ch0 check passed write adc read ch1\n expect adc1.65v --timeout 2000 log adc ch1 check passed一眼就能明白在干什么。关键命令是init、write、expect、read、log、sleep、export一共七个。没有复杂的控制流延迟和超时全部用参数控制。这里设置的超时参数单位是毫秒实际调试时我一般给 2000ms 左右因为在低速 SWD 连接下 RTT 轮询间隔可能达到几十毫秒太短容易误报。2.3 数据导出与格式化设计嵌入式调试不只是“看日志”很多时候要抓数据传感器采样曲线、电压波动、功耗变化、通信帧序列。RTT Logger 能记录但格式是纯文本时间戳后处理很麻烦。rttsh 的export命令实现了数据结构化输出。init write start logging\n sleep 5000 write stop logging\n export --format csv --output ./data/adc_samples.csvCSV 文件的列可以自定义默认是timestamp, direction, data。direction 表示是 MCU 发上来的还是 PC 发下去的数据。也支持 JSON 格式方便给后续的数据分析脚本喂料[ {ts: 1719300000.412, dir: up, data: adc3.29v}, {ts: 1719300000.510, dir: up, data: adc3.28v} ]这个导出功能在后面接 AI Agent 做记录分析的时候特别有用——AI 拿到结构化数据就不需要去猜日志格式了。3. 核心功能实操从交互式调试到脚本验证3.1 从零开始跑通第一条 RTT 链路第一次使用 rttsh 时连接参数是最容易让人困惑的地方。我建议按这个步骤走确认 MCU 型号字符串。--device参数接受 J-Link 官方支持的所有型号名比如STM32F407VG、nRF52840_xxAA、ESP32等。不确定时可以运行JLinkExe输入?查看支持列表或者直接查 Segger 的 device 列表。确认调试接口和速率。默认是swd对大多数 Cortex-M 芯片都适用。JTAG 只在少部分场景需要比如同时调试 MCU 和外部 FPGA 时。速度建议从 1000kHz 起步如果板子走线长或者有干扰再降低。确认 RTT 控制块地址。这是最核心的一步。固件里如果没有显式指定SEGGER_RTT控制块地址调试器需要在内存里搜索特征字符串SEGGER RTT。rttsh 支持两种方式# 自动搜索需要指定搜索范围 rttsh --device STM32F407VG --rtt-search 0x20000000 0x20020000 # 显式指定推荐搜索太慢或失败时用 rttsh --device STM32F407VG --rtt-addr 0x20000000我强烈建议在固件里直接把控制块地址固定下来并编译期暴露出宏定义而不是依赖运行时搜索。原因很简单搜索范围一大启动时间能从几百毫秒拖到好几秒而且如果控制块所在的内存区域有变化会导致零匹配。连接成功后在 REPL 中执行init会打印控制块的位置和通道数。RTT 默认有up[0]和down[0]两个通道如果固件配置了多通道init输出里能看到对应的缓冲区索引。第一条交互命令通常就是发一个help\n或者任意固件能响应的指令rttsh w help\n rttsh r --timeout 1000注意r命令默认读满整个缓冲区或者直到超时返回的内容是原始字符串。如果你的固件日志里有 ANSI 转义序列也可以加--strip-ansi参数清理输出。3.2 如何设计一个能落地的批量验证脚本批量脚本的典型场景是产测和回归验证。我拿一个实际项目举例一块带温湿度传感器、LED 和按键的 IoT 控制板需要验证以下行为设备启动后打印boot ok温湿度读取命令返回的数值在合理范围LED 控制命令生效固件回显led on且实际电流测量变化但脚本只能验证到回显层面按键事件上报功能正常对应的 rttsh 脚本# iot_board_smoke_test.rttsh init expect boot ok --timeout 5000 # 温湿度读取连续读5次检查格式 for i in 1 2 3 4 5 write sense read\n expect temp --timeout 1000 expect hum --timeout 1000 done # LED 控制 write led on\n expect led on --timeout 500 write led off\n expect led off --timeout 500 # 按键上报脚本模拟发送按键事件固件回显 write button event send 1\n expect button1 --timeout 1000 log ALL PASS这里有个细节脚本 DSL 里我为什么要支持for i in 1 2 3 4 5这种看起来僵硬的循环而不是提供更高级的while因为 RTT 的交互本质是“发一条等一条”你控制不了 MCU 内部状态机的时序。固定次数循环是最安全、最不容易死锁的验证模型。实际跑这个脚本的时候每条expect背后其实有一个内部状态机在等待匹配串同时循环监听 RTT 上行数据一旦发现匹配就立即返回。跑完后的真实输出和退出码$ rttsh --device STM32F407VG --script iot_board_smoke_test.rttsh [PASS] boot ok [PASS] temp [PASS] hum [PASS] led on [PASS] led off [PASS] button1 [RESULT] ALL PASS $ echo $? 0测试环境里我在脚本开头加了set stop-on-fail true的隐式默认行为所以任意一条不匹配会立即终止并返回非零码目的就是为了让 CI 能立刻阻断流水线。3.3 带超时控制的 read 和 expect 细节expect是整个脚本里最重要的原语它背后的实现值得展开说。J-Link RTT 的读取本质是不断轮询目标 RAM 中的环形缓冲区来检查是否出现新数据。rttsh 的expect命令会启动一个高频轮询线程把每次轮询拿到的数据追加到内部缓冲区然后做子串匹配。匹配成功时会把“命中点之前剩余的数据”打印出来方便你确认上下文匹配超时时则把整个缓冲区内容都 dump 出来便于排查为什么没匹配上。这个设计在实际调脚本时救了我很多次——很多时候固件回显和预期值之间差一个回车符你光看 expect 失败信息根本不知道实际数据是什么。rttsh expect temp25.3 --timeout 2000 [INFO] matched temp25.3 [INFO] before match: adc3.3v sense ok \n如果是不想立即返回的持续读取场景比如抓一段实时波形用read --duration 10000或者read --lines 100前者按时间采集后者按行数采集。采集期间数据只进缓冲不输出结束后统一打印或者导出。4. 让 AI 真正参与在板调试的实践4.1 AI 调试的痛点与 rttsh 的解法“AI 在板调试”这个说法现在很热但真正落地的时候有一个绕不开的问题AI Agent 怎么跟板子对话LLM 本质上是一个文本交互模型它没有物理层能力它能做的就是读工具返回的文本、生成下一条工具指令。所以要让 AI 调试嵌入式板子核心环节就是把“板子状态”变成“文本流”同时把“文本指令”变成“板子能懂的命令”。rttsh 正好提供了这个双向文本管道。我的做法是在 rttsh 外面包一层薄的工具适配层让 AI Agent比如 Copilot、Claude 或者本地跑的开源模型通过函数调用接口操作板子。每个函数对应一条 rttsh 命令行def board_send(command: str) - str: 发送命令到板子并读取返回值 result subprocess.run( [rttsh, --device, STM32F407VG, --batch, fwrite {json.dumps(command chr(10))}; read --timeout 1000], capture_outputTrue, textTrue, timeout10 ) return result.stdout这个适配层的意义在于AI 看到的是一个干净的“输入命令输出日志”黑盒。它不需要知道 SWD 是什么、控制块在哪、超时怎么设这些都是 rttsh 屏蔽掉的细节。4.2 实测让 AI 自动排查一个启动崩溃问题我实际做过一次这样的实验。现象是固件上电后串口无输出我怀疑是某个外设初始化卡死了。传统的做法是打断点、看寄存器、或者手动在 RTT 里发命令一条条试。但用 rttsh 配合 AI整个流程变成了第一步让 AI 先读一段板子日志rttsh --device STM32F407VG --batch init; read --duration 3000AI 拿到输出后发现日志停在了i2c1 init start这行。第二步AI 主动发一条外设状态查询命令board_send(debug i2c1 status)板子回显i2c1 status: PLL unlocked。AI 根据这个信息判断是 I2C 时钟配置问题然后自动去查数据手册中 PLL 锁定条件最后给出建议检查 I2C 外设时钟使能位。整个过程里AI 每次决策都基于真实的板级反馈而不是凭空猜。这里要强调一点rttsh 并不会为了让 AI 用而牺牲稳定性反而因为它本身是确定性的 CLI 工具AI 拿到的每一步输出都是真实板子状态不会出现“AI 以为操作成功了实际没生效”的情况。这是所谓“在板调试”和“靠日志猜测”最大的区别。4.3 适合 AI 使用的批量查询模式AI 交互模型需要的是简洁、短的往返。我后来给 rttsh 加了一个--query模式专门给 AI 用把一次性问答压缩成单命令rttsh --device STM32F407VG --query adc read all {status: ok, ch0: 3.29, ch1: 1.65, ch2: 2.02}--query模式会做三件额外的事自动初始化 RTT、固定 2 秒超时、把输出包装成单行 JSON。这让 AI 的每次函数调用都极其短平快Tokens 消耗也少很多。在调试循环里一次完整的人机对话从“发命令、等结果、解析日志、决策下一步”从原来的 30 秒缩短到了平均 5 秒左右。不过我必须提醒AI 在板调试目前还是辅助性质。AI 对硬件细节的理解仍然有限遇到电源纹波、信号完整性这类模拟域问题时文本对话帮不上忙。但用于定位逻辑层问题、快速筛选可疑外设、自动化遍历各种寄存器配置组合这套方案已经相当可靠。5. 与 CI/CD 系统的深度集成5.1 为什么嵌入式项目也需要 CI 跑板级测试如果你的固件开发还停留在“本地编译烧录人工点几下验证”的阶段那么接 CI 的收益是立竿见影的。固件项目里最容易出问题的恰恰是那些隐蔽的回归某个驱动改动影响了另一个模块的初始化时序某次编译选项调整导致 RTT 缓冲地址偏移这些在开发者本地不一定能马上发现但到了集成阶段就爆炸。rttsh 的脚本化验证 退出码机制正好解决“无人值守环境下怎么自动验证硬件行为”这个命题。CI 机器上连上 J-Link 和被测板跑一组验证脚本通过就继续流水线不通过就留现场日志给开发者。5.2 GitLab CI 集成示例GitLab CI 的配置核心是定义 job 的script部分。下面是我在一个 STM32 项目里实际使用的配置片段stages: - build - board_test board_test: stage: board_test tags: - runner-with-jlink variables: DEVICE: STM32F407VG JLINK_PATH: /opt/SEGGER/JLink script: - export PATH$PATH:$JLINK_PATH # 先烧录本次构建产物 - JLinkExe -device $DEVICE -if swd -speed 4000 -CommanderScript flash.jlink # 等待目标复位完成 - sleep 2 # 跑板级验证脚本 - rttsh --device $DEVICE --script verify/system_smoke.rttsh # 导出测试日志 - rttsh --device $DEVICE --script verify/export_rtt_logs.rttsh --output ./artifacts/rtt_logs.csv artifacts: paths: - artifacts/ when: always expire_in: 1 week我踩过一个很实在的坑GitLab Runner 默认每次 job 会启动一个全新的 shell 环境如果日志里用了export PATH加了 J-Link 路径前置子进程调用是没问题的。但有些 Runner 配置里两个 job 并行跑会共享同一块 J-Link 硬件导致冲突。解决办法是给 runner 打 tags 限制并发或者用 GitLab 的resource_group字段board_test: resource_group: jlink-1这样同一个硬件资源不会被两个 job 同时抢占。如果你手上只有一块 J-Link这个配置是必须的。5.3 GitHub Actions 集成示例GitHub Actions 的思路类似但更侧重“自包含 runner”。如果你的 GitHub Actions runner 直接跑在有 J-Link 的物理机上配置如下jobs: board-test: runs-on: [self-hosted, jlink] steps: - uses: actions/checkoutv4 - name: Flash firmware run: | JLinkExe -device STM32F407VG -if swd -speed 4000 -CommanderScript flash.jlink - name: Run board verification run: | rttsh --device STM32F407VG --script verify/board_smoke.rttsh - name: Upload RTT logs if: always() uses: actions/upload-artifactv4 with: name: rtt_logs path: rtt_log_*.csv这里更推荐用 self-hosted runner因为 GitHub 官方托管的 runner 在天上物理上不可能直接连你的开发板。如果你只有云端 runner一个可行的折中方案是搭一个小型中继服务把 rttsh 通过 WebSocket 暴露给远程调用但延迟和稳定性都很考验人我一般只在演示场景用。5.4 CI 中的烧录、复位与等待时序处理CI 里最容易翻车的不是脚本本身而是硬件链路的状态变迁。JLinkExe 烧录完成后目标 MCU 通常会自动复位运行但复位后 RTT 控制块可能还没初始化好立即执行 rttsh 的init大概率会搜索失败。我总结出一个比较稳的时序模板烧录复位后先sleep 2具体值取决于固件启动到 RTT 初始化的时间用 rttsh 执行一次init --retry 5 --delay 500失败自动重试执行正式验证脚本收集日志和状态码--retry 5 --delay 500是一个很实用的参数组合每 500ms 重试一次 init最多 5 次。这段时间足够多数 MCU 完成早期初始化和 RTT 控制块搬移。6. 常见问题与避坑记录6.1 连接不上和 RTT 控制块搜索失败这是新人遇到最多的问题。典型报错是[ERROR] RTT control block not found in range 0x20000000-0x20020000。第一反应先确认固件确实初始化过 RTT。检查SEGGER_RTT_Init()是否被调用以及SEGGER_RTT_CB结构体有没有被链接进最终镜像。用 arm-none-eabi-nm 查arm-none-eabi-nm -S firmware.elf | grep SEGGER_RTT如果没有这个符号说明 RTT 代码根本没编译进去。另外部分 bootloader 会把固件放在非零地址的 flash 区域RTT 控制块的地址是链接器决定的如果搜索范围完全不对会一直失败。第二个常见原因是被调试器的“自适应时钟”坑了。某些开发板的 SWD 接口走线特别长或者目标芯片供电偏低默认速度 4000kHz 基本连不上。我遇到过一块 STM32 板子在 4000kHz 下稳定连接但改到 400kHz 才正常的案例。出现连接失败时把--speed降到 1000 或者 400 试一下。第三个原因是某宝上某些便宜的“J-Link 兼容”调试器对 RTT 的支持并不完整。如果init一直超时但 JLinkExe 访问内存又是正常的可以先用 JLinkExe 手动读一遍控制块头部确认硬件链路本身没问题。6.2 expect 永远等不到匹配串这个问题的排查点通常不在 rttsh而在固件的日志输出格式。RTT 上行通道的写入是异步的如果固件用的是SEGGER_RTT_printf它会做格式化速度较慢如果直接SEGGER_RTT_WriteString则要求输出内容是一个以\0结尾的字符串。两种方式都可能导致输出不会立即出现所以 expect 的超时时间不要给太短。最典型的一个问题固件日志行的末尾到底是\n还是\r\n很多嵌入式日志框架用\r\n而你在 expect 里写的是\n结尾的子串导致匹配不到。我的经验是 expect 用不带换行的核心子串比如boot ok而不是boot ok\n让匹配器做子串匹配而不是整行匹配。6.3 导出数据里的中文乱码和格式问题RTT 通道本质是字节流不做任何编码转换。如果你的固件日志包含 UTF-8 中文导出的 CSV 默认用系统编码打开可能会乱码。建议导出时强制指定 UTF-8 with BOMexport --format csv --encoding utf-8-sig --output logs.csv但更稳妥的做法是让固件侧尽量输出 ASCII 码范围内的日志。嵌入式日志本来就是给人看的调试信息混入中文在实际的 CI 数据流里不值当。Excel 打开 CSV 时如果单条日志里包含逗号也会被拆分成多列。rttsh 的 CSV 导出默认对所有字段做双引号包裹但如果你的数据分析脚本用 pandas 读取需要注意quoting参数设为csv.QUOTE_ALL才能正确解析。6.4 多通道 RTT 的配置问题如果你的固件使用多通道 RTT比如 channel 0 跑终端日志channel 1 跑传感器高频数据rttsh 支持按通道读写rttsh w -c 1 sensor start\n rttsh r -c 1 --duration 5000注意这里通道编号是 RTT 控制块里的索引不是 J-Link 硬件通道。如果控制块里没有初始化对应通道读写会直接报错。配置多通道时我建议在固件里显式初始化并保证SEGGER_RTT_CONFIG_NUM_UP_BUFFERS和NUMBER_OF_DOWN_BUFFERS的值与你使用的通道数一致。6.5 Windows 下驱动冲突的隐藏陷阱Windows 上同时装了 SEGGER 的 J-Link 驱动和其他工具链的驱动比如 Keil 自带版本、IAR 自带版本经常出现 DLL 版本冲突。rttsh 加载的是动态库如果你系统上的 JLinkARM.dll 是旧版可能会在某些新设备上失效。排查方法很简单直接用 SEGGER 官方的最新安装包重新覆盖驱动然后确认你的 PATH 环境变量里没有混入旧版本 DLL 的路径。这个坑在 CI 的 Windows runner 上尤其常见因为大家图省事会直接拷贝旧 DLL 过来用。7. 个人体验总结与后续扩展思路rttsh 这个工具从最初的一个 500 行的 Python 脚本逐步演变成现在支持完整脚本化、结构化导出、CI 集成和 AI 对接的命令行工具中间经历了大量实际项目的打磨。我个人最大的感受是嵌入式调试工具的“自动化”和“确定性”比“多强大”重要得多。一个能稳定返回状态码、输出结构化数据、可以被脚本和 AI 调用的工具带来的效率提升是几何级的。现在我日常的开发流里rttsh 已经成了基础设施。本地调试时开 REPL 手动敲白天写完代码顺手跑一组 smoke test提交 MR 后 GitLab CI 自动烧录验证甚至在用 AI 做问题排查时整个对话链路也走的是 rttsh 的输出。这套流程在过去的半年里帮我抓到了不下五个只有在长时间压力测试下才会暴露的回归 bug。后续我想做的扩展有两个方向。第一是给 rttsh 加一个基于 gRPC 的远程接口这样即使开发板在实验室人在办公室也可以通过安全的远程通道执行同样的脚本第二是做一个简单的 VS Code 插件把 rttsh 的交互模式嵌入 IDE让 AI 调试助手可以直接读取板子实时状态而不需要切窗口。这些方向还在规划中等有可用的版本再回来分享。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询