2. 设备树语法入门:节点、属性、标签、包含文件、注释规则
好,咱们正式开始啃设备树的语法。说实话,很多刚接触设备树的兄弟,第一反应是「这不就是个配置文件嘛」。嗯,从某种意义上说,它确实是。但你要真把它当普通配置文件写,后面调试起来会非常痛苦。
我个人习惯把设备树看作一块「硬件描述画板」。你在上面画节点、填属性、打标签,最后编译成二进制传给内核。内核拿到它,就知道板子上接了哪些外设、怎么初始化。
这一节,咱们把最核心的语法规则过一遍。不绕弯子,直接上干货。
2.1 节点:设备树的基本单元
节点就是设备树里的「文件夹」。每个节点代表一个硬件设备、一个总线、或者一个功能块。节点可以嵌套,形成树状结构。
/dts-v1/;
/ {
model = "Rockchip RK3588 EVB";
compatible = "rockchip,rk3588-evb";
cpu0: cpu@0 {
device_type = "cpu";
reg = <0x0>;
};
i2c0: i2c@ffd00000 {
compatible = "rockchip,rk3588-i2c";
reg = <0xffd00000 0x1000>;
status = "okay";
};
};
看到没?根节点 / 下面挂了两个子节点:cpu@0 和 i2c@ffd00000。节点名后面的 @ 跟的是地址,用来区分同类型设备。比如两个 I2C 控制器,地址不同,节点名就不同。
i2c@ffd00000。这样查手册时一眼就能对上,省得来回翻。
2.2 属性:描述节点的「形容词」
属性就是节点的配置项。每个属性都有名字和值。常用的属性类型就几种:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "rockchip,rk3588-i2c"; |
用于匹配驱动 |
| 32位整数 | reg = <0xffd00000 0x1000>; |
地址和大小 |
| 64位整数 | reg = <0x0 0xfe000000 0x0 0x200000>; |
高32位在前 |
| 布尔值 | status = "okay"; |
"okay" 或 "disabled" |
| 字符串列表 | compatible = "rockchip,rk3588-i2c", "snps,designware-i2c"; |
逗号分隔 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
十六进制字节 |
这里重点说一下 compatible。它是驱动匹配的关键。内核启动时,会遍历设备树节点,拿 compatible 的值去跟驱动注册的 of_match_table 比对。匹配上了,驱动就 probe。
compatible 时把厂商名和型号写反了,比如写成 "rk3588-i2c,rockchip"。内核匹配规则是「从左到右依次匹配」,顺序错了就匹配不上。驱动死活不 probe,查了两天才发现是这个问题。
2.3 标签:给节点起个「外号」
标签就是节点的别名。你看上面代码里的 cpu0: 和 i2c0:,冒号前面的就是标签。标签的作用是方便在其他地方引用这个节点。
&i2c0 {
clock-frequency = <400000>;
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
看到 &i2c0 了吗?这就是通过标签引用节点。在根节点里定义 i2c0: i2c@ffd00000,然后在其他地方用 &i2c0 来追加内容。这个机制非常灵活,尤其适合多板卡复用同一个 SoC 设备树的情况。
你想想看,如果不用标签,你想给 I2C0 加个 EEPROM,就得把整个 I2C0 节点复制一遍。用了标签,只需要写一段追加代码就行。
2.4 包含文件:别重复造轮子
设备树支持 #include 预处理指令。用法跟 C 语言一样。Rockchip 的 SDK 里,通常会把 SoC 级别的定义放在 .dtsi 文件里,板级差异放在 .dts 文件里。
// rk3588.dtsi
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/clock/rk3588-cru.h>
/ {
compatible = "rockchip,rk3588";
i2c0: i2c@ffd00000 {
compatible = "rockchip,rk3588-i2c";
reg = <0xffd00000 0x1000>;
interrupts = <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru CLK_I2C0>;
status = "disabled";
};
};
// my-board.dts
/dts-v1/;
#include "rk3588.dtsi"
&i2c0 {
status = "okay";
clock-frequency = <400000>;
};
这样设计的好处很明显:SoC 升级时,只需要改 rk3588.dtsi,板级文件基本不用动。我在项目里见过有人把所有东西都塞在一个 .dts 里,结果换了个 DDR 型号,改得满头大汗。
.dtsi,板级差异放 .dts。包含路径用 <> 表示系统搜索路径,用 "" 表示当前目录。
2.5 注释规则:三种方式随你选
设备树支持三种注释风格,跟 C 语言完全一致:
// 单行注释—— 最常用,我基本只用这个/* 多行注释 */—— 适合大段说明/** 文档注释 */—— 偶尔用来生成文档
// 这是单行注释,简单明了
/*
* 这是多行注释
* 适合写一段说明文字
* 比如解释为什么这个节点要这么配
*/
/**
* 文档注释风格
* 有些工具能解析这种格式
*/
我个人建议:日常开发用 // 就够了。多行注释容易忘记闭合,编译报错时找半天。嗯,别问我怎么知道的。
2.6 知识结构总览
下面这张图把设备树语法的核心要素串起来了。你可以把它当作「语法速查地图」:
2.7 避坑指南
最后,分享几个我实际项目中遇到的坑:
- 分号别漏:每个属性结尾必须有
;。漏一个,编译报错能报到你怀疑人生。我习惯写完一行就顺手敲分号。 - 逗号别多:字符串列表用逗号分隔,但最后一个后面不要加逗号。有些编译器会报 warning。
- 标签别重名:整个设备树里标签必须唯一。重名了,编译不会报错,但引用时会混乱。
- 地址对齐:
reg属性的地址和大小,必须跟#address-cells、#size-cells匹配。RK3588 默认是<2>和<2>,也就是 64 位地址。
reg,只写了 32 位地址 <0xffd00000 0x1000>,但父总线 #address-cells = <2>。结果内核解析出来的地址完全不对,I2C 读写全部失败。查了两天才发现是地址宽度不匹配。
好了,设备树语法基础就这些。说白了就是节点套节点,属性填属性,标签做引用,包含文件做分离。把这些规则记熟,后面写设备树就跟写配置文件一样顺手。