第二讲:设备树语法入门——节点、属性、标签、别名、包含文件

好,咱们正式开始啃设备树的语法。说实话,很多刚接触Linux驱动的朋友,一看到.dts文件里那些花括号、分号、@符号就头大。我当年第一次看的时候也懵——这玩意儿到底是C语言还是JSON?其实都不是,它就是一种树形结构的描述语言,专门用来告诉内核:你的硬件长什么样。

这一讲,咱们把最核心的五个概念讲透:节点、属性、标签、别名、还有.dtsi包含文件。搞懂这些,你就能看懂90%的设备树文件了。

1. 节点(Node)——设备树的骨架

节点就是设备树里的“树枝”。每个节点代表一个硬件设备或总线。比如CPU、内存、I2C控制器、GPIO控制器,都是节点。

语法很简单:

/ {
    cpu {
        compatible = "arm,cortex-a7";
        reg = <0x10000000 0x1000>;
    };

    memory {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };
};

看到没?根节点是/,下面挂两个子节点:cpu和memory。每个节点用花括号包起来,最后加分号。节点里可以再嵌套子节点,形成树状结构。

我的经验:节点名最好跟芯片手册里的模块名保持一致。我见过有人把UART节点写成"my_uart",结果内核驱动匹配不上,查了半天才发现是名字写错了。老老实实用"uart0"、"uart1"这种标准名,省心。

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

属性就是描述节点特征的东西。每个属性都有一个名字和一个值。常见的属性类型有:

属性类型 写法示例 说明
字符串 compatible = "arm,cortex-a7"; 用于驱动匹配,最重要
32位整数 reg = <0x10000000 0x1000>; 地址和长度,尖括号包着
64位整数 reg = <0x0 0x80000000 0x0 0x40000000>; 高32位在前,低32位在后
布尔值 status = "okay"; 或者"disabled",没有true/false
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号,十六进制

这里我要特别强调compatible属性。它就像设备的“身份证号”,驱动通过它来匹配设备。格式一般是"厂商,型号",比如"ti,am335x-uart"、"fsl,imx6q-uart"。我踩过坑:有一次把逗号写成了点,结果驱动死活不认,查了两天才发现。

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

标签就是节点的别名,方便在其他地方引用。语法是在节点名前加个名字和冒号:

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

这里的uart0就是标签。有了标签,你就可以在别处用&uart0来引用这个节点。比如在板级文件里覆盖它的属性:

&uart0 {
    status = "okay";
    clock-frequency = <115200>;
};
为什么要用标签? 因为SoC的.dtsi文件里定义了所有外设,但默认都是disabled。板级.dts文件通过标签引用,只把用到的外设打开。这样SoC厂商和板卡厂商各管各的,互不干扰。

4. 别名(Alias)——给节点编号

别名跟标签有点像,但用途不同。别名是在根节点的aliases子节点里定义的:

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

别名的主要作用是给设备分配固定的编号。比如系统里有两个串口,你想让UART0永远是/dev/ttyS0,UART1永远是/dev/ttyS1,那就用别名固定下来。

我记得有一次调试,发现串口设备号每次启动都不一样。查了半天,原来是aliases没写对,内核按dtb里的顺序乱排的。加上别名后,世界清净了。

5. 包含文件(.dtsi)——代码复用利器

.dtsi文件就是设备树的头文件。SoC厂商会把芯片内部的所有外设定义放在.dtsi里,板卡厂商只需要包含它,然后添加板级特有的设备(比如网口、LED、按键)。

语法跟C语言的#include一样:

// imx6q.dtsi (SoC厂商提供)
#include "imx6q-pinfunc.h"

/ {
    soc {
        uart1: serial@02020000 {
            compatible = "fsl,imx6q-uart";
            reg = <0x02020000 0x4000>;
            status = "disabled";
        };
    };
};
// my-board.dts (板卡厂商编写)
#include "imx6q.dtsi"

&uart1 {
    status = "okay";
    pinctrl-0 = <&pinctrl_uart1>;
};
注意:.dtsi文件里不要写板级特有的东西,比如GPIO编号、电源电压。这些应该在.dts里定义。我曾经见过有人把LED的GPIO写死在.dtsi里,结果换了板子还得改SoC文件,那叫一个痛苦。

6. 实战小技巧——如何快速验证语法

写设备树最怕什么?语法错误。编译时dtc报错,一行行查很费劲。我分享几个经验:

  • 分号别漏:每个属性结束要加分号,每个节点结束也要加分号。漏一个,编译就报错。
  • 花括号配对:用带缩进的编辑器,一眼就能看出括号是否匹配。
  • @符号别乱用:节点名里的@后面跟的是地址,比如serial@10000000。如果节点没有地址,就别加@。
  • 先编译再烧录:我习惯每次改完.dts都执行make dtbs,确认没报错再烧到板子上。省得启动时内核panic。

嗯,这一讲的内容就这些。说白了,设备树语法就是一套描述硬件的规则。节点是骨架,属性是血肉,标签和别名是引用方式,.dtsi是复用手段。把这五个概念搞明白,你就能自己写一个简单的设备树文件了。

下一讲,咱们聊聊设备树编译和反编译,看看.dts怎么变成.dtb,以及怎么从.dtb反推回.dts。这可是调试利器,别错过。