☰
rttsh:脚本化J-Link RTT调试工具,支持Lua与CI集成
2026/10/3 6:55:01 网站建设 项目流程

嵌入式调试这件事,最让人抓狂的往往不是代码写错了,而是你明明知道问题在哪,却没法方便地看到变量值。J-Link RTT 算是解决了"看"的问题,但原生的 RTT Viewer 和 RTT Client 在批量操作、脚本化、CI 集成这些场景下就显得力不从心了。我最近花了不少时间写了一个叫 rttsh 的命令行工具,核心目标就一个:把 RTT 的读写能力变成可脚本化、可管道化、可自动化的一等公民。它支持 Lua 脚本驱动,能直接对接 CI 流水线,也能在 AI 辅助调试的场景里当"手和眼"用。这篇文章我会把整个工具的设计思路、核心实现、踩过的坑和实际使用心得完整拆一遍,适合正在做嵌入式调试自动化、想摆脱 GUI 点点点的朋友参考。

1. 为什么原生 RTT 工具在自动化场景下不够用

1.1 RTT Viewer 的交互模式与批量需求的根本矛盾

J-Link RTT 的官方工具链里,RTT Viewer 是最常用的一个。它的工作模式很直观:连上 J-Link,选好目标芯片,打开 RTT 控制块,然后就能在终端窗口里看到目标板通过SEGGER_RTT_printf输出的日志,也能手动输入命令发回去。对于"我盯着看日志"这种场景,它够用。

但问题在于,一旦你想做下面这些事,RTT Viewer 就完全帮不上忙了:

  • 每隔 100ms 自动往目标板发一条查询命令,把返回结果存成 CSV
  • 在 CI 里跑完固件烧录后,自动抓取 30 秒 RTT 日志,检查有没有ASSERT关键字
  • 用脚本控制目标板进入不同工作模式,采集每种模式下的功耗数据
  • 让 AI 助手通过命令行接口读取当前变量值,辅助定位问题

这些需求的共同点是:需要程序化地控制 RTT 的读写节奏,而不是靠人手动操作。RTT Viewer 是个 GUI 程序,没有提供命令行接口,也没有脚本扩展能力,你没法在 shell 脚本里调用它。

有人可能会说,那用 J-Link 的 SDK 自己写个 C 程序不就行了?可以,但成本很高。你要处理 J-Link DLL 的加载、RTT 控制块的搜索、缓冲区读写、连接管理等一堆底层细节,写出来还得自己维护。对于"我就想快速搞个自动化脚本"的需求来说,这个投入产出比太低了。

1.2 RTT Client 的能力边界在哪里

RTT Client 比 RTT Viewer 轻量一些,它是个纯命令行程序,可以通过 telnet 方式连接 RTT 端口。这看起来好像能脚本化了?实际用起来还是有不少限制。

首先,RTT Client 的 telnet 接口是单向的文本流,你没法方便地区分"这次读取的数据"和"上次残留的数据",也没有结构化的返回格式。其次,它的连接管理比较脆弱,目标板复位或者 J-Link 重新枚举之后,连接经常需要手动重连。再者,它不支持 Lua 或其他脚本语言嵌入,你没法在工具内部做复杂的逻辑判断,只能在外层用 shell 拼凑,很容易写出脆弱的脚本。

我实际试过用 RTT Client + expect 脚本做自动化,结论是:能跑,但维护起来很痛苦。expect 对二进制数据的处理很别扭,超时控制也不够精细,稍微复杂一点的交互逻辑就会变成一堆难以阅读的 spawn/send/expect 嵌套。

1.3 脚本化 RTT 工具需要具备哪些核心能力

基于上面的分析,我梳理了一个合格的脚本化 RTT 工具应该具备的能力:

