3. 设备树(Device Tree)基础:设备树起源、语法结构、节点与属性、dts/dtsi/dtb文件格式
3.1 设备树的起源与设计动机
在早期的嵌入式 Linux 系统中,板级硬件信息(如内存地址、中断号、外设寄存器基址)被硬编码在 arch/arm/mach-xxx 下的 C 语言板级文件中。每更换一款硬件平台,就必须重新编译内核,导致内核镜像与硬件高度耦合,无法实现“一次编译,多处运行”。
设备树(Device Tree)的引入解决了这一痛点。它将硬件描述从内核源码中剥离,以结构化的树形数据格式独立存在,由 Bootloader(如 U-Boot)传递给内核。内核在启动时解析设备树,动态创建平台设备并完成驱动匹配,从而实现内核与硬件描述的彻底解耦。
核心动机: 将硬件配置信息从内核源码中分离,实现“同一内核镜像支持不同硬件平台”的通用启动模型。
历史背景: 设备树规范源于 Open Firmware(IEEE 1275),PowerPC 架构最早采用。ARM Linux 社区在 2011 年左右(Linux 3.x 时代)开始大规模迁移至设备树,至 2014 年后新平台已强制要求使用设备树。
3.2 设备树语法结构
设备树源文件采用树形层级结构,以 节点(Node) 和 属性(Property) 为核心元素。每个节点代表一个硬件设备或总线,属性描述该设备的配置参数。
3.2.1 基本语法规则
- 根节点: 使用
/表示,是整个设备树的起点。 - 节点定义:
node-name@unit-address,其中unit-address通常为寄存器基址或设备地址。 - 属性赋值:
property-name = <value>;或property-name = "string";。 - 标签(Label): 使用
label: node-name格式,便于在其他位置引用。 - 包含文件: 使用
#include预处理指令引入.dtsi文件。
// 设备树源文件示例:简单 SoC 设备树
/dts-v1/;
#include "soc-common.dtsi"
/ {
model = "Example Gateway Board";
compatible = "vendor,gateway-v1", "vendor,gateway";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB RAM
};
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";
};
can0: can@20000000 {
compatible = "vendor,can-controller";
reg = <0x20000000 0x1000>;
interrupts = <0 45 4>;
clocks = <&clk_can>;
status = "disabled";
};
};
};
3.2.2 节点与属性详解
| 元素 | 说明 | 示例 |
|---|---|---|
| 节点 | 表示一个硬件设备、总线或功能单元 | uart0: serial@10000000 |
| compatible | 驱动匹配的关键字符串,格式为 "厂商,设备型号" |
"vendor,can-controller" |
| reg | 描述设备地址空间,格式为 <基址 长度> |
<0x20000000 0x1000> |
| interrupts | 中断号、触发类型等中断描述信息 | <0 45 4> |
| clocks | 引用时钟控制器节点 | <&clk_can> |
| status | 设备启用状态:"okay" 或 "disabled" |
"disabled" |
| #address-cells | 子节点 reg 属性中地址字段占用的 32 位单元数 |
<1> |
| #size-cells | 子节点 reg 属性中长度字段占用的 32 位单元数 |
<1> |
常见错误:
compatible 字符串必须与驱动中 of_match_table 定义的字符串完全一致(包括大小写和厂商前缀),否则驱动无法匹配到设备。
3.3 设备树节点层级关系 SVG 图
下图展示了一个典型网关 SoC 设备树的节点层级结构,从根节点到 CAN 外设节点的完整路径:
3.4 dts / dtsi / dtb 文件格式
设备树在开发、编译和运行阶段使用三种不同的文件格式:
| 文件后缀 | 全称 | 格式 | 用途 |
|---|---|---|---|
| .dts | Device Tree Source | 文本(ASCII) | 设备树源文件,开发者直接编写和修改 |
| .dtsi | Device Tree Source Include | 文本(ASCII) | 公共设备树头文件,被 .dts 通过 #include 包含 |
| .dtb | Device Tree Blob | 二进制 | 编译后的设备树二进制文件,由 Bootloader 加载并传递给内核 |
3.4.1 dtsi 文件的作用
.dtsi 文件用于存放 SoC 级公共硬件描述,例如 CPU 核心、中断控制器、时钟控制器、通用外设等。不同板级 .dts 文件通过 #include 引入同一个 .dtsi,然后覆盖或添加板级特有的属性(如内存大小、外设状态)。
// soc-common.dtsi —— SoC 级公共描述
/ {
compatible = "vendor,soc-v1";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
};
intc: interrupt-controller@10400000 {
compatible = "arm,gic-400";
reg = <0x10400000 0x10000>;
interrupt-controller;
#interrupt-cells = <3>;
};
clk_can: clock-controller@10500000 {
compatible = "vendor,can-clock";
reg = <0x10500000 0x1000>;
#clock-cells = <0>;
clock-frequency = <40000000>;
};
};
3.4.2 编译流程:从 dts 到 dtb
设备树编译器(Device Tree Compiler,DTC)将 .dts 源文件编译为 .dtb 二进制文件。编译命令如下:
# 编译单个 dts 文件
dtc -I dts -O dtb -o gateway.dtb gateway.dts
# 反编译 dtb 为 dts(调试用)
dtc -I dtb -O dts -o gateway_decompiled.dts gateway.dtb
# 在内核构建系统中自动编译
make dtbs
编译流程:
.dts + .dtsi → 预处理(展开 #include) → DTC 编译 → .dtb 二进制文件。
3.5 设备树编译与加载流程 SVG 图
下图展示了从设备树源文件到内核解析的完整数据流:
3.6 设备树与驱动匹配机制
当内核启动时,设备树解析子系统(drivers/of/)完成以下关键步骤:
- dtb 解析: 内核从 Bootloader 传递的寄存器地址(
r2或设备树地址)读取 dtb 数据,调用unflatten_device_tree()将其展开为内存中的device_node树结构。 - 平台设备创建: 对于
compatible = "simple-bus"的总线节点,内核遍历其子节点,调用of_platform_populate()为每个外设节点创建platform_device。 - 驱动匹配: 当驱动注册时,内核将驱动的
of_match_table中的compatible字符串与设备节点的compatible属性进行匹配。匹配成功则调用驱动的probe()函数。
// 驱动中典型的 of_match_table 定义
static const struct of_device_id can_of_match[] = {
{ .compatible = "vendor,can-controller", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, can_of_match);
static struct platform_driver can_driver = {
.probe = can_probe,
.remove = can_remove,
.driver = {
.name = "gateway-can",
.of_match_table = can_of_match,
},
};
module_platform_driver(can_driver);
匹配优先级: 设备树匹配优先于传统的
platform_device.name 匹配。如果驱动同时提供了 of_match_table 和 id_table,内核会优先使用设备树匹配路径。
3.7 本章小结
| 知识点 | 核心要点 |
|---|---|
| 设备树起源 | 解决板级文件硬编码问题,实现内核与硬件描述解耦 |
| 语法结构 | 树形层级:根节点 → 总线节点 → 外设节点;属性描述配置 |
| 节点与属性 | compatible 驱动匹配、reg 地址空间、interrupts 中断、status 启用状态 |
| 文件格式 | .dts 源文件 → .dtsi 头文件 → DTC 编译 → .dtb 二进制 |
| 加载流程 | Bootloader 加载 dtb → 内核解析 → 创建 platform_device → 驱动匹配 probe |