2. 设备树基础
在嵌入式 Linux 的早期阶段,板级硬件信息几乎全部以 C 代码的形式硬编码在内核源码中。以 ARM 架构为例,arch/arm/mach-xxx/ 目录下充斥着大量 board-xxx.c 文件,每个文件描述一块特定开发板的串口地址、I2C 控制器、GPIO 引脚、时钟频率等。这种做法带来三个严重问题:一是内核源码树被大量板级文件污染,二是同一 SoC 的不同板卡需要复制粘贴大量重复代码,三是硬件变更必须重新编译内核。设备树(Device Tree)正是为解决这些问题而引入的硬件描述机制。
2.1 历史背景:从 OpenFirmware 到 ARM 的全面采用
设备树并非 Linux 原创,它最早源自 OpenFirmware(IEEE 1275)标准,用于在 PowerPC 平台上描述硬件。2005 年前后,PowerPC 社区已经普遍使用设备树,而 ARM 社区仍坚持硬编码方式。随着 ARM SoC 数量爆发式增长,内核维护者 Linus Torvalds 在 2011 年发出著名邮件,明确要求 ARM 平台必须转向设备树,否则拒绝合并新的板级代码。此后,ARM Linux 全面采用设备树,mach-xxx 目录逐步被清理,硬件描述从内核代码中剥离出来,成为独立的、可被 bootloader 传递的数据结构。
这一转变的核心价值在于:内核与硬件描述解耦。同一份内核镜像可以支持多块板卡,只要它们使用相同的 SoC 架构;硬件差异通过不同的设备树二进制文件在启动时注入。
2.2 DTS、DTC、DTB 三者关系
设备树在工程实践中涉及三种形态,理解它们的关系是掌握设备树的第一步:
- DTS(Device Tree Source):人类可读的文本源文件,通常以
.dts为后缀,描述板级硬件。复杂 SoC 会拆分为.dtsi(include 文件)供多块板卡复用。 - DTC(Device Tree Compiler):编译工具,将 DTS 编译为 DTB,也可将 DTB 反编译回 DTS。内核源码中的
scripts/dtc/即其实现。 - DTB(Device Tree Blob):二进制格式,由 bootloader(如 U-Boot)在启动时传递给内核,内核解析后生成
device_node与platform_device。
三者的转换关系可以用下面的信号链图清晰表达:
典型编译命令如下:
# 编译 DTS 为 DTB
dtc -I dts -O dtb -o board.dtb board.dts
# 反编译 DTB 为 DTS,用于调试
dtc -I dtb -O dts -o board_decompiled.dts board.dtb
# 内核构建系统中通常使用
make ARCH=arm CROSS_COMPILE=arm-linux- dtbs
2.3 设备树在 Linux 启动流程中的作用
设备树在启动流程中扮演“硬件清单”的角色。以 ARM 平台为例,典型流程如下:
- BootROM 加载 bootloader(如 U-Boot)到内存并运行。
- U-Boot 将内核镜像(zImage/Image)与 DTB 加载到内存指定地址。
- U-Boot 通过
bootz或bootm命令跳转内核,同时将 DTB 地址写入寄存器(如 ARM 的x0或r2)。 - 内核启动早期,
setup_arch()调用unflatten_device_tree()解析 DTB,生成device_node树。 - 内核根据设备树中的
compatible属性匹配驱动,将节点转换为platform_device,触发驱动probe()。
2.4 基本术语与核心概念
设备树采用树形结构,每个节点描述一个设备或总线。以下术语必须掌握:
| 术语 | 含义 | 示例 |
|---|---|---|
node |
设备树中的一个节点,代表一个设备或总线 | uart0: serial@101f1000 |
property |
节点的属性,描述硬件参数 | compatible = "arm,pl011"; |
compatible |
驱动匹配的关键属性,格式为“厂商,型号” | "ti,omap4-uart" |
reg |
寄存器地址与长度,格式为 <地址 长度> |
reg = <0x101f1000 0x1000>; |
interrupts |
中断号与触发类型 | interrupts = <0 12 4>; |
clocks |
时钟源引用 | clocks = <&clk_uart0>; |
phandle |
节点引用标识,用于跨节点引用 | &uart0 编译后生成唯一 phandle |
status |
设备使能状态 | status = "okay"; 或 "disabled"; |
2.5 案例:一块虚拟开发板的设备树片段
下面是一个简化的 DTS 片段,描述一个 SoC 上的 UART0 与 I2C 控制器:
/dts-v1/;
/ {
model = "MyBoard v1.0";
compatible = "myvendor,myboard", "myvendor,mysoC";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 0x1000>;
interrupts = <0 12 4>;
clocks = <&clk_uart0>;
status = "okay";
};
i2c0: i2c@101f2000 {
compatible = "arm,pl031-i2c";
reg = <0x101f2000 0x1000>;
clock-frequency = <100000>;
status = "disabled";
};
};
};
内核启动后,可以通过 /proc/device-tree/ 查看解析结果:
# 查看根节点 compatible
cat /proc/device-tree/compatible
# 查看 uart0 的 reg 属性(二进制需 hexdump)
hexdump -C /proc/device-tree/soc/serial@101f1000/reg
# 查看所有设备树节点
ls /proc/device-tree/soc/
2.6 常见误区与复盘
reg 中的地址可能是总线地址,需结合 ranges 属性理解。
make dtbs 并更新 bootloader 加载的 DTB 文件。
dtc 反编译实际加载的 DTB,确认内核看到的节点与预期一致;再结合 dmesg | grep -i of_ 查看内核解析日志,定位 compatible 匹配失败或 reg 地址冲突等问题。
掌握 DTS、DTC、DTB 三者的转换关系,理解设备树在启动流程中的传递与解析时机,并熟悉 compatible、reg、interrupts 等核心属性,是后续进行板级资源管理与驱动适配的基础。设备树不是“配置文件”,而是内核与硬件之间的契约,契约的准确性直接决定驱动能否正确加载。