能力维度具体要求原生工具是否满足
连接管理自动搜索 RTT 控制块,支持指定芯片型号和接口速度部分满足
读写接口提供结构化的读/写 API,支持超时和重试不满足
脚本嵌入支持 Lua 等脚本语言,可在工具内做逻辑判断不满足
管道友好输出可重定向到文件或其他程序,支持 stdin 输入部分满足
CI 集成非交互模式运行,返回码可判断成功失败不满足
数据导出支持 CSV、JSON 等结构化格式导出不满足

rttsh 的设计就是围绕这张表来的。它用 C 写核心的 J-Link 交互层,用 Lua 做脚本层,命令行接口设计成管道友好的风格,整体是一个"能嵌进任何自动化流程"的工具。

2. rttsh 的整体架构与 J-Link 底层交互逻辑

2.1 分层设计:C 核心 + Lua 脚本层 + CLI 外壳

rttsh 的架构分三层,这个分层不是拍脑袋定的,而是根据"哪些部分需要高性能、哪些部分需要灵活性"来划分的。

最底层是C 核心层,负责和 J-Link DLL 打交道。这一层做的事情包括:加载 J-Link 动态库、建立和目标芯片的调试连接、搜索 RTT 控制块在目标内存中的地址、读写 RTT 上行和下行缓冲区。这些操作对性能敏感,而且需要直接操作内存和硬件接口,用 C 写最合适。

中间层是Lua 脚本层。Lua 在这里的角色是"胶水"和"逻辑处理器"。C 核心层把 RTT 读写能力暴露成 Lua 函数,比如rtt.read()、rtt.write()、rtt.read_until(),Lua 脚本就可以用这些函数组合出复杂的交互逻辑。选 Lua 而不是 Python 或 JavaScript,主要考虑是 Lua 解释器体积极小、嵌入成本低、启动速度快,而且语法简单,嵌入式工程师上手门槛低。

最外层是CLI 外壳,负责解析命令行参数、加载脚本文件、管理执行流程、处理信号和退出码。这一层决定了工具怎么被调用,是"rttsh -s script.lua"还是"rttsh --read-until ASSERT --timeout 30s",都在这层定义。

三层之间的数据流是这样的:CLI 解析参数后,初始化 C 核心层建立 J-Link 连接,然后加载 Lua 脚本并把 C 核心层的 API 注册进去,脚本执行过程中通过 API 读写 RTT 数据,最后 CLI 根据脚本执行结果返回退出码。

2.2 RTT 控制块搜索:从内存扫描到地址缓存

RTT 工作的前提是找到目标内存中的 RTT 控制块。这个控制块是 SEGGER RTT 库在目标端定义的一个结构体,包含了上行缓冲区、下行缓冲区的描述信息。J-Link 提供了JLINK_RTTERMINAL_Control这个 API 来获取控制块信息,但它的工作方式是:在目标内存的特定区域搜索 RTT 控制块的签名。

这里有个实际使用中很容易踩的坑:搜索范围。J-Link 默认的搜索范围是有限的,如果你的 RTT 控制块被链接器放在了比较特殊的位置(比如某些 RTOS 把 RTT 缓冲区放在特定的 RAM 段),默认搜索可能找不到。rttsh 里我加了--search-addr和--search-size两个参数,允许手动指定搜索的起始地址和范围。

另一个坑是搜索时机。如果目标板还没运行到 RTT 初始化完成,控制块还没建立,搜索就会失败。rttsh 的处理策略是:先尝试搜索,失败后等待一段时间重试,最多重试 N 次。这个 N 和等待间隔可以通过--search-retry和--search-delay配置。实测下来,对于大多数应用,重试 5 次、每次间隔 200ms 就能覆盖绝大多数启动场景。

搜索到控制块之后,rttsh 会把控制块地址缓存起来。后续的读写操作直接用这个地址,不再重复搜索。但这里要注意:如果目标板复位了,控制块地址可能会变。所以 rttsh 在检测到连续读失败时,会自动触发一次重新搜索。这个逻辑在长时间运行的采集场景里特别重要。

2.3 上行/下行缓冲区的读写机制与超时控制

