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 地址或序列号等。它回答的是“设备树在交付前需要改什么”。

两者关系可以概括为:EFI_DT_PROTOCOL 负责“读”,EFI_DT_FIXUP_PROTOCOL 负责“改”。读取是修正的前提,修正是传递前的最后一道加工。

二、设备树传递机制:从固件到操作系统

设备树的传递不是简单地把一个地址写进寄存器,而是一条有顺序的链路。典型流程如下:

  1. 平台固件从 Flash、FVIP 或内嵌数组加载原始 DTB,校验魔数 0xd00dfeed 与版本。
  2. 在 DXE 阶段安装 EFI_DT_PROTOCOL,使其他驱动可定位并读取该 FDT。
  3. 启动 OS 前,引导程序或固件调用 EFI_DT_FIXUP_PROTOCOL.Fixup(),按需修正节点。
  4. 将修正后的 FDT 地址写入启动参数,例如通过 EFI_DT_FIXUP_PROTOCOL 的 Fixup 结果或 EFI_CONFIGURATION_TABLE 中的 EFI_DTB_TABLE_GUID 暴露给 OS。
  5. OS 内核解析 FDT,完成设备枚举与驱动匹配。

其中,EFI_DTB_TABLE_GUID 是 UEFI 与 OS 之间传递设备树的重要约定。Linux 内核在 EFI 启动路径中会检查该配置表项,若存在则将其作为设备树来源之一。这样,设备树无需依赖 bootloader 手工传参,固件即可完成交付。

UEFI 设备树传递链路与 ACPI 共存策略 固件设备树来源 Flash / FVIP / 内嵌 DTB 魔数 0xd00dfeed 校验 EFI_DT_PROTOCOL GetDeviceTree() 读取 FDT 基址与长度 EFI_DT_FIXUP_PROTOCOL Fixup() 修正 /chosen、/memory 等 典型修正项 bootargs / initrd-start/end /memory 节点容量 MAC、序列号、运行时内存 status = "okay" 使能节点 EFI_DTB_TABLE_GUID 配置表暴露 FDT 地址 OS 启动时读取 Linux 内核(DT 路径) 解析 FDT → 设备枚举 → 驱动匹配 compatible / reg / interrupts 不依赖 ACPI 命名空间 Linux 内核(ACPI 路径) 解析 ACPI 表 → 设备枚举 → 驱动匹配 DSDT / SSDT / _HID / _CRS 不依赖 FDT 节点 ACPI 共存策略 ① DT 优先,ACPI 关闭 ② ACPI 优先,DT 仅补充 ③ 双描述符并存,按设备选 ④ 通过 /chosen 传递选择

三、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 地址不一致。这会造成驱动拿到错误基址,表现为“设备存在但访问异常”。
排障时优先确认三件事:FDT 魔数是否正确、EFI_DTB_TABLE_GUID 是否已安装、Fixup 后 FDT 是否被重新分配。这三项覆盖了大多数“设备树传不过去”的问题。

总结来看,EFI_DT_PROTOCOL 提供设备树的读取入口,EFI_DT_FIXUP_PROTOCOL 提供交付前的修正能力,二者配合 EFI_DTB_TABLE_GUID 完成从固件到 OS 的传递。ACPI 共存不是简单的取舍,而是通过节点使能、_STA 控制和启动策略实现的可配置设计。掌握这条链路,才能在 UEFI 环境下稳定地完成设备树适配。