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@0memory@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 前缀表示十六进制
  • 可以用 &nbsp; 表示无符号数

小技巧:如果你需要 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 常见错误与避坑指南

写设备树这么多年,我总结了几条最容易犯的错:

  1. 分号漏了:每个属性结尾必须有分号,包括最后一个。我刚开始时经常漏,编译报错才想起来。
  2. 逗号位置不对:字符串列表用逗号分隔,但数字列表用空格。混了就出问题。
  3. 地址格式错误:节点名中的地址不带 0x,但属性值中的地址要带。这个规则确实有点反直觉。
  4. 大小写搞混:设备树是大小写敏感的。Memorymemory 是两个不同的节点名。

我曾经在调试一个 SPI 设备时,怎么都读不到数据。折腾了两天,最后发现是 reg 属性的地址写错了——节点名里写的是 spi@100,但 reg 写成了 <0x200>。内核按 reg 去寻址,当然找不到设备。从那以后,我每次都会仔细核对节点名和 reg 是否一致。

3.6 小结

节点和属性是设备树的基石。说白了,你只要记住:

  • 节点用 名称@地址 命名,地址可选
  • 属性有字符串、数字、布尔、字节数组四种基本类型
  • 标签用 名称: 定义,用 &名称 引用
  • 分号、逗号、括号一个都不能少

嗯,掌握了这些,你就已经能看懂大部分设备树文件了。下一节咱们会深入讲 reg 属性的地址映射规则,那个才是真正考验人的地方。到时候我会分享一个我调试 PCIe 设备时遇到的地址解析问题,保证让你印象深刻。