2、设备树语法入门:节点、属性、标签、别名、路径、phandle

好,咱们正式开始啃设备树的语法。说实话,很多工程师一看到设备树文件(.dts)就头大,觉得像天书。其实没那么玄乎,它就是个描述硬件的结构化文本。我当年第一次接触时,也觉得这玩意儿怎么这么多花括号和分号。但摸透了核心的几个概念,你会发现它比想象中简单。

2.1 节点(Node)—— 设备树的基本单元

节点是什么?说白了就是硬件上的一个“东西”。可以是一个CPU、一个I2C控制器、一个GPIO引脚,甚至是一颗LED灯。每个节点用一对花括号 { } 包裹起来。

来看个最简单的例子:

/ {
    cpu {
        compatible = "arm,cortex-a7";
    };
};

这里 / 是根节点,代表整个板子。根节点下面挂了一个 cpu 子节点。嗯,这里要注意:节点名字可以随便起,但最好见名知意。我在项目中见过有人把I2C节点命名为 i2c_1,也有人叫 i2c@0x1000。我个人习惯用 i2c@地址 这种格式,因为一看就知道它映射到哪个地址空间。

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

属性就是描述节点特征的东西。每个属性由键值对组成,格式是 key = value;。常见的属性类型有:

  • 字符串compatible = "arm,cortex-a7";
  • 整数reg = <0x1000 0x100>;
  • 字符串列表compatible = "ti,am335x-uart", "ns16550";
  • 布尔值status = "okay";status = "disabled";

这里有个坑,我刚开始做移植时经常踩:compatible 属性是驱动匹配的关键。内核就是靠这个字符串找到对应的驱动。如果你写错了,驱动根本不会加载。我曾经把一个 compatible 写成了 "ti,am335x-uart" 少了个逗号,结果排查了一整天。

核心原则:每个设备节点至少要有 compatible 属性,否则内核不知道它是什么设备。

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

标签就是节点的别名,方便在其他地方引用。语法很简单:在节点名前加个 标签名:

uart0: serial@1000 {
    compatible = "ns16550";
    reg = <0x1000 0x100>;
};

这里的 uart0 就是标签。有了标签,你就可以在别处用 &uart0 来引用这个节点。我个人习惯给所有重要的外设都加上标签,比如 uart0i2c1spi2。这样在板级文件中引用时,代码可读性会好很多。

2.4 别名(Alias)—— 系统级的“快捷方式”

别名和标签有点像,但作用范围不同。别名定义在根节点的 aliases 子节点里,是给整个系统用的。

/ {
    aliases {
        serial0 = &uart0;
        serial1 = &uart1;
    };
};

为什么需要别名?举个例子,U-Boot启动时需要通过 serial0 找到控制台串口。如果直接写 &uart0,万一换了板子,UART0变成了UART1,你就得改代码。用别名的话,只需要改 aliases 里的映射关系就行。嗯,这就是解耦的思想。

2.5 路径(Path)—— 从根节点出发的“导航”

路径就是从根节点到目标节点的完整路径,用斜杠分隔。比如:

/soc/serial@1000

路径在设备树覆盖(overlay)和引用时非常有用。我记得有一次调试一个复杂的设备树,节点嵌套了五六层,路径写错一个字母,整个设备树解析失败。从那以后,我写路径时都会用 dtc -I dtb -O dts 反编译确认一下。

2.6 phandle —— 节点间的“指针”

phandle 是设备树里最巧妙的设计之一。它本质上是一个数字ID,用来在节点之间建立引用关系。比如一个GPIO控制器节点,其他设备要引用它的某个引脚:

gpio0: gpio@1000 {
    #gpio-cells = <2>;
    gpio-controller;
};

led {
    compatible = "gpio-leds";
    gpios = <&gpio0 15 0>;
};

这里 &gpio0 会被编译器自动替换成一个phandle数字。内核解析时,通过这个数字就能找到对应的GPIO控制器节点。你想想看,如果没有phandle,你怎么在设备树里表达“这个LED连在GPIO0的第15号引脚上”?用字符串?那解析起来得多麻烦。

小技巧:编译后的设备树(.dtb)里,phandle是自动生成的。你不需要手动指定,用 &标签 就行。但如果你反编译一个.dtb文件,会看到类似 phandle = <0x00000001>; 这样的属性,那就是编译器帮你填的。

2.7 实战中的常见模式

说了这么多理论,咱们看一个实际项目中常见的设备树片段:

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    aliases {
        serial0 = &uart0;
        mmc0 = &mmc1;
    };

    chosen {
        stdout-path = &uart0;
    };

    leds {
        compatible = "gpio-leds";
        led0 {
            label = "beaglebone:green:usr0";
            gpios = <&gpio1 21 0>;
            linux,default-trigger = "heartbeat";
        };
    };
};

看到没?这里用到了节点、属性、标签、别名、phandle。每个概念都在实际工作中有它的位置。我刚开始做设备树移植时,就是对着这种真实案例一点点啃下来的。

避坑指南:我曾经在项目中犯过一个低级错误——在同一个设备树文件里定义了重复的phandle。编译时没报错,但内核启动时直接panic。后来发现是include了两个头文件,里面都定义了同一个GPIO控制器的节点。所以,用include时要格外小心,最好用 /include/ 或者 #include 时检查一下有没有重复定义。

2.8 小结

好了,这一章的内容就这些。节点是骨架,属性是血肉,标签和别名是方便引用的工具,路径是定位方式,phandle是节点间的纽带。把这几个概念搞清楚了,设备树在你眼里就不再是一团乱麻。

下一章咱们会深入讲 reg 属性和地址映射,那是设备树里最绕的部分之一。做好准备,咱们继续往下走。