4. 设备树语法详解
设备树(Device Tree)在 Android 内核适配中承担着“硬件描述与内核解耦”的关键角色。它把 SoC 内部外设、板级连接、引脚复用、时钟与中断等硬件信息,从 C 代码中抽离为可读、可复用、可覆盖的文本结构。要真正掌握平台适配,必须先吃透 DTS 的语法骨架:节点如何定义、属性如何赋值、数据类型如何区分、标签如何引用、文件如何包含,以及条件编译如何按目标板裁剪配置。
一、节点定义:树形结构与命名规则
设备树本质上是一棵以根节点 / 为起点的树。每个节点用大括号包裹,节点名通常采用 名称@地址 的形式,其中地址部分称为 unit-address,用于区分同一总线上地址不同的同类设备。节点内部可以继续嵌套子节点,形成层级关系。
/ {
model = "My Android Board";
compatible = "vendor,board", "vendor,soc";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a55";
reg = <0x0>;
};
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <10>;
status = "okay";
};
};
节点名应体现设备类型,unit-address 必须与 reg 属性中的首地址一致,否则内核在解析时可能无法正确匹配。根节点下的 #address-cells 与 #size-cells 决定了子节点 reg 属性中地址和长度各占几个 32 位单元,这是初学者最容易忽略却影响全局的规则。
二、属性赋值与数据类型
属性是节点的“键值对”,值可以是空、字符串、32 位整数数组、字节序列或混合形式。DTS 编译器 dtc 会根据写法推断类型,因此必须严格区分。
| 写法 | 数据类型 | 典型用途 |
|---|---|---|
status = "okay"; |
字符串 | 设备使能/禁用 |
reg = <0x10000000 0x1000>; |
32 位整数数组 | 地址与长度 |
mac-address = [00 11 22 33 44 55]; |
字节序列 | MAC 地址等原始数据 |
clock-names = "core", "bus"; |
字符串列表 | 多时钟命名 |
interrupts = <0 10 4>; |
整数数组 | 中断号与触发类型 |
vendor,flag; |
空属性(布尔) | 开关式标记 |
需要特别注意:< > 内是 32 位单元,[ ] 内是字节,字符串必须加双引号。若把地址写成字符串,内核读取 reg 时会得到错误结果。自定义属性建议加厂商前缀,如 vendor,,避免与标准属性冲突。
三、标签引用与 phandle
标签(label)写在节点名之前,形如 uart0: serial@10000000。标签本身不进入最终 DTB 的节点名,但会被 dtc 记录,供其他节点通过 &uart0 引用。引用后生成 phandle,实现节点之间的关联,例如引脚控制器、时钟、电源域和中断控制器的连接。
&uart0 {
pinctrl-0 = <&uart0_pins>;
clocks = <&clk_uart0>;
status = "okay";
};
&i2c1 {
touchscreen@38 {
compatible = "vendor,ts";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <5 2>;
};
};
这种“先定义、后引用、再覆盖”的写法,是设备树支持多板级复用的核心机制。板级 DTS 可以只写差异部分,通过 &label 追加或修改属性,而不必复制整个 SoC 节点。
四、包含机制与条件编译
DTS 使用 #include 包含头文件,使用 /include/ 包含其他 DTS/DTI 文件。典型工程会分层组织:SoC 级 .dtsi 描述片内外设,板级 .dts 描述外设连接与使能,最终由 Makefile 编译为 DTB。
#include <dt-bindings/gpio/gpio.h>
#include "soc-vendor.dtsi"
#include "board-common.dtsi"
/ {
model = "Vendor Android Board";
compatible = "vendor,board", "vendor,soc";
};
&uart0 {
status = "okay";
};
条件编译则通过 C 预处理器宏实现,常用于同一份 DTS 适配多个硬件版本或 Android 变体:
#ifdef CONFIG_BOARD_REV_B
&uart0 {
status = "disabled";
};
#endif
#ifdef CONFIG_ANDROID_VENDOR_BOOT
&firmware {
android {
compatible = "android,firmware";
};
};
#endif
.dtsi,把“板级差异”放在 .dts,把“版本差异”交给宏或 overlay。这样既能复用,又能避免一份文件膨胀到难以维护。五、综合关系图
下图把节点、属性、标签、包含与条件编译放在同一张关系视图中,帮助建立整体认知:
六、常见误区与复盘
- 地址与 unit-address 不一致:节点名写
serial@10000000,但reg写成<0x20000000 0x1000>,会导致资源映射错误。 - 数据类型混用:把
reg写成字符串,或把布尔属性写成"true",内核解析结果与预期不符。 - 标签未定义就引用:
&uart0在包含链中找不到对应 label,dtc 直接报错。 - 覆盖顺序错误:板级 DTS 中
&uart0的覆盖必须出现在包含 SoC dtsi 之后,否则会被后续定义覆盖。 - 条件编译宏未生效:宏需在编译 DTB 的 Makefile 中通过
-D或内核配置传入,否则#ifdef分支不会参与编译。
dtc -I dtb -O dts 反编译生成的 DTB,确认最终合并结果是否符合预期。很多“驱动 probe 失败”的问题,根源都在 DTS 合并后的属性值或 status 状态上。掌握节点、属性、标签、包含与条件编译这五个语法要素后,就能读懂绝大多数 Android 平台 DTS,并能按板级差异进行安全覆盖。后续无论是添加外设、调整引脚,还是适配 Android 特有的 firmware 与 vendor 节点,都可以在这套语法框架内完成。