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到操作系统,主要有三条路径。我按实际项目中遇到的频率排序:

  1. 通过UEFI配置表(最常用)—— UEFI规范定义了一个标准GUID,设备树blob通过这个GUID注册到配置表中。操作系统启动时,直接从配置表里取。
  2. 通过Boot Parameter结构—— 有些BootLoader会在启动参数里传递设备树地址,UEFI也可以这么做。
  3. 通过文件系统加载—— 操作系统内核自己从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启动流程中的位置 SEC/PEI阶段 硬件初始化 DXE/BDS阶段 设备树加载与准备 ★ 设备树在此登场 TSL阶段 OS读取设备树 ExitBootServices() 设备树传递路径 路径1:UEFI配置表 OS内核获取 路径2:Boot Parameter 启动参数传递 路径3:文件系统加载 EFI分区读取 ACPI vs 设备树抉择 ACPI x86/ARM服务器 · 硬件固定 设备树 ARM嵌入式/RISC-V · 灵活 UEFI可同时支持ACPI和设备树,最终由操作系统选择

本章小结:设备树在UEFI启动流程中,于DXE阶段登场,通过配置表传递给操作系统。选择ACPI还是设备树,取决于平台类型和硬件变化频率。作为固件工程师,我的建议是:能上ACPI就上ACPI,设备树留给那些硬件还没定型的场景。