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 基地址是 0xffd00000,节点名就写 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 型号,改得满头大汗。

建议:养成好习惯,把 SoC 固定配置放 .dtsi,板级差异放 .dts。包含路径用 <> 表示系统搜索路径,用 "" 表示当前目录。

2.5 注释规则:三种方式随你选

设备树支持三种注释风格,跟 C 语言完全一致:

  • // 单行注释 —— 最常用,我基本只用这个
  • /* 多行注释 */ —— 适合大段说明
  • /** 文档注释 */ —— 偶尔用来生成文档
// 这是单行注释,简单明了

/*
 * 这是多行注释
 * 适合写一段说明文字
 * 比如解释为什么这个节点要这么配
 */

/**
 * 文档注释风格
 * 有些工具能解析这种格式
 */

我个人建议:日常开发用 // 就够了。多行注释容易忘记闭合,编译报错时找半天。嗯,别问我怎么知道的。

2.6 知识结构总览

下面这张图把设备树语法的核心要素串起来了。你可以把它当作「语法速查地图」:

设备树语法核心要素 根节点 / 子节点 cpu@0 子节点 i2c@ffd00000 子节点 uart@ffeb0000 属性 compatible = "..." reg = <...> status = "okay" 标签 i2c0: i2c@ffd00000 &i2c0 { ... } 引用追加内容 包含文件 #include "rk3588.dtsi" SoC 级 .dtsi 板级 .dts 注释 // 单行 | /* 多行 */ | /** 文档 */ 节点 + 属性 + 标签 + 包含 + 注释 = 完整设备树 编译后生成 .dtb,内核解析后驱动硬件

2.7 避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 分号别漏:每个属性结尾必须有 ;。漏一个,编译报错能报到你怀疑人生。我习惯写完一行就顺手敲分号。
  • 逗号别多:字符串列表用逗号分隔,但最后一个后面不要加逗号。有些编译器会报 warning。
  • 标签别重名:整个设备树里标签必须唯一。重名了,编译不会报错,但引用时会混乱。
  • 地址对齐:reg 属性的地址和大小,必须跟 #address-cells、#size-cells 匹配。RK3588 默认是 <2> 和 <2>,也就是 64 位地址。
我曾经犯过的错:有一次写 I2C 节点的 reg,只写了 32 位地址 <0xffd00000 0x1000>,但父总线 #address-cells = <2>。结果内核解析出来的地址完全不对,I2C 读写全部失败。查了两天才发现是地址宽度不匹配。

好了,设备树语法基础就这些。说白了就是节点套节点,属性填属性,标签做引用,包含文件做分离。把这些规则记熟,后面写设备树就跟写配置文件一样顺手。