1. 设备树基础:什么是设备树(DTS)、为什么需要设备树、设备树在Linux内核中的角色
1.1 什么是设备树(Device Tree)
设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点来记录系统中所有可访问的设备、总线、中断控制器、时钟、GPIO、内存地址等硬件信息。设备树源文件(.dts)经过编译生成设备树二进制文件(.dtb),在系统启动时由引导加载程序传递给内核。
核心定义:设备树是一个硬件描述文件,它将板级硬件细节从内核代码中剥离,使同一份内核镜像可以支持多种硬件平台。
一个典型的设备树节点结构如下:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
device_type = "cpu";
reg = <0x0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
uart0: serial@44e09000 {
compatible = "ti,am3352-uart", "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
status = "okay";
};
};
术语速查:DTS(Device Tree Source)→ 设备树源文件;DTB(Device Tree Blob)→ 设备树二进制文件;DTC(Device Tree Compiler)→ 设备树编译器;DTBO(Device Tree Blob Overlay)→ 设备树覆盖二进制文件。
1.2 为什么需要设备树
在设备树出现之前,Linux 内核使用大量的板级文件(board-*.c)来注册硬件信息。这种方式导致内核代码膨胀、维护困难、不同板卡之间代码重复严重。设备树的引入解决了以下核心问题:
| 对比维度 | 传统板级文件方式 | 设备树方式 |
|---|---|---|
| 硬件描述位置 | 硬编码在 C 代码中 | 独立的外部描述文件 |
| 内核镜像通用性 | 每块板卡需编译专用内核 | 同一内核镜像适配多板 |
| 硬件变更代价 | 修改内核源码并重新编译 | 仅修改 DTS 文件并重新编译 |
| 代码维护成本 | 高,大量重复代码 | 低,结构清晰可复用 |
| 热插拔支持 | 不支持动态添加设备 | 支持 Overlay 动态加载 |
| 社区主流架构 | ARM 32 位早期版本 | ARM64、RISC-V、x86 等 |
关键转折:Linux 内核主线从 2011 年(v3.0 左右)开始强制要求 ARM 架构使用设备树,拒绝接受新的板级文件。这一决策彻底改变了嵌入式 Linux 的硬件适配方式。
设备树带来的直接收益包括:
- 内核与硬件解耦:硬件描述独立于内核代码,板卡厂商只需提供 DTS 文件。
- 二进制兼容:同一份
zImage配合不同的.dtb即可启动不同硬件。 - 社区协作效率:SoC 核心 IP 的 DTSI 文件可复用,板级差异仅体现在 DTS 中。
- 动态覆盖能力:通过 Overlay 机制,支持运行时添加/移除设备,为热插拔驱动奠定基础。
1.3 设备树在Linux内核中的角色
设备树在内核启动和运行过程中扮演着“硬件数据库”的角色,其生命周期贯穿整个系统运行过程:
- 启动阶段:Bootloader(如 U-Boot)将 DTB 加载到内存,并将 DTB 物理地址传递给内核。
- 内核早期初始化:内核解压后,
setup_arch()函数解析 DTB,建立内存映射、CPU 拓扑、中断控制器等基础信息。 - 驱动匹配与设备注册:内核遍历 DTB 中的节点,通过
compatible属性匹配驱动,自动创建设备对象。 - 运行时资源查询:驱动通过 OF(Open Firmware)API 读取 DTB 中的寄存器地址、中断号、DMA 通道、时钟频率等资源。
- 动态覆盖(Overlay):支持在系统运行后加载 DTBO 文件,动态添加或修改设备节点,实现热插拔外设驱动。
核心角色总结:设备树是内核与硬件之间的“翻译官”,它将硬件描述标准化、外部化、动态化,使 Linux 内核能够以统一的方式管理千差万别的嵌入式硬件。
1.4 设备树的核心数据结构
内核中使用 struct device_node 和 struct property 两个核心结构体来管理设备树:
// 设备节点结构体
struct device_node {
const char *name; // 节点名称
const char *type; // device_type 属性
phandle phandle; // 节点句柄
const char *full_name; // 完整路径名
struct property *properties; // 属性链表
struct device_node *parent; // 父节点
struct device_node *child; // 子节点
struct device_node *sibling; // 兄弟节点
...
};
// 属性结构体
struct property {
char *name; // 属性名
int length; // 属性值长度
void *value; // 属性值指针
struct property *next; // 下一个属性
...
};
关键 API 速查:驱动开发中常用的 OF API 包括
of_find_node_by_path()、of_property_read_u32()、of_iomap()、irq_of_parse_and_map()、of_get_named_gpio() 等,这些函数构成了驱动与设备树之间的桥梁。
1.5 设备树与驱动匹配机制
Linux 内核通过 compatible 属性实现设备与驱动的匹配。匹配过程遵循以下优先级:
| 匹配方式 | 匹配依据 | 示例 | 优先级 |
|---|---|---|---|
| compatible 精确匹配 | 驱动 of_match_table 中的第一个 compatible 值 | "ti,am3352-uart" |
最高 |
| compatible 兼容匹配 | 驱动 of_match_table 中的后续 compatible 值 | "ti,omap3-uart" |
次高 |
| 设备名称匹配 | 节点 name 属性与驱动 id_table 匹配 | "serial" |
中 |
| 设备类型匹配 | 节点 device_type 属性匹配 | "cpu" |
低 |
实战要点:编写设备树时,
compatible 属性应遵循“具体在前,通用在后”的原则。例如 "ti,am3352-uart", "ti,omap3-uart",这样既能精确匹配特定型号,又能兼容同系列驱动。