2. 设备树语法入门:节点、属性、标签、路径
设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形层次结构将系统中的CPU、内存、总线、外设等硬件信息组织起来。对于I2C设备驱动开发者而言,掌握设备树的基本语法是编写正确设备节点的前提。本节将系统讲解设备树的四个核心语法要素:节点(node)、属性(property)、标签(label)和路径(path)。
学习提示:设备树语法与C语言结构体初始化有相似之处,但更强调层次关系和硬件地址描述。建议在学习过程中对照实际硬件手册进行练习。
2.1 节点(node)—— 设备树的骨架
节点是设备树的基本组成单元,每个节点代表一个硬件设备、总线或控制器。节点通过花括号 { } 定义,内部可以包含属性和子节点。节点名称通常采用 名称@地址 的格式,其中地址部分为寄存器基地址或设备地址。
节点定义格式:
[label:] node-name[@unit-address] {
[property-list]
[child-node-list]
};
以下是一个典型的I2C控制器节点示例:
i2c@1c2b000 {
compatible = "allwinner,sun8i-h3-i2c";
reg = <0x01c2b000 0x400>;
interrupts = <0 7 4>;
clocks = <&ccu CLK_BUS_I2C0>;
#address-cells = <1>;
#size-cells = <0>;
/* 子节点:挂载在I2C总线上的设备 */
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
2.2 属性(property)—— 描述设备特征
属性是附加在节点上的键值对,用于描述设备的各项特征。属性值可以是字符串、整数、整数数组、字节数组或字符串列表。常用的属性类型如下表所示:
| 属性类型 | 语法格式 | 示例 | 说明 |
|---|---|---|---|
| 字符串 | property = "string"; |
compatible = "atmel,24c02"; |
用于标识设备兼容性 |
| 32位整数 | property = <value>; |
reg = <0x50>; |
I2C从设备地址 |
| 整数数组 | property = <val1 val2 ...>; |
reg = <0x1c2b000 0x400>; |
地址+长度组合 |
| 布尔值 | property; (无值) |
status = "disabled"; |
存在即表示真 |
| 字节数组 | property = [hex-bytes]; |
local-mac-address = [00 11 22 33 44 55]; |
MAC地址等二进制数据 |
| 字符串列表 | property = "str1", "str2"; |
clock-names = "core", "bus"; |
多个字符串组合 |
常见错误:I2C设备节点中
reg 属性必须与 #address-cells 和 #size-cells 的设定一致。对于7位I2C地址,通常使用 #address-cells = <1>; #size-cells = <0>;,此时 reg = <0x50>; 表示从设备地址为0x50。
2.3 标签(label)—— 节点的别名
标签是节点的引用标识符,用于在设备树的其他位置或驱动代码中引用该节点。标签定义在节点名称之前,使用冒号分隔。通过标签可以避免硬编码路径,提高代码的可维护性。
// 定义标签
&i2c0: i2c@1c2b000 {
status = "okay";
};
// 在其他位置通过 & 引用标签
&i2c0 {
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
标签在驱动代码中的使用方式:
/* 内核驱动中通过标签引用节点 */
struct device_node *np = of_find_node_by_path("/soc/i2c@1c2b000");
/* 或使用标签对应的别名 */
struct device_node *np = of_find_node_by_name(NULL, "i2c0");
2.4 路径(path)—— 节点的定位符
路径是从根节点到目标节点的完整定位字符串,使用斜杠分隔各级节点名称。路径是设备树中唯一标识一个节点的方式,常用于驱动代码中查找特定设备节点。
路径格式:
/soc/i2c@1c2b000/eeprom@50
上述路径表示:根节点 → soc节点 → i2c@1c2b000节点 → eeprom@50节点
路径与标签的对比:
| 特性 | 路径(Path) | 标签(Label) |
|---|---|---|
| 表示方式 | /soc/i2c@1c2b000 |
&i2c0 |
| 唯一性 | 全局唯一 | 全局唯一(需手动定义) |
| 可读性 | 完整但冗长 | 简洁易记 |
| 修改影响 | 节点移动后路径失效 | 标签不变,引用自动更新 |
| 驱动中使用 | of_find_node_by_path() |
of_find_node_by_name() |
内核驱动中通过路径查找节点的典型代码:
#include <linux/of.h>
#include <linux/i2c.h>
static int my_i2c_probe(struct i2c_client *client)
{
struct device_node *np;
const char *compatible;
u32 reg_val;
/* 方法1:通过路径查找节点 */
np = of_find_node_by_path("/soc/i2c@1c2b000/eeprom@50");
if (!np) {
dev_err(&client->dev, "无法找到设备节点\n");
return -ENODEV;
}
/* 方法2:通过别名查找(需要设备树中定义别名) */
np = of_find_node_by_name(NULL, "eeprom");
/* 读取属性 */
if (of_property_read_u32(np, "reg", ®_val) == 0) {
dev_info(&client->dev, "I2C地址: 0x%02x\n", reg_val);
}
of_node_put(np);
return 0;
}
2.5 综合示例:完整的I2C设备树节点
以下是一个包含温度传感器和EEPROM的完整I2C设备树节点示例,综合运用了节点、属性、标签和路径四种语法要素:
/dts-v1/;
#include "sun8i-h3.dtsi"
/ {
model = "My I2C Test Board";
compatible = "mycompany,myboard", "allwinner,sun8i-h3";
soc {
/* I2C控制器节点,使用标签方便引用 */
&i2c0: i2c@1c2b000 {
compatible = "allwinner,sun8i-h3-i2c";
reg = <0x01c2b000 0x400>;
interrupts = <0 7 4>;
clocks = <&ccu CLK_BUS_I2C0>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* 温度传感器子节点 */
temperature@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&pio>;
interrupts = <6 10 2>; /* PG10, falling edge */
#thermal-sensor-cells = <1>;
};
/* EEPROM子节点 */
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
read-only;
};
};
};
};
2.6 常见语法错误与调试技巧
在编写设备树节点时,以下错误最为常见,需要特别注意:
- 缺少分号:每个属性定义和节点结束都必须以分号结尾
- 地址格式错误:I2C地址应为7位或10位,且与
#address-cells匹配 - 标签重复:同一设备树文件中标签名必须唯一
- 路径大小写:设备树节点名称区分大小写
调试建议:使用
dtc -I dts -O dtb -o output.dtb input.dts 编译设备树,通过编译错误信息定位语法问题。编译后的DTB文件可以使用 fdtdump 工具查看实际结构。