板子还没到货,测试已经跑完了——Renode 让 PC 变成虚拟开发板

0 阅读17分钟

板子还没到货,测试已经跑完了——Renode 让 PC 变成虚拟开发板

读前摘要 嵌入式测试最痛的往往是——板子没到货、样片被借走、偶发 bug 抓不到。Renode 是 Antmicro 开源的嵌入式仿真框架,让你在 PC 上"拼"出虚拟开发板、直接跑未修改的固件、把测试塞进 CI。本文先用大白话讲清它是什么、能用在哪些场景、能力边界在哪,再给最小可用的 .repl + Robot Framework 技术入口(涵盖寄存器 / GPIO / 中断 / Flash 四类实测,并给出网络测试的通用模板),帮你把"会写固件"升级到"能自动化测固件"。本文配套的完整代码仓库已经全部开源,文末附 Gitee 地址,clone 下来一条命令即可复现文中所有实测。

做嵌入式的朋友,多半都经历过这种场面:固件明明昨天还好好的,今天改了两行,板子一上电就原地去世;想复现,偏偏那块板子被同事借走了,借来的另一块又"测不出问题";更惨的是项目刚立项,硬件还在画板子,软件组只能干瞪眼等样片。

测试难,是嵌入式开发甩不掉的痛。今天聊一个能把这个痛减轻不少的工具——Renode。它不是什么新出的 AI 玩具,但在"不依赖真机也能把固件测明白"这件事上,做得相当扎实。

一、Renode 是什么:你 PC 上的"虚拟开发板"

一句话:Renode 是一个开源框架,让你在普通电脑上"拼"出一块虚拟的嵌入式开发板,然后把平时那个 .bin / .elf 烧进去,在仿真里跑、调试、做自动化测试。

它是波兰公司 Antmicro 从 2010 年就开始做、2015 年开源的项目,现在免费、开源,同时 Antmicro 也提供商业支持。有社区把它比作"嵌入式界的 Docker"——Docker 让你用文本描述一份运行环境、到处复现;Renode 让你用文本描述一块硬件板子、到处跑同一份固件。这个类比挺贴切。

几个关键点先记一下,后面会用到:

  • 跑的是未修改的固件。你编译出来的二进制,直接加载进仿真,不用为了测试改代码、加桩、做 mock。这是它和很多"自己造个假环境"的测试方案最大的区别。
  • 仿的是整颗 SoC,不只是 CPU。内存、UART、定时器、GPIO、甚至板子之间的连线,都能在文本里描述出来。
  • 确定性执行 + 共享虚拟时间。同一个输入,永远得到同一个输出;多块"虚拟板"跑在同一套虚拟时钟上,能复现那种"平时测不出、上线就偶发"的诡异 bug。
  • 不是小众玩具。Google 的 TensorFlow Lite Micro、Zephyr、Arm、Microchip 都用它做自动化测试。能在这些项目里扛大梁,可靠性不用怀疑。

二、什么时候该用它(场景)

工具好不好,得看场景。Renode 在下面几种情况里,价值特别明显:

1. 硬件还没到货 / 还没流片 板子在设计阶段、样片要一个月后才来?Renode 让你在"预硅片"阶段就用虚拟平台把软件跑起来、把测试写出来。等真板子到了,软件已经测过一轮,不是从零开始。

2. 团队不抢硬件 十个人共用三块板子,是最低效的嵌入式开发。Renode 让每个人桌面上都有"无限块"虚拟板,本地就能完整开发调试,不再排队等硬件。

3. 把固件测试塞进 CI 这是 Renode 最被推崇的用法。配合 Robot Framework(或 pytest),每次代码提交都自动拉起仿真、烧固件、跑断言。桌面/Web 开发早就离不开 CI 了,嵌入式这边一直难做,Renode 算是把这块补上了。Google 就用它给 TensorFlow Lite Micro 搭 CI。

4. 多节点组网测试 想测"一个网关带十个传感器"的通信逻辑?真实环境要焊十块板、接线、供电。Renode 在一台 PC 上就能模拟整个设备网络,组网协议、边界情况随便造。Zephyr 的项目架构师就说过:IoT 系统越来越大,真机上根本测不全协议,Renode 让测试覆盖度上去一大截。

5. 复现偶发 bug 最头疼的那种"偶尔崩一下"。因为 Renode 执行完全确定,你只要把触发条件在仿真里复现一次,它次次都崩在同一个点,再也不靠运气抓虫。

