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,它的典型工作流如下:
- 在入口点注册一个或多个 Protocol(如自定义的
EFI_DEVICE_TREE_PROTOCOL)。 - 通过 DEPEX 声明依赖,例如依赖
gEfiDevicePathProtocolGuid或存储访问 Protocol。 - 从存储介质或配置变量中读取 DTB 二进制,校验头部魔数
0xd00dfeed。 - 解析 FDT 结构(header、memory reservation、structure block、strings block)。
- 遍历节点,为每个
status = "okay"的设备创建句柄并安装 DevicePath、自定义 Protocol。 - 将设备树信息以 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 的完整信号与数据流,帮助建立整体位置感。
五、典型场景: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。
六、常见误区与复盘
- 误区一:在 PEI 阶段解析完整 DTB。PEI 阶段内存受限、服务能力弱,完整 FDT 解析应放在 DXE,PEI 只传递 DTB 地址或 HOB。
- 误区二:忽略 DEPEX 导致顺序错乱。解析驱动若早于存储驱动,会读不到 DTB;消费驱动若早于解析驱动,会找不到 Protocol。
- 误区三:把设备树信息写死在驱动里。这会让 DTB 形同虚设,失去“一份描述、多阶段复用”的价值。
- 误区四:只关注 UEFI 阶段,忘记向 OS 传递。BDS 阶段需要通过配置表、DevicePath 或内存保留方式把 DTB 交给 OS Loader,否则内核拿不到设备树。
Start(),优先检查三件事——句柄是否已创建、Protocol 是否已安装、Supported() 中的 compatible 字符串是否与 DTB 完全一致(大小写、连字符都敏感)。