第二节:设备树基础语法——节点、属性、标签、路径的规则与写法

好,咱们正式开始啃设备树这块硬骨头。说实话,我刚接触Zephyr那会儿,看到.dts文件里一堆花括号和分号,第一反应是「这玩意儿是C语言吗?」后来才明白,设备树有自己的语法体系,跟C八竿子打不着。今天我就把节点、属性、标签、路径这四个核心概念掰开揉碎了讲给你听。

一、节点(node)——设备树的骨架

节点是什么?说白了就是设备树里的「文件夹」。每个节点代表一个硬件设备或总线,比如UART、GPIO、SPI控制器。节点用花括号包裹,里面可以嵌套子节点。

看个最简单的例子:

/ {
    soc {
        uart0 {
            compatible = "ns16550";
            reg = <0x40002000 0x1000>;
        };
    };
};

这里/是根节点,soc是子节点,uart0是孙节点。每个节点都以分号结尾,花括号里的内容就是节点的「内部世界」。

我的习惯:写节点名时尽量用全小写+下划线,比如i2c_controller而不是I2CController。Zephyr社区大部分设备树都这么写,保持一致能少踩很多坑。

二、属性(property)——节点的「身份证」

属性就是节点的配置信息。常见的属性类型有:

  • 字符串compatible = "ns16550"; —— 告诉内核这个设备兼容哪个驱动
  • 整数reg = <0x40002000 0x1000>; —— 地址和大小
  • 布尔值status = "okay";status = "disabled";
  • 数组interrupts = <0 1>;

属性值用分号结尾,多个值用空格隔开。注意看< >尖括号,它表示一个32位整数的数组。

避坑指南:我曾经在项目里把reg属性的地址写成了十进制,结果编译通过但运行时外设死活不工作。后来发现设备树里地址默认是十六进制,但< >里写十进制也不会报错——这就是个隐形的坑。所以,我建议所有地址都写成十六进制,比如0x40002000,一眼就能看出是地址。

三、标签(label)——给节点起个「外号」

标签就是节点的别名。为什么需要标签?因为设备树里节点路径可能很长,比如/soc/uart0,每次引用都写全路径太累赘。标签用冒号跟在节点名后面:

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

这里uart0就是标签。之后在别的节点里引用时,直接用&uart0就行:

&uart0 {
    status = "okay";
    current-speed = <115200>;
};

你想想看,如果没有标签,你得写成&/soc/serial@40002000,多麻烦。标签让代码更简洁,也更容易维护。

我的经验:标签命名最好跟节点功能相关。比如UART就叫uart0uart1,SPI就叫spi0spi1。别用my_device这种名字,三个月后你自己都看不懂。

四、路径(path)——从根到叶的「导航」

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

  • / —— 根节点
  • /soc —— soc节点
  • /soc/uart0 —— uart0节点
  • /soc/i2c0/touch@1a —— 挂在i2c0总线上的触摸芯片

路径在&引用时也会用到。比如你想在另一个文件里修改某个节点的属性,就得用路径或标签来定位它。

嗯,这里要注意:路径里的节点名可以包含@符号,后面跟的是地址或寄存器偏移。比如serial@40002000,这个@40002000不是必须的,但加上后能让人一眼看出这个外设的基地址。

五、综合示例——把四个概念串起来

咱们看一个完整的例子,把节点、属性、标签、路径都揉在一起:

/dts-v1/;

/ {
    model = "My Custom Board";
    compatible = "mycompany,myboard";

    chosen {
        zephyr,console = &uart0;
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;

        uart0: serial@40002000 {
            compatible = "ns16550";
            reg = <0x40002000 0x1000>;
            interrupts = <10 1>;
            status = "disabled";
        };

        i2c0: i2c@40003000 {
            compatible = "snps,designware-i2c";
            reg = <0x40003000 0x1000>;
            #address-cells = <1>;
            #size-cells = <0>;

            touch: touch@1a {
                compatible = "mycompany,touch";
                reg = <0x1a>;
                interrupt-gpios = <&gpio0 5 0>;
            };
        };
    };
};

这个例子里:

  • /是根节点,有modelcompatible属性
  • chosen节点是Zephyr的特殊节点,用来指定系统控制台
  • uart0i2c0是标签,分别指向serial@40002000i2c@40003000
  • touch节点是i2c0的子节点,路径是/soc/i2c0/touch@1a
  • &uart0&gpio0是引用标签的写法

重要提醒:设备树里#address-cells#size-cells这两个属性决定了reg属性的解析方式。#address-cells = <1>表示地址占1个32位整数,#size-cells = <1>表示大小也占1个32位整数。如果写错了,驱动读到的地址会完全不对。我曾经在这个问题上折腾了一整天,最后发现是#size-cells写成了0,导致驱动认为reg里只有地址没有大小。

六、常见错误与调试技巧

写设备树时最容易犯的几个错误:

  1. 漏了分号:每个属性、每个节点结束都要有分号。漏一个分号,整个文件编译不过。
  2. 花括号不匹配:节点嵌套多了,很容易少写一个花括号。我建议用编辑器的高亮匹配功能。
  3. 标签重复:同一个设备树里不能有两个相同的标签。Zephyr编译时会报错,但错误信息可能不太直观。
  4. 路径写错:引用节点时路径必须完全匹配,大小写敏感。

调试小技巧:用dtc -I dts -O dtb your.dts把设备树编译成二进制,再用dtc -I dtb -O dts your.dtb反编译回来,看看有没有语法错误。Zephyr的构建系统也会在编译时检查设备树,但提前用dtc检查能更快发现问题。

好了,这一节的内容就这些。节点、属性、标签、路径这四个概念是设备树的基石,搞懂了它们,后面学设备树绑定、覆盖、宏定义就轻松多了。下一节咱们聊聊设备树绑定——就是那个告诉Zephyr「这个节点对应哪个驱动」的机制。