RTT 的通信模型是双缓冲的:目标板通过上行缓冲区(Up Buffer)发数据给主机,主机通过下行缓冲区(Down Buffer)发数据给目标板。rttsh 的读写 API 就是围绕这两个缓冲区设计的。

读操作的核心逻辑是:轮询上行缓冲区的读指针,如果发现读指针和写指针不一致,说明有新数据,就把数据取出来。这里的关键是轮询间隔。间隔太短会浪费 CPU,间隔太长会丢数据(如果缓冲区满了,目标板的新数据会覆盖旧数据)。rttsh 默认的轮询间隔是 1ms,对于大多数场景够用。如果目标板输出速率很高,可以调到 100us。

写操作相对简单:把数据写入下行缓冲区,更新写指针。但要注意下行缓冲区的大小。如果一次写入的数据超过缓冲区大小,就需要分片写入。rttsh 的rtt.write()会自动处理分片,但分片之间需要等待目标板消费数据,否则会覆盖未读数据。这个等待逻辑我用了一个简单的流控:写入一片后,检查下行缓冲区的剩余空间,空间不足就等待一段时间再写下一片。

超时控制是脚本化工具的生命线。rttsh 的每个读写操作都支持超时参数。比如rtt.read_until("OK", 5000)表示最多等 5 秒,如果 5 秒内没读到 "OK" 就返回超时错误。这个超时不是简单的 sleep,而是带轮询的等待,一旦读到目标字符串就立即返回,不会浪费时间。

3. Lua 脚本层的 API 设计与典型用法

3.1 核心 API 清单与参数说明

Lua 脚本层是 rttsh 的灵魂,它的 API 设计直接决定了工具好不好用。我把 API 分成四类:连接管理、读操作、写操作、辅助工具。

连接管理类:

  • rtt.connect(options):建立 J-Link 连接。options 是个 table,支持device(芯片型号)、speed(接口速度 kHz)、interface(SWD/JTAG)等字段。
  • rtt.disconnect():断开连接。
  • rtt.reconnect():重新连接,用于目标板复位后的恢复。

读操作类:

  • rtt.read(timeout_ms):读取当前上行缓冲区中的所有数据,返回字符串。timeout_ms 是等待超时。
  • rtt.read_until(pattern, timeout_ms):持续读取直到匹配到 pattern,返回匹配到的完整数据。pattern 支持普通字符串和 Lua 模式。
  • rtt.read_line(timeout_ms):读取一行(以换行符为界)。
  • rtt.read_bytes(n, timeout_ms):读取指定字节数。

写操作类:

  • rtt.write(data):写入字符串或字节数组。
  • rtt.write_line(data):写入并追加换行符。
  • rtt.write_then_read(data, pattern, timeout_ms):写入后等待特定响应,这是最常用的组合操作。

辅助工具类:

  • rtt.log(msg):输出日志到 stderr,不影响 stdout 的数据流。
  • rtt.sleep(ms):休眠指定毫秒数。
  • rtt.export_csv(filename, rows):把数据导出为 CSV 文件。
  • rtt.export_json(filename, data):把数据导出为 JSON 文件。

这些 API 的设计原则是:常用操作一行搞定,复杂操作可以组合。比如"发命令等响应"这个最高频的操作,直接用write_then_read就行,不用自己拼 write 和 read_until。

3.2 用 read_until 实现命令-响应模式的交互

命令-响应模式是嵌入式调试里最常见的交互方式。目标板收到一条命令,执行后返回一个结果,主机等待这个结果。用 rttsh 的 Lua API 实现这个模式非常直接:

-- 发送命令并等待响应 local resp = rtt.write_then_read("GET_TEMP\n", "TEMP=%d+", 3000) if resp then local temp = resp:match("TEMP=(%d+)") rtt.log("当前温度: " .. temp) else rtt.log("读取温度超时") end

这里有几个细节值得说。第一,write_then_read的 pattern 参数用的是 Lua 模式,%d+匹配一个或多个数字,比普通字符串匹配灵活得多。第二,返回值 resp 是匹配到的完整数据,你可以用string.match进一步提取需要的字段。第三,超时返回 nil,脚本里要判断 nil 并处理超时情况,不能假设一定有响应。

