设备树基本语法剖析:节点、属性与 compatible 匹配机制
设备树基本语法剖析:节点、属性与 compatible 匹配机制

在 Linux 3.x 之前的老版本内核中,ARM 架构的板级硬件描述全部以 C 语言结构体的形式硬编码在 arch/arm/mach-xxx/board-xxx.c 文件中。随着 ARM 芯片种类爆发式增长,内核源码中充斥着海量重复的板级垃圾代码。Linus Torvalds 当年在邮件列表中怒斥这一混乱现状,随后 Linux 社区全面引入了从 Open Firmware 体系借鉴而来的**设备树(Device Tree)**机制。
设备树将“硬件拓扑描述”与“操作系统驱动源码”进行了物理层面的彻底解耦。通过文本格式的设备树源文件(.dts / .dtsi),内核在启动时能够动态解析出整块板卡的硬件全貌。
本文深入剖析设备树的树状数据结构、标准节点属性语法,以及驱动与硬件之间的核心纽带——compatible 匹配状态机。
设备树的树状层次拓扑与标准节点语法
设备树在逻辑上是一棵树(Tree),有且仅有一个根节点 /。每一个外设(如 I2C 控制器、GPIO、网卡、传感器)都被表示为树上的一个节点(Node),节点内部包含若干键值对形式的属性(Properties)。
一个标准的设备树语法骨架如下:
/dts-v1/;
/ {
// 根节点定义
model = "Industrial Edge AI Gateway Box";
compatible = "custom,edgebox-v1", "rockchip,rk3568";
#address-cells = <2>;
#size-cells = <2>;
// 1. 静态内存节点
memory@0 {
device_type = "memory";
reg = <0x0 0x00000000 0x0 0x80000000>; // 2GB 内存:基地址 0x0,大小 2GB
};
// 2. 片上总线控制器节点 (SOC Internal Bus)
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges; // 表示子总线地址与父总线地址 1:1 映射
// I2C 控制器硬件外设
i2c1: i2c@fe5a0000 {
compatible = "rockchip,rk3399-i2c";
reg = <0xfe5a0000 0x1000>; // 物理寄存器基地址与长度 (4KB)
interrupts = <0 32 4>; // 中断号与触发类型
clocks = <&cru 120>; // 关联时钟源
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
// 挂载在 I2C1 总线上的从机传感器子节点
temp_sensor: sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>; // I2C 7位从机物理地址
status = "okay";
};
};
};
};
核心属性的物理寻址语义(#address-cells 与 #size-cells)
设备树中最容易让初学者产生混淆的,是 #address-cells 与 #size-cells。
这两个属性定义了其子节点的 reg 属性中,分别用几个 32 位整数(u32 单元)来表示物理基地址(Address)和地址长度(Length):
寻址规则推演:
在父节点 soc 中:
- #address-cells = <1>; ---> 子节点的 reg 属性中,用 1 个 32 位数表示地址
- #size-cells = <1>; ---> 子节点的 reg 属性中,用 1 个 32 位数表示长度
因此子节点 i2c@fe5a0000 的 reg 写法为:
reg = <0xfe5a0000 0x1000>; (基地址 0xfe5a0000,长度 0x1000)
而在 i2c1 控制器节点中 (作为父节点):
- #address-cells = <1>; ---> I2C 从机地址用 1 个数表示
- #size-cells = <0>; ---> I2C 从机没有地址长度概念 (长度占用 0 个 cells)
因此子节点 sensor@48 的 reg 写法为:
reg = <0x48>; (仅需写一个 0x48 从机地址即可!)
驱动与设备的灵魂纽带:compatible 匹配机制
Linux 内核中的 platform_bus 平台总线通过 compatible 属性建立驱动与硬件的握手。
compatible 属性是一个由逗号分隔的字符串列表,遵循 "厂商名,芯片型号" 的规范。列表按照**从最具体到最通用(From most specific to most general)**的顺序排列:
// 示例: 兼容特定板卡,同时向后兼容通用驱动
compatible = "custom,opt-sensor-v2", "custom,opt-sensor-v1", "generic-i2c-sensor";
在驱动程序源码中,开发者通过 of_device_id 结构体数组声明自己支持的硬件列表:
#include <linux/of.h>
#include <linux/platform_device.h>
static const struct of_device_id my_sensor_match_table[] = {
{ .compatible = "custom,opt-sensor-v2", .data = (void *)MODEL_V2 },
{ .compatible = "custom,opt-sensor-v1", .data = (void *)MODEL_V1 },
{ /* 哨兵结束符 */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_match_table);
static struct platform_driver my_sensor_driver = {
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.driver = {
.name = "my_opt_sensor_drv",
.of_match_table = my_sensor_match_table,
},
};
module_platform_driver(my_sensor_driver);
内核 matching 状态机执行流
当内核启动遍历设备树生成 platform_device 时:
- 内核取出设备节点的
compatible字符串列表中的第一个字符串("custom,opt-sensor-v2"); - 扫描已注册的驱动链表,如果找到匹配的驱动,匹配成功!调用驱动的
probe()并传入私有数据(MODEL_V2); - 如果未找到,继续尝试第二个字符串(
"custom,opt-sensor-v1"),直到完全匹配或全部失败。
理解了设备树的树状结构、单元单元划分与 compatible 优先级匹配,才能在 Linux BSP 移植与外设驱动开发中驾轻就熟。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)