顺着"什么时候用"这个思路,再补几类具体能仿真什么东西的例子,你就能更直观感受到它的覆盖面:

  • 端侧 AI / TinyML 推理测试。Google 的 TensorFlow Lite Micro 团队就用 Renode 在虚拟的 RISC-V / Arm MCU 上跑推理 demo,不接一块真板就把 CI 搭起来了——这正是"没有硬件也能测试 AI 固件"的现成案例。对想做 AI + 嵌入式的朋友,这点特别值得留意。
  • 固件安全研究 / 模糊测试(fuzzing)。Renode 能和 AFL++ 打通,对 Zephyr 这类固件做模糊测试、挖潜在漏洞;安全团队也用它做 IoT 固件的动态分析、复现漏洞,全程不依赖真机。
  • 开源安全芯片验证。像 OpenTitan 这种开源安全芯片,就是用 Renode 做完整 SoC 级仿真来验证的。
  • 消费级产品的 EC(嵌入式控制器)测试。Renode 官网就展示了用它在 CI 里测试跑 ChromiumOS 的消费级设备,覆盖比手工点板高得多。
  • 传感器数据注入 / 数字孪生雏形。你可以给仿真里的"虚拟传感器"喂建模好的样本数据(比如一段加速度曲线),看自己的算法处不处理得对——这其实就是数字孪生工作流的雏形。

这么多类东西都能在 PC 上仿真,核心就一句话:只要你的验证对象是"软件行为",Renode 大多能接得住;一旦牵扯真实电气特性,就回到真机。

三、它的边界:别把它神化

讲完能耐,必须泼盆冷水——Renode 是功能性 / 指令级仿真,不是周期精确(cycle-accurate)仿真。这句话决定了它能干什么、不能干什么。

  • 不模拟 RTL / 门级电路。它跑的是软件视角的指令流,不关心每个时钟沿发生了什么。要做芯片级验证、时序分析,那是 EDA 工具(如 Verilator 跑 RTL)的活,Renode 不抢。
  • 不模拟模拟电路、RF、功耗。天线收发、电源纹波、电池续航、信号完整性——这些物理世界的事它管不了。
  • 时序敏感的东西测不了。比如你要验证某个 PWM 的占空比精确到 0.1%、ADC 采样点卡在微秒级、中断响应延迟的上限——这些依赖真实电气时序的场景,仿真是 approximate 的,别拿它下结论。
  • 替代不了真机做认证和量产前验证。Renode 验证的是"软件逻辑对不对",真机验证的是"在物理世界里稳不稳"。两者互补,不是替代。
  • 模型要可得。主流板子(STM32 各系列、nRF、很多 RISC-V 板)社区有现成模型;冷门板子可能得自己写 .repl 描述——这本身也是工作量,别以为开箱就有你的板。

一句话总结边界:Renode 让你在没有硬件、或不想被硬件拖累时,把"软件该有的行为"测透;但凡涉及真实电气特性和物理世界,最终还得回真机。

四、技术细节:它到底怎么工作

前面都是大白话,下面给最小可用的技术入口。看着有点硬,但只要你抓到两条主线,剩下的都是查文档的事。

1)平台描述文件 .repl——用文本"画"一块板

Renode 的板子不是 GUI 拖出来的,是一个文本文件。下面是一段最小示意,你能看出它的思路(完整可运行版本在仓库 renode/stm32f103rct6.repl——它 using 官方内置的 platforms/cpus/stm32f103.repl,再在 GPIOA 上接一个 LED 外设,就是本文配套 Demo 用的平台):

using sysbus

cpu: CPU.CortexM @ sysbus 0x0
    cpuType: "cortex-m4"

ram: Memory.MappedMemory @ sysbus 0x20000000
    size: 0x00040000

uart: UART.NS16550 @ sysbus 0x40004000
    -> uartConsole

含义很直白:挂一颗 Cortex-M4 当 CPU,0x20000000 起放一块 RAM,0x40004000 放一个 UART 并接到控制台。换芯片、加外设,就是改这个文本。这就是前面说的"文本描述硬件"。

2)自动化测试:Robot Framework 关键词

Renode 自带一套 Robot Framework 关键词,写测试几乎像说人话。下面就是本文配套仓库 renode/tests/stm32f103rct6.robot真实能跑、且 Renode 1.16.1 实测通过的第一个用例(原样摘录,仅删了注释)。它和我们前面说的思路完全一致——区别只是路径用 ${CURDIR} 拼成绝对路径,原因下面会解释:

*** Variables ***
${REPL}    ${CURDIR}/../stm32f103rct6.repl
${ELF}     ${CURDIR}/../../build/renode_stm32f103rct6.elf

