夜雨聆风学习资料网

ARTICLE · 1015967

我把 no_std 嵌入式内核搬上了电脑:80 个测试全绿,不用开发板

我把 no_std 嵌入式内核搬上了电脑:80 个测试全绿,不用开发板

嵌入式开发最常见的借口是什么?——"这没法单测,得上板子。"

我为自己写的 Rust 多任务内核做了 80 个单元测试,全部跑在普通电脑上:cargo test 一敲,调度器、信号量、内存分配器、软件定时器全部验证,不需要任何开发板。怎么做到的?核心一句话:

给 no_std 内核造一个"host 版移植层",把碰硬件的边界收窄到一层接口上,剩下的全是纯逻辑——而纯逻辑是可以被测试的。

为什么内核"看起来"没法测

内核代码直接面对三样东西:关中断、读时钟、换栈。直觉上它们都离不开硬件,于是结论顺理成章:"上板子吧。"

但仔细想:内核里真正容易写错、真正值得反复回归的是什么?是决策——调度器该选哪个任务、信号量该唤醒谁、定时器到没到点、队列满没满。这些决策不碰任何寄存器,它们只是数据结构和状态机的组合。碰硬件的动作(关中断、换栈)反而就那么几个,且高度重复。

所以解法不是"把测试搬上板子",而是反过来:把内核搬下板子。

解法:一个 trait,两副"身体"

我的内核(Xtask,Rust 写的可移植多任务内核)把所有硬件依赖收进一个移植层接口:临界区、开/关中断、读 systick、保存任务现场……内核的其余部分只认这个接口,完全不认识具体芯片。

于是:

  • 真机上
    ,接口由芯片实现(RISC-V 的 GD32VF103、Cortex-M 的 STM32 系……)
  • 电脑上
    ,接口由一个 "HostPorting" 实现——它假装自己是单片机

同一个内核,两副身体。测试时链接 host 版,跑的就是真内核,不是 mock。

host 移植层要"假装"什么

1. 假装"关中断"。 真机上关中断是保证临界区互斥的手段;host 上没有中断,就用测试线程 + 锁模拟同样的互斥语义——测的是"临界区里的逻辑对不对",不是"关中断这个动作灵不灵"。

2. 假装时间。 内核要读 systick 才能做延时、超时、时间片。host 版用一个可以手动拨动的假时钟:测试里说"现在过了 5 个 tick",内核就以为过了 5 个 tick。这带来一个真机上做不到的能力——时间旅行。测试可以精确地把时钟拨到"刚好超时前一 tick",验证边界行为。

3. 假装换栈。 真机上上下文切换靠汇编把栈指针掉包;host 测试里不真切栈——因为"换栈动作对不对"由 QEMU 执行级验证负责(这是另一篇文章的题)。host 层测的是调度决策:该不该换、换谁。

于是验证体系自然分成三层,各司其职:

层
回答的问题
手段
host 单测
决策对不对
cargo test
,80 项
QEMU 执行级
换栈/中断动作对不对
24 项内核自测全绿自退出
真机
时序与硬件对不对
开发板(GD32VF103 已验)

数字说话

写稿时仓库的状态是:host 回归实测 80/80 全绿(2026-09-02,rustc 1.97 复现)。⚠️ 仓库 README 标注 148/148,计数口径已提 issue 澄清(见文末注)——若口径澄清后数字有变,以作者更新为准。关键是这些测试不是摆设——每个"踩坑"回归都做过阳性对照:先把 bug 喂回去确认它变红,再修复确认变绿。一个永远绿的测试可能是假的;一个先红后绿的测试才是真的。

你也能复现(3 步)

git clone https://github.com/gqf2008/Xtask && cd Xtask # 注意:仓库 .cargo/config.toml 默认 thumbv7em target(见 issue #12), # 干净环境请显式指定 host: cargo test --lib --target x86_64-pc-windows-msvc # 【实测后填入输出:实测 80 passed; 0 failed】

不需要开发板,不需要 QEMU,只需要 Rust 工具链。

抛砖引玉

这个方法不限于内核。给任何嵌入式项目划一条"硬件边界",把边界内的纯逻辑(状态机、协议解析、算法)全部搬上 host——你会发现原来"没法测"的东西,大部分只是"懒得抽接口"。


内核开源:github.com/gqf2008/Xtask(QEMU 免板可跑,欢迎 star)。完整过程——从任务结构体到 SMP、无 PendSV 架构移植——写进了我的新书《用 Rust 手写 RTOS 内核》(出版中)。下篇预告:任务切换到底切了什么?一次"偷梁换柱"看懂上下文切换。

    相关学习资料