驱动移植实战:外设内存映射与设备树适配方法详解 - 第3节
3.1 设备树的起源与设计动机
在早期的嵌入式 Linux 系统中,板级硬件信息(如内存基地址、中断号、GPIO 映射)被硬编码在 arch/arm/mach-xxx 下的 C 语言板文件中。每更换一款 SoC 或外设,就必须重新编译内核,导致内核源码中充斥着大量平台相关的“垃圾代码”。
为了解决这一痛点,PowerPC 社区率先引入了 设备树(Device Tree) 概念,随后被 ARM、RISC-V 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化文本形式独立存在,内核在启动时解析该描述动态生成设备节点。
3.1.1 设备树在嵌入式系统中的位置
设备树位于 Bootloader(如 U-Boot)与 Linux 内核之间,充当硬件信息的“桥梁”。
3.2 DTS / DTC / DTB 文件格式
设备树涉及三种文件形态,理解它们的转换关系是入门的第一步。
| 文件类型 | 扩展名 | 格式 | 作用 |
|---|---|---|---|
| DTS | .dts |
文本(ASCII) | 设备树源文件,开发者编写硬件描述 |
| DTC | — | 编译器工具 | 将 DTS 编译为 DTB |
| DTB | .dtb |
二进制(FDT 格式) | 内核启动时直接解析的二进制设备树 |
| DTSI | .dtsi |
文本(ASCII) | 设备树包含文件,用于 SoC 级公共描述 |
3.2.1 编译流程
DTS 通过 DTC 工具编译为 DTB,也可以反编译 DTB 为 DTS 进行调试。
dtc -I dtb -O dts -o dump.dts /sys/firmware/fdt 反编译当前运行的 DTB,快速验证设备树是否被正确加载。
3.3 设备树语法入门
设备树语法基于节点(node)和属性(property),采用树形层级结构描述硬件拓扑。以下是最核心的语法规则。
3.3.1 基本结构
一个完整的 DTS 文件包含一个根节点 /,其下挂载若干子节点,每个节点可以包含属性和子节点。
/dts-v1/;
/ {
compatible = "vendor,board-name";
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x00 0x100>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB RAM */
};
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
3.3.2 节点命名规则
节点名称格式为 node-name@unit-address,其中 @unit-address 通常对应设备在总线上的起始地址。
| 示例 | 说明 |
|---|---|
cpu@0 |
CPU 核心 0,地址为 0 |
serial@10000000 |
串口外设,基地址 0x10000000 |
i2c@20000000 |
I2C 控制器,基地址 0x20000000 |
3.3.3 常用属性
- compatible:驱动匹配的关键字符串,格式为
"厂商,设备型号",内核通过该属性查找对应驱动。 - reg:描述设备地址空间,格式为
<address length>,地址和长度由#address-cells和#size-cells决定。 - interrupts:描述中断号、触发类型,格式依赖
interrupt-parent和#interrupt-cells。 - status:设备状态,常用值
"okay"(启用)或"disabled"(禁用)。
3.3.4 节点引用与标签
使用 标签名: 节点名@地址 定义标签,后续通过 &标签名 引用节点,实现追加或修改属性。
// 定义标签
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x1000>;
status = "disabled";
};
// 在其他地方引用并修改
&uart0 {
status = "okay";
clock-frequency = <24000000>;
};
/dts-v1/;,会导致 DTC 编译失败。另外,reg 属性的地址和长度必须与 #address-cells / #size-cells 严格对齐,否则内核解析会出错。
3.4 设备树层级关系与地址映射
设备树的树形结构天然反映了 SoC 内部的总线拓扑。根节点代表整个板级系统,子节点代表挂载在总线上的外设。
3.5 设备树与驱动的绑定机制
内核通过 compatible 属性将设备树节点与驱动进行匹配。当驱动注册时,内核遍历设备树,查找 compatible 值匹配的节点,并调用驱动的 probe 函数。
3.5.1 匹配流程
- 内核启动时,解析 DTB 构建
device_node树。 - 总线驱动(如 platform bus)扫描设备树,为每个节点创建
platform_device。 - 驱动注册时,内核比较驱动的
of_match_table与节点的compatible值。 - 匹配成功则调用驱动的
probe函数,并将设备树中的资源(reg、interrupts 等)传递给驱动。
// 驱动中的匹配表示例
static const struct of_device_id my_driver_of_match[] = {
{ .compatible = "vendor,my-device" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_driver_of_match,
},
};
module_platform_driver(my_driver);
compatible 字符串必须与驱动中 of_match_table 的字符串完全一致(包括大小写和厂商前缀),否则驱动无法绑定。
3.6 本章小结
设备树是嵌入式 Linux 驱动开发中不可或缺的硬件描述语言。通过本章的学习,你应该掌握:
- 设备树解决了板级代码硬编码问题,实现了内核与硬件描述的分离。
- DTS 是文本源文件,DTC 是编译器,DTB 是内核解析的二进制文件。
- 设备树语法以节点和属性为核心,
compatible、reg、interrupts是最常用的属性。 - 驱动通过
of_match_table与设备树节点进行匹配绑定。