4、设备树在UEFI启动流程中的位置
这一章,我们来聊聊设备树在UEFI启动流程中到底扮演什么角色。说白了,就是搞清楚它什么时候出场、怎么把接力棒交给操作系统,以及什么时候该用ACPI而不是设备树。
我个人习惯把UEFI启动流程想象成一场接力赛。硬件初始化是第一棒,UEFI固件是第二棒,操作系统内核是第三棒。设备树,就是第二棒和第三棒之间传递的那根接力棒。
4.1 固件启动阶段:设备树何时登场?
UEFI启动大致分几个阶段:SEC(安全验证)、PEI(EFI前期初始化)、DXE(驱动执行环境)、BDS(启动设备选择),最后是TSL(操作系统加载前期)。
设备树真正登场,是在DXE阶段后期到BDS阶段。为什么不是更早?因为PEI阶段内存都还没完全初始化好,你拿什么去解析设备树?
关键节点:设备树在UEFI中通常以FDT(Flattened Device Tree)形式存在,要么打包在固件镜像里,要么从存储介质加载。
我在项目中遇到过一种情况:某ARM服务器平台,UEFI固件在DXE阶段加载设备树,然后根据设备树内容去初始化外设。结果设备树里一个GPIO控制器的reg属性写错了,导致整个PCIe总线枚举失败。嗯,这种坑踩过一次就记住了。
设备树在固件中的典型加载路径:
// 伪代码:UEFI中加载设备树的典型流程
EFI_STATUS LoadDeviceTree(OUT VOID **DtBlob, OUT UINTN *DtSize)
{
// 1. 从固件配置表查找设备树
EFI_CONFIGURATION_TABLE *ConfigTable = gST->ConfigurationTable;
for (UINTN i = 0; i < gST->NumberOfTableEntries; i++) {
if (CompareGuid(&ConfigTable[i].VendorGuid, &gFdtTableGuid)) {
*DtBlob = ConfigTable[i].VendorTable;
*DtSize = fdt_totalsize(*DtBlob);
return EFI_SUCCESS;
}
}
// 2. 如果没找到,从固件文件系统加载
return LoadDtFromFirmwareVolume(DtBlob, DtSize);
}
小技巧:我建议在固件中保留两份设备树——一份是出厂默认的,另一份是给用户自定义的。这样即使更新出错,还能回滚。
4.2 设备树传递到OS的路径
设备树从UEFI到操作系统,主要有三条路径。我按实际项目中遇到的频率排序:
- 通过UEFI配置表(最常用)—— UEFI规范定义了一个标准GUID,设备树blob通过这个GUID注册到配置表中。操作系统启动时,直接从配置表里取。
- 通过Boot Parameter结构—— 有些BootLoader会在启动参数里传递设备树地址,UEFI也可以这么做。
- 通过文件系统加载—— 操作系统内核自己从EFI系统分区读取设备树文件。
你想想看,为什么第一条路径最常用?因为配置表是UEFI规范定义的标准接口,所有兼容UEFI的操作系统都知道怎么找。我在调试一个64位ARM系统时,就遇到过操作系统从配置表里拿到的设备树指针是错的,结果内核直接panic。后来发现是固件里注册配置表的时机太早了,内存重映射后指针失效了。
注意:设备树指针在ExitBootServices()之后必须仍然有效。因为操作系统拿到设备树时,UEFI运行时服务已经退出了。如果设备树放在UEFI分配的内存里,这块内存不能被回收。
传递路径的示意图:
UEFI固件空间
│
├── DXE阶段:创建设备树blob
│ └── 注册到EFI配置表 (gFdtTableGuid)
│
├── BDS阶段:可能修改设备树(添加chosen节点等)
│
└── ExitBootServices() 前
└── 操作系统读取配置表
└── 获取设备树地址
└── 内核解耦设备树
4.3 ACPI与设备树的抉择
这个问题,说白了就是:什么时候用ACPI,什么时候用设备树?
我个人经验是这么看的:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| x86平台 | ACPI | x86生态成熟,ACPI是标配,设备树支持不完善 |
| ARM服务器 | ACPI | ARM服务器规范要求ACPI,用于标准化硬件描述 |
| ARM嵌入式 | 设备树 | 硬件变化快,设备树灵活,修改成本低 |
| RISC-V平台 | 设备树 | 生态初期,设备树更简单,ACPI支持还在完善 |
| 混合架构 | 两者都支持 | UEFI可以同时提供ACPI表和设备树,让OS选择 |
我曾经在一个ARM服务器项目上,客户坚持要用设备树,说ACPI太复杂。结果硬件升级后,设备树要改几十个节点,ACPI只需要改几个表。嗯,从那以后我学乖了——服务器场景,老老实实用ACPI。
核心原则:ACPI适合硬件固定的平台,设备树适合硬件变化频繁的平台。UEFI可以同时支持两者,但最终选择权在操作系统。
为什么会有这种差异?ACPI的设计思路是「操作系统不需要知道硬件细节,ACPI表告诉它怎么操作就行」。设备树则是「我把硬件长什么样告诉你,你自己看着办」。两种哲学,没有绝对的好坏。
在实际的UEFI固件中,我见过三种处理方式:
- 纯ACPI:只生成ACPI表,不提供设备树。适合x86和ARM服务器。
- 纯设备树:只提供设备树,不生成ACPI。适合嵌入式设备。
- 双模式:同时生成ACPI表和设备树,通过启动配置或硬件 strap 选择。适合需要兼容多种OS的平台。
避坑指南:我曾经在双模式平台上遇到一个问题——ACPI表和设备树描述的硬件资源不一致,导致某个PCIe设备在Linux下工作正常,在Windows下蓝屏。后来统一了资源描述才解决。所以,如果你做双模式,一定要保证两种描述完全一致。
最后说一句,设备树在UEFI中的地位,其实有点像「备胎」。ACPI是正室,设备树是那个随时准备上场的替补。但在嵌入式领域,这个「备胎」反而是主角。做固件开发,两个都得会,因为你永远不知道下一个项目会遇到什么平台。
本章小结:设备树在UEFI启动流程中,于DXE阶段登场,通过配置表传递给操作系统。选择ACPI还是设备树,取决于平台类型和硬件变化频率。作为固件工程师,我的建议是:能上ACPI就上ACPI,设备树留给那些硬件还没定型的场景。