1. 设备树基础概念:起源、作用与基本语法结构
1.1 设备树的起源:从“硬编码”到“描述性”的演进
在早期的嵌入式 Linux 内核(如 Linux 2.4 / 2.6)中,板级硬件信息(内存基址、外设寄存器地址、中断号、GPIO 映射等)被直接硬编码在 arch/arm/mach-xxx 下的 C 语言板级文件中。每增加一款新硬件平台,就需要新增或修改大量 C 代码,导致内核源码树急剧膨胀,且不同厂商的代码难以统一维护。
为了解决这一痛点,PowerPC 社区率先引入了 Open Firmware 中的设备树概念,随后被 ARM、RISC-V、x86 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核代码中剥离出来,形成一个独立的数据结构,由 Bootloader(如 U-Boot)传递给内核,内核解析后动态创建平台设备。
关键转折点: Linux 3.x 之后,ARM 架构强制要求使用设备树,传统的板级文件(board-*.c)被逐步废弃。这一决策使得同一份内核镜像可以支持多个硬件平台,只需更换不同的 .dtb 文件即可。
1.2 设备树的作用与核心价值
设备树(Device Tree)是一种描述硬件资源的树形数据结构,其核心作用可归纳为以下四点:
| 作用维度 | 具体说明 | 对驱动开发的影响 |
|---|---|---|
| 硬件描述标准化 | 使用统一的节点/属性语法描述 CPU、内存、外设、中断、GPIO、时钟等 | 驱动通过 of_* API 读取硬件参数,无需硬编码 |
| 平台解耦 | 同一份内核镜像通过更换 .dtb 文件适配不同硬件平台 | 驱动代码无需为每款硬件单独维护板级文件 |
| 动态配置 | Bootloader 可根据硬件版本、外设选型动态修改 .dtb | 驱动在 probe 时获取运行时配置,灵活性大幅提升 |
| 社区生态统一 | Linux 主线内核要求所有新平台必须提供设备树支持 | 驱动开发者只需关注设备树绑定文档,降低协作成本 |
驱动开发者视角: 设备树本质上是一个“硬件参数数据库”。你的以太网驱动不再需要关心“PHY 地址是 0x01 还是 0x02”,而是通过设备树节点中的
reg 属性动态获取。这使得同一份驱动二进制可以适配不同硬件设计。
1.3 设备树的基本语法结构
设备树源文件(.dts)采用树形层级结构,由 节点(node) 和 属性(property) 组成。每个节点代表一个硬件设备或总线,属性描述该设备的配置参数。
1.3.1 节点定义
/dts-v1/;
/ {
model = "Embedded Linux Board";
compatible = "vendor,board-rev1";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256MB RAM */
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@10000000 {
compatible = "vendor,uart-ip";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
};
eth0: ethernet@20000000 {
compatible = "vendor,gmac";
reg = <0x20000000 0x4000>;
interrupts = <0 45 4>;
phy-mode = "rgmii";
phy-handle = <&phy0>;
mdio {
#address-cells = <1>;
#size-cells = <0>;
phy0: ethernet-phy@1 {
reg = <0x1>;
compatible = "ethernet-phy-ieee802.3-c22";
};
};
};
};
};
1.3.2 语法要素速查表
| 语法元素 | 示例 | 说明 |
|---|---|---|
| 根节点 | / { }; |
设备树的顶层容器,所有设备节点都位于其下 |
| 节点名称 | uart0: serial@10000000 |
格式:[标签:]名称[@单元地址],标签用于引用 |
| 属性 | compatible = "vendor,gmac"; |
键值对,值类型包括字符串、整数、数组、布尔等 |
| reg 属性 | reg = <0x20000000 0x4000>; |
描述设备地址空间:<基址 长度> |
| interrupts | interrupts = <0 45 4>; |
中断描述:<中断控制器类型 中断号 触发方式> |
| phandle | phy-handle = <&phy0>; |
引用其他节点,&phy0 指向标签为 phy0 的节点 |
| 包含文件 | #include "soc.dtsi" |
.dtsi 文件用于定义 SoC 公共部分,.dts 文件引用并覆盖 |
1.3.3 设备树层级结构示意
1.4 设备树编译与使用流程
- 编写 .dts 源文件:开发者根据硬件原理图编写设备树源文件,通常包含一个 .dtsi(SoC 公共定义)和多个 .dts(板级具体配置)。
- 编译为 .dtb 二进制:使用
dtc(Device Tree Compiler)工具将 .dts 编译为 .dtb 文件。 - Bootloader 加载 .dtb:U-Boot 等 Bootloader 从存储介质(eMMC、NAND、网络)读取 .dtb 文件到内存,并在启动内核时传递其地址。
- 内核解析并创建设备:内核启动早期调用
unflatten_device_tree()将 .dtb 解析为device_node树,随后平台总线根据compatible属性匹配驱动并调用 probe。
常见误区: 设备树不是编程语言,不支持循环、条件判断或函数调用。它纯粹是静态的硬件描述数据。所有“逻辑”行为(如根据某个 GPIO 电平选择 PHY 地址)必须在驱动代码中实现,不能依赖设备树。
1.5 设备树与驱动适配的关联
对于以太网 MAC 驱动而言,设备树提供了以下关键信息:
- MAC 控制器基址与中断:通过
reg和interrupts属性获取。 - PHY 绑定方式:
phy-handle引用 PHY 节点,phy-mode指定接口模式(RGMII、RMII、MII 等)。 - PHY 地址与类型:PHY 节点的
reg属性定义 MDIO 总线上的 PHY 地址,compatible属性标识 PHY 标准或厂商。 - 时钟与复位:
clocks、resets属性提供时钟源和复位控制。
驱动开发者在编写以太网驱动时,应优先使用 of_* 系列 API(如 of_property_read_u32()、of_parse_phandle())从设备树中读取配置,而不是在驱动代码中硬编码任何硬件参数。