2、设备树基础语法(上):节点与属性的定义,根节点、别名节点、标签的使用,&引用语法

好,咱们正式开始啃设备树的语法。说实话,我刚接触设备树那会儿,看着一堆花括号和分号,第一反应是「这玩意儿到底算代码还是配置文件?」后来做多了才明白,它其实是一棵描述硬件资源的树,每个树枝叫节点,每片叶子叫属性。今天咱们就把这棵树的结构彻底捋清楚。

2.1 节点(node)—— 树的骨架

设备树里的一切都是节点。一个节点代表一个硬件设备或总线。比如一个串口、一个GPIO控制器、一个I2C总线,都是节点。

节点的基本语法长这样:

node_name {
    property1 = <value>;
    property2 = "string";
    child_node {
        ...
    };
};

注意那个分号,每个属性后面都要加分号,花括号后面也要加分号。我见过不少新手在这上面栽跟头——少个分号,编译直接报错,找半天找不到原因。

节点名可以带地址后缀,比如 uart@fe010000,@后面的数字通常是寄存器基地址。这主要是为了区分同类型设备,比如两个串口:

uart@fe010000 {
    compatible = "ns16550";
    reg = <0xfe010000 0x1000>;
};

uart@fe020000 {
    compatible = "ns16550";
    reg = <0xfe020000 0x1000>;
};

你看,名字不同,地址不同,但都是串口。内核通过节点名里的地址来区分它们。

2.2 属性(property)—— 节点的血肉

属性就是描述节点特征的数据。常见的属性类型有几种:

属性类型 写法示例 说明
字符串 compatible = "my-device"; 用于匹配驱动
32位整数 reg = <0x1000 0x200>; 地址和长度
64位整数 reg = <0x00000000 0x10000000>; 高32位在前
字符串列表 interrupt-names = "rx", "tx"; 用逗号分隔
二进制数据 mac-address = [00 11 22 33 44 55]; 用方括号括起来

我个人习惯把 compatible 属性放在每个节点的第一行。为什么?因为内核匹配驱动时第一个就看它。你把它藏到后面,别人读代码时还得翻半天。

小技巧:compatible 属性的值通常写成 "厂商,设备型号" 的格式,比如 "ti,am3359-uart"。这样既能避免命名冲突,又能一眼看出是谁家的芯片。

2.3 根节点 —— 树的起点

每个设备树文件只有一个根节点,用斜杠 / 表示。它就像文件系统的根目录,所有其他节点都挂在它下面。

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

    chosen {
        stdout-path = &uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a8";
        };
    };
};

根节点里通常放的是整个板级的信息:CPU类型、内存布局、选定的控制台等。注意那个 chosen 节点,它不是描述硬件,而是告诉内核「你该用哪个串口打印日志」。嗯,这个节点挺特殊的,后面讲内核启动流程时还会提到。

2.4 别名节点(aliases)—— 给节点起个短名

设备树里节点路径往往很长,比如 /soc/serial@fe010000。每次引用都写全路径,手都酸了。别名节点就是用来解决这个问题的。

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

有了别名,你在其他地方就可以直接用 serial0 代替那串长路径。内核启动时也会根据别名来分配设备编号。比如 serial0 对应 /dev/ttyS0,serial1 对应 /dev/ttyS1。

注意:别名不是必须的,但强烈建议加上。我曾经接手过一个项目,板子上三个串口,设备树里没写别名,结果每次启动设备编号都乱跳,调试串口一会儿是 ttyS0 一会儿是 ttyS2,搞得人崩溃。加上别名后,世界清净了。

2.5 标签(label)与 & 引用语法

标签是设备树里最常用的「指针」机制。你可以在节点前面加个标签,然后在其他地方用 &标签名 来引用它。

uart0: serial@fe010000 {
    compatible = "ns16550";
    reg = <0xfe010000 0x1000>;
    interrupts = <32>;
};

这里 uart0: 就是标签。注意标签和节点名之间用冒号隔开。有了标签,你就可以在其他地方扩展这个节点:

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

这段代码的意思是:找到标签为 uart0 的节点,给它添加两个新属性。这种写法在设备树里叫「追加节点」。它特别适合在板级文件里修改 SoC 级文件中的节点。

举个例子,SoC 厂商提供的 am335x.dtsi 里定义了串口的基本信息:

uart0: serial@fe010000 {
    compatible = "ns16550";
    reg = <0xfe010000 0x1000>;
    interrupts = <32>;
    status = "disabled";
};

注意状态是 disabled,因为不是所有板子都用这个串口。然后在你的板级文件 myboard.dts 里:

#include "am335x.dtsi"

&uart0 {
    clock-frequency = <24000000>;
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
    status = "okay";
};

你看,不用重新定义整个节点,只需要用 &uart0 引用它,然后添加板级特有的属性。这就是标签和引用的威力——实现了 SoC 级和板级代码的解耦。

核心要点:标签 + & 引用 = 设备树的「面向对象」特性。你可以在一个文件里定义基础结构,在另一个文件里扩展它。这比把所有东西写在一个大文件里要清晰得多。

2.6 实战中的常见坑

说了这么多,我分享几个实际项目中踩过的坑:

  • 标签重复:两个文件里定义了同名的标签,编译时不会报错,但后面的会覆盖前面的。我曾经因为 include 了两个头文件,结果串口标签被覆盖,调试了整整一天。
  • 引用未定义的标签:如果你写了 &some_label 但 nowhere 定义了这个标签,编译会报错。检查一下是不是拼写错了,或者忘记 include 对应的头文件。
  • 属性覆盖 vs 追加:如果你在同一个节点里重复定义同一个属性,后面的值会覆盖前面的。但如果你用 &标签 追加,新属性和旧属性是合并的,不会覆盖。
  • 分号遗漏:这个我提过,但值得再说一遍。设备树编译器对分号很敏感,少一个就报错。我习惯写完一个节点后,先数花括号和分号的数量,再编译。

2.7 知识体系总览

为了让你对本章内容有个整体印象,我画了一张图:

设备树基础语法知识体系 根节点 / 节点定义 (node) 属性定义 (property) 引用机制 (&) 节点名 + 地址后缀 @ 子节点嵌套 字符串 / 整数 / 数组 compatible / reg / interrupts 标签 (label) 定义 别名 (aliases) 节点 标签 + & 引用 = 代码复用 SoC级与板级解耦的关键

这张图把本章的核心内容串起来了。你看,根节点是起点,下面分支出节点定义、属性定义和引用机制三大块。节点定义关注的是「怎么描述一个硬件」,属性定义关注的是「用什么数据来描述」,而引用机制关注的是「怎么复用已有的描述」。三者缺一不可。

好了,设备树的基础语法上篇就到这里。下篇我们会深入 reg 属性的地址编码、中断映射、以及 #address-cells 和 #size-cells 这些让人头疼的概念。到时候咱们再接着聊。

课后练习:打开你手头开发板的内核源码,找到 arch/arm/boot/dts/ 目录,随便挑一个 .dts 文件,试着找出根节点、别名节点、以及至少三个带标签的节点。然后看看这些标签在其他地方是怎么被引用的。动手做一遍,比看十遍都管用。

可可精品资料库