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;反编译则是逆过程。
语法结构与节点、属性
设备树源文件采用类似 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/DTB/DTC 关系、语法结构、节点与属性、编译与反编译流程,是后续外设 Bring-up 的基础。只有先确认“设备树描述正确、DTB 实际生效”,驱动匹配与硬件初始化才有可靠前提。