3、设备树文件结构:DTS文件的语法结构、节点(node)、属性(property)、值(value)的规则与写法
设备树(Device Tree)本质上是一种描述硬件拓扑与资源配置的树形数据结构,其源文件(DTS)采用类似 C 语言的声明式语法。理解 DTS 的语法结构、节点(node)、属性(property)与值(value)的规则,是编写、修改和排查设备树问题的基本功。本章将从语法骨架出发,逐层拆解节点与属性的组织方式,并结合实际案例说明如何正确书写和校验设备树片段。
3.1 DTS 语法骨架与词法规则
DTS 文件采用树形嵌套的括号结构,整体以分号(;)作为语句结束符,以花括号({})界定节点范围。一个最基本的 DTS 文件由根节点“/”开始,内部包含若干子节点,每个子节点又可以继续嵌套。语法上,DTS 与 C 语言有相似之处,支持 /* */ 块注释和 // 行注释,也支持宏定义与 #include 预处理指令(通常配合 .dtsi 头文件使用)。
核心语法骨架如下:
/ {
model = "Vendor Board Rev2";
compatible = "vendor,board-rev2", "vendor,board";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
reg = <0x0>;
clock-frequency = <1200000000>;
};
};
memory@0 {
device_type = "memory";
reg = <0x0 0x40000000>;
};
soc {
#address-cells = <1>;
#size-cells = <1>;
i2c@1000 {
compatible = "vendor,i2c";
reg = <0x1000 0x100>;
clock-frequency = <100000>;
};
};
};
词法层面需要特别注意:节点名和属性名区分大小写,且不能包含空格、逗号、括号等特殊字符;字符串值必须使用双引号包裹;32 位整数数组使用尖括号 <> 包裹,多个值用空格分隔;字节串使用方括号 [] 包裹,每个字节为两位十六进制数。DTS 编译器(DTC)对格式要求严格,任何括号不匹配、分号缺失或引号未闭合都会导致编译失败。
核心原理:DTS 的语法本质是“声明式”的——它只描述硬件“有什么、在哪里、怎么配置”,而不描述“如何操作”。操作系统内核通过解析 DTS 生成设备模型,驱动再根据 compatible 属性匹配对应的驱动代码。因此,DTS 的语法结构直接决定了内核看到的硬件拓扑。
3.2 节点(node)的规则与写法
节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线、控制器或抽象层次。节点可以嵌套,形成父子关系,反映硬件拓扑的从属结构。节点名称由两部分组成:节点名(node-name)和可选的单元地址(unit-address),格式为 node-name@unit-address。
节点命名规则如下:
- 节点名:必须为 1~31 个字符,只能包含小写字母、数字、逗号、点、下划线和连字符,且必须以字母或数字开头。推荐使用小写字母和数字,避免使用下划线开头。
- 单元地址:用于区分同一层级下同类型的不同实例,通常对应设备在总线上的地址(如 I2C 从机地址、寄存器基地址)。单元地址前必须加
@符号,且只能包含十六进制数字(0-9, a-f)。 - 根节点:固定为
/,是整个设备树的起点,没有父节点。 - 标签(label):节点前可以加标签,格式为
label: node-name@unit-address,标签用于在 DTS 其他位置引用该节点(通过&label语法)。
例如,一个 I2C 控制器节点可以写为:
&i2c0 {
status = "okay";
clock-frequency = <400000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
这里 eeprom@50 表示该 EEPROM 设备在 I2C 总线上的从机地址为 0x50。节点名 eeprom 是通用设备名,@50 是单元地址。注意,同一父节点下,节点名+单元地址的组合必须唯一,否则 DTC 会报重复节点错误。
3.3 属性(property)与值(value)的规则
属性是节点的“键值对”,用于描述设备的特性、配置参数、资源映射等。属性名必须为 1~31 个字符,只能包含小写字母、数字、逗号、点、下划线和连字符,且必须以字母或数字开头。属性值可以是空(表示布尔真)、字符串、32 位无符号整数数组、字节串,或这些类型的组合。
常见的属性值类型及写法如下表:
| 值类型 | 语法格式 | 示例 | 典型用途 |
|---|---|---|---|
| 空值(布尔) | 仅属性名,无值 | status = "okay"; 或 disable-rtc; |
表示功能开关或标志位 |
| 字符串 | 双引号包裹 | compatible = "vendor,device"; |
设备兼容性标识、型号名称 |
| 32 位整数数组 | 尖括号包裹,空格分隔 | reg = <0x1000 0x100>; |
寄存器地址、长度、中断号、时钟频率 |
| 字节串 | 方括号包裹,十六进制字节 | local-mac-address = [00 11 22 33 44 55]; |
MAC 地址、序列号、固件版本 |
| 混合类型 | 使用逗号分隔不同值块 | gpios = <&gpio1 2 0>, <&gpio2 5 0>; |
GPIO 描述、多资源引用 |
其中,compatible 属性是最关键的属性之一,它定义了设备与驱动的匹配关系。其值通常为“厂商,型号”格式的字符串列表,内核会按顺序与驱动注册的 compatible 表进行匹配。例如:
compatible = "vendor,ethernet", "simple-bus";
这表示该设备首先尝试匹配 vendor,ethernet 驱动,若失败则尝试 simple-bus 驱动。字符串列表按优先级从高到低排列。
3.4 标准属性与地址编码规则
设备树规范定义了一些标准属性,用于描述设备资源映射和中断信息。最核心的是 reg 属性,它配合父节点的 #address-cells 和 #size-cells 属性共同解释地址和长度信息。
#address-cells 表示地址字段占用的 32 位整数个数,#size-cells 表示长度字段占用的 32 位整数个数。例如:
soc {
#address-cells = <1>;
#size-cells = <1>;
uart@2000 {
reg = <0x2000 0x100>;
// 地址 = 0x2000,长度 = 0x100
};
};
对于 64 位地址的系统,#address-cells 可以设为 2,此时 reg 中的地址字段需要两个 32 位整数拼接表示。例如:
memory@0 {
device_type = "memory";
reg = <0x0 0x0 0x80000000>;
// 64位地址 = 0x00000000_00000000,长度 = 0x80000000
};
中断属性 interrupts 的格式由父节点的 #interrupt-cells 决定。对于 ARM GIC(通用中断控制器),通常 #interrupt-cells = <3>,三个字段分别表示中断类型(0=SPI 共享外设中断,1=PPI 私有外设中断)、中断号、触发类型(1=上升沿,4=高电平)。
经验提醒:在编写 reg 属性时,务必确认父节点的 #address-cells 和 #size-cells 值。如果父节点未定义这两个属性,默认值为 1。地址和长度字段的个数不匹配,会导致 DTC 编译警告,且内核解析时可能得到错误的资源地址。
3.5 案例拆解:从零编写一个 I2C EEPROM 节点
下面通过一个完整案例,演示如何在实际项目中编写和校验设备树节点。假设某嵌入式板卡上有一颗 I2C EEPROM(型号 AT24C02),挂接在 SoC 的 I2C0 控制器上,I2C0 的寄存器基地址为 0x1000,地址空间长度 0x100。系统使用 ARM GIC 中断控制器,I2C0 使用 SPI 中断号 25,高电平触发。
初始现象与目标:内核启动后无法识别 EEPROM 设备,i2cdetect 工具扫描不到 0x50 地址。需要编写正确的设备树节点,使内核 I2C 子系统能够枚举到该设备。
关键输入参数:
- SoC 数据手册:I2C0 基地址 0x1000,中断号 25(SPI),触发类型高电平(4)
- EEPROM 数据手册:I2C 从机地址 0x50(7 位地址),页大小 8 字节
- 原理图:EEPROM 的 WP 引脚接 GND(写保护禁用),A0/A1/A2 接地(地址 0x50)
分析判断过程:首先确认 I2C0 控制器节点是否已存在于 SoC 的 .dtsi 文件中。通常 SoC 厂商会提供基础 .dtsi,其中已定义 i2c0 节点及其寄存器、中断属性。我们只需在板级 .dts 文件中通过 &i2c0 引用并添加子节点即可。
实际操作步骤:
// 在板级 .dts 文件中
&i2c0 {
status = "okay";
clock-frequency = <400000>; // 提升到 400kHz 快速模式
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
read-only; // 如果 WP 引脚接高电平,则添加此属性
};
};
编译设备树后,内核启动日志中应能看到类似 i2c 0x50: at24c02 detected 的信息。使用 i2cdetect -y 0 命令,应能在 0x50 位置看到设备编号(如 50 或 UU)。
结果对比与复盘:如果仍然无法识别,需要检查以下几点:
- 确认
status = "okay"是否已设置(默认可能为"disabled") - 确认 I2C 控制器节点本身是否使能,以及引脚复用(pinmux)是否正确配置
- 使用
i2cdetect -r -y 0强制扫描,排除设备 NACK 问题 - 检查 EEPROM 的地址引脚是否与原理图一致,0x50 对应 A0=A1=A2=0
风险提示:在修改设备树时,不要随意更改 SoC 厂商 .dtsi 中已定义的标准节点属性(如 reg、interrupts),除非你完全理解其含义。错误修改可能导致整个外设控制器无法工作,甚至引发内核启动崩溃。正确的做法是在板级 .dts 中通过 &label 引用节点,仅覆盖需要修改的属性。
3.6 常见误区与总结复盘
在编写 DTS 文件时,初学者常犯以下几类错误,需要特别注意:
| 误区类型 | 错误示例 | 正确做法 | 后果 |
|---|---|---|---|
| 节点名大小写混用 | I2C@1000 |
i2c@1000 |
DTC 编译报错,或与驱动匹配失败 |
| reg 字段个数与父节点不匹配 | 父节点 #address-cells=<2>,子节点 reg = <0x1000> |
reg = <0x0 0x1000> |
地址解析错误,设备资源申请失败 |
| 字符串值未加引号 | compatible = vendor,device; |
compatible = "vendor,device"; |
语法错误,编译失败 |
| 整数数组使用逗号分隔 | reg = <0x1000, 0x100>; |
reg = <0x1000 0x100>; |
DTC 解析异常,值被截断 |
| 忘记设置 status 属性 | 节点未写 status |
显式设置 status = "okay" |
设备默认禁用,无法工作 |
总结复盘时,建议遵循以下编写流程:
- 查阅 SoC 数据手册,确认外设的寄存器基地址、中断号、时钟频率等硬件参数。
- 查看厂商
.dtsi,了解已有节点定义,避免重复定义或冲突。 - 在板级
.dts中引用并覆盖,使用&label语法添加子节点或修改属性。 - 编译并检查警告,使用
make dtbs编译,仔细阅读 DTC 输出的 warning 信息。 - 运行时验证,通过
/proc/device-tree或dtc -I fs -O dts /proc/device-tree导出实际生效的设备树,确认节点和属性与预期一致。
掌握 DTS 的语法结构、节点与属性的规则,是设备树开发的基础。下一阶段的学习应聚焦于 compatible 匹配机制、中断映射、GPIO 描述等进阶主题,但所有进阶内容都建立在本章所讲的语法骨架之上。建议读者在编写每个节点时,都对照本章的规则表逐项检查,形成严谨的编写习惯。