4. UEFI中的设备树支持
在 UEFI 固件环境中,设备树(Device Tree,DT)并不是天然存在的启动描述方式。传统 PC 固件以 ACPI 为主,而嵌入式与 ARM/RISC-V 平台则大量依赖设备树。为了让 UEFI 同时服务两类生态,UEFI 规范定义了 EFI_DT_PROTOCOL 与 EFI_DT_FIXUP_PROTOCOL,分别解决“设备树从哪里来、如何被读取”和“设备树在交给操作系统前如何被修正”的问题。理解这两个协议以及它们与 ACPI 的共存策略,是设备树适配工程的核心基础。
一、核心原理:两个协议的分工
EFI_DT_PROTOCOL 是设备树的“访问入口”。它通常由平台固件在 DXE 阶段安装,向其他驱动暴露一个已加载到内存中的设备树 Blob(Flattened Device Tree,FDT)。其关键接口是 GetDeviceTree(),调用者可以获取 FDT 的基地址与长度,从而读取节点、属性、compatible、reg、interrupts 等信息。它回答的是“设备树在哪里”。
EFI_DT_FIXUP_PROTOCOL 是设备树的“修正通道”。它提供 Fixup() 接口,允许在启动操作系统前对 FDT 做受控修改,例如:填充运行时内存节点、修正 /chosen 下的 bootargs、写入 linux,initrd-start/end、调整 /memory 节点、注入 MAC 地址或序列号等。它回答的是“设备树在交付前需要改什么”。
二、设备树传递机制:从固件到操作系统
设备树的传递不是简单地把一个地址写进寄存器,而是一条有顺序的链路。典型流程如下:
- 平台固件从 Flash、FVIP 或内嵌数组加载原始 DTB,校验魔数
0xd00dfeed与版本。 - 在 DXE 阶段安装
EFI_DT_PROTOCOL,使其他驱动可定位并读取该 FDT。 - 启动 OS 前,引导程序或固件调用
EFI_DT_FIXUP_PROTOCOL.Fixup(),按需修正节点。 - 将修正后的 FDT 地址写入启动参数,例如通过
EFI_DT_FIXUP_PROTOCOL的Fixup结果或EFI_CONFIGURATION_TABLE中的EFI_DTB_TABLE_GUID暴露给 OS。 - OS 内核解析 FDT,完成设备枚举与驱动匹配。
其中,EFI_DTB_TABLE_GUID 是 UEFI 与 OS 之间传递设备树的重要约定。Linux 内核在 EFI 启动路径中会检查该配置表项,若存在则将其作为设备树来源之一。这样,设备树无需依赖 bootloader 手工传参,固件即可完成交付。
三、ACPI 共存策略:不是二选一,而是可配置
在真实产品中,同一颗 SoC 可能既需要支持 Windows(ACPI)又需要支持 Linux(DT)。因此固件往往同时准备 ACPI 表与设备树,并通过策略决定 OS 看到什么。常见做法有四类:
| 策略 | 固件行为 | 适用场景 | 风险点 |
|---|---|---|---|
| DT 优先 | 不发布 ACPI 表,仅暴露 FDT | 纯 Linux/Android 嵌入式 | Windows 无法启动 |
| ACPI 优先 | 发布完整 ACPI,DT 仅作补充 | PC/服务器形态 | DT 节点可能被忽略 |
| 双描述符并存 | 同时发布 ACPI 与 FDT | 多 OS 兼容平台 | 设备重复枚举、资源冲突 |
| 运行时选择 | 依据启动项或 /chosen 决定 | 可配置产品 | 策略逻辑复杂,需测试覆盖 |
其中“双描述符并存”最容易出问题。若 ACPI 与 DT 同时描述同一个 I2C 控制器,Linux 可能先由 ACPI 枚举出设备,又由 DT 枚举一次,导致驱动重复 probe 或资源申请失败。工程上通常通过 status = "disabled" 关闭 DT 中与 ACPI 重叠的节点,或在 ACPI 侧使用 _STA 返回 0 来屏蔽。
四、实操案例:修正 /chosen 并传递启动参数
下面是一段典型的 DXE 驱动代码,演示如何定位 FIXUP 协议并修正 /chosen 节点。实际工程中需先 LocateProtocol,再构造 FDT 修改缓冲区。
EFI_STATUS Status;
EFI_DT_FIXUP_PROTOCOL *DtFixup;
EFI_DT_PROTOCOL *DtProtocol;
Status = gBS->LocateProtocol(&gEfiDtFixupProtocolGuid,
NULL, (VOID **)&DtFixup);
if (EFI_ERROR(Status)) {
DEBUG((EFI_DBG, "DT FIXUP protocol not found\n"));
return Status;
}
Status = gBS->LocateProtocol(&gEfiDtProtocolGuid,
NULL, (VOID **)&DtProtocol);
if (!EFI_ERROR(Status)) {
Status = DtProtocol->GetDeviceTree(DtProtocol,
&FdtBase, &FdtSize);
DEBUG((EFI_DBG, "FDT base=%p size=0x%x\n",
FdtBase, FdtSize));
}
/* 修正 /chosen/bootargs 与 initrd 地址 */
Status = DtFixup->Fixup(DtFixup,
EFI_DT_FIXUP_FLAGS_ALL,
&FdtBase, &FdtSize);
Fixup() 时要注意:FDT 可能被重新分配到更大的缓冲区,因此必须使用返回后的 FdtBase 与 FdtSize,不能继续使用旧地址。
五、误区与复盘
- 误区一:认为 DT 与 ACPI 只能二选一。实际上 UEFI 允许两者并存,关键是避免同一设备被重复描述。
- 误区二:在 Fixup 中直接原地修改 FDT。若新属性导致节点膨胀,原缓冲区可能越界,必须使用协议返回的新缓冲区。
- 误区三:忽略
EFI_DTB_TABLE_GUID。只把 FDT 地址写进自定义变量,OS 可能根本读不到。 - 误区四:ACPI 与 DT 的
reg/_CRS地址不一致。这会造成驱动拿到错误基址,表现为“设备存在但访问异常”。
EFI_DTB_TABLE_GUID 是否已安装、Fixup 后 FDT 是否被重新分配。这三项覆盖了大多数“设备树传不过去”的问题。
总结来看,EFI_DT_PROTOCOL 提供设备树的读取入口,EFI_DT_FIXUP_PROTOCOL 提供交付前的修正能力,二者配合 EFI_DTB_TABLE_GUID 完成从固件到 OS 的传递。ACPI 共存不是简单的取舍,而是通过节点使能、_STA 控制和启动策略实现的可配置设计。掌握这条链路,才能在 UEFI 环境下稳定地完成设备树适配。