08 — 等价性原则:为什么真机和 QEMU 用同一份设备树
本节你将学到
- 「变体设备树」的隐性成本:每一个 delta 都在稀释模拟验证的等价性
- 变体消灭的完整路径:唯一合法的差异是 U-Boot fixup 的等价物,由构建脚本对 dtb 副本注入
- 这个原则在实践中抓到的真 bug:一个误诊(reg_sd1_vmmc)和一个 dtsi 真 bug(irq-gpios 旧名)
- patch 生成基线的坑:untracked 文件不进 diff,intent-to-add 补上
变体的诱惑与代价
模拟器起步时,造一份「QEMU 变体设备树」几乎是本能:把模拟不了的节点禁掉、把差异写进去,先跑通再说。本项目也这么走过——变体 dts 一度承载着 MAC 地址、被禁的 regulator、一个 model 标记。但每一个 delta 都在回答一个不舒服的问题:在 QEMU 里验证通过的东西,到真机上还算数吗?模拟的意义就是等价性验证,变体每多一行,验证的价值就薄一分。
正确的不变量应该是:真机和 QEMU 用同一份设备树源,唯一允许的差异来自启动链本身——真机有 U-Boot 在 bootz 前做 fixup(往 dtb 里写 MAC 地址),QEMU 直启没有。那么这个差异的正确归宿是启动链侧:由构建脚本对编译出的 dtb 副本用 fdtput 注入 MAC,和 U-Boot 干的事一模一样。设备树归设备树,bootloader 的活归启动链。
消灭变体时还顺带抓出一个潜伏 bug:make-qemu-dtb.sh 里 dtc 完全干净的输出被 grep -v WARNING 滤空后,管道退出码被误判为编译失败——变体时代靠杂输出掩盖着,单一来源第一次编译就暴露了。
等价性原则抓出的两个真问题
变体消灭后,真机 dtsi 原样跑在 QEMU 里——这正是原则开始回报的时候。触摸链路调通时,GPIO1_IO9 上有一颗pin被 reg_sd1_vmmc(SD 卡电源使能)占用,goodix 的 INT 进不来。第一反应是在变体里禁掉这个 regulator(模拟的 SD 卡不需要电源开关)——但那是变体思维的回潮。真机上这个 regulator 存在且触摸能用,说明差异在别处:深挖下去发现 dtsi 里 gt9147 节点用的是 interrupt-gpios——4.9 内核时代的旧名,7.1 的 goodix 驱动找 irq-gpios 找不到,回退到 DT interrupts 属性,没人把那颗pin从输出态设回输入。真机同样中招,只是 22 章的章节 dts 早已单独换名规避了。修在共享 dtsi(三处同步),真机 base dtb 同样受益。
另一个方向:如果当时禁了 regulator 就"修好"了,这个 dtsi 真 bug 就被变体永久掩盖。等价性原则的价值恰恰在这:它逼着差异浮出水面,而不是让变体把问题扫进地毯下。
工程细节:patch 生成的基线
消灭变体重建 linux 补丁时踩了两个坑,记下来。第一,git diff 不含 untracked 文件——板级 dtsi 作为新文件从未被 commit,diff 里根本没有它;git add -N .(intent-to-add)之后再 diff 才是完整的 rolled-up。第二,diff 基线要钉上游 tag(v7.1),而不是 submodule 的 HEAD——HEAD 可能带着之前的补丁状态,基线一错 patch 内容就不完整。
单源之后的世界
消灭变体之后,设备树的验证语义变成一句话:QEMU 里过的就是真机树上过的。剩下的模拟/真机差异只有两类,都诚实可列:启动链(真机 U-Boot vs QEMU 直启)和外设模型深度(QEMU 缺的还是缺,但设备树层面完全一致)。CI 冒烟、外设模型回归、驱动章节的模拟练习,全部建立在同一份树上——这是整个模拟工程里最值钱的一条纪律。