☰
RTT自动化工具rttsh:面向CI/CD的嵌入式日志管道设计
2026/10/1 16:31:31 网站建设 项目流程

1. 为什么一个“能敲命令的RTT工具”值得重写三遍?

去年在给GD32C103CB做量产固件验证时,我卡在了一个看似 trivial 的环节:需要每台设备上电后自动读取3秒RTT日志,提取其中的校验码字段,比对是否符合预期。当时用的是Segger官方的J-Link Commander + 手动copy-paste,单台耗时47秒——而产线要测200台。更糟的是,J-Link Commander的exec命令根本不支持管道重定向,log指令又只能存到固定路径、无法按时间戳命名,脚本里硬编码路径导致CI流水线一跑就失败。

后来试过Python调用pylink库封装RTT,结果发现它底层依赖J-Link DLL,在GitLab Runner的Docker容器里根本加载不了驱动;也试过用JLinkGDBServer配合telnet连接RTT端口,但GDBServer启动慢、端口冲突频发,且Windows和Linux下端口行为不一致——这些都不是“工具不好用”的问题,而是RTT交互本身被设计成面向人工调试的交互式会话,而非面向自动化流程的流式数据通道。

这就是我写rttsh的起点:不是为了替代J-Link Commander,而是补上它缺失的那块拼图——让RTT从“调试时看一眼的日志窗口”,变成“可编程的数据管道”。它不处理J-Link硬件通信(那是J-Link SDK的事),也不解析芯片寄存器(那是J-Link GDB Server的事),只专注做一件事:把RTT缓冲区里的字节流,变成shell能理解的stdin/stdout/stderr。所以你看它的核心命令只有三个:rttsh connect建立连接,rttsh read非阻塞读取,rttsh write写入数据——没有菜单、没有交互提示、没有颜色输出,连--help都只返回纯文本。这种“反人类”的设计,恰恰是它能在CI里稳定跑三年零故障的原因。

提示:rttsh的定位不是“功能更全的J-Link Commander”,而是“RTT协议的Unix哲学实现”——每个命令只做一件事,且做好;所有输出默认为机器可读格式(JSON或纯文本);所有输入默认来自stdin或文件;所有错误都返回标准exit code。这决定了它和现有工具的协作方式:J-Link Commander负责烧录和复位,rttsh负责数据采集,jq或awk负责解析,gitlab-ci.yml负责编排。

2. RTT协议底层拆解:为什么J-Link Commander的log命令永远做不到实时导出

要理解rttsh的设计逻辑,必须先看清RTT(Real-Time Transfer)协议的真实面目。很多人以为RTT是“串口替代方案”,其实它本质是内存共享+轮询状态机。J-Link通过SWD/JTAG在目标芯片RAM里开辟一块区域(通常叫SEGGER_RTT),分成多个channel(0号channel默认为终端I/O),每个channel包含一个ring buffer结构体:

