ARTICLE · 1015967
我把 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 层测的是调度决策:该不该换、换谁。
于是验证体系自然分成三层,各司其职:
cargo test | ||
数字说话
写稿时仓库的状态是:host 回归实测 80/80 全绿(2026-09-02,rustc 1.97 复现)。
你也能复现(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 内核》(出版中)。下篇预告:任务切换到底切了什么?一次"偷梁换柱"看懂上下文切换。