⚠️ 法律与用途声明:本文讨论上板测试的自动化验证闭环设计,基于开源工具(Conan 2 / GitHub Actions / OpenOCD / JLink),属于经验分享。所有企业名、项目名、内部脚本与板卡型号均已脱敏,代码示例中的标识符均为占位。读者须遵守所在地区法律法规。

一、编译过了,不等于板上跑通
做过交叉编译的人多半经历过这种绝望:CI 绿了,编出来的 ELF 没报错,传到板子上一跑,直接 segfault,串口连个像样的报错都没有。你在 CI 上跑了几百遍都过的代码,到真板上就是不干人事。
这不是 CI 有 bug,这是构建环境和运行环境的鸿沟。CI 验证的是"这套源码在这套工具链下能编出二进制";它没验证"这个二进制在这块板子的这颗 CPU、这套外设、这个内存布局下能正常运行"。编译过了只证明代码语法对、链接通,不证明它在真硬件上跑得通。

所以光有构建闭环不够,还得有上板验证闭环——把构建产物真正弄到板子上跑一遍,自动采集结果,自动判定通过还是失败。这一篇就讲这条闭环怎么搭。
二、闭环的六个环节
上板验证不是"烧上去看看"那么简单,它是一条有头有尾的流水线,六个环节缺一不可:

把这条流水线写成伪代码,大概长这样:
def board_test_loop(run_key, target, bundle_dir):
# 1. 校验:产物本身合法吗(架构对不对、解释器有没有)
elf = validate_elf(bundle_dir, target)
# 2. 部署:把产物弄到板上(烧录 or 推送)
deploy(elf, target)
# 3. 运行:在板上执行
# 4. 采集:捕获输出(串口 or SSH stdout)
output = run_and_capture(target)
# 5. 解析:从输出里提取结构化结果
results = parse_protocol(output)
# 6. 判定:根据结果判定通过/失败,写报告
verdict = judge(results)
save_report(run_key, target, verdict, results)
六个环节各管一件事,下面挑几个关键的展开说。
三、部署:按目标分方式,不能一刀切
板子分两大类,部署方式完全不同:

def deploy(elf, target):
if target.kind == "baremetal":
# MCU:用烧录工具把 bin 写进 Flash
flash(elf, tool=target.flash_tool, # JLink / OpenOCD / PyOCD
addr=target.flash_addr,
device=target.device_name)
elif target.kind == "linux":
# Linux 板:scp/adb push 推过去,赋执行权限
ssh_push(elf, host=target.ssh_host,
remote_path=target.remote_path)
ssh_run(target.ssh_host, f"chmod +x {target.remote_path}")
裸机 MCU(Cortex-M 类)的部署是烧录——用 JLink、OpenOCD 或 PyOCD,把二进制写进 Flash 的指定地址。这要求你知道板子的 Flash 起始地址、器件名、用的探针类型,每换一块板子这些参数都不同。
Linux 板(Cortex-A 类)的部署是推送——通过 SSH 或 ADB 把 ELF 传到板子上的某个临时目录,赋个执行权限就行。比烧录简单,但要处理网络可达性、路径权限这些事。
两种方式差异巨大,但它们的输出是统一的——都是"产物已经到了板上、可以跑了"这个状态。这个统一很关键,因为后面的运行和采集环节不需要关心目标是什么类型的板子。
四、采集:统一输出协议,别靠人眼看串口
板子跑完之后,怎么知道结果?最土的办法是开个串口终端,人眼看输出。但这没法自动化——CI 不能挂着一个人盯串口。
解法是约定一套机器可读的输出协议。板子上跑的程序,按统一格式往输出里打标记:

