第二课:设备树基础语法——节点、属性、标签、别名、包含与宏

各位同学,欢迎来到设备树学习的第二站。上一章我们聊了设备树是干什么的,今天咱们就动手拆解它的语法结构。说白了,设备树就是一个描述硬件信息的文本文件,但它的语法规则很严谨,写错了内核就不认账。

我个人习惯把设备树比作一张「硬件清单」。你想想看,你要给一个嵌入式板子写驱动,总得告诉内核:CPU 是什么、内存多大、I2C 控制器挂在哪、GPIO 引脚怎么接……这些信息,全写在设备树里。而节点和属性,就是这张清单上的「条目」和「具体参数」。

一、节点(Node)与属性(Property)

节点是设备树的基本组成单元。每个节点代表一个硬件设备或总线,用花括号 {} 包裹。属性则是节点的具体描述,比如地址、中断号、时钟频率等。

来看一个最简单的例子:

/dts-v1/;

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard";

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0>;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };
};

这里 / 是根节点,代表整个板子。根节点下面有两个子节点:cpu@0 和 memory@80000000。每个节点里都有属性,比如 compatible、reg。属性值可以是字符串、数字、数组等。

核心要点:

  • 节点名格式:名称@地址(地址可选,用于区分同类型设备)
  • 属性名小写,值用双引号或尖括号包裹
  • reg 属性描述地址和大小,格式为 <起始地址 长度>

我在项目中遇到过一个问题:有同事把 reg 的地址写成了十进制,结果内核解析时直接崩溃。嗯,这里要注意,reg 里的数字默认是十六进制,别写成 0x80000000 以外的格式。

二、标签(Label)与别名(Alias)

标签和别名,说白了就是给节点起个「外号」。为什么要这么做?因为设备树里节点名可能很长,比如 soc/spi@ff110000,每次引用都写全路径太痛苦了。

标签的写法很简单,在节点名前加个冒号:

spi0: spi@ff110000 {
    compatible = "vendor,spi-controller";
    reg = <0xff110000 0x1000>;
};

这样,spi0 就成了这个节点的标签。在其他地方引用时,可以用 &spi0 来指向它。比如在板级文件里覆盖属性:

&spi0 {
    status = "okay";
    clock-frequency = <1000000>;
};

别名则是在根节点下的 aliases 节点里定义的:

/ {
    aliases {
        spi0 = &spi0;
        serial0 = &uart0;
    };
};

我个人习惯用标签来引用节点,因为更直观。别名主要用于一些历史遗留的驱动,或者需要给用户空间提供固定名称的场景。

避坑指南:我曾经在写设备树时,给两个不同的节点起了相同的标签,结果编译报错说「重复定义」。标签在整个设备树里必须是唯一的,这点和 C 语言的变量名一样。

三、包含文件(#include)与 dtsi

一个复杂的板子,设备树可能有上千行。如果所有内容都写在一个文件里,维护起来简直是噩梦。所以设备树支持 #include 预处理指令,和 C 语言一样。

.dtsi 文件是设备树的「头文件」,通常存放 SoC 公共部分,比如 CPU 核心、中断控制器、时钟等。而 .dts 文件是具体的板级文件,只写板子特有的内容。

举个例子:

// myboard.dts
#include "soc.dtsi"
#include "myboard-pinctrl.dtsi"

/ {
    model = "MyBoard v2.0";
    compatible = "vendor,myboard";

    &uart0 {
        status = "okay";
        baud-rate = <115200>;
    };
};

而 soc.dtsi 里定义了 SoC 的公共节点:

// soc.dtsi
/ {
    uart0: uart@ff100000 {
        compatible = "vendor,uart";
        reg = <0xff100000 0x1000>;
        interrupts = <0 33 4>;
        status = "disabled";
    };
};

这样,不同板子只需要包含同一个 soc.dtsi,然后覆盖自己需要的节点即可。你想想看,如果每个板子都重复写一遍 UART 的定义,那得多累?

最佳实践:

  • SoC 公共部分放 .dtsi,板级特有部分放 .dts
  • 用 #include 而不是 /include/(后者是旧语法,不推荐)
  • 覆盖节点时用 &标签 语法,不要重新定义整个节点

四、注释与宏定义

设备树的注释和 C 语言一样,支持两种风格:

  • // 单行注释
  • /* 多行注释 */

我建议在设备树里多写注释,尤其是那些「为什么这么配」的地方。比如:

// 这个 GPIO 必须拉高,否则 PMIC 不启动
gpio-hog {
    gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
    output-high;
};

宏定义则依赖 C 预处理器。设备树文件经过 cpp(C 预处理器)处理后才被编译,所以你可以用 #define 定义常量:

#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW  1

/ {
    led {
        gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>;
    };
};

不过要注意,宏定义只在 .dts 文件里有效,.dtsi 里如果用了宏,必须在包含之前定义好。我曾经踩过这个坑:在 soc.dtsi 里用了 IRQ_TYPE_EDGE_RISING,但忘记在板级文件里 #define,结果编译报错说宏未定义。

警告:不要滥用宏定义。设备树是描述硬件的,不是写算法的。如果宏定义太多,反而降低了可读性。我个人只在 GPIO 电平、中断类型等少量场景使用宏。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

设备树基础语法知识体系 设备树语法 节点与属性 标签与别名 包含与dtsi 注释与宏 关键点 • 节点名格式:名称@地址 • reg属性:<起始地址 长度> • compatible:驱动匹配 关键点 • 标签:节点前加冒号 • 引用:&标签名 • 别名:aliases节点 关键点 • .dtsi:SoC公共部分 • .dts:板级特有部分 • #include预处理 关键点 • // 单行注释 • /* */ 多行注释 • #define宏定义 核心原则: 分层设计(SoC公共 + 板级特有) + 清晰命名 + 适度注释

好了,这一章的内容就到这里。设备树的语法其实不复杂,关键是多写多练。你可以在自己的开发板上试着写一个简单的设备树,从根节点开始,慢慢添加 CPU、内存、串口等节点。遇到问题不要怕,编译器的报错信息会告诉你哪里写错了。

记住,设备树是给内核看的「硬件说明书」,写清楚、写规范,后面调试驱动时会省很多事。

小练习:试着写一个包含两个 LED 节点的设备树,每个 LED 占用一个 GPIO,并用标签和别名分别引用它们。编译通过后,你就算入门了。

可可精品资料库