实际使用中,我建议给每个命令-响应交互都设一个合理的超时。超时太短会误判(目标板还在处理),超时太长会拖慢整个脚本。经验值是:简单查询 1-2 秒,复杂操作 5-10 秒,固件升级之类的操作 30 秒以上。

3.3 批量脚本验证:循环采集与条件触发

批量脚本验证是 rttsh 的强项。比如你要验证目标板在不同电压下的工作状态,可以写一个循环,每次设置电压、等待稳定、采集数据:

local voltages = {3300, 3000, 2700, 2400, 2100} local results = {} for _, mv in ipairs(voltages) do rtt.write_line("SET_VOLTAGE " .. mv) rtt.read_until("VOLTAGE_SET", 2000) rtt.sleep(500) -- 等待电压稳定 local data = rtt.read_until("STATUS_OK", 3000) if data then local current = data:match("CURRENT=(%d+)") table.insert(results, {voltage = mv, current = current}) rtt.log(string.format("电压 %dmV, 电流 %s", mv, current)) else rtt.log(string.format("电压 %dmV 采集失败", mv)) end end rtt.export_csv("voltage_sweep.csv", results)

这个脚本展示了几个实用技巧。第一,用 table 存采集结果,最后统一导出,避免频繁写文件。第二,每次操作后都有超时判断,失败不会导致整个脚本崩溃。第三,用rtt.log输出进度信息到 stderr,这样即使 stdout 被重定向到文件,你也能在终端看到进度。

条件触发是另一个常见需求。比如你想在目标板输出特定错误码时自动抓取上下文:

while true do local line = rtt.read_line(1000) if line and line:match("ERROR_CODE=(%d+)") then local code = line:match("ERROR_CODE=(%d+)") rtt.log("捕获到错误码: " .. code) -- 抓取错误前后的上下文 local context = rtt.read(2000) rtt.export_json("error_" .. code .. ".json", { code = code, context = context, timestamp = os.time() }) end end

这种"持续监听 + 条件触发"的模式,在长时间稳定性测试里特别有用。你可以让它跑一整夜,第二天早上看抓到了哪些异常。

4. 把 rttsh 塞进 CI 流水线的实操细节

4.1 非交互模式运行与退出码约定

CI 环境里没有人在旁边看着,工具必须能非交互运行,并且用退出码告诉流水线"成功还是失败"。rttsh 的退出码约定是这样的:

退出码含义CI 处理建议
0脚本执行成功,所有断言通过继续下一步
1脚本执行失败,断言未通过标记构建失败
2连接失败,无法建立 J-Link 连接检查硬件连接
3脚本语法错误或运行时错误检查脚本
4超时,操作未在指定时间内完成检查目标板状态

在 CI 脚本里,你可以这样用:

#!/bin/bash set -e # 烧录固件 JLinkExe -device STM32F407VG -if SWD -speed 4000 -autoconnect 1 -CommanderScript flash.jlink # 运行 RTT 测试脚本 rttsh -s test_boot.lua --device STM32F407VG --speed 4000 if [ $? -ne 0 ]; then echo "RTT 测试失败" exit 1 fi # 抓取启动日志并检查 rttsh --read-until "BOOT_COMPLETE" --timeout 30s --output boot.log if ! grep -q "BOOT_COMPLETE" boot.log; then echo "启动未完成" exit 1 fi

这里的关键是set -e和显式的退出码检查。rttsh 在非交互模式下不会等待用户输入,所有操作要么成功要么超时失败,不会卡住流水线。

4.2 日志采集与断言检查的脚本模板

CI 里最常见的 RTT 用途是"抓日志 + 查关键字"。我整理了一个通用的脚本模板,可以直接改改用:

-- ci_check.lua local keywords = { {pattern = "ASSERT", desc = "断言失败", fatal = true}, {pattern = "HardFault", desc = "硬件错误", fatal = true}, {pattern = "WARN", desc = "警告", fatal = false}, } local log_file = io.open("rtt_full.log", "w") local fatal_count = 0 local warn_count = 0 local start_time = os.time() -- 采集 30 秒 while os.time() - start_time < 30 do local data = rtt.read(1000) if data and #data > 0 then log_file:write(data) log_file:flush() for _, kw in ipairs(keywords) do if data:find(kw.pattern) then rtt.log("检测到: " .. kw.desc) if kw.fatal then fatal_count = fatal_count + 1 else warn_count = warn_count + 1 end end end end end log_file:close() rtt.log(string.format("采集完成: 致命错误 %d, 警告 %d", fatal_count, warn_count)) if fatal_count > 0 then os.exit(1) end

这个模板的要点:日志实时写文件并 flush,避免程序崩溃时丢数据;致命错误和警告分开计数,只有致命错误才让 CI 失败;采集时间用os.time()控制,简单可靠。

4.3 多设备并行测试时的资源隔离

如果你在 CI 里同时测多个设备(比如一个流水线跑多个 DUT),就要注意 J-Link 的资源隔离。每个 rttsh 实例会占用一个 J-Link 探针,如果多个实例抢同一个探针,会报"J-Link is already in use"。

解决方案有两个。一是用序列号指定探针:rttsh 支持--serial参数,你可以在 CI 配置里给每个并行任务分配不同的 J-Link 序列号。二是用文件锁做互斥:在脚本开头用flock或者 Lua 的文件锁机制,确保同一时间只有一个实例访问特定探针。

# 用 flock 做互斥 flock /tmp/jlink_001.lock rttsh -s test.lua --serial 123456789

实测下来,用序列号指定是最干净的方式,前提是你的 J-Link 探针都有唯一序列号(正版都有,这个不用担心)。

5. AI 辅助调试场景下 rttsh 的独特价值

5.1 让 AI 助手通过命令行读取目标板状态

现在用 AI 辅助调试越来越普遍,但 AI 助手有个天然短板:它看不到你的硬件。你告诉它"程序跑飞了",它只能根据代码猜,没法直接看目标板的实际状态。rttsh 在这里可以当 AI 的"眼睛和手"。

具体做法是:把 rttsh 的命令行接口暴露给 AI 助手(比如通过一个简单的 shell 工具封装),AI 就可以执行rttsh --read-var g_system_state这样的命令,直接读取目标板上的变量值。rttsh 支持通过 RTT 读取任意内存地址,只要你知道变量的地址(从 map 文件里查),就能读。

# 读取指定地址的 4 字节变量 rttsh --read-mem 0x20000000 --size 4 --format hex # 读取并解析为整数 rttsh --read-mem 0x20000000 --size 4 --format int

这个能力配合 AI 的推理能力,可以做出很有意思的调试流程:AI 先读几个关键变量,根据值判断程序状态,然后决定下一步读什么或者发什么命令。整个过程不需要人干预。

5.2 结构化输出让 AI 更容易解析

AI 助手解析文本的能力很强,但结构化数据永远比自由文本更好处理。rttsh 支持--format json输出,把读取结果包装成 JSON:

{ "timestamp": "2024-01-15T10:30:00Z", "operation": "read_mem", "address": "0x20000000", "size": 4, "value": 42, "raw": "2A000000" }

这种格式 AI 可以直接解析,不需要做复杂的文本提取。我在实际使用中发现,给 AI 提供结构化输出,它的判断准确率明显高于给它一堆原始日志让它自己找。

5.3 脚本化交互降低 AI 的操作复杂度

AI 直接操作硬件有个风险:它可能发出错误的命令,把目标板搞挂。rttsh 的 Lua 脚本层可以起到"安全护栏"的作用。你可以预定义一组安全的操作脚本,AI 只能调用这些脚本,不能直接发任意命令。

比如定义一个safe_query.lua:

