尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RTT自动化工具rttsh:面向CI/CD的嵌入式日志管道设计

RTT自动化工具rttsh:面向CI/CD的嵌入式日志管道设计 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的设计逻辑必须先看清RTTReal-Time Transfer协议的真实面目。很多人以为RTT是“串口替代方案”其实它本质是内存共享轮询状态机。J-Link通过SWD/JTAG在目标芯片RAM里开辟一块区域通常叫SEGGER_RTT分成多个channel0号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可中断CtrlC能立即终止循环而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完成复杂交互假设固件要求上电后发送ATVER?获取版本号并验证响应是否含v2.3.1。传统做法是写Python脚本调用pylink而rttsh只需一行shell# 发送AT命令等待2秒响应提取版本号并校验 echo -ne ATVER?\r\n | rttsh write --channel 0 \ sleep 0.1 \ rttsh read --channel 0 --timeout 400 \ | grep -q v2\.3\.1 \ echo PASS || echo FAIL这段命令的执行逻辑是echo -ne ATVER?\r\n生成原始字节流含\r\n换行符rttsh write --channel 0将其写入RTT channel 0注意不加--follow写完即退出sleep 0.1留出MCU处理时间实测GD32C103CB响应延迟约80msrttsh read --channel 0 --timeout 400读取400ms内的所有数据对应80次5ms轮询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(length0) | 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 ATTEST\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直接终止jobGitLab自动标记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何时用Commanderrttsh不是J-Link Commander的竞品而是它的“下游数据处理器”。理解两者的边界才能避免误用。下面用一张对比表说明核心差异维度J-Link Commanderrttsh协同场景核心定位硬件控制中心烧录、复位、寄存器读写RTT数据管道读/写/转发Commander烧录固件 → rttsh验证输出协议层级底层JTAG/SWD协议栈RTT内存协议应用层Commander不感知RTTrttsh不处理SWD输出格式人类可读文本含ANSI颜色、进度条机器可读流纯文本/JSONCommander日志用于debugrttsh输出用于CI错误处理交互式提示如Unknown device标准exit code1连接失败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 USB001指定序列号每个实例运行独立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控制块通常位于0x20000000SRAM起始地址附近但不同固件可能有偏移。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倍拖慢流水线。
返回列表