ARTICLE DETAIL

资讯详情

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

基于J-Link RTT的命令行调试工具rttsh:Lua脚本与CI集成实践

基于J-Link RTT的命令行调试工具rttsh:Lua脚本与CI集成实践 1. 为什么我要自己写一个 RTT 命令行工具嵌入式调试这件事干过的都懂。板子一旦跑起来最怕的不是代码编译不过而是程序在板子里跑飞了、卡死了、数据不对了你却只能靠一根调试线和一个 IDE 的调试窗口去猜。J-Link 的 RTTReal Time Transfer技术算是把这个痛点解决了一大半——它允许 MCU 通过调试接口直接往主机端输出日志速度比串口快得多而且不占用 UART 资源。但问题来了Segger 官方给的 RTT Viewer 和 RTT Logger 都是 GUI 工具手动点一点看看日志还行一旦你想做自动化、批量验证、CI 流水线里的板级测试这些图形界面就成了拦路虎。我平时的工作流里板子调试、固件验证、数据采集是高频操作。每次都要打开 RTT Viewer手动连接手动保存日志再手动分析效率低得让人抓狂。更别说在 CI 环境里根本没有图形界面给你点。于是我就想能不能做一个命令行版的 RTT 工具支持脚本化能自动连接、自动读日志、自动导出文件还能嵌入到自动化流程里这就是rttsh的由来。rttsh 是一个基于 J-Link RTT 的命令行工具核心目标就三个第一把 RTT 的连接、读取、写入、导出全部命令行化第二支持 Lua 脚本扩展让你能用脚本控制整个调试流程第三能无缝嵌入 CI 和批量验证场景。它适合谁用嵌入式工程师、固件开发者、测试工程师尤其是那些需要频繁做板级调试、批量烧录验证、日志自动化采集的人。如果你还在用 GUI 工具手动复制粘贴日志那这个工具能帮你省下大量重复劳动。我写这个工具的初衷很简单让 RTT 调试像写 shell 脚本一样自然。你可以用一条命令连接板子用另一条命令把日志导出到文件再用 Lua 脚本写一个自动化测试流程最后把它塞进 CI 里跑。整个过程不需要人工干预也不需要图形界面。下面我就把这个工具的设计思路、核心实现、实操步骤和踩过的坑完整地分享出来。2. 整体设计与核心思路拆解2.1 为什么选择命令行 Lua 脚本的组合做工具选型的时候我考虑过几种方案。第一种是直接用 Python 写一个脚本调用 pylink 或者 JLinkExe 的命令行接口。第二种是做一个纯 C 的命令行工具直接调用 J-Link SDK。第三种是做一个命令行框架内嵌一个脚本引擎让用户自己写逻辑。第一种方案的问题在于Python 环境依赖太重CI 里装 Python 包有时候会遇到网络问题而且 pylink 的 API 封装层次比较高遇到底层问题不好排查。第二种方案性能最好但开发成本高而且用户想改个逻辑就得重新编译灵活性太差。第三种方案是我最终选择的用 C 写核心的 RTT 通信层用 Lua 做脚本层。Lua 的好处是轻量、嵌入简单、语法友好而且不需要额外的运行时环境编译进二进制就行。这个组合的核心优势在于核心层负责稳定地和 J-Link 通信脚本层负责灵活地编排逻辑。你可以把 RTT 连接、读取、写入这些操作封装成 Lua 函数然后在脚本里像搭积木一样组合它们。比如你可以写一个脚本先连接板子然后循环读取 RTT 数据遇到特定关键字就触发某个动作最后把数据导出到 CSV 文件。整个过程不需要重新编译工具改脚本就行。另一个考虑是跨平台。J-Link 本身支持 Windows、Linux、macOS所以工具也必须能跨平台。C Lua 的组合在这方面很成熟Windows 上用 MSVC 或者 MinGW 编译Linux 上用 GCCmacOS 上用 Clang基本不需要改代码。相比之下如果选 C# 或者 Python跨平台部署会麻烦很多。2.2 RTT 通信层的核心机制RTT 的工作原理简单说就是 MCU 在内存里划出一块缓冲区J-Link 通过调试接口直接读写这块内存主机端就能实时拿到 MCU 输出的数据。这块缓冲区叫 RTT 控制块里面包含了上行通道和下行通道的描述信息。上行通道是 MCU 往主机发数据下行通道是主机往 MCU 发数据。rttsh 的通信层要做的就是找到 RTT 控制块的内存地址解析通道信息然后通过 J-Link 的 API 读写这些通道。听起来简单但实际操作中有几个坑。第一RTT 控制块的地址不是固定的它取决于链接脚本里_SEGGER_RTT符号的位置。第二不同版本的 J-Link 驱动API 行为可能有差异。第三多通道场景下你得知道每个通道对应什么功能。我的做法是在工具启动时先通过 J-Link 的JLINK_RTTERMINAL_Control接口去探测 RTT 控制块。如果探测失败就回退到手动指定地址的方式。探测成功后解析出通道数量和每个通道的缓冲区大小然后启动一个后台线程去轮询上行通道的数据。轮询间隔我设的是 1 毫秒实测下来既能保证实时性又不会太占 CPU。下行通道的写入相对简单直接调用JLINK_RTTERMINAL_Write就行。但要注意写入之前得确认下行通道的缓冲区有足够空间否则会丢数据。我在工具里加了一个重试机制如果写入失败就等几毫秒再试最多重试三次。2.3 脚本层的设计哲学脚本层的设计目标就一个让用户用最少的代码完成最常见的任务。我把 RTT 操作封装成了几个核心 Lua 函数rtt.connect(device, interface, speed)连接 J-Link 并启动 RTTrtt.read(channel, timeout)从指定通道读取数据rtt.write(channel, data)向指定通道写入数据rtt.export(file, format)把读取到的数据导出到文件rtt.close()关闭连接这些函数的设计原则是“开箱即用”。比如rtt.connect的默认参数就是最常见的配置设备型号自动探测接口用 SWD速度用 4000 kHz。如果你不确定设备型号直接调rtt.connect()就行工具会尝试自动识别。脚本层的另一个重点是错误处理。Lua 本身有pcall和xpcall我在每个核心函数里都加了错误捕获如果 J-Link 连接失败或者 RTT 探测失败会返回明确的错误信息而不是直接崩溃。这样用户在脚本里可以用if not rtt.connect() then ... end来处理异常情况。3. 核心细节解析与实操要点3.1 环境准备与依赖安装在开始用 rttsh 之前你得先把环境搭好。首先是 J-Link 驱动去 Segger 官网下载最新的 J-Link Software Pack安装完之后确保JLinkExe或者JLink.exe在系统 PATH 里。Windows 用户注意Win11 下驱动安装可能会遇到签名问题建议用管理员权限安装装完之后重启一次。然后是编译工具链。如果你直接下载预编译的二进制这一步可以跳过。但如果想自己编译Linux 下需要gcc、make、libusb-devWindows 下需要 MSVC 或者 MinGWmacOS 下需要 Xcode Command Line Tools。Lua 库我推荐用 Lua 5.4编译的时候把lua.h、lauxlib.h、lualib.h的路径指对就行。编译命令大概长这样git clone https://github.com/yourname/rttsh.git cd rttsh mkdir build cd build cmake .. make -j4编译完成后你会得到一个rttsh可执行文件。把它放到 PATH 里或者直接用绝对路径调用。注意Windows 下编译时如果遇到JLINKARM_Open链接错误检查一下 J-Link SDK 的库路径有没有加到 CMakeLists.txt 里。我踩过这个坑折腾了半天才发现是库路径写错了。3.2 连接板子的正确姿势连接板子是第一步也是最容易出问题的一步。rttsh 的连接命令格式如下rttsh connect --device STM32F407VG --interface SWD --speed 4000如果你不确定设备型号可以省略--device参数工具会尝试自动探测。但自动探测有时候会失败尤其是板子上电时序不对的时候。我的经验是先给板子上电再执行连接命令这样成功率最高。连接成功后工具会输出 RTT 控制块的地址和通道信息。比如RTT control block found at 0x20000000 Up channel 0: buffer size 1024 Down channel 0: buffer size 16这时候你就可以开始读数据了。读取命令有两种模式一种是实时打印到终端一种是后台采集到缓冲区。实时打印用rttsh read --channel 0 --follow后台采集用rttsh read --channel 0 --buffer。提示如果连接失败先检查 J-Link 的 USB 连接是否正常然后确认板子的调试接口有没有被禁用。有些 MCU 在低功耗模式下会关闭调试接口这时候需要先唤醒板子。3.3 Lua 脚本的编写与调试Lua 脚本是 rttsh 的灵魂。你可以把一系列 RTT 操作写成一个脚本然后用rttsh run script.lua执行。下面是一个最简单的脚本示例-- demo.lua local ok rtt.connect(STM32F407VG, SWD, 4000) if not ok then print(连接失败) os.exit(1) end rtt.write(0, hello from host\n) for i 1, 10 do local data rtt.read(0, 1000) if data then print(收到: .. data) end end rtt.close()这个脚本做了三件事连接板子、往下行通道写一条消息、循环读取上行通道的数据。rtt.read的第二个参数是超时时间单位是毫秒。如果超时没读到数据返回nil。写 Lua 脚本的时候有几个技巧可以让你少踩坑。第一用pcall包裹可能失败的操作比如连接和读写。第二把常用逻辑封装成函数比如“等待特定字符串出现”这种操作写一次就够了。第三用print输出调试信息rttsh 会把脚本的print输出重定向到标准输出方便你在终端里看。注意Lua 脚本里的os.exit会直接终止 rttsh 进程如果你在 CI 里用记得在退出前调用rtt.close()否则 J-Link 连接可能不会正常释放。3.4 数据导出与格式选择数据导出是 rttsh 的另一个核心功能。你可以把 RTT 读取到的数据导出成文本、CSV 或者二进制文件。导出命令如下rttsh export --channel 0 --format csv --output log.csv --duration 10这个命令会采集 10 秒钟的数据然后导出成 CSV 文件。CSV 格式的好处是方便后续用 Excel 或者 Python 分析。如果你要导出二进制数据比如音频流或者传感器原始数据可以用--format bin。导出的时候要注意缓冲区大小。rttsh 默认的缓冲区是 1 MB如果数据量很大可能会丢数据。这时候可以用--buffer-size参数调大缓冲区比如--buffer-size 1048576010 MB。但缓冲区太大也会占内存所以要根据实际情况权衡。提示如果导出的 CSV 文件里出现乱码检查一下数据的编码格式。RTT 传输的是原始字节流如果 MCU 端输出的是 UTF-8 字符串导出时用--encoding utf-8就行。4. 实操过程与核心环节实现4.1 从零搭建一个自动化测试脚本假设你有一个 STM32 板子上面跑了一个固件你需要验证它在不同输入下的输出是否符合预期。手动测试的话你得每次烧录固件、打开 RTT Viewer、手动输入、手动看输出。用 rttsh你可以把整个过程自动化。第一步写一个 Lua 脚本定义测试用例-- test.lua local test_cases { {input cmd1\n, expected response1}, {input cmd2\n, expected response2}, {input cmd3\n, expected response3}, } rtt.connect(STM32F407VG, SWD, 4000) for i, case in ipairs(test_cases) do rtt.write(0, case.input) local data rtt.read(0, 2000) if data and string.find(data, case.expected) then print(用例 .. i .. 通过) else print(用例 .. i .. 失败: 期望 .. case.expected .. , 实际 .. (data or nil)) end end rtt.close()第二步把这个脚本放到 CI 流水线里。比如在 GitHub Actions 里你可以写一个 workflow先烧录固件然后运行rttsh run test.lua根据退出码判断测试是否通过。- name: Run RTT tests run: | rttsh run test.lua这个流程的关键在于rttsh 的退出码要能反映测试结果。我在工具里加了一个约定如果脚本里调用了os.exit(1)rttsh 就返回非零退出码CI 就会标记为失败。这样你就不需要解析输出文本直接看退出码就行。4.2 批量验证场景下的参数调优批量验证的时候你可能会同时连接多个板子或者在一个板子上跑很多轮测试。这时候性能调优就很重要了。我实测下来有几个参数对性能影响最大参数默认值建议值说明轮询间隔1 ms0.5 ms降低间隔可以提高实时性但 CPU 占用会上升缓冲区大小1 MB4 MB大批量数据采集时调大避免丢数据超时时间1000 ms500 ms批量测试时缩短超时加快失败检测J-Link 速度4000 kHz8000 kHz板子支持的话调高速度加快数据传输调这些参数的时候建议一次只改一个然后跑一轮测试看效果。我试过把轮询间隔降到 0.1 ms结果 CPU 占用直接飙到 30%得不偿失。0.5 ms 是一个比较平衡的值。另一个批量验证的技巧是复用 J-Link 连接。如果你要跑很多轮测试不要每轮都重新连接而是在脚本开头连接一次然后循环执行测试用例最后再关闭。这样能省下不少连接建立的时间。4.3 数据导出文件的后续处理导出的 CSV 文件你可以直接用 Python 的 pandas 做分析。比如你想统计某个传感器数据的平均值和标准差import pandas as pd df pd.read_csv(log.csv) print(df[sensor_value].describe())如果导出的数据量很大比如几十万行pandas 可能会有点慢。这时候可以用dask或者直接分块读取。我一般会在导出的时候加一个--split参数把数据按时间切分成多个文件每个文件 10 万行这样后续处理会快很多。提示CSV 文件里的时间戳默认是相对时间从 0 开始。如果你需要绝对时间可以在脚本里用os.time()记录开始时间然后在导出时加上偏移量。5. 常见问题与排查技巧实录5.1 连接失败的各种原因与解法连接失败是最高频的问题。我整理了一个排查表按顺序检查基本能解决 90% 的情况现象可能原因解决方法找不到 J-LinkUSB 驱动未安装重新安装 J-Link Software Pack连接超时板子未上电或调试接口禁用检查电源确认调试接口使能RTT 控制块未找到固件未初始化 RTT确认固件里调用了SEGGER_RTT_Init读取数据为空通道号错误用rttsh info查看通道信息数据乱码编码不匹配检查 MCU 端输出编码调整--encoding其中“RTT 控制块未找到”是最让人头疼的。有时候固件里明明初始化了 RTT但工具就是找不到。这种情况多半是因为 RTT 控制块的内存地址被优化掉了或者链接脚本里_SEGGER_RTT符号被重命名了。我的解法是在固件里加一句volatile修饰防止编译器优化然后在 rttsh 里手动指定地址rttsh connect --rtt-address 0x200000005.2 数据丢包与缓冲区溢出数据丢包是另一个常见问题。RTT 的上行通道缓冲区是有限的如果 MCU 输出数据的速度超过了主机读取的速度缓冲区就会溢出导致丢包。我遇到过好几次采集到的日志中间缺了一段排查了半天才发现是缓冲区太小。解决方法是双管齐下一方面调大 MCU 端的 RTT 缓冲区另一方面调大 rttsh 的读取缓冲区。MCU 端的缓冲区大小在SEGGER_RTT_Conf.h里改把BUFFER_SIZE_UP从默认的 1024 改成 4096 或者更大。rttsh 端的缓冲区用--buffer-size参数调。还有一个技巧是降低 MCU 的输出频率。如果 MCU 每毫秒输出一次日志主机端可能来不及处理。改成每 10 毫秒输出一次丢包概率会大大降低。5.3 Lua 脚本的常见错误Lua 脚本的错误主要有三类语法错误、运行时错误、逻辑错误。语法错误最容易发现rttsh 会在加载脚本时直接报错。运行时错误通常是调用了不存在的函数或者参数类型不对比如rtt.read(0)传了字符串而不是数字。逻辑错误最隐蔽比如循环条件写错了导致脚本跑飞。我的建议是写脚本的时候多用print输出中间状态。比如在循环里打印当前迭代次数在读取数据后打印数据长度。这样一旦出问题你能快速定位到是哪一步错了。注意Lua 的字符串拼接用..不是。我见过不少新手在这里栽跟头包括我自己刚开始的时候。5.4 CI 环境下的特殊处理在 CI 里跑 rttsh有几个特殊点要注意。第一CI 环境通常没有图形界面所以不能用任何依赖 GUI 的功能。第二CI 的 USB 权限可能受限需要配置 udev 规则或者用管理员权限运行。第三CI 的超时时间通常比较短脚本要尽量高效避免长时间等待。我在 GitHub Actions 里跑 rttsh 的时候遇到过一次 USB 设备权限问题。解法是在 workflow 里加一步- name: Set USB permissions run: sudo chmod 666 /dev/bus/usb/*/*这个命令把 USB 设备的权限放开让 rttsh 能正常访问 J-Link。当然生产环境里不建议这么做最好是用 udev 规则精确控制。6. 一些实操心得与扩展思路rttsh 这个工具我从第一版写到现在迭代了大概十几个版本。最开始只支持基本的读写后来加了 Lua 脚本、数据导出、CI 集成功能越来越完善。踩过的坑也不少比如 J-Link 驱动版本不兼容导致连接失败Lua 栈溢出导致脚本崩溃缓冲区溢出导致数据丢包。每一个问题都花了不少时间排查但解决之后工具的稳定性提升了很多。如果你也想自己做一个类似的工具我的建议是先把核心通信层做稳定再考虑脚本层。通信层不稳脚本层再灵活也没用。另外多写测试用例尤其是边界情况比如连接超时、缓冲区满、数据格式错误。这些测试能帮你在早期发现大部分问题。这个工具后续还可以扩展的方向挺多。比如支持多 J-Link 同时连接做并行测试比如加一个 WebSocket 接口让远程也能实时看 RTT 数据比如集成到 VS Code 里做一个插件直接在编辑器里看日志。这些想法我都在考虑但优先级最高的还是把现有的功能做扎实。最后分享一个小技巧如果你在 Windows 上用 rttsh建议把 J-Link 的安装路径加到系统环境变量里这样工具就能自动找到JLinkARM.dll不用每次手动指定路径。这个设置一次就行省事很多。
返回列表