-- 只允许查询,不允许修改 local allowed_queries = { ["status"] = "GET_STATUS", ["version"] = "GET_VERSION", ["error"] = "GET_LAST_ERROR", } local query = arg[1] if not allowed_queries[query] then rtt.log("不允许的查询: " .. query) os.exit(3) end local resp = rtt.write_then_read(allowed_queries[query] .. "\n", ".+\n", 2000) print(resp)

AI 只能通过rttsh -s safe_query.lua status这样的方式查询,没法执行危险操作。这个设计在多人协作或者 AI 自主调试的场景里特别重要。

6. 实际使用中踩过的坑与性能调优经验

6.1 缓冲区溢出导致的数据丢失

RTT 的上行缓冲区大小是固定的(默认 1KB,可以在目标端配置)。如果目标板输出速率超过主机读取速率,缓冲区会满,新数据会覆盖旧数据。我踩过一次坑:目标板在 100ms 内输出了 2KB 日志,而我的脚本每 500ms 才读一次,结果丢了将近一半的数据。

解决办法有两个。一是提高读取频率,把轮询间隔从 500ms 降到 50ms 甚至更低。二是增大目标端缓冲区,在SEGGER_RTT_Conf.h里把BUFFER_SIZE_UP调大。实测下来,对于日志量大的场景,把上行缓冲区调到 4KB 或 8KB,配合 100ms 的读取间隔,基本不会丢数据。

还有一个隐蔽的坑:Lua 脚本里的 sleep 会阻塞读取。如果你在脚本里写了rtt.sleep(5000),这 5 秒内工具不会读 RTT,缓冲区可能就满了。正确的做法是用rtt.read(5000)代替 sleep,这样在等待的同时还在持续读取。

6.2 目标板复位后的连接恢复

目标板复位是调试过程中的常态,但复位会导致 RTT 控制块地址变化,原来的连接就失效了。rttsh 的处理策略是自动重连,但重连的时机和方式有讲究。

我最初的实现是:检测到读失败就立即重连。结果发现目标板复位过程中 J-Link 连接会短暂断开,立即重连经常失败。后来改成:检测到连续 3 次读失败后,等待 500ms 再重连,重连失败则指数退避重试。这个策略稳定多了。

-- 带重连的读取封装 function safe_read(timeout) local data, err = rtt.read(timeout) if err then rtt.log("读取失败,尝试重连...") rtt.sleep(500) if rtt.reconnect() then rtt.log("重连成功") return rtt.read(timeout) else rtt.log("重连失败") return nil end end return data end

6.3 高频读写下的 CPU 占用优化

rttsh 默认的 1ms 轮询间隔在低频场景下没问题,但如果你同时跑多个实例,或者目标板输出速率很高,CPU 占用会明显上升。我做过测试:单实例 1ms 轮询,CPU 占用约 3-5%;4 个实例并行,CPU 占用能到 20%。

优化手段有几个。一是动态调整轮询间隔:没有数据时逐渐增大间隔(比如从 1ms 增到 10ms),有数据时立即降回 1ms。这个自适应策略能把空闲时的 CPU 占用降到 1% 以下。二是用事件驱动代替轮询:J-Link 的 RTT API 其实支持回调模式,但配置起来比较复杂,我目前还没在 rttsh 里实现,算是一个后续优化方向。

还有一个容易忽略的点:Lua 脚本本身的性能。Lua 很快,但如果你在脚本里做大量的字符串拼接(比如log = log .. data),性能会下降。正确的做法是用 table 收集数据,最后用table.concat一次性拼接。

7. 从 rttsh 延伸出的几个实用场景

7.1 固件升级过程中的进度监控

固件升级(比如通过 Bootloader 升级)是个耗时操作,期间目标板会输出进度信息。用 rttsh 可以实时监控升级进度,并在异常时自动中止:

