1. 设备树基础概念:设备树(DTS)的起源、作用与基本语法结构
1.1 设备树的起源与设计动机
在早期的 Linux 内核中,板级硬件信息(如内存基址、外设寄存器地址、中断号、GPIO 引脚分配)被硬编码在 arch/arm/mach-xxx 目录下的 C 语言板文件中。这种“代码即配置”的方式带来了严重的维护灾难:
- 代码膨胀:每增加一块新板卡,就需要新增或修改大量 C 代码。
- 耦合度高:硬件描述与内核驱动逻辑紧密绑定,无法实现“一次编译,多板运行”。
- 碎片化严重:厂商的板级补丁难以合入主线,导致内核版本分裂。
为了解决上述问题,PowerPC 社区率先引入 设备树(Device Tree) 机制,随后被 ARM、RISC-V、X86(ACPI 之外)等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化数据文件的形式传递给内核,由内核在启动时解析并生成平台设备。
关键转变: 从“代码描述硬件” → “数据描述硬件”。设备树使得同一份内核镜像可以支持多种硬件平台,只需更换对应的设备树二进制文件(.dtb)。
1.2 设备树的作用与工作流程
设备树在 Linux 系统启动过程中扮演着“硬件清单”的角色。其完整工作流程如下:
- 编写 DTS 源文件:开发者根据硬件原理图,编写
.dts文件,描述 CPU、内存、外设、中断控制器、GPIO 控制器等硬件节点。 - 编译为 DTB:使用
dtc(Device Tree Compiler)将.dts编译为二进制.dtb文件。 - Bootloader 加载:U-Boot 等引导程序将
.dtb文件加载到内存中,并将地址传递给内核。 - 内核解析:Linux 内核在启动早期调用
unflatten_device_tree()函数,将 DTB 展开为内存中的 设备树结构体(Device Tree Structure)。 - 驱动匹配:总线驱动(如 platform bus)遍历设备树节点,根据
compatible属性匹配对应的驱动,并完成资源(中断、GPIO、内存地址)的映射。
注意: 设备树是静态的硬件描述,不处理热插拔设备(如 USB、PCIe)。热插拔设备通常由 ACPI 或设备树覆盖(overlay)机制处理。
1.3 设备树的基本语法结构
设备树源文件(.dts)采用一种层次化的、类似 C 语言风格的语法。一个完整的设备树由以下核心元素构成:
| 语法元素 | 表示方式 | 说明 |
|---|---|---|
| 节点(Node) | node-name@unit-address |
表示一个硬件设备或总线,unit-address 通常为寄存器基址 |
| 属性(Property) | property-name = value; |
描述节点的特性,如寄存器地址、中断号、时钟频率等 |
| 根节点(Root Node) | / { ... }; |
设备树的顶层容器,所有设备节点都挂载在根节点下 |
| 标签(Label) | label: node-name |
为节点定义别名,方便在其他地方引用或覆盖 |
| 包含文件 | #include "file.dtsi" |
引入芯片级或板级通用定义,支持 C 预处理器 |
1.4 标准属性详解
设备树规范定义了一系列标准属性,用于描述硬件资源的分配与关联。以下是中断控制器和 GPIO 映射中最重要的几个属性:
| 属性名 | 值类型 | 作用描述 |
|---|---|---|
compatible |
字符串列表 | 驱动匹配的关键,格式为 "厂商,设备型号",如 "ti,omap3-gpio" |
reg |
32 位整数对 | 描述寄存器地址范围,格式为 <基址 长度> |
interrupt-parent |
phandle | 指定该设备连接的中断控制器节点引用 |
interrupts |
32 位整数数组 | 描述中断号、触发类型等,格式由中断控制器定义 |
gpio-controller |
空属性 | 标记该节点是一个 GPIO 控制器 |
#gpio-cells |
32 位整数 | 指定引用 GPIO 时需要多少个 cell(通常为 2:引脚号 + 标志) |
gpios / cs-gpios |
phandle + cells | 引用外部 GPIO 引脚,如 <&gpio1 15 GPIO_ACTIVE_HIGH> |
1.5 完整设备树示例
以下是一个简化的设备树片段,展示了一个 SoC 内部的中断控制器(GIC)和 GPIO 控制器的定义,以及一个外部设备(按键)如何引用这些资源:
/dts-v1/;
#include "soc-common.dtsi"
/ {
model = "Example Board v1.0";
compatible = "example,board-v1", "example,board";
/* 内存节点 */
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; /* 1GB RAM */
};
/* 中断控制器:GIC v2 */
gic: interrupt-controller@f1001000 {
compatible = "arm,cortex-a7-gic";
interrupt-controller; /* 标记为中断控制器 */
#interrupt-cells = <3>; /* 格式: <类型 编号 触发方式> */
reg = <0xf1001000 0x1000>,
<0xf1002000 0x2000>;
};
/* GPIO 控制器 */
gpio1: gpio@f2000000 {
compatible = "snps,dw-apb-gpio";
reg = <0xf2000000 0x1000>;
gpio-controller; /* 标记为 GPIO 控制器 */
#gpio-cells = <2>; /* 格式: <引脚号 标志> */
interrupt-parent = <&gic>; /* 连接到 GIC */
interrupts = <0 32 4>; /* SPI 32, 高电平触发 */
};
/* 外部设备:按键,使用 GPIO 并产生中断 */
button@0 {
compatible = "gpio-keys";
interrupt-parent = <&gpio1>; /* 连接到 GPIO 控制器 */
interrupts = <15 2>; /* GPIO1 引脚15, 下降沿触发 */
gpios = <&gpio1 15 1>; /* GPIO1 引脚15, 低电平有效 */
label = "User Button";
linux,code = <116>; /* KEY_POWER */
};
};
常见错误: 初学者容易混淆
interrupt-parent 和 gpios 中的 phandle 引用。前者指向中断控制器节点,后者指向 GPIO 控制器节点。两者虽然都是引用,但语义完全不同:中断是事件信号,GPIO 是通用输入输出引脚。
1.6 设备树节点与硬件设备的映射关系
设备树的层次结构直接反映了 SoC 内部的总线拓扑和外设连接关系。以下 SVG 图展示了一个典型的嵌入式系统设备树结构:
1.7 设备树覆盖(Overlay)机制
设备树覆盖允许在系统运行时动态添加或修改设备树节点,常用于 FPGA 重配置、扩展板热插拔等场景。覆盖文件(.dtbo)的语法与基础 DTS 相同,但使用 /plugin/ 关键字标识:
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&i2c1>;
__overlay__ {
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
};
};
实践建议: 在编写设备树时,优先使用
.dtsi 文件复用 SoC 级定义,板级 .dts 只做引脚复用、外设使能和属性覆盖。这可以大幅减少重复代码,并提高设备树的可维护性。
1.8 本章小结
- 设备树将硬件描述从内核源码中解耦,实现了“数据驱动硬件发现”。
- 设备树的工作流程为:DTS → DTC → DTB → Bootloader → 内核解析 → 驱动匹配。
- 基本语法包括节点、属性、根节点、标签和包含文件,其中
compatible是驱动匹配的核心属性。 - 中断控制器和 GPIO 控制器通过
interrupt-parent、interrupts、gpio-controller、#gpio-cells和gpios属性建立映射关系。 - 设备树覆盖机制支持运行时动态修改硬件拓扑,适用于可扩展平台。