2. UEFI基础回顾

在进入设备树适配的工程实践之前,有必要把 UEFI 的启动骨架重新梳理一遍。设备树适配并不是孤立地“往固件里塞一份 DTB”,它深度依赖 UEFI 的启动阶段划分、Protocol 服务发现机制以及驱动模型。理解这些基础,才能判断“设备树应该在哪个阶段被解析”“由谁把设备树信息转换成 UEFI 设备”“驱动如何拿到设备树节点”,从而避免把适配工作做成一次性的硬编码。

一、UEFI 启动流程的六个阶段

UEFI 规范把平台初始化到操作系统加载的过程划分为若干阶段,工程上通常归纳为 SEC、PEI、DXE、BDS、TSL、RT 六个阶段。每个阶段承担不同的职责,也决定了设备树信息在何时可用。

  • SEC(Security):上电后最先执行,建立临时内存(CAR)、处理复位向量,为 PEI 准备执行环境。
  • PEI(Pre-EFI Initialization):完成早期内存初始化、芯片组基础配置,产出 HOB(Hand-Off Block)向 DXE 传递信息。
  • DXE(Driver Execution Environment):UEFI 的核心阶段,加载并调度大量 DXE 驱动,构建 Protocol 数据库,形成完整的 Boot Services。
  • BDS(Boot Device Selection):根据启动策略选择启动项,加载并执行 OS Loader 或 UEFI Shell。
  • TSL(Transient System Load):OS Loader 运行阶段,仍可使用 Boot Services 与 Runtime Services。
  • RT(Runtime):调用 ExitBootServices 后进入,仅保留 Runtime Services,OS 接管硬件。

对设备树适配而言,关键窗口在 DXE 与 BDS 之间:设备树通常由固件从 Flash、变量或内嵌镜像中取出,在 DXE 阶段被解析并转化为 UEFI 设备与 Protocol,最终在 BDS 阶段传递给 OS Loader。

二、DXE 阶段:设备树适配的主战场

DXE 阶段由 DXE Core、DXE Dispatcher 和大量 DXE Driver 组成。DXE Core 负责维护 Protocol 数据库、句柄数据库和事件机制;DXE Dispatcher 按照依赖关系(通过 DEPEX 描述)决定驱动加载顺序。设备树适配驱动本质上就是一个 DXE Driver,它的典型工作流如下:

  1. 在入口点注册一个或多个 Protocol(如自定义的 EFI_DEVICE_TREE_PROTOCOL)。
  2. 通过 DEPEX 声明依赖,例如依赖 gEfiDevicePathProtocolGuid 或存储访问 Protocol。
  3. 从存储介质或配置变量中读取 DTB 二进制,校验头部魔数 0xd00dfeed。
  4. 解析 FDT 结构(header、memory reservation、structure block、strings block)。
  5. 遍历节点,为每个 status = "okay" 的设备创建句柄并安装 DevicePath、自定义 Protocol。
  6. 将设备树信息以 HOB 或配置表形式传递给后续驱动与 OS Loader。

DXE 阶段的一个工程要点是“顺序敏感”。如果设备树解析驱动早于存储驱动加载,就读不到 DTB;如果晚于依赖它的设备驱动,设备驱动就拿不到节点信息。因此 DEPEX 的设计直接决定适配是否稳定。

三、Protocol 机制:设备树信息的“接口契约”

UEFI 用 Protocol 代替传统 BIOS 的中断调用和固定入口,Protocol 是一组函数指针加数据结构的集合,通过 GUID 唯一标识。驱动之间不直接调用,而是通过 LocateProtocol、HandleProtocol、OpenProtocol 在句柄数据库上查找并打开接口。

设备树适配中常见的 Protocol 使用方式包括:

  • 用 gBS->InstallMultipleProtocolInterfaces 把设备树节点对应的设备注册为句柄。
  • 用 gBS->LocateHandleBuffer 按 Protocol GUID 枚举所有设备节点。
  • 用 gBS->OpenProtocol 获取父设备的 DevicePath,拼接子节点路径。
  • 自定义 Protocol 暴露 GetNodeByPath、GetProperty 等函数,供其他驱动查询设备树属性。

这种机制带来的好处是解耦:设备树解析驱动不需要知道谁会消费这些信息,消费方也不需要知道 DTB 来自哪里,双方只依赖 Protocol 契约。

