3. 设备树基础
在 UEFI 环境下做设备树适配,第一步不是改代码,而是读懂并写对 DTS。设备树本质上是一份“硬件描述数据”,它把 SoC 内部总线、外设、引脚、时钟、中断等硬件拓扑,用可读的文本(DTS)表达出来,再由 DTC 编译成二进制 DTB,最终由固件或操作系统解析。理解 DTS 语法、节点与属性、地址编码和中断描述,是后续所有适配工作的地基。
一、核心原理:DTS、DTC 与 DTB 的关系
DTS(Device Tree Source)是源文件,DTC(Device Tree Compiler)是编译器,DTB(Device Tree Blob)是编译产物。三者关系可以概括为:DTS 描述硬件,DTC 翻译硬件,DTB 传递硬件。在 UEFI 阶段,固件通常需要把 DTB 作为配置表(Configuration Table)传递给操作系统,或者由 UEFI 自身解析 DTB 来初始化设备。
DTS 的语法结构非常简洁,核心只有三样东西:节点(node)、属性(property)和值(value)。节点用大括号包裹,属性用“键 = 值”表示,值可以是字符串、整数、数组、字节序列或空。节点可以嵌套,形成树状层级,这正是“设备树”名字的由来。
/dts-v1/;
/ {
model = "MyBoard UEFI Platform";
compatible = "vendor,myboard", "vendor,soc";
#address-cells = <2>;
#size-cells = <2>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a72";
reg = <0x0>;
};
};
uart0: serial@10000000 {
compatible = "snps,dw-apb-uart";
reg = <0x0 0x10000000 0x0 0x1000>;
interrupts = <0 32 4>;
clocks = <&clk_uart>;
status = "okay";
};
};
上面这段 DTS 已经包含了本节要讲的大部分要素:根节点、CPU 节点、UART 节点、reg 地址、interrupts 中断、clocks 时钟引用、status 状态。接下来逐一拆解。
二、节点与属性:设备树的“骨架”和“血肉”
节点是设备树的结构单元,每个节点代表一个硬件对象或逻辑分组。节点命名通常采用“节点名@地址”的形式,例如 serial@10000000,其中 serial 是节点名,10000000 是单元地址(unit-address),用于区分同一类设备的多个实例。节点名应尽量使用通用名称,如 cpu、memory、serial、i2c、spi、gpio 等。
属性是节点的“血肉”,用来描述节点的具体特征。常见属性包括:
compatible:最重要的属性,用于驱动匹配,格式为“厂商,型号”,可包含多个字符串,按优先级排列。reg:描述设备地址空间,格式由父节点的#address-cells和#size-cells决定。interrupts:描述中断信息,通常包含中断类型、中断号和触发方式。clocks、clock-names:描述时钟源和时钟名称。status:设备状态,okay表示启用,disabled表示禁用。#address-cells、#size-cells:定义子节点 reg 属性的地址和长度编码格式。
status 属性非常关键。很多设备在固件阶段需要保持 disabled,避免与 UEFI 驱动争抢资源;等操作系统启动后再由内核启用。三、地址编码:reg 与 #address-cells/#size-cells
地址编码是 DTS 中最容易出错的部分。一个节点的 reg 属性由若干组“地址 + 长度”组成,每组包含多少个 32 位单元,由父节点的 #address-cells 和 #size-cells 决定。例如父节点设置 #address-cells = <2>、#size-cells = <2>,那么子节点的 reg 就是 4 个 32 位整数:高 32 位地址、低 32 位地址、高 32 位长度、低 32 位长度。
如果父节点是 #address-cells = <1>、#size-cells = <1>,则 reg 只有两个整数:地址和长度。这种差异在 32 位和 64 位 SoC 之间非常常见,写错会导致地址解析完全偏移。
| 父节点设置 | reg 格式示例 | 含义 |
|---|---|---|
| #address-cells=<1> #size-cells=<1> | <0x10000000 0x1000> | 地址 0x10000000,长度 0x1000 |
| #address-cells=<2> #size-cells=<2> | <0x0 0x10000000 0x0 0x1000> | 64 位地址 0x10000000,64 位长度 0x1000 |
| #address-cells=<2> #size-cells=<1> | <0x0 0x10000000 0x1000> | 64 位地址,32 位长度 |
四、中断描述与常见绑定
中断描述依赖中断控制器节点。以 ARM GIC 为例,中断控制器节点通常包含 interrupt-controller 和 #interrupt-cells 属性。#interrupt-cells 决定子节点 interrupts 属性中每个中断需要几个单元。对于 GIC,常见格式是 <中断类型 中断号 触发方式>,其中中断类型 0 表示 SPI,1 表示 PPI;触发方式 4 表示高电平触发,1 表示上升沿触发。
intc: interrupt-controller@f9010000 {
compatible = "arm,gic-400";
reg = <0x0 0xf9010000 0x0 0x10000>,
<0x0 0xf9020000 0x0 0x20000>;
interrupt-controller;
#interrupt-cells = <3>;
};
uart0: serial@10000000 {
compatible = "snps,dw-apb-uart";
reg = <0x0 0x10000000 0x0 0x1000>;
interrupts = <0 32 4>;
interrupt-parent = <&intc>;
};
常见绑定还包括:gpio 绑定(gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>)、pinctrl 绑定(pinctrl-0 = <&uart0_pins>)、clock 绑定(clocks = <&clk_uart>)。这些绑定不是随意写的,而是由内核文档 Documentation/devicetree/bindings/ 定义的标准接口。UEFI 适配时,如果固件需要解析这些属性,必须遵循同样的绑定规则。
五、DTC 编译与实操流程
DTS 写完后,需要用 DTC 编译成 DTB。常用命令如下:
# 编译 DTS 为 DTB
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译 DTB 为 DTS,用于检查
dtc -I dtb -O dts -o myboard_check.dts myboard.dtb
# 查看 DTB 内容
fdtdump myboard.dtb
在 UEFI 工程中,DTB 通常被嵌入固件镜像,或者放在特定分区由固件加载。适配时建议先单独编译 DTB,用 fdtdump 确认节点和属性正确,再集成到 UEFI 构建流程中。
reg 的地址和长度写反、#address-cells 与父节点不一致、interrupts 单元数不符合 #interrupt-cells、compatible 字符串拼写错误。这些问题在编译阶段不一定报错,但会导致运行时设备无法识别或地址访问异常。六、案例复盘:UEFI 下 UART 设备树适配
假设某 ARM 平台在 UEFI 阶段需要初始化 UART 输出调试日志。DTS 中 UART 节点如下:
uart0: serial@10000000 {
compatible = "snps,dw-apb-uart";
reg = <0x0 0x10000000 0x0 0x1000>;
interrupts = <0 32 4>;
clocks = <&clk_uart>;
status = "okay";
};
适配步骤:第一,确认根节点 #address-cells=<2>、#size-cells=<2>,保证 reg 解析正确;第二,确认 compatible 与 UEFI 驱动匹配表一致;第三,确认 status 为 okay;第四,确认时钟和中断父节点已正确引用。若 UART 无输出,优先检查 reg 地址是否与 SoC 手册一致,再检查时钟是否使能。
通过这一节,你应该已经掌握:DTS 的基本语法、节点与属性的写法、地址编码规则、中断描述方式、常见绑定以及 DTC 编译流程。这些内容是后续在 UEFI 中解析和修改设备树的必备基础。