2. 设备树基础

在嵌入式 Linux 的早期阶段,板级硬件信息大多以 C 代码形式硬编码在内核源码的 arch/arm/mach-xxx 目录中。每换一块板子,就要新增或修改大量板级文件,导致内核中充斥着重复、碎片化的硬件描述。ARM 社区曾出现“每个板子一份 board-xxx.c”的维护困境,硬件差异与内核代码强耦合,严重阻碍了内核的统一与演进。设备树(Device Tree,DT)正是为解决这一问题而引入的硬件描述机制:它把“硬件是什么、接在哪里、如何配置”从内核代码中剥离出来,用一份独立的数据结构描述,由内核在启动时解析并生成对应的设备与驱动匹配信息。

设备树最早来源于 OpenFirmware(IEEE 1275)规范,PowerPC 平台长期使用它描述系统硬件。ARM 在 2011 年前后大规模采纳设备树,逐步删除板级 C 文件,实现“一个内核镜像支持多块板”。其核心价值在于:硬件描述与内核代码解耦、同一内核支持多平台、驱动通过兼容性字符串匹配设备、板级差异只需替换设备树二进制。

设备树不是驱动,也不是内核配置,它只描述“硬件存在性与连接关系”。驱动仍然负责初始化硬件、注册子系统、处理中断与电源等行为。

DTS、DTSI、DTB、DTC 的关系

设备树在工程中会经历“源文件 → 编译 → 二进制 → 内核解析”的链路。理解这条链路,是 Bring-up 阶段定位问题的前提。

  • DTS(Device Tree Source):设备树源文件,通常以 .dts 结尾,描述某块具体板子的硬件。例如 board-a.dts。
  • DTSI(Device Tree Source Include):可被包含的设备树片段,通常描述 SoC 级公共部分,如 soc.dtsi、cpu.dtsi。板级 DTS 通过 #include 引入并覆盖差异。
  • DTC(Device Tree Compiler):设备树编译器,把 DTS/DTSI 编译为 DTB,也可把 DTB 反编译回 DTS。它类似“设备树领域的 gcc”。
  • DTB(Device Tree Blob):编译后的二进制设备树,由 bootloader 加载并传递给内核。内核启动时解析 DTB,展开为 device_node 与 platform_device。

它们之间的转换关系可以用下面的信号链式图来理解:源文件经过预处理、语法解析、语义检查、扁平化打包,最终生成 DTB;反编译则是逆过程。

设备树编译与反编译流程 DTS / DTSI 板级 + SoC 源文件 .dts / .dtsi DTC 编译器 预处理 / 解析 / 检查 dtc -I dts -O dtb DTB 二进制 扁平化设备树 .dtb 内核解析 device_node platform_device 编译 打包 加载 反编译 dtc -I dtb -O dts 关键命令示例 dtc -I dts -O dtb -o board.dtb board.dts # 编译 DTS 为 DTB dtc -I dtb -O dts -o board_decompiled.dts board.dtb # 反编译 DTB 为 DTS fdtdump board.dtb # 快速查看 DTB 内容 提示:内核构建时通常由 Makefile 自动调用 dtc,无需手工执行。

语法结构与节点、属性

设备树源文件采用类似 C 的语法,但结构更接近“树形配置”。一个 DTS 文件由若干节点(node)和属性(property)组成,节点可以嵌套,形成父子层级。根节点用 / 表示,所有设备都挂在根节点之下。

/dts-v1/;
#include "soc.dtsi"

/ {
    model = "Demo Board A";
    compatible = "vendor,demo-a", "vendor,demo";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;
    };

    uart0: serial@10000000 {
        compatible = "vendor,uart-v2";
        reg = <0x10000000 0x1000>;
        interrupts = <10>;
        clocks = <&clk_uart>;
        status = "okay";
    };
};

节点名通常采用 name@address 形式,地址用于区分同一类设备的多个实例。节点标签(如 uart0:)用于在其它位置通过 &uart0 引用该节点。属性则是键值对,常见属性包括:

属性作用示例
compatible驱动匹配的关键字符串,按从具体到通用排列"vendor,uart-v2"
reg寄存器地址与长度,格式为 <地址 长度><0x10000000 0x1000>
interrupts中断号或中断描述<10>
clocks时钟源引用<&clk_uart>
status设备使能状态:okay / disabled"okay"
pinctrl-0引脚复用配置引用<&uart0_pins>
compatible 是驱动与设备匹配的“身份证”。Bring-up 时若驱动不 probe,第一件事就是核对 DTS 中的 compatible 是否与驱动 of_match_table 一致。

编译与反编译流程

在工程实践中,设备树通常由内核构建系统自动编译。以 ARM 平台为例,DTS 放在 arch/arm/boot/dts/ 下,Makefile 中通过 dtb-$(CONFIG_ARCH_XXX) += board-a.dtb 声明。执行 make dtbs 即可生成 DTB。若需单独调试,可直接调用 dtc。

# 在内核源码根目录编译所有 DTB
make ARCH=arm dtbs

# 只编译指定 DTB
make ARCH=arm vendor-demo-a.dtb

# 手工编译并保留符号信息
dtc -I dts -O dtb -o board.dtb -@ board.dts

# 反编译查看实际生效内容
dtc -I dtb -O dts -o board.dts board.dtb

反编译在排障中非常有用:当怀疑 bootloader 传递的 DTB 与源码不一致时,可以把实际 DTB 反编译出来,确认 status、reg、compatible 是否被覆盖或裁剪。常见现象是:源码中 status = "okay",但实际 DTB 中仍是 disabled,导致驱动不加载。

案例与误区复盘

某项目在 Bring-up 阶段发现 UART 无法输出。检查 DTS 中 uart0 节点 status = "okay",compatible 与驱动匹配,但串口始终无打印。最终定位到板级 DTS 通过 #include 引入 SoC dtsi 后,又在后面用同名节点覆盖,把 reg 写成了另一组地址。反编译实际 DTB 后,reg 与预期不符,修正后正常。

常见误区一:把 DTS 当成“配置文件”,随意添加内核不认识的属性,导致解析告警或行为异常。常见误区二:忽略 DTSI 与 DTS 的覆盖顺序,后定义的属性会覆盖先定义的。常见误区三:只改源码不重新编译 DTB,bootloader 仍加载旧 DTB。

掌握设备树的起源、DTS/DTB/DTC 关系、语法结构、节点与属性、编译与反编译流程,是后续外设 Bring-up 的基础。只有先确认“设备树描述正确、DTB 实际生效”,驱动匹配与硬件初始化才有可靠前提。