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 / disabledoverlay 常靠它开关设备
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 分区与编译产物是否一致。这三步能覆盖绝大多数设备树适配问题。

设备树源文件 → DTC 编译 → DTB → 内核解析 DTS 板级源文件 sm8650-mtp.dts 顶层 / 节点 / 属性 DTSI 公共包含片段 soc.dtsi / pmic.dtsi 可复用 / 可覆盖 #include cpp 预处理 展开宏与包含 生成 .dts.tmp DTC 编译器 -I dts -O dtb 语法检查 / 扁平化 DTB 扁平化二进制 bootloader 加载 内核 early_init _dt_scan dtc -I dtb -O dts 反编译 关键规则: 1. 同一节点同一属性,后定义覆盖先定义;子节点为合并而非替换。 2. reg 的解析格式由父节点 #address-cells / #size-cells 决定,cells 数量必须匹配。