2、设备树语法入门:节点(node)、属性(property)、标签(label)与路径(path)

好,咱们正式开始啃设备树的语法。说实话,我第一次看设备树文件时,感觉就像在看一本天书——一堆花括号、分号,还有各种奇怪的字符串。但别怕,它的核心语法其实特别简单,就那么几个概念。

设备树说白了,就是用一种树形结构来描述硬件。你想想看,一个电路板上有CPU、内存、I2C控制器、SPI控制器、GPIO……这些东西天然就是树状连接的。设备树就是把这个树给“写”出来。

2.1 节点(Node)—— 树上的每一个“挂件”

节点是设备树最基本的组成单元。每个节点代表一个硬件设备或总线。我习惯把节点想象成一个文件夹,里面可以放文件(属性),也可以再放子文件夹(子节点)。

节点的语法长这样:

/ {
    node1 {
        // 这是节点1的内容
    };
    node2 {
        // 这是节点2的内容
    };
};

注意看,根节点用 / 表示,所有设备都挂在这个根下面。每个节点用花括号 { } 包裹,最后用分号 ; 结束。嗯,这里要注意:分号千万别漏了,我刚开始写设备树时,经常因为漏了分号导致编译报错,找半天才发现。

节点可以无限嵌套。比如一个I2C控制器下面,可以挂多个I2C从设备:

/ {
    i2c0: i2c@ff010000 {
        compatible = "snps,designware-i2c";
        reg = <0xff010000 0x1000>;
        
        eeprom@50 {
            compatible = "atmel,24c02";
            reg = <0x50>;
        };
        
        rtc@51 {
            compatible = "nxp,pcf8563";
            reg = <0x51>;
        };
    };
};

这里 i2c@ff010000 是一个节点,它下面又挂了 eeprom@50rtc@51 两个子节点。每个节点都有自己的地址,用 @ 符号标识。

我的小技巧: 节点命名时,建议用“设备类型@地址”的格式。比如 uart@ff000000gpio@ff001000。这样一看就知道是什么设备、在哪个地址,特别清晰。

2.2 属性(Property)—— 节点的“身份证”

属性就是描述节点特征的数据。每个节点可以有多个属性,属性值可以是字符串、数字、数组等。

常见的属性类型:

属性类型 写法示例 说明
字符串 compatible = "snps,dw-apb-uart"; 用于匹配驱动,最重要属性之一
32位整数 reg = <0xff010000 0x1000>; 地址和大小,用尖括号包裹
布尔值 status = "disabled"; 用字符串表示,常见有"okay"和"disabled"
字符串列表 interrupt-names = "rx", "tx"; 多个字符串用逗号分隔
二进制数据 mac-address = [00 11 22 33 44 55]; 用方括号包裹十六进制数

我个人觉得,compatible 属性是最关键的。它就像设备的“身份证号”,内核通过它来找到对应的驱动程序。比如 compatible = "snps,dw-apb-uart",内核就会去驱动列表里找能处理这个字符串的驱动。

避坑指南: 我曾经在一个项目里,把 compatible 字符串写错了——少了个逗号。结果内核死活加载不了驱动,我排查了整整两天。后来才发现是 "snps,dw-apb-uart" 写成了 "snpsdw-apb-uart"。所以,compatible 字符串一定要和设备驱动里写的一模一样,多一个空格都不行。

2.3 标签(Label)—— 给节点起个“外号”

标签就是给节点起一个别名。为什么要用标签?因为设备树里节点的路径可能很长,比如 /soc/i2c@ff010000/eeprom@50,每次引用都写全路径太麻烦了。

标签的写法是在节点名前加一个名字和冒号:

/ {
    soc {
        i2c0: i2c@ff010000 {
            // 这个节点的标签是 i2c0
        };
    };
};

有了标签,你就可以在其他地方用 &i2c0 来引用这个节点。比如在另一个文件里覆盖它的属性:

&i2c0 {
    clock-frequency = <400000>;  // 把I2C频率改成400kHz
    status = "okay";
};