rtt.write_line("START_UPDATE") local last_progress = 0 local stall_count = 0 while true do local line = rtt.read_line(5000) if not line then stall_count = stall_count + 1 if stall_count > 3 then rtt.log("升级卡住,中止") rtt.write_line("ABORT_UPDATE") os.exit(1) end else stall_count = 0 local progress = line:match("PROGRESS=(%d+)") if progress then progress = tonumber(progress) if progress > last_progress then rtt.log(string.format("升级进度: %d%%", progress)) last_progress = progress end end if line:match("UPDATE_DONE") then rtt.log("升级完成") break end if line:match("UPDATE_FAIL") then rtt.log("升级失败") os.exit(1) end end end

这个脚本的关键是卡住检测:如果连续多次读不到数据,说明升级可能卡住了,主动中止比干等要好。

7.2 长时间稳定性测试的数据记录

稳定性测试通常要跑几个小时甚至几天,期间需要持续记录关键指标。rttsh 的 CSV 导出功能在这里很好用:

local csv = io.open("stability.csv", "w") csv:write("timestamp,heap_free,stack_usage,task_count,error_count\n") local start = os.time() while os.time() - start < 86400 do -- 跑 24 小时 local data = rtt.read_until("STATS", 10000) if data then local heap = data:match("HEAP=(%d+)") local stack = data:match("STACK=(%d+)") local tasks = data:match("TASKS=(%d+)") local errors = data:match("ERRORS=(%d+)") csv:write(string.format("%d,%s,%s,%s,%s\n", os.time(), heap, stack, tasks, errors)) csv:flush() end rtt.sleep(60000) -- 每分钟采集一次 end csv:close()

注意这里用了rtt.sleep(60000)而不是rtt.read(60000),因为采集间隔是 1 分钟,不需要持续读取。但前面说过 sleep 会阻塞读取,所以这种场景下要确保目标板的 RTT 缓冲区足够大,能撑过 1 分钟的输出量。如果撑不住,就得改成短间隔读取 + 累积判断。

7.3 多目标板同步采集的脚本编排

有些测试需要多个目标板同步工作,比如一个发命令、一个收响应。rttsh 支持多实例,你可以用 shell 脚本编排:

#!/bin/bash # 启动两个采集实例,分别连不同的 J-Link rttsh -s sender.lua --serial 111111 & PID1=$! rttsh -s receiver.lua --serial 222222 & PID2=$! # 等待两个实例完成 wait $PID1 RET1=$? wait $PID2 RET2=$? if [ $RET1 -ne 0 ] || [ $RET2 -ne 0 ]; then echo "同步测试失败" exit 1 fi

这种编排方式简单直接,适合两个到三个设备的场景。设备再多的话,建议用更专业的测试框架来管理。

8. 关于 rttsh 后续可以怎么扩展

rttsh 目前的核心功能已经能覆盖大部分脚本化 RTT 的需求,但还有几个方向值得继续做。一个是事件驱动的 RTT 读取,用 J-Link 的回调机制代替轮询,进一步降低 CPU 占用和延迟。另一个是内置的断言库,把常见的日志检查、变量范围检查封装成 Lua 函数,让 CI 脚本写起来更简洁。还有一个是和主流测试框架的集成,比如把 rttsh 包装成 pytest 的 fixture,这样用 Python 写测试的团队也能直接用。

我在实际使用中最大的体会是:脚本化调试工具的价值不在于功能多,而在于接口稳。rttsh 的 API 我尽量保持简单和稳定,因为一旦你的 CI 脚本依赖了某个接口,改起来成本很高。所以每次加新功能,我都会先问:这个功能能不能用现有的 API 组合出来?如果能,就不加新 API。这个原则让 rttsh 的接口数量一直控制在一个很小的范围内,维护起来轻松很多。

最后分享一个小技巧:如果你在用 rttsh 做长时间采集,建议在脚本里加一个"心跳"输出,每隔一段时间往 stderr 打一行日志。这样即使 stdout 被重定向到文件,你在终端也能看到工具还活着。这个习惯帮我避免了好几次"以为在跑其实早就挂了"的尴尬。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询