四、驱动模型与设备树适配的对应关系

UEFI 驱动模型(UEFI Driver Model)把驱动分为总线驱动、设备驱动和混合驱动。总线驱动负责枚举子设备并创建句柄,设备驱动通过 Supported() 和 Start() 绑定到具体句柄。设备树适配正好可以映射到这一模型:

UEFI 驱动模型角色 设备树适配中的对应物 关键动作
总线驱动 设备树解析驱动 / FDT Bus Driver 解析 DTB,为每个节点创建句柄并安装 Protocol
设备驱动 具体外设驱动(如 I2C、SPI、GPIO) 在 Supported() 中判断节点 compatible 是否匹配
DevicePath 节点路径(如 /soc/i2c@1c2a0000/touch@38) 作为句柄的唯一标识与父子关系载体
DEPEX 依赖设备树 Protocol 或存储 Protocol 保证解析顺序与消费顺序正确

下面这张图展示了从 DTB 到 UEFI 设备句柄、再到 OS 的完整信号与数据流,帮助建立整体位置感。

UEFI 启动阶段与设备树适配数据流 SEC PEI DXE BDS TSL RT DTB 来源 Flash / UEFI 变量 / 内嵌镜像 FDT 解析驱动(DXE Driver) 校验 0xd00dfeed 遍历节点 / 读取 compatible Protocol 数据库 / 句柄数据库 EFI_DEVICE_TREE_PROTOCOL DevicePath + 自定义节点 Protocol 读取 安装 设备驱动 Supported() / Start() I2C / SPI / GPIO OS Loader / 内核 通过配置表或 DevicePath 获取 DTB Linux 解析设备树并匹配驱动 LocateProtocol 传递 关键约束: 1. DTB 必须在 DXE 阶段可访问,且解析驱动 DEPEX 早于消费驱动; 2. 节点信息通过 Protocol 暴露,避免驱动之间直接耦合;3. BDS 阶段需把 DTB 或等价信息交给 OS Loader。

五、典型场景:I2C 触摸屏的设备树适配

假设 SoC 上有一路 I2C 控制器,挂载一颗触摸屏,设备树片段如下:

i2c@1c2a0000 {
    compatible = "vendor,i2c-v2";
    reg = <0x1c2a0000 0x1000>;
    status = "okay";

    touch@38 {
        compatible = "vendor,touch-ts";
        reg = <0x38>;
        interrupt-parent = <&gpio0>;
        interrupts = <12 2>;
    };
};

在 UEFI 侧,适配过程大致是:FDT 解析驱动遍历到 i2c@1c2a0000,创建 I2C 控制器句柄并安装 DevicePath;再遍历子节点 touch@38,创建触摸屏句柄,把 reg、interrupts 等属性以自定义 Protocol 暴露。I2C 设备驱动在 Supported() 中检查句柄是否具备 I2C 控制器 Protocol 且子节点 compatible 匹配,匹配成功后 Start() 完成初始化。整个过程没有硬编码地址,全部来自 DTB。

工程建议:把设备树解析驱动设计成“只解析、不绑定”,把设备绑定交给标准 UEFI 驱动模型,这样同一份 DTB 可以同时服务 UEFI 阶段和 OS 阶段,减少两套配置的维护成本。

六、常见误区与复盘

  • 误区一:在 PEI 阶段解析完整 DTB。PEI 阶段内存受限、服务能力弱,完整 FDT 解析应放在 DXE,PEI 只传递 DTB 地址或 HOB。
  • 误区二:忽略 DEPEX 导致顺序错乱。解析驱动若早于存储驱动,会读不到 DTB;消费驱动若早于解析驱动,会找不到 Protocol。
  • 误区三:把设备树信息写死在驱动里。这会让 DTB 形同虚设,失去“一份描述、多阶段复用”的价值。
  • 误区四:只关注 UEFI 阶段,忘记向 OS 传递。BDS 阶段需要通过配置表、DevicePath 或内存保留方式把 DTB 交给 OS Loader,否则内核拿不到设备树。
排障提示:若设备驱动始终不执行 Start(),优先检查三件事——句柄是否已创建、Protocol 是否已安装、Supported() 中的 compatible 字符串是否与 DTB 完全一致(大小写、连字符都敏感)。