设备树语法:节点、属性、标签、引用、包含文件、宏定义

好,咱们今天来啃设备树语法这块硬骨头。说实话,我刚入行那会儿,看到设备树文件里一堆花括号、冒号、分号,心里直犯嘀咕——这玩意儿到底是代码还是配置文件?后来被项目逼着啃了几天,才发现它其实没那么玄乎。

设备树说白了就是一套描述硬件信息的「结构化语言」。它不像C语言那样有复杂的逻辑控制,更像是一张填好的硬件信息登记表。咱们今天就把这张表的填法彻底搞明白。

节点:设备树的基本单元

每个设备树文件,最核心的东西就是节点。你可以把节点想象成一个硬件模块的「身份证」。

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

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

看到没?根节点用 / 表示,里面套着 cpumemory 两个子节点。每个节点用花括号包起来,最后用分号结束。这个格式必须严格,少个分号,整个设备树就解析失败了。

我曾经在项目里犯过一个低级错误:节点末尾忘了加分号。编译时提示语法错误,我愣是找了半小时才找到问题。从那以后,我写设备树都会先检查分号有没有漏掉。

属性:描述节点的关键信息

节点里面放的是属性。属性就是键值对,用来描述这个硬件模块的各种参数。

属性类型 示例 说明
字符串 compatible = "arm,cortex-a7"; 用于匹配驱动
32位整数 reg = <0x10000000>; 地址或数值
整数数组 reg = <0x10000000 0x1000>; 地址+长度
布尔值 status = "okay"; 控制设备启用/禁用

我个人习惯把 compatible 属性放在每个节点的第一行。为什么?因为内核加载驱动时,第一个找的就是这个属性。你把它放前面,别人看代码时一眼就知道这个节点对应哪个驱动。

标签:给节点起个名字

标签就是节点的别名。有了标签,你就不用写一长串的路径去引用某个节点了。

uart1: serial@10001000 {
    compatible = "ns16550";
    reg = <0x10001000 0x100>;
};

这里的 uart1 就是标签。后面你想引用这个串口节点,直接写 &uart1 就行,不用写 /soc/serial@10001000 这么一长串。

我建议标签命名要有规律。比如所有UART节点都用 uart0uart1uart2 这种格式。我在一个项目里见过有人用 my_serial_port_1 这种名字,结果后面引用时拼写错误,编译报错才被发现。

引用:把分散的节点串起来

引用是设备树里最灵活的特性。你可以在一个文件里定义节点,在另一个文件里通过引用给它添加属性。

// 在基础板级文件里
&uart1 {
    pinctrl-0 = <&uart1_pins>;
    status = "okay";
};

这段代码的意思是:找到标签为 uart1 的节点,往里面添加引脚控制和状态信息。这种写法特别适合做板级适配——核心芯片的节点定义不变,不同开发板通过引用来定制自己的配置。

你想想看,如果没有引用机制,每个开发板都得把整个设备树复制一份,改几个参数。那维护起来得多痛苦?

包含文件:代码复用

设备树支持 #include 预处理指令,用法和C语言一样。

#include "soc.dtsi"
#include "memory.dtsi"

/ {
    model = "MyBoard V2";
};

.dtsi 文件是设备树的头文件,通常放一些通用的定义。比如某个芯片厂商会提供 soc.dtsi,里面定义了所有外设的节点。你做板级开发时,直接包含这个文件,然后通过引用修改需要的部分就行。

实际项目中,我见过有人把整个设备树写在一个文件里,足足三千多行。后来换了个芯片型号,改起来简直要命。正确的做法是把通用部分拆成 .dtsi,板级差异放在 .dts 里。这样换芯片时只需要改 .dtsi,换板子时只需要改 .dts

宏定义:让设备树更灵活

设备树支持用 #define 定义宏,配合 #include 使用效果很好。

#define UART_BASE 0x10001000
#define UART_SIZE 0x100

uart1: serial@UART_BASE {
    reg = <UART_BASE UART_SIZE>;
};

宏定义最大的好处是:当你有多个相同类型的外设时,改一个宏就能同步更新所有地方。比如你有4个UART,基地址分别是 0x100010000x100020000x100030000x10004000,用宏定义可以避免手写时把某个地址写错。

嗯,这里要注意:宏定义只在预处理阶段生效,编译完成后就没了。所以你不能在设备树运行时动态改变宏的值。

知识体系结构图

下面这张图把设备树语法的核心要素串起来了,你可以对照着理解它们之间的关系。

设备树语法 节点 (Node) 属性 (Property) 标签 (Label) 引用 (&) 包含文件 (#include) 宏定义 (#define) 核心关系 • 节点是设备树的基本单元,属性描述节点的特征 • 标签给节点起别名,引用通过标签修改节点内容 • 包含文件实现代码复用,宏定义提高可维护性

这张图把六个要素分成了两层。底层是节点和属性——这是设备树的基础,你必须掌握。上层是标签、引用、包含文件、宏定义——这些是进阶技巧,能让你的设备树更简洁、更易维护。

我个人经验:刚开始学设备树时,先把节点和属性搞明白。能看懂一个简单的设备树文件后,再学标签和引用。最后才是包含文件和宏定义。别想着一步到位,容易把自己绕晕。

好了,设备树语法的核心内容就这些。记住一个原则:设备树是描述硬件的,不是写程序的。它的语法虽然看起来像代码,但本质上是一份结构化的配置文件。你把它当成填表格来理解,反而更容易上手。