这种用法在设备树中特别常见。你想想看,芯片厂商提供了一个基础设备树,你只需要在自己的板级文件里用标签引用,然后修改或添加一些属性就行了,不用动原始文件。

我的习惯: 给所有重要的控制器都加上标签。比如 i2c0、spi1、uart2、gpio3 这种。命名规则建议用“设备类型+序号”,简单明了。我见过有人用 i2c_a、i2c_b 这种命名,说实话,不如数字序号直观。

2.4 路径(Path)—— 找到节点的“导航”

路径就是节点在设备树中的完整位置。从根节点 / 开始,用斜杠分隔每一级节点名。

比如:

/soc/i2c@ff010000/eeprom@50

这个路径表示:根节点 → soc节点 → i2c@ff010000节点 → eeprom@50节点。

路径在设备树中有两种使用场景:

  1. 引用节点:&{/soc/i2c@ff010000} 这种语法来引用一个节点。
  2. 覆盖节点: 在多个设备树文件(.dtsi和.dts)中,用路径来定位要修改的节点。

举个例子,假设芯片厂商的 .dtsi 文件里定义了一个节点:

/ {
    soc {
        uart0: uart@ff000000 {
            compatible = "snps,dw-apb-uart";
            reg = <0xff000000 0x1000>;
            status = "disabled";
        };
    };
};

在你的板级 .dts 文件里,你可以这样启用它:

&uart0 {
    status = "okay";
    clock-frequency = <115200>;
};

这里 &uart0 就是通过标签引用。如果没有标签,你就得写全路径:

&{/soc/uart@ff000000} {
    status = "okay";
};

说实话,全路径写法太啰嗦了,我基本不用。能用标签就用标签,代码看起来清爽很多。

2.5 实战:写一个简单的设备树

好了,理论说完了,咱们来写一个完整的例子。假设我们要描述一个开发板,上面有:

  • 一个CPU(ARM Cortex-A72)
  • 一个UART控制器(地址0xff000000)
  • 一个I2C控制器(地址0xff010000),下面挂了一个温度传感器(地址0x48)
  • 一个GPIO控制器(地址0xff020000)

设备树文件长这样:

/dts-v1/;

/ {
    model = "MyDevBoard";
    compatible = "mycompany,mydevboard";

    #address-cells = <1>;
    #size-cells = <1>;

    cpu@0 {
        compatible = "arm,cortex-a72";
        reg = <0x0>;
    };

    uart0: uart@ff000000 {
        compatible = "snps,dw-apb-uart";
        reg = <0xff000000 0x1000>;
        interrupts = <0 33 4>;
        status = "okay";
    };

    i2c0: i2c@ff010000 {
        compatible = "snps,designware-i2c";
        reg = <0xff010000 0x1000>;
        interrupts = <0 34 4>;
        #address-cells = <1>;
        #size-cells = <0>;
        status = "okay";

        temp_sensor: temperature-sensor@48 {
            compatible = "ti,tmp102";
            reg = <0x48>;
        };
    };

    gpio0: gpio@ff020000 {
        compatible = "snps,dw-apb-gpio";
        reg = <0xff020000 0x1000>;
        interrupts = <0 35 4>;
        #gpio-cells = <2>;
        status = "okay";
    };
};
关键点总结:
  • 每个节点都有 compatible 属性,用于驱动匹配
  • reg 属性描述地址和大小
  • 标签(如 uart0、i2c0)方便在其他地方引用
  • 子节点(如 temperature-sensor)挂在父节点下面
  • status 控制设备是否启用
注意: 设备树里的地址都是物理地址,不是虚拟地址。内核启动后,会解析设备树,然后根据这些地址来映射寄存器空间。我曾经犯过一个错,把地址写成了0xFF000000(大写),结果编译报错。设备树里地址统一用小写十六进制。

嗯,到这里,设备树的四个核心概念就讲完了。节点是骨架,属性是血肉,标签是外号,路径是导航。把这四个搞明白,你就能看懂大部分设备树文件了。

下一章咱们会深入讲 compatiblereg 这两个最重要的属性,以及它们在内核里是怎么被解析的。到时候我会分享一个我调试驱动时遇到的真实案例,保证让你印象深刻。