第二章 设备树语法入门:节点、属性、标签与路径

好,咱们正式开始动手写设备树了。这一章我带你过一遍最核心的语法要素——节点、属性、标签和路径。说白了,设备树就是一个描述硬件拓扑的树形结构,你只要搞懂这四个东西,剩下的就是查手册填参数了。

2.1 节点(Node):硬件单元的化身

设备树里的每个硬件单元,都对应一个节点。比如一个UART、一个GPIO控制器、一个I2C总线,都是节点。节点用花括号 { } 包裹,里面可以嵌套子节点。

我个人习惯把节点想象成「硬件设备的身份证」。每个节点都有一个名字,格式是 node-name@unit-address。其中 unit-address 通常是设备在总线上的起始地址。

/ {
    soc {
        uart0: serial@10000000 {
            compatible = "ns16550a";
            reg = <0x10000000 0x1000>;
        };
    };
};

你看,serial@10000000 就是一个节点。名字是 serial,地址是 0x10000000。这里有个小细节——uart0: 是标签,后面会讲。

我的经验:节点名尽量用通用名称,比如 serial、gpio、i2c。别用你板子上的具体型号,否则换芯片时改起来很痛苦。我在项目中见过有人把节点名写成 "my_custom_uart_2023",结果换内核版本时解析失败,排查了半天。

2.2 属性(Property):描述节点的关键信息

属性就是节点的「参数」。每个属性都有一个名字和一个值。常见的属性类型有:

  • 字符串compatible = "ti,omap3-gpio";
  • 32位整数reg = <0x48000000 0x1000>;
  • 布尔值status = "disabled";status = "okay";
  • 数组gpios = <&gpio1 15 0>;

嗯,这里要注意——compatible 属性是最重要的。它告诉内核这个设备用什么驱动来匹配。格式是 "厂商,型号",比如 "ti,omap3-gpio"。我建议你养成习惯,所有自定义设备都加上厂商前缀,避免和内核主线驱动冲突。

gpio1: gpio@48000000 {
    compatible = "ti,omap3-gpio";
    reg = <0x48000000 0x1000>;
    interrupts = <29>;
    gpio-controller;
    #gpio-cells = <2>;
};
避坑指南:我曾经在项目中漏写了 gpio-controller; 这个布尔属性,结果内核死活不认这个节点是GPIO控制器。查了两天才发现,原来这个属性是告诉内核「我是GPIO控制器」,没有它,gpios 引用就解析不了。

2.3 标签(Label):给节点起个外号

标签就是节点的别名。格式是 标签名: 放在节点名前。比如:

uart0: serial@10000000 {
    ...
};

有了标签,你就可以在其他地方通过 &uart0 来引用这个节点。这有什么用?你想想看,当你要在板级文件中修改某个SoC内部节点的属性时,总不能把整个节点复制一遍吧?用标签引用就优雅多了。

&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
};

我个人习惯给所有重要的外设都加上标签,哪怕当前不需要引用。因为后续调试时,你很可能需要临时修改某个节点的属性。有标签在,改起来就是一行代码的事。

注意:标签名不能重复,也不能以数字开头。我见过有人用 0uart 做标签,结果编译报错。标签名建议用 外设名+编号 的格式,比如 uart0i2c1gpio2

2.4 路径(Path):从根到节点的完整路线

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

/soc/serial@10000000

路径主要用于 device_node 的查找。在内核代码中,你可以用 of_find_node_by_path() 函数通过路径找到节点。不过在实际的dts文件中,我们很少直接写路径,更多是用标签引用。

但路径有个重要用途——在 aliases 节点中给设备起短名:

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

这样,uboot或内核就可以通过 serial0 这个别名快速找到对应的串口节点。我在调试早期启动时经常用这个技巧,省得记一堆地址。

2.5 实战:一个完整的GPIO控制器节点

来,咱们把上面学的串起来,写一个完整的GPIO控制器节点:

/ {
    soc {
        gpio0: gpio@48000000 {
            compatible = "ti,omap3-gpio";
            reg = <0x48000000 0x1000>;
            interrupts = <29>;
            gpio-controller;
            #gpio-cells = <2>;
            interrupt-controller;
            #interrupt-cells = <2>;
            status = "okay";
        };
    };
};

这个节点包含了:

  • 标签gpio0,方便其他地方引用
  • compatible:匹配驱动
  • reg:寄存器基地址和大小
  • interrupts:中断号
  • gpio-controller:声明自己是GPIO控制器
  • #gpio-cells:每个GPIO描述需要2个cell(引脚号和标志)
  • status:默认启用
我的建议:写设备树时,先搭骨架(节点层级),再填血肉(属性)。别一上来就纠结某个属性的值对不对。先把结构写对,属性值后面可以慢慢调。我在项目中就是这么干的,先让设备树能编译通过,再逐个外设调试。

2.6 常见错误与调试技巧

最后分享几个我踩过的坑:

  1. 漏加分号:每个属性结尾必须有分号。编译报错时先检查这个。
  2. 标签重复:两个节点用了同一个标签名,编译会报重复定义。
  3. 路径写错:引用节点时路径大小写不对,内核找不到节点。
  4. 属性类型搞混:比如 reg 应该是 <地址 长度>,有人写成字符串。

调试时,我常用 dtc -I dtb -O dts 反编译dtb文件,看看实际解析出来的设备树长什么样。如果和你写的不一样,那肯定是语法有问题。

好了,这一章就到这里。节点、属性、标签、路径这四个概念,你只要动手写几个dts文件,很快就能掌握。下一章我们深入GPIO binding,看看怎么在设备树中描述GPIO的引脚复用和电气特性。