3. 设备树语法精讲(一):节点(node)与属性(property)的定义规则
好,咱们今天正式进入设备树语法的核心部分。说实话,我刚开始接触设备树时,觉得这东西不就是个配置文件嘛,有啥难的?结果第一次写就踩了坑——一个分号没加,内核直接不认我的设备。从那以后,我对语法规则就格外上心了。
设备树说白了,就是用一种树形结构来描述硬件。每个硬件设备都是一个节点,节点下面挂属性。嗯,就这么简单。但简单归简单,规则你得搞清楚。
3.1 节点的定义规则
节点是设备树的基本组成单元。每个节点代表一个硬件设备或总线。来看个最基础的例子:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard";
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
这里有几个关键点:
- 根节点:用
/表示,是整个设备树的起点 - 子节点:如
cpu@0、memory@80000000 - 节点名格式:
名称@地址,地址部分可选
节点命名规范:
- 名称只能包含小写字母、数字、逗号、点、下划线、连字符
- 地址部分用十六进制,不带
0x前缀 - 同一层级下,节点名必须唯一
我个人习惯把节点名写得尽量简洁。比如 uart@1e000000 就比 serial-uart@1e000000 清爽得多。你想想看,内核解析时只看 compatible 属性,节点名其实只是个标签。
3.2 属性的定义规则
属性就是挂在节点上的键值对。值可以是字符串、数字、数组,或者它们的组合。我见过不少新手在属性值格式上栽跟头,咱们一个个说清楚。
3.2.1 字符串属性
compatible = "vendor,device";
model = "My Custom Board";
status = "okay";
字符串用双引号括起来。多个字符串可以用逗号分隔,但实际是多个值:
compatible = "vendor,device-v2", "vendor,device-v1";
内核匹配驱动时,会从左到右依次尝试。我在项目中就遇到过,驱动只支持 v1,但设备树写了 v2 在前,结果匹配失败。所以顺序很重要,最匹配的放最前面。
3.2.2 数字属性
reg = <0x10000000 0x1000>;
interrupts = <0 29 4>;
数字用尖括号 <> 括起来,多个数字用空格分隔。注意:
- 数字默认是 32 位,大端序
- 可以用
0x前缀表示十六进制 - 可以用
表示无符号数
小技巧:如果你需要 64 位数字,用两个 32 位数字拼起来。比如地址 0x100000000 写成 <0x1 0x00000000>。嗯,这确实有点绕,但设备树就是这么设计的。
3.2.3 布尔属性
布尔属性比较特殊——它不需要值。存在就表示 true,不存在就是 false:
device-tree-identifier;
/* 等价于 device-tree-identifier = <1>; 但更简洁 */
我曾经在调试一个网卡驱动时,发现设备死活不工作。查了半天,原来是少了个 non-removable 属性。加上就好了。这种空属性最容易漏掉,写设备树时得多留个心眼。
3.2.4 字节数组属性
local-mac-address = [00 11 22 33 44 55];
字节数组用方括号 [] 括起来,每个字节用两位十六进制表示,空格分隔。这个在描述 MAC 地址、UUID 时很常用。
3.3 节点与属性的组合规则
节点可以嵌套,属性可以混合类型。来看个复杂点的例子:
i2c@1c00000 {
compatible = "vendor,i2c-controller";
reg = <0x1c00000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <16>;
};
rtc@68 {
compatible = "dallas,ds1307";
reg = <0x68>;
};
};
这里有几个要点:
#address-cells和#size-cells定义了子节点reg属性的格式- 子节点的
reg只包含地址,没有大小(因为#size-cells = <0>) - 每个子节点必须有唯一的
reg值
注意:#address-cells 和 #size-cells 只影响直接子节点,不会向下传递。我见过有人以为设了根节点的就能管到所有节点,结果地址解析全乱了。
3.4 标签与引用
设备树支持用标签来引用其他节点。这个功能我特别喜欢,能让设备树结构清晰很多:
&uart1 {
status = "okay";
clock-frequency = <115200>;
};
这里的 &uart1 就是引用。你可以在其他地方定义节点,然后通过标签来修改或扩展它。这在处理多个板级变体时特别有用。
标签的定义很简单:
uart1: serial@1e000000 {
compatible = "vendor,uart";
reg = <0x1e000000 0x100>;
};
uart1: 就是标签,后面跟节点名。引用时用 &uart1。
3.5 常见错误与避坑指南
写设备树这么多年,我总结了几条最容易犯的错:
- 分号漏了:每个属性结尾必须有分号,包括最后一个。我刚开始时经常漏,编译报错才想起来。
- 逗号位置不对:字符串列表用逗号分隔,但数字列表用空格。混了就出问题。
- 地址格式错误:节点名中的地址不带
0x,但属性值中的地址要带。这个规则确实有点反直觉。 - 大小写搞混:设备树是大小写敏感的。
Memory和memory是两个不同的节点名。
我曾经在调试一个 SPI 设备时,怎么都读不到数据。折腾了两天,最后发现是 reg 属性的地址写错了——节点名里写的是 spi@100,但 reg 写成了 <0x200>。内核按 reg 去寻址,当然找不到设备。从那以后,我每次都会仔细核对节点名和 reg 是否一致。
3.6 小结
节点和属性是设备树的基石。说白了,你只要记住:
- 节点用
名称@地址命名,地址可选 - 属性有字符串、数字、布尔、字节数组四种基本类型
- 标签用
名称:定义,用&名称引用 - 分号、逗号、括号一个都不能少
嗯,掌握了这些,你就已经能看懂大部分设备树文件了。下一节咱们会深入讲 reg 属性的地址映射规则,那个才是真正考验人的地方。到时候我会分享一个我调试 PCIe 设备时遇到的地址解析问题,保证让你印象深刻。