4. 设备树基础
在 Android GKI 架构下,内核与硬件描述被有意解耦:内核镜像提供通用驱动与子系统,而“这块板子上到底挂了哪些设备、寄存器基地址是多少、中断接到哪根线、时钟由谁提供”等信息,全部交给设备树(Device Tree)描述。理解 DTS、DTSI、DTB、DTC 四者的分工与编译链路,是后续做 GKI 设备树适配、overlay 合并与启动排障的基本功。
一、四个核心概念的分工
设备树生态里有四个高频缩写,它们分别对应“源、公共源、编译器、二进制”四个角色,混淆它们会导致排查方向完全跑偏。
- DTS(Device Tree Source):板级设备树源文件,通常以
.dts结尾,描述某一款具体设备(如sm8650-mtp.dts)的顶层结构,通过#include引入若干 DTSI。 - DTSI(Device Tree Source Include):可被包含的公共片段,常以 SoC 或平台为单位,如
soc.dtsi、pmic.dtsi,用于描述 SoC 内部外设、引脚控制器、时钟树等可复用内容。 - DTC(Device Tree Compiler):设备树编译器,把人类可读的 DTS/DTSI 文本编译成扁平化二进制 DTB,同时也能反向把 DTB 反编译回 DTS。
- DTB(Device Tree Blob):最终被 bootloader 加载、由内核在启动早期解析的二进制设备树,是运行时真正生效的形态。
一句话记忆:DTS/DTSI 是“写给人看的”,DTB 是“给内核看的”,DTC 是两者之间的翻译官。
二、DTC 编译流程与产物形态
内核构建系统并不会直接调用裸的 dtc,而是通过 scripts/Makefile.lib 中的规则,先由 C 预处理器(cpp)处理 #include、宏与条件编译,再交给 dtc 生成 DTB。因此 DTS 里可以写 #include、#define,这与纯文本包含有本质区别。
# 典型编译链路(内核构建系统内部)
*.dts + *.dtsi
│ cpp 预处理(展开 #include / 宏)
▼
*.dts.tmp(预处理后的完整源)
│ dtc -I dts -O dtb -o xxx.dtb
▼
xxx.dtb(扁平化二进制设备树)
│ bootloader 加载并传给内核
▼
内核 early_init_dt_scan() 解析
手工验证时可以直接调用 dtc,常用命令如下:
# DTS 编译为 DTB
dtc -I dts -O dtb -o board.dtb board.dts
# DTB 反编译回 DTS,便于查看实际生效内容
dtc -I dtb -O dts -o board.dts board.dtb
# 查看 DTB 头部信息(大小、版本、结构块偏移)
fdtdump board.dtb | head -n 20
调试设备树时,最有效的手段往往不是反复读源码,而是把最终 DTB 反编译出来,确认“合并后到底长什么样”。很多问题出在 include 顺序与覆盖关系,而非语法本身。
三、设备树语法与节点属性
设备树语法本身非常精简,核心只有“节点、属性、值”三类元素。节点用大括号嵌套表达层级,属性用 键 = 值 表达,值可以是字符串、32 位整数数组、字节序列或空(布尔属性)。
/dts-v1/;
/ {
model = "Example GKI Board";
compatible = "vendor,example-board", "qcom,sm8650";
#address-cells = <2>;
#size-cells = <2>;
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@a000000 {
compatible = "vendor,uart-v2";
reg = <0x0a000000 0x1000>;
interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&gcc_uart_clk>;
status = "okay";
};
};
};
其中几个属性在 GKI 适配中出现频率极高,必须理解其语义:
| 属性 | 作用 | 适配关注点 |
|---|---|---|
compatible | 驱动匹配的关键字,按从具体到通用的顺序排列 | 写错或顺序颠倒会导致驱动不 probe |
reg | 寄存器地址与长度,格式由父节点 #address-cells/#size-cells 决定 | cells 数量不匹配会解析错位 |
interrupts | 中断号与触发类型 | 需与 interrupt-parent 配合 |
status | 设备使能状态,okay / disabled | overlay 常靠它开关设备 |
phandle | 节点引用句柄,由 label 自动生成 | 跨节点引用时钟、电源的基础 |
四、include 覆盖机制与典型误区
DTSI 的价值在于复用,但复用也带来覆盖顺序问题。设备树采用“后写覆盖先写”的合并规则:同一节点同一属性,后出现的定义会覆盖先前的值;而子节点则是合并而非替换。因此 #include 的先后顺序直接决定最终结果。
#include "soc-base.dtsi" // 先引入 SoC 公共定义
#include "board-pmic.dtsi" // 再引入板级 PMIC 覆盖
#include "board-display.dtsi" // 最后引入显示相关覆盖
&uart0 {
status = "okay"; // 覆盖 dtsi 中的 disabled
};
常见误区一:把 DTSI 当成“头文件”随意包含,导致同一节点被多次定义、属性互相覆盖,最终 DTB 与预期不符。误区二:只改 DTS 却忘记重新编译 DTB,bootloader 仍加载旧 blob。误区三:忽略
#address-cells 与 #size-cells 的继承关系,reg 解析错位却误判为驱动 bug。复盘时建议固定一套动作:先反编译最终 DTB 确认节点与属性,再对照 DTS/DTSI 的 include 顺序定位覆盖点,最后检查 bootloader 实际加载的 DTB 分区与编译产物是否一致。这三步能覆盖绝大多数设备树适配问题。