设备树基础语法:节点、属性、标签、别名、包含文件

好,咱们今天来啃设备树最核心的语法部分。说实话,我刚接触Zephyr那会儿,看到设备树文件里一堆花括号和冒号,第一反应是「这玩意儿跟JSON长得有点像啊」。后来用多了才发现,它比JSON灵活得多,但也更容易踩坑。

设备树说白了就是一套描述硬件的「说明书」。你的板子上有哪些外设、它们挂在哪个总线上、寄存器地址是多少、中断号是多少……全写在这里。Zephyr的驱动会去读这份说明书,然后决定怎么初始化硬件。

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

节点就是设备树里的「一个硬件设备」或「一个总线」。每个节点用一对花括号 { } 包裹,里面可以放属性,也可以嵌套子节点。

/dts-v1/;

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

你看,soc 是一个节点,serial@40002000 是它的子节点。节点名字后面跟 @地址 是一种惯例,表示这个外设的基地址。我个人习惯把地址写全,这样看代码时一眼就能知道外设映射到了哪里。

小提示: 节点名可以随便起,但 compatible 属性才是驱动匹配的关键。名字只是给人看的,compatible 才是给代码看的。

属性(Property)—— 描述硬件的「形容词」

属性就是键值对,用来描述节点的特征。常见的属性类型有:

属性类型 写法示例 说明
字符串 compatible = "ns16550"; 驱动匹配的关键字
32位整数 interrupts = <10>; 中断号、时钟频率等
64位整数 reg = <0x40002000 0x1000>; 地址和长度对
布尔值 status = "okay"; 启用或禁用设备
字符串数组 clock-names = "uart", "pclk"; 多个字符串用逗号分隔

嗯,这里要注意 status 属性。我在项目中遇到过好几次,明明设备树里配好了外设,驱动就是加载不上。查了半天,发现 status = "disabled" 没改成 "okay"。Zephyr 默认很多外设是禁用的,你得手动打开。

避坑指南: 我曾经在 reg 属性里写错了地址长度,导致驱动只映射了前 256 字节,后面的寄存器全读成了 0。调试了整整一个下午才发现。所以 reg 里的第二个值一定要跟芯片手册对得上。

标签(Label)—— 给节点起个「外号」

标签就是节点前面的那个名字,比如 uart0:。有了标签,你就可以在别的地方引用这个节点,而不需要写完整的路径。

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

// 在别的文件里引用
&uart0 {
    status = "okay";
    current-speed = <115200>;
};

看到 &uart0 了吗?这就是通过标签引用节点。我建议你给每个重要的外设都加上标签,尤其是 UART、SPI、I2C 这些经常需要修改配置的。不然每次都要写 &soc/serial@40002000,手都酸了。

别名(Alias)—— 更短的「外号」

别名跟标签有点像,但它是全局的,放在根节点的 aliases 下面:

/ {
    aliases {
        uart-console = &uart0;
        led0 = &green_led;
    };
};

为什么需要别名?因为 Zephyr 的很多子系统会通过别名来查找设备。比如控制台默认找 uart-console 这个别名指向的 UART。你想想看,如果换个板子,UART 的节点路径变了,但只要别名不变,代码就不用改。

我的经验: 在做多板支持时,别名是「解耦」的神器。板级差异全藏在设备树里,应用层代码只看别名,完全不用关心具体硬件路径。

包含文件(.dtsi)—— 别重复造轮子

.dtsi 文件就是设备树的「头文件」。芯片厂商会把 SoC 的通用定义放在 .dtsi 里,板级文件通过 #include 把它拉进来,然后只改自己板子特有的部分。

// stm32f4.dtsi(芯片级)
/ {
    soc {
        uart1: serial@40011000 {
            compatible = "st,stm32-uart";
            reg = <0x40011000 0x400>;
            interrupts = <37>;
            status = "disabled";
        };
    };
};

// my_board.dts(板级)
#include "stm32f4.dtsi"

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

你看,芯片级文件里把 UART 的寄存器地址、中断号都定义好了,板级文件只需要打开它、配个波特率就行。这就是设备树的「分层设计」思想。

说实话,我刚开始写设备树时总想把所有东西都塞到一个文件里。后来维护了三个板子之后,发现改一个 SoC 的公共配置要改三个文件,累得够呛。从那以后,我再也不敢偷懒了,老老实实把 SoC 级定义抽到 .dtsi 里。

小技巧: Zephyr 的 .dtsi 文件可以嵌套包含。比如 stm32f4.dtsi 可以再包含 armv7-m.dtsi,把 Cortex-M4 内核的公共配置也抽出来。层级越多,复用性越好。

总结一下

设备树的这五个语法要素,说白了就是一套「硬件描述语言」:

  • 节点:描述一个硬件设备或总线
  • 属性:描述节点的具体参数
  • 标签:给节点起个短名字,方便引用
  • 别名:全局的、更抽象的引用方式
  • 包含文件:把公共定义抽出来,避免重复

我个人觉得,设备树最难的不是语法本身,而是「什么时候该用标签,什么时候该用别名」。我的原则是:如果只是驱动内部引用,用标签;如果是给应用层或子系统用的,用别名。这样分工明确,代码也清晰。

下一章咱们聊聊怎么在 Zephyr 里实际写一个设备树文件,然后看驱动是怎么读取这些属性的。到时候我会拿一个真实的 I2C 传感器来演示,保证你能上手。