1. 设备树基础概念:DTS 的起源、作用与基本语法结构
1.1 设备树的起源:从“硬编码”到“描述式”的演进
在早期的嵌入式 Linux 系统中,板级硬件信息(如内存地址、外设寄存器基址、中断号、GPIO 引脚分配)被直接硬编码在 arch/arm/mach-xxx 下的 C 语言板级文件中。每更换一款硬件平台,就必须修改内核源码并重新编译,导致内核镜像与硬件高度耦合,维护成本急剧上升。
为了解决这一问题,PowerPC 社区率先提出了 设备树(Device Tree) 的概念,随后被 ARM、RISC-V、x86 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核代码中剥离出来,以结构化数据的形式独立存在,内核在启动时解析该数据,动态构建设备模型。
| 对比维度 | 传统板级文件方式 | 设备树方式 |
|---|---|---|
| 硬件描述位置 | 内核源码 arch/arm/mach-xxx |
独立的 .dts / .dtsi 文件 |
| 硬件变更影响 | 需修改内核源码并重新编译 | 仅修改设备树文件,内核无需重编译 |
| 多板支持 | 每个板子一个内核镜像 | 同一内核镜像 + 不同设备树文件 |
| 驱动与硬件耦合度 | 高(驱动直接引用板级宏定义) | 低(驱动通过设备树节点匹配) |
关键理解: 设备树不是“驱动”,而是硬件的“数据清单”。驱动代码通过设备树节点中的属性值来获取硬件参数,实现“数据与逻辑分离”。
1.2 设备树的作用:硬件资源的“结构化清单”
设备树在嵌入式 Linux 启动流程中扮演着 硬件信息传递者 的角色。Bootloader(如 U-Boot)将设备树二进制文件(.dtb)加载到内存,并将首地址传递给内核。内核通过 unflatten_device_tree() 函数将其解析为树状数据结构,供驱动子系统使用。
设备树主要描述以下硬件资源:
- CPU 信息:核心数量、架构、时钟频率、缓存大小。
- 内存布局:物理内存起始地址与大小。
- 总线与外设:I2C、SPI、UART、MMC、USB 等控制器及其从设备。
- 中断控制器:中断控制器类型、中断号映射、中断触发方式。
- GPIO 控制器与引脚复用:GPIO 控制器基址、引脚功能配置。
- 时钟与复位:时钟源、分频系数、复位信号连接。
- 电源管理:电压调节器、电源域、唤醒源。
1.3 设备树的基本语法结构
设备树源文件(.dts)采用树形层次结构,以 节点(node) 和 属性(property) 为核心元素。每个节点代表一个硬件设备或总线,属性描述该设备的配置参数。
1.3.1 节点定义与层次关系
/dts-v1/;
/ {
model = "Embedded Linux Board v1.0";
compatible = "vendor,board-v1", "vendor,board";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
status = "okay";
};
i2c0: i2c@10001000 {
compatible = "vendor,i2c";
reg = <0x10001000 0x1000>;
interrupts = <0 34 4>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
};
};
1.3.2 核心语法要素
| 语法元素 | 格式 | 说明 |
|---|---|---|
| 根节点 | / { ... }; |
设备树的顶层容器,所有设备节点都位于其下 |
| 子节点 | node-name@unit-address |
node-name 为节点名称,unit-address 为设备基址(可选) |
| 标签(label) | uart0: serial@10000000 |
为节点定义别名,便于在其它地方引用或覆盖 |
| 属性 | property-name = value; |
描述节点特性,值可以是字符串、整数、数组、二进制数据等 |
| reg 属性 | reg = <address size>; |
描述设备地址空间,由 #address-cells 和 #size-cells 决定单元长度 |
| compatible 属性 | compatible = "vendor,device"; |
驱动匹配的关键字符串,内核通过该属性查找对应驱动 |
| interrupts 属性 | interrupts = <type irq flags>; |
描述中断连接,具体格式由中断控制器定义 |
| status 属性 | status = "okay" / "disabled"; |
控制设备是否启用,常用于覆盖机制中动态修改 |
1.3.3 属性值类型速查
- 字符串:
compatible = "vendor,device"; - 32 位整数:
reg = <0x10000000 0x1000>; - 64 位整数:
reg = <0x00000000 0x80000000 0x00000000 0x10000000>;(需配合#address-cells = <2>) - 字符串列表:
compatible = "vendor,device", "generic,device"; - 二进制数据:
mac-address = [00 11 22 33 44 55]; - 布尔值:
dma-coherent;(仅存在即表示 true)
实践要点: 编写设备树时,
compatible 属性应遵循“具体在前,通用在后”的原则。例如 "vendor,board-v1", "vendor,board",内核会按顺序匹配驱动,优先使用最具体的驱动。
1.4 设备树如何描述硬件资源:一个完整的实例
以下是一个典型的嵌入式 SoC 设备树片段,展示了如何描述 CPU、内存、中断控制器、GPIO 和 I2C 总线等核心硬件资源:
/dts-v1/;
/ {
model = "Cortex-A7 Development Board";
compatible = "myvendor,devboard";
#address-cells = <1>;
#size-cells = <1>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a7";
reg = <0x0>;
clock-frequency = <1000000000>;
};
cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a7";
reg = <0x1>;
clock-frequency = <1000000000>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
intc: interrupt-controller@1c00000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x01c00000 0x1000>,
<0x01c01000 0x1000>;
};
gpio0: gpio@1c20000 {
compatible = "myvendor,gpio";
reg = <0x1c20000 0x1000>;
interrupts = <0 45 4>;
#gpio-cells = <2>;
gpio-controller;
};
i2c0: i2c@1c30000 {
compatible = "myvendor,i2c";
reg = <0x1c30000 0x1000>;
interrupts = <0 46 4>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <100000>;
status = "okay";
temp_sensor: sensor@48 {
compatible = "national,lm75";
reg = <0x48>;
};
};
};
1.5 设备树覆盖机制的引入背景
在实际嵌入式项目中,经常遇到以下场景:
- 硬件变体:同一款 SoC 被用于多个不同外设配置的板卡。
- 扩展模块:通过接插件连接不同的子板(如摄像头、显示屏、传感器模块)。
- 运行时配置:系统启动后根据硬件检测结果动态启用或禁用某些设备。
如果为每种组合都维护一份完整的 .dts 文件,会导致文件数量爆炸式增长,且维护困难。设备树覆盖(Device Tree Overlay) 机制应运而生:它允许在基础设备树(base DTB)之上,动态加载或卸载增量修改片段(.dtbo),实现硬件描述的模块化与运行时灵活性。