第2章:设备树基础语法——节点、属性、标签与别名

设备树说白了就是一套描述硬件的“数据结构”。你给它一个树形结构,它告诉内核:这里有个I2C控制器,那里有个GPIO引脚,时钟频率是多少。我刚开始接触设备树时,觉得它跟XML差不多,后来踩了几个坑才发现——语法细节才是魔鬼。

这一章我们就把最基础的四个概念讲透:节点、属性、标签、别名。你把这四个东西搞明白了,后面写设备树就跟写配置文件一样顺手。

2.1 节点(Node)——设备树的骨架

节点是设备树的基本组成单元。每个节点代表一个硬件设备或总线。节点之间是父子关系,形成一棵树。

节点的写法很简单:

/dts-v1/;

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

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

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

看到没?根节点是 /,里面挂了一个 cpu 节点和一个 memory@80000000 节点。节点名后面可以跟 @地址,用来区分同一层级下相同名字的节点。

节点命名规则:

  • 节点名只能包含小写字母、数字、连字符(-)和逗号(,)
  • 不能以数字开头
  • 如果节点有地址,用 @ 分隔,地址通常是十六进制
  • 同一父节点下,节点名必须唯一

我在项目中遇到过一个问题:两个外设用了相同的节点名,结果内核只识别了最后一个。嗯,从那以后我养成了给节点加地址后缀的习惯,哪怕地址是0。

2.2 属性(Property)——节点的“身份证”

属性就是节点的配置信息。每个属性都有一个名字和一个值。值的类型可以是字符串、整数、字节数组、布尔值等。

常见的属性写法:

uart0: serial@10000000 {
    compatible = "fsl,imx6-uart", "ns16550";
    reg = <0x10000000 0x4000>;
    interrupts = <0 59 4>;
    clocks = <&clks IMX6QDL_CLK_UART>;
    status = "okay";
    dma-names = "rx", "tx";
};

这里我列几个最常用的属性类型:

属性类型 写法 说明
字符串 "string" 用双引号包裹
32位整数 <0x12345678> 尖括号包裹,十六进制或十进制
64位整数 <0x00000000 0x12345678> 两个32位拼一个64位
字节数组 [01 02 03 04] 方括号包裹,空格分隔
布尔值 属性名本身 存在即为true,不存在为false
字符串列表 "str1", "str2" 逗号分隔多个字符串

避坑指南:我曾经在 compatible 属性里把厂商名写反了——应该是 "厂商,设备",我写成了 "设备,厂商"。结果内核匹配不到驱动,查了两天才发现。记住:厂商在前,设备在后,这是约定俗成的规矩。

2.3 标签(Label)——给节点起个“外号”

标签是节点的引用名。你可以在节点名前加一个标签,然后在其他地方用 &标签 来引用这个节点。

看个例子:

uart0: serial@10000000 {
    compatible = "ns16550";
    reg = <0x10000000 0x4000>;
};

// 在其他地方引用
&uart0 {
    status = "okay";
};

这里 uart0 就是标签。后面用 &uart0 就能找到这个节点,往里面追加属性或修改属性。

标签的命名规则:

  • 只能包含字母、数字、下划线
  • 不能以数字开头
  • 不能是关键字(比如 compatible、reg 这些)
  • 整个设备树里标签必须唯一

注意:标签不是必须的。但如果你需要在其他地方修改这个节点,就必须给它加标签。我习惯给所有外设节点都加标签,哪怕暂时用不到——因为你永远不知道后面会不会需要覆盖某个属性。

2.4 别名(Aliases)——给节点一个“短路径”

别名放在根节点的 aliases 节点里,作用是给一些常用节点起一个简短的名字。内核启动时,有些驱动会通过别名来查找设备。

写法如下:

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

    uart0: serial@10000000 {
        // ...
    };

    uart1: serial@10001000 {
        // ...
    };
};

别名和标签的区别:

对比项 标签(Label) 别名(Aliases)
定义位置 节点名前 根节点下的 aliases 节点
引用方式 &标签 &别名 或字符串查找
主要用途 在dts文件中引用和修改节点 给内核提供固定名称的设备路径
作用域 整个设备树编译单元 全局可见,常用于bootloader传递

说白了,标签是给开发者用的,别名是给内核用的。你想想看,如果内核启动时想找串口0,它不可能去遍历整个设备树——直接查 aliases 里的 serial0 就搞定了。

2.5 知识体系总览

这一章的内容其实就围绕一个核心:如何用设备树描述硬件。下面这张图帮你理清四个概念之间的关系:

设备树基础语法知识体系 设备树基础语法知识体系 根节点 / 节点 (Node) 硬件设备/总线 属性 (Property) 配置信息 标签 (Label) 引用名 别名 (Aliases) 短路径 节点是骨架,属性是血肉,标签是别名,别名是给内核的快捷方式 四者配合,完成硬件描述 实际写法示例 uart0: serial@10000000 { // 标签: uart0, 节点: serial@10000000 compatible = "ns16550"; // 属性: 字符串 };

2.6 实战中的常见坑

讲完理论,我分享几个实际项目中踩过的坑:

坑1:标签重复

我曾经在一个包含多个dtsi文件的工程里,两个文件都定义了 uart0 标签。编译时没报错,但运行时串口0死活不工作。后来发现第二个定义覆盖了第一个。记住:标签在整个编译单元里必须唯一,包括所有include进来的文件。

坑2:属性值类型搞错

有一次我把 reg 属性的地址写成了字符串:reg = "0x10000000"。内核解析时直接崩溃。属性值类型必须严格匹配——地址用 <>,字符串用 "",不能混用。

坑3:别名和标签混淆

新手容易把 aliases 当成标签来用。其实别名是给内核用的固定名称,标签是给开发者用的引用名。我建议:所有外设节点都加标签,只在需要给内核提供固定路径时才加别名。

2.7 本章小结

这一章我们讲了四个核心概念:

  • 节点:设备树的骨架,每个硬件对应一个节点
  • 属性:节点的配置信息,类型要严格匹配
  • 标签:给节点起外号,方便在其他地方引用
  • 别名:给内核提供固定路径,常用于串口、MMC等标准设备

这些语法看起来简单,但实际项目中80%的设备树问题都出在语法细节上。我建议你动手写几个简单的dts文件,用dtc编译一下,看看报错信息——这是最快的学习方式。