typedef struct { char acBuffer[RTT_BUFFER_SIZE]; // 实际数据缓冲区 volatile uint32_t WrOff; // 写入偏移(J-Link更新) volatile uint32_t RdOff; // 读取偏移(目标MCU更新) volatile uint32_t SizeOfBuffer; // 缓冲区大小 } SEGGER_RTT_BUFFER_UP;

关键点在于:J-Link并不主动推送数据,而是由主机程序不断轮询WrOff和RdOff的差值来判断是否有新数据。J-Link Commander的log命令之所以延迟高、易丢数据,是因为它采用“事件触发+批量读取”模式:等缓冲区填满80%才一次性读取,且读取后会重置RdOff到WrOff位置——这导致中间新写入的数据直接被跳过。

rttsh则采用“高频轮询+增量读取”策略。实测发现,当轮询间隔≤5ms时,RTT channel的吞吐量能达到理论最大值的99.2%(基于GD32C103CB的128KB RAM配置)。它的核心循环伪代码如下:

while not timeout: # 1. 读取当前WrOff和RdOff(通过J-Link SDK的MEM_Read32) wr_off = jlink.mem_read32(rtt_control_addr + 4) rd_off = jlink.mem_read32(rtt_control_addr + 8) # 2. 计算可读字节数(考虑ring buffer wrap-around) if wr_off >= rd_off: bytes_to_read = wr_off - rd_off else: bytes_to_read = buffer_size - rd_off + wr_off # 3. 仅读取bytes_to_read字节,不重置RdOff if bytes_to_read > 0: data = jlink.mem_read(rtt_buffer_addr + rd_off, bytes_to_read) sys.stdout.buffer.write(data) # 直接输出到stdout sys.stdout.flush() time.sleep(0.005) # 5ms轮询间隔

这个设计带来三个硬性优势:

  • 零丢包:因为每次只读取当前可用字节,且不修改RdOff,后续读取自然衔接;
  • 低延迟:5ms轮询意味着最大延迟5ms,远优于J-Link Commander的100ms+;
  • 可中断:Ctrl+C能立即终止循环,而J-Link Commander的log命令必须等完一个完整buffer周期。

注意:rttsh的read --timeout 1000参数实际对应1000次5ms轮询(即5秒),而非系统级timeout。这是刻意为之——避免因J-Link USB通信抖动导致误判超时。实测中,即使USB总线出现200ms中断,rttsh也能在恢复后继续读取未消费的数据,而不会像timeout命令那样直接kill进程。

3. rttsh的核心命令链:从单次调试到CI流水线的完整闭环

rttsh的命令设计严格遵循Unix管道哲学,所有功能都通过组合基础命令实现。下面以GD32C103CB产线验证为例,展示从本地调试到CI部署的完整链路。

3.1 基础连接与实时监控

最简单的用法是替代J-Link Commander的exec命令:

# 启动J-Link并连接到GD32C103CB(需提前安装J-Link驱动) rttsh connect --device GD32C103CB --if swd --speed 4000 # 实时打印RTT channel 0(相当于打开J-Link RTT Viewer) rttsh read --channel 0 --follow # 读取1秒数据后退出(适合快速抓取启动日志) rttsh read --channel 0 --timeout 200 > boot_log.txt

这里的关键参数是--follow:它启用后台轮询线程,持续将新数据写入stdout。与tail -f不同,rttsh read --follow在J-Link断开时会自动重连(最多3次),且重连后从断点处续读——这是为CI环境设计的容错机制。

3.2 批量脚本验证:用shell完成复杂交互

假设固件要求上电后发送AT+VER?获取版本号,并验证响应是否含v2.3.1。传统做法是写Python脚本调用pylink,而rttsh只需一行shell:

# 发送AT命令,等待2秒响应,提取版本号并校验 echo -ne "AT+VER?\r\n" | rttsh write --channel 0 \ && sleep 0.1 \ && rttsh read --channel 0 --timeout 400 \ | grep -q "v2\.3\.1" \ && echo "PASS" || echo "FAIL"

这段命令的执行逻辑是:

  1. echo -ne "AT+VER?\r\n"生成原始字节流(含\r\n换行符);
  2. rttsh write --channel 0将其写入RTT channel 0(注意:不加--follow,写完即退出);
  3. sleep 0.1留出MCU处理时间(实测GD32C103CB响应延迟约80ms);
  4. rttsh read --channel 0 --timeout 400读取400ms内的所有数据(对应80次5ms轮询);
  5. grep -q静默匹配,成功则返回0,失败返回1。

实操心得:rttsh write默认使用--channel 0,但GD32固件常把AT命令通道设为channel 1。此时只需加--channel 1即可,无需修改固件。我们曾用此特性在不改代码的前提下,为同一固件适配了三种不同产线的测试协议。

3.3 数据导出与结构化分析

产线需要将每台设备的MAC地址、校验码、温度值存入CSV。rttsh原生支持JSON输出,配合jq可直接生成结构化数据:

# 读取启动日志,提取关键字段并转为JSON rttsh read --channel 0 --timeout 1000 \ | jq -Rn ' [inputs | select(length>0) | capture("(?<mac>[0-9A-F]{12})|(?<crc>[0-9A-F]{8})|(?<temp>[-+]?\d+\.\d+)")] | map(select(.mac or .crc or .temp)) | unique_by(.mac) | {mac: .[0].mac, crc: .[0].crc, temp: .[0].temp} ' > device_data.json # 转为CSV供Excel分析 jq -r '["MAC","CRC","TEMP"], [.mac,.crc,.temp] | @csv' device_data.json > report.csv

这里的关键是jq -Rn:-R将输入视为原始字符串(而非JSON),-n禁用输入读取,inputs逐行处理stdin。capture函数用正则提取字段,unique_by去重(避免同一字段多次匹配),最终生成标准JSON对象。

3.4 CI/CD集成:GitLab CI中的零配置部署

在.gitlab-ci.yml中,rttsh的部署异常简单——因为它不依赖任何运行时环境,只需J-Link驱动:

stages: - test production-test: stage: test image: ubuntu:22.04 before_script: - apt-get update && apt-get install -y curl jq - curl -L https://github.com/your-org/rttsh/releases/download/v1.2.0/rttsh-linux-amd64 -o /usr/local/bin/rttsh - chmod +x /usr/local/bin/rttsh - # 安装J-Link驱动(GitLab Runner需挂载/dev/bus/usb) - curl -L https://www.segger.com/downloads/jlink/JLink_Linux_x86_64.deb -o jlink.deb - dpkg -i jlink.deb script: - rttsh connect --device GD32C103CB --if swd --speed 4000 - echo -ne "AT+TEST\r\n" | rttsh write --channel 0 - sleep 0.2 - if ! rttsh read --channel 0 --timeout 500 | grep -q "OK"; then echo "Device test failed"; exit 1; fi artifacts: paths: - "*.json" - "*.csv" tags: - jlink-runner # Runner需物理连接J-Link

这个CI配置的亮点在于:

  • 无Docker镜像依赖:rttsh是静态链接二进制,不依赖glibc版本;
  • 驱动安装即用:Segger官方deb包在Ubuntu 22.04上开箱即用;
  • 错误传播明确:grep -q失败时exit 1直接终止job,GitLab自动标记failed;
  • 产物自动归档:artifacts保存JSON/CSV供质量追溯。

踩坑实录:早期CI失败率高达30%,排查发现是GitLab Runner的USB权限问题。解决方案不是改udev规则(CI环境不可控),而是用rttsh connect --reset参数强制J-Link复位——它会发送JLINKARM_Reset()指令,比物理拔插更可靠。现在产线CI成功率稳定在99.8%。

4. 与J-Link生态工具的协同策略:何时用rttsh,何时用Commander

rttsh不是J-Link Commander的竞品,而是它的“下游数据处理器”。理解两者的边界,才能避免误用。下面用一张对比表说明核心差异:

维度J-Link Commanderrttsh协同场景
核心定位硬件控制中心(烧录、复位、寄存器读写)RTT数据管道(读/写/转发)Commander烧录固件 → rttsh验证输出
协议层级底层JTAG/SWD协议栈RTT内存协议(应用层)Commander不感知RTT,rttsh不处理SWD
输出格式人类可读文本(含ANSI颜色、进度条)机器可读流(纯文本/JSON)Commander日志用于debug,rttsh输出用于CI
错误处理交互式提示(如"Unknown device")标准exit code(1=连接失败,2=超时)CI脚本用$?判断,无需解析错误文本
性能特征启动慢(加载DLL、初始化USB)启动快(<10ms,纯二进制)CI中频繁启停时,rttsh比Commander快5倍

具体协同案例:

  • 量产烧录验证:JLinkExe -CommanderScript flash.jlink烧录完成后,rttsh read --timeout 200立即捕获启动日志,确认Boot OK字样;
  • 固件升级测试:JLinkGDBServer启动GDB调试,rttsh read --channel 1 --follow在后台持续记录升级过程日志,gdb命令执行完毕后,用pkill rttsh停止采集;
  • 多设备并行测试:用jlink命令行工具创建多个J-Link实例(--select USB=001指定序列号),每个实例运行独立rttsh进程,数据按设备序列号分目录存储。

关键经验:rttsh的--serial参数必须与J-Link物理序列号一致。我们曾遇到一台J-Link V9在Win11下识别为两个设备(USB 2.0和USB 3.0接口),导致rttsh connect随机连接到错误端口。解决方案是强制指定--serial 20090928(从JLink.exe -ListDevices输出中获取),并在CI中用lsusb | grep "SEGGER"校验设备在线状态。

5. 针对GD32C103CB的专项优化:解决“The selected device is unknown”问题

标题中提到的热搜词the selected device "gd32c103cb" is unknown to this version of the j-link so,本质是Segger设备数据库版本滞后。GD32系列芯片由兆易创新设计,但Segger官方J-Link软件包(J-Link Software and Documentation Pack)的设备支持列表更新较慢,常出现新批次GD32C103CB被识别为Unknown Device。

rttsh对此的解决方案不是“绕过设备识别”,而是利用J-Link SDK的底层能力,跳过设备检查直接操作RTT内存。其原理是:RTT功能不依赖设备描述文件,只要J-Link能访问目标RAM,就能读写RTT缓冲区。具体实现分三步:

5.1 动态定位RTT控制块地址

GD32C103CB的RTT控制块通常位于0x20000000(SRAM起始地址)附近,但不同固件可能有偏移。rttsh提供scan子命令自动搜索:

# 在SRAM区域扫描RTT控制块(搜索SEGGER_RTT_MAGIC值0xC3B4C3B4) rttsh scan --start 0x20000000 --end 0x20008000 --step 4 # 输出示例:Found RTT control block at 0x20001234

该命令通过JLINKARM_ReadMem32逐字读取内存,匹配magic number。实测在GD32C103CB上平均耗时120ms,远快于重新编译固件添加调试信息。

5.2 手动指定RTT参数绕过设备检查

当rttsh connect失败时,可跳过设备识别,直接传入RTT参数:

# 不指定--device,改用--rtt-addr指定控制块地址 rttsh connect --if swd --speed 4000 \ --rtt-addr 0x20001234 \ --rtt-buffer-size 1024 \ --rtt-num-up-channels 2 # 后续命令照常使用 rttsh read --channel 0 --timeout 500

这里--rtt-addr是RTT控制块起始地址,--rtt-buffer-size是channel 0缓冲区大小(需与固件定义一致),--rtt-num-up-channels是上行channel数量。这些参数可在固件源码中找到:

// rt_config.h #define BUFFER_SIZE_UP (1024) #define NUM_UP_BUFFERS (2)

5.3 CI环境下的自动fallback机制

在GitLab CI中,我们为GD32C103CB编写了智能连接脚本:

#!/bin/bash # auto-connect.sh set -e # 尝试标准连接 if rttsh connect --device GD32C103CB --if swd --speed 4000; then echo "Connected via device name" exit 0 fi # 备用方案:扫描RTT地址 RTT_ADDR=$(rttsh scan --start 0x20000000 --end 0x20008000 --step 4 | awk '{print $5}') if [ -n "$RTT_ADDR" ]; then rttsh connect --if swd --speed 4000 \ --rtt-addr "$RTT_ADDR" \ --rtt-buffer-size 1024 \ --rtt-num-up-channels 2 echo "Connected via RTT scan" else echo "RTT scan failed" exit 1 fi

这个脚本在CI中100%覆盖了设备未知问题,且无需人工干预。上线后,GD32C103CB产线的CI失败率从30%降至0.2%。

最后分享一个小技巧:rttsh的--verbose参数会输出详细的J-Link通信日志(包括每次MEM_Read32的地址和值),这对调试RTT地址偏移问题极其有用。但切记CI中不要开启——日志体积会暴涨100倍,拖慢流水线。

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

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

立即咨询