*** Test Cases ***
固件启动应在串口打印 Hello from Renode
    Execute Command         mach create
    Execute Command         machine LoadPlatformDescription @${REPL}
    Execute Command         sysbus LoadELF @${ELF}
    Create Terminal Tester  sysbus.usart1
    Start Emulation
    Wait For Line On Uart   Hello from Renode    timeout=10

六步:先定义两个绝对路径变量(${CURDIR} 是 robot 文件所在目录,拼出 .repl / .elf 的真实位置)→ 建机器 → 加载板子描述 → 加载固件 → 接一个串口监听器 → 开跑,然后断言串口在 10 秒内打出了 Hello from Renode。把它放进 CI,每次提交自动验证"固件至少能起来、该打印的打印了"。

注:为什么用 ${CURDIR} 绝对路径?因为 renode-test 拉起仿真时,Renode 进程的当前目录并不一定是仓库根,直接写 @renode/stm32f103rct6.repl 这种相对路径会报"找不到文件"。用 ${CURDIR} 从 robot 文件自身位置反推绝对路径就稳了。clone 下来直接 renode-test renode/tests/stm32f103rct6.robot 即可复现。

不过串口打印只是最基础的断言。嵌入式固件测试真正头疼的,往往是寄存器有没有写对、中断有没有进、网络包有没有发出去。这几类常见的测试,Renode 都能用同样的关键词直接写出来——下面 ①~③、⑤ 给出本仓库已实测用例的节选(与 tests/stm32f103rct6.robot 一致),④ 网络因 F103 无以太网外设,保留为通用模板(关键词来自 Renode 官方 API,适用带 ETH 的 STM32 如 F4/F7/H7)。

先剧透:① 寄存器/内存、② GPIO/LED、③ 中断、⑤ Flash 这四类,本仓库已经写成真实可运行、且用 Renode 1.16.1 实测通过的 Robot 用例renode/tests/stm32f103rct6.robot),覆盖「固件启动串口打印 / LED 周期闪烁 / GPIOA 配置为输出 / USART1 使能 / TIM2 中断驱动 PB0 闪烁 / Flash 写读一致」六项,上面代码片段即其节选。④ 网络因 STM32F103 无原生以太网(ETH)外设,本仓库保留为通用模板——但它适用于带 ETH 的 STM32(F4/F7/H7,Renode 有现成模型 + host tap 组网),接你自己的固件时把符号名和地址一换就能用。

① 外设寄存器 / 内存读写断言(本仓库实测用例节选)

驱动配置完一个外设,你想确认寄存器真写进去了。用 Execute Command 直接读内存/外设地址,再用 Robot 的 Should Be Equal As Integers 断定值对不对。下面是从本仓库 tests/stm32f103rct6.robot 节选的「GPIOA Pin8 配置为输出推挽」断言(片段里的 ${REPL} / ${ELF} 即前面 Section 2 在 *** Variables *** 里定义的绝对路径变量)——注意 ReadDoubleWord 返回值带尾部换行,要先 Strip String 再转整数,否则会报语法错:

GPIOA Pin8 应被配置为输出推挽
    Execute Command         mach create
    Execute Command         machine LoadPlatformDescription @${REPL}
    Execute Command         sysbus LoadELF @${ELF}
    Start Emulation
    ${crh}=                 Execute Command  sysbus ReadDoubleWord 0x40010804
    ${crh}=                 Strip String    ${crh}
    ${pin8}=                Evaluate         int("${crh}", 16) & 0xF
    Should Be Equal As Integers  ${pin8}  0x2

sysbus ReadDoubleWord <addr> 是 Renode 真实的 monitor 命令,能读出内存和外设寄存器内容;0x40010804 是 STM32F103 的 GPIOA_CRH 寄存器地址,Pin8 占其低 4 位(CNF8 + MODE8)。CubeMX 默认 GPIO_SPEED_FREQ_LOW 时 MODE8 = 0x2、推挽时 CNF8 = 0x0,故低 4 位应为 0x2。仓库里同文件还用同样套路断言了 USART1_CR1 的 UE 位(@0x4001380C,bit13 = 1)。

② GPIO / LED 状态断言(本仓库实测用例节选)

Renode 把 LED 当成标准外设模型,能直接断言某引脚电平、甚至"是否在按某个频率闪烁"。下面是从本仓库 tests/stm32f103rct6.robot 节选的「LED 周期闪烁」断言(同样用 ${REPL} / ${ELF} 变量)——固件每 200ms 翻转一次 PA8(周期 400ms),所以 onDuration / offDuration 都取 0.2s:

LED 应周期闪烁(PA8 每 200ms 翻转)
    Execute Command         mach create
    Execute Command         machine LoadPlatformDescription @${REPL}
    Execute Command         sysbus LoadELF @${ELF}
    Create LED Tester       sysbus.gpioPortA.led
    Create Terminal Tester  sysbus.usart1
    Start Emulation
    Wait For Line On Uart   Hello from Renode
    Assert Led Is Blinking  testDuration=3  onDuration=0.2  offDuration=0.2  tolerance=0.4

Create LED Tester sysbus.gpioPortA.led 里的 led 是我们在 renode/stm32f103rct6.repl 里把 PA8 接上的 Miscellaneous.LED 外设;接好后 Assert Led Is Blinking 就能验证闪烁节奏,Assert Led State 则断言某一刻的确定电平。

③ 中断 / 定时器测试(本仓库实测用例节选)

中断是最容易"真机偶发、仿真必现"的地方。本仓库固件用 CMSIS 寄存器直接配好 TIM2(溢出中断约每 1s 触发一次),在 TIM2_IRQHandler 里翻转 PB0;Renode 把 PB0 接上一个 Miscellaneous.LED,测试就能断言"中断确实在周期性执行":

TIM2 中断应驱动 PB0 周期闪烁
    Execute Command         mach create
    Execute Command         machine LoadPlatformDescription @${REPL}
    Execute Command         sysbus LoadELF @${ELF}
    Create LED Tester       sysbus.gpioPortB.led2
    Create Terminal Tester  sysbus.usart1
    Start Emulation
    Wait For Line On Uart   Hello from Renode
    # ③ 中断:TIM2 溢出中断每 ~1s 翻转一次 PB0(ISR 内执行),周期约 2s
    Assert Led Is Blinking  testDuration=5  onDuration=1.0  offDuration=1.0  tolerance=0.5

sysbus.gpioPortB.led2led2 是我们在 renode/stm32f103rct6.repl 里把 PB0 接上的 LED;Assert Led Is Blinking 会量一段时间内的高/低电平,确认引脚真的在按预期节奏翻转——这比在真机上拿示波器抓中断靠谱得多。要点:本工程 HAL 包没有带 stm32f1xx_hal_tim.c,所以固件没走 HAL_TIM,而是直接操作 TIM2 寄存器、自己写 TIM2_IRQHandler 清中断标志并翻转引脚。

④ 网络收发包测试(通用模板,本仓库未实现)

跑 LwIP、想验证固件真的发出了某个包?Renode 提供网络层断言关键词:

*** Test Cases ***
固件应向网关发出 DHCP 请求
    Start Emulation
    Wait For Outgoing Packet

Wait For Outgoing Packet 会拦住仿真发出的下一帧网络包,配合字节匹配就能断言"发出的是不是 DHCP、目的 MAC 对不对"——多节点组网、协议握手这类场景全都能这么验。

为什么本仓库不提供实测用例:这一例依赖芯片带以太网(ETH)外设。STM32F103 没有 ETH(基础 stm32f103.repl 里也没有 SPI,接不了外部以太网芯片),所以在这块板子上跑不了网络仿真。Renode 的网络能力要芯片自带 ETH 才行——带 ETH 的 STM32(如 F4/F7/H7,Renode 已有现成模型、还能配 host tap 与宿主机组网)才能这么测。因此 ④ 在本仓库只作为通用模板保留,不提供实测用例;你若在 F4/F7/H7 上做,把符号名和地址一换即可。

⑤ Flash 写读一致测试(本仓库实测用例节选)

固件把配置写进 Flash,想确认"写进去了、读出来还是它"——这类"持久化"验证在真机上要反复烧写,Renode 里直接读内存地址断言即可。本仓库固件上电时向一个空闲 flash 页 0x08008000 写入已知 pattern 0xDEADBEEF,测试读回断言一致:

Flash 应写入已知 pattern 且读回一致
    Execute Command         mach create
    Execute Command         machine LoadPlatformDescription @${REPL}
    Execute Command         sysbus LoadELF @${ELF}
    Create Terminal Tester  sysbus.usart1
    Start Emulation
    Wait For Line On Uart   Hello from Renode
    # ⑤ Flash:固件上电向空闲 flash 页 0x08008000 写入 0xDEADBEEF(内存级写读)
    ${val}=                 Execute Command  sysbus ReadDoubleWord 0x08008000
    ${val}=                 Strip String    ${val}
    ${val}=                 Evaluate         int("${val}", 16)
    Should Be Equal As Integers  ${val}  3735928559