BENCHMARK_START
MODULE|<模块名>|cases=<用例数>
RESULT|<用例名>|<测量值>
CASE_ERROR|<用例名> [<失败迭代号>/<总迭代数>]
BENCHMARK_END
这套协议的设计原则是简单、行式、无歧义:
- 用
BENCHMARK_START和BENCHMARK_END包住整个输出段,解析器知道从哪开始、到哪结束。 - 每个用例的结果是一行
RESULT|名字|值,竖线分隔,机器一拆就出来。 - 失败的用例走
CASE_ERROR,不和正常的 RESULT 混在一起。
采集端(watcher)只需要从串口或 SSH 的输出流里抓这几行,不依赖任何额外的日志格式。Cortex-M 从串口抓,Cortex-A 从 SSH stdout 抓,但协议格式完全一样——采集端不用为每种板子写一套解析器。
这是一个很实用工程设计:用统一的输出协议屏蔽硬件差异。板子那边只要按格式打标记,这边就能统一解析。你加一块新板子,不用改采集代码,只要新板子上的程序也按这套协议输出。
五、判定:用例的返回值就是结论
有了结构化的输出,判定就简单了。约定是:每个用例返回 1 表示通过,返回 0 表示失败。
def judge(results):
for case in results.cases:
if case.return_value == 0:
return {
"verdict": "FAIL",
"failed_case": case.name,
}
return {"verdict": "PASS"}
判定逻辑就这么直白——所有用例都返回 1 才算 PASS,有一个返回 0 就是 FAIL。不做模糊打分、不做阈值判断(那是 benchmark 性能评估的事,下一篇讲),只回答一个问题:跑通了没有。
这个"二元判定"是上板验证的核心价值。构建告诉你"编得过",上板验证告诉你"跑得通"。两个都绿,这个包才算真的经过了端到端验证。
六、watcher:让闭环自己转起来
前面五个环节写好了,但谁来跑这条闭环?答案是一个叫 watcher 的自动执行者。它的工作模式是这样:

def watcher_main():
while True:
# 扫描有没有新的 board bundle 到达
bundle = scan_latest_bundle()
if bundle is None:
sleep(poll_interval)
continue
# 加锁,防止多个 watcher 抢同一份 bundle
with file_lock(f"runtime/runs/{bundle.run_key}/watch.lock"):
# 跑完整闭环
verdict = board_test_loop(
bundle.run_key, bundle.target, bundle.path
)
# 结果写到独立目录(NO.A11 的 run_key 隔离)
save_report(bundle.run_key, bundle.target, verdict)
watcher 几个关键设计:
- 轮询扫描:定期看有没有新 bundle 传过来(NO.A11 讲的 board bundle),有就开始跑。不用人触发。
- 锁防并发:同一份 bundle 只能一个 watcher 跑,靠文件锁串行。避免两台 watcher 同时烧一块板子。
- 结果按 run_key 存:写报告时用 NO.A11 的三键模型,每个 attempt 独立目录,不覆盖。
- 支持预演:可以只跑到"部署"这一步就停,不真跑,用于验证部署链路通不通。
watcher 的本质是把上板验证这条闭环从人坐在板子前手动操作,变成机器自动跑、自动判、自动写报告。人只需要看最终的报告——PASS 还是 FAIL、哪个用例挂了、输出的值是多少。
七、几条攒下来的判断
编译过了不等于跑通。构建验证语法和链接,上板验证运行。两者都过,才算端到端验证。这条鸿沟不用上板跨不过去。
部署按目标分,但输出要统一。MCU 烧录和 Linux 推送差异巨大,但它们之后的环节(运行、采集、判定)应该完全一致——靠统一输出协议屏蔽硬件差异。
输出协议要机器可读。别靠人眼看串口。用简单的行式标记(START/RESULT/END),让采集端一行代码就能解析,加新板子不用改采集逻辑。
判定要二元。上板验证只回答"跑通没跑通",不掺性能评估。简单、明确、可自动化。
闭环要能自己转。watcher 轮询 + 锁 + 自动判定 + 按键存结果,让人从"坐在板子前手动操作"解放出来,只看最终报告。
这套闭环搭起来之后,最直观的变化是:构建一完成,产物自动到板、自动跑、自动出报告,你打开登记表直接看 PASS 还是 FAIL。从"能编"到"能验证",这一步自动化,是嵌入式 CI 从半自动走向全自动的关键一跃。
下一篇,我们讲上板测试的进阶:benchmark CI 闭环——不光验证"跑通没跑通",还要量化"跑得快不快",把性能数据也纳入自动化闭环。

参考链接:
[1] GitHub Actions:self-hosted runner 上的硬件测试 https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners
[2] OpenOCD(开源烧录与调试) https://openocd.org/
[3] PyOCD(Python 烧录工具,支持多种 MCU) https://pyocd.io/
[4] JLink(SEGGER 烧录调试器) https://www.segger.com/products/debug-probes/j-link/
[5] Conan 2:交叉编译与上板验证 https://docs.conan.io/2/how-tos/cross_platform/cross_building_with_conan.html
[6] Android Debug Bridge(ADB,Linux 板部署) https://developer.android.com/tools/adb