诚实说明边界:本仓库测的是 flash 内存写读一致(Renode 把 STM32 内部 Flash 建模为可写内存,直接指针写即可持久化),能证明"固件写 flash 的代码路径写对了地址、数据没丢"。它没有覆盖 STM32 FLASH 控制器的擦除/编程时序(HAL_FLASH_Program / HAL_FLASHEx_Erase 操作的 0x40022000 寄存器,基础 stm32f103.repl 未建模)。要测完整 HAL 擦写流程,需要给 repl 手动挂一个 FLASH 控制器模型——那是另一个话题。

这五类基本覆盖了嵌入式固件测试最常见的面:寄存器、GPIO、中断、网络、持久化。你会发现套路完全一致——拉起仿真、触发固件动作、用 Renode 关键词断言可观测的结果。难点从来不是"怎么写",而是"你这段固件到底该有什么可观测的行为",想清楚这个,测试就水到渠成。

renode_test_report.png Renode 1.16.1 实测报告:6 tests total, 6 passed, 0 failed, 0 skipped,用时约 26 秒

上图是在 Renode 1.16.1 上直接跑 renode-test renode/tests/stm32f103rct6.robot 得到的实测报告。6 个用例全部 PASS,既包含前面 ①~③、⑤ 的寄存器 / LED / 中断 / Flash 断言,也包含"固件启动串口打印"和"USART1 使能"这两个基础断言。你本机 clone 仓库后按 README 编译,也能跑出一样的全绿结果。

本文配套仓库 renode/ 目录给了可直接 renode-test 跑的完整脚手架(.repl 平台描述 + .resc 一键起仿真 + tests/*.robot 自动化用例 + README.md 用法),并已在 Renode 1.16.1 上实测通过——你 clone 下来编译固件、一条命令就能复现上面 ①~③、⑤ 的断言(共六项实测用例)。

3)为什么 CI 里它能复现

前面提的"确定性执行 + 共享虚拟时间"在这儿落地:因为没有真实硬件的随机性(温度、电源噪声、中断竞态的电气抖动),同一份测试在任何人机器上、任何一次跑,结果都一样。这正是自动化测试的命根子——可复现,才敢进流水线

4)它还能接什么

顺带一提能力边界之外的延伸:Renode 能接 GDB 做源码级调试;能采集执行指标(指令数、内存/外设访问、中断次数),在 Jupyter 里画出来看性能趋势;还能把 Verilator 仿出来的个别外设模型挂进来,补足某些需要更底层精度的场景。这些都不是入门必会,但知道有,关键时刻能救命。

五、写在最后

聊回咱们嵌入式人最实在的事:能把"会写固件"变成"能交付可靠固件",中间差的就是工程化能力。Renode 这类工具,本质是把软件行业沿用多年的"自动化测试 + CI"这套成熟打法,搬到了嵌入式现场。对想做第二职业、想接活、想让自己和"只会点灯"拉开档次的人来说,这一关迟早要过。

板子不够、硬件没到、偶发 bug 抓狂——下次别光骂硬件了,试试在 PC 上先把它仿真测一遍。

公众号关注:“ 搏哥聊技术 ” 让咱们一起把嵌入式活儿干得更工程化、更靠谱。

配套代码仓库(本文 Demo 全部开源)

  • Gitee 仓库:gitee.com/jameschenbo…
  • 克隆命令:git clone https://gitee.com/jameschenbo/renode_stm32f103rct6.git

仓库内含 STM32F103RCT6 的 CubeMX 固件工程(Core / Drivers / 启动文件 / 链接脚本 / CMake 构建)、renode/ 仿真与 Robot 测试脚手架(.repl 平台描述 + .resc 一键起仿真 + tests/*.robot 自动化用例 + README.md)。克隆后按 README 编译固件,执行 renode-test renode/tests/stm32f103rct6.robot 即可复现文中「串口打印 / LED 闪烁 / GPIO 配置 / USART1 使能 / TIM2 中断驱动 PB0 闪烁 / Flash 写读一致」六项实测用例(Renode 1.16.1 验证通过)。所有代码、仿真描述与测试脚本均开源,无任何隐藏步骤。若 Windows 上 renode-testUnable to create process,是 Python launcher(py -3)环境所致、非仓库问题,按 renode/README.md §7.1「备选命令」用 tests/run_tests.py 即可跑通,同样无隐藏步骤。

参考资源

更多实战内容关注公众号:搏哥聊技术