设备树语法入门:节点、属性、标签、别名与包含文件

设备树这东西,说白了就是一套描述硬件的「说明书」。内核启动时读它,才知道你板子上接了哪些外设、内存多大、中断怎么连。我刚开始接触设备树时也觉得它啰嗦,后来调过几个驱动才明白——没有它,ARM 平台早就被各种板级补丁淹没了。

这一章我们就把设备树最基础的几个概念讲透:节点、属性、标签、别名,还有怎么用 #include 复用公共代码。你把这些搞清楚了,后面写驱动才能顺手。

1. 节点(node)——设备树的骨架

设备树的每个「设备」或「总线」都是一个节点。节点从根 / 开始,一层套一层,像文件系统的目录结构。

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

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

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };
};

每个节点用 节点名@地址 标识。地址部分通常写寄存器基址或单元号,没有地址时可以省略 @ 后面的部分。我见过不少新手把 cpu@0 写成 cpu0,虽然也能解析,但不符合规范,容易在调试时产生歧义。

我的习惯:节点名尽量用通用名称,比如 serial、mmc、i2c,不要用芯片型号。这样换 SoC 时只需要改 compatible 和 reg,结构不用动。

2. 属性(property)——节点的「身份证」

属性就是键值对,告诉内核这个节点是什么、怎么用。常见的属性类型有四种:

属性类型 写法示例 说明
字符串 compatible = "ti,am335x-adc"; 驱动匹配的关键,格式通常是「厂商,设备名」
32位整数 reg = <0x44e0d000 0x1000>; 地址和长度,用尖括号包裹
布尔值 status = "disabled"; 存在即 true,常用 status 控制设备启用/禁用
数组 interrupts = <0 56 4>; 多个值用空格分隔,尖括号包裹

这里我特别想强调 compatible 属性。驱动就是靠它来匹配设备的。你在驱动里写 of_match_table,内核会拿这个字符串跟设备树里的 compatible 比对。我曾经在一个项目里把 compatible 写成了 "ti,am335x-adc" 但驱动里写的是 "ti,am335x-adc-tsc",结果设备死活不 probe,查了两天才发现是字符串没对齐。

注意:compatible 的匹配顺序是从左到右。内核会先尝试最具体的字符串,匹配不上再试更通用的。所以把最精确的型号写在最前面。

3. 标签(label)——给节点起个「外号」

标签就是节点的别名,方便在其他地方引用。语法很简单:在节点名前加一个标识符,后面跟冒号。

&uart0 {
    status = "okay";
    pinctrl-0 = &uart0_pins;
};

这里的 uart0 就是标签。你可以在任何地方用 &uart0 来引用这个节点。我个人的习惯是:所有重要的外设节点都加上标签,哪怕当前不需要覆盖。因为产品迭代时,你永远不知道下一版硬件会不会改引脚。

标签的命名规则:

  • 只能包含字母、数字、下划线
  • 不能以数字开头
  • 不能跟关键字冲突(比如 memory、chosen)

实战经验:我在一个 FPGA 项目中,用标签实现了「一个设备树文件支持两种硬件版本」。通过 &fpga_core 标签,在顶层 dts 里根据 compatible 选择不同的覆盖内容。这样维护起来特别清爽。

4. 别名(aliases)——给节点编号

别名和标签不同。标签是给开发者看的,别名是给内核用的。别名定义在根节点的 aliases 子节点里,通常用来给设备分配固定的序号。

/ {
    aliases {
        serial0 = &uart0;
        serial1 = &uart1;
        mmc0 = &mmc1;
        mmc1 = &mmc2;
    };
};

内核启动时,会解析 aliases 节点,把序号和设备对应起来。比如你插了串口线,系统里显示 /dev/ttyS0,这个 0 就是从 serial0 来的。

为什么要用别名?我遇到过一个问题:两个板子用的 SoC 相同,但 UART 的硬件实例编号不同。没有别名的话,一个板子上 ttyS0 是调试串口,另一个板子上 ttyS0 可能是蓝牙。加上别名后,统一用 serial0 指向调试串口,驱动层就不用改了。

建议:每个板级 dts 文件里都写上 aliases,哪怕只有一两个设备。这是低成本换来的高可移植性。

5. 包含文件(dtsi)——代码复用的利器

.dtsi 文件就是设备树的「头文件」。SoC 厂商会把通用的外设定义放在 .dtsi 里,板级文件通过 #include 引入,然后只写自己特有的部分。

// am33xx.dtsi(SoC 通用定义)
/ {
    ocp {
        uart0: serial@44e09000 {
            compatible = "ti,am3352-uart", "ti,omap3-uart";
            reg = <0x44e09000 0x2000>;
            interrupts = <72>;
            status = "disabled";
        };
    };
};

// am335x-boneblack.dts(板级文件)
#include "am33xx.dtsi"

&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
};

你看,SoC 文件里把 uart0 定义好,默认 disabled。板级文件只需要 #include 进来,然后通过标签覆盖 status 和引脚配置。这样 SoC 厂商维护一份,所有板子都能用。

我参与过一个项目,团队里有人把整个 SoC 的定义直接复制到板级文件里,结果换了芯片型号后,每个板子都要改几十处。后来我强制要求用 #include 分层管理,维护成本直接降了一半。

避坑指南:我曾经在 #include 时用了相对路径 "../soc/am33xx.dtsi",结果换了编译目录就找不到文件了。后来统一用 <...> 加 -I 编译选项指定搜索路径,再没出过这种问题。

知识体系总览

下面这张图把本章的核心概念串起来了。你看一眼,心里就有谱了。

设备树核心概念关系图 根节点 / cpus memory aliases compatible reg status 标签: uart0 别名: serial0 dtsi 包含 核心逻辑 节点描述硬件 → 属性提供细节 → 标签/别名方便引用 → dtsi 实现复用

从这张图你能看到:根节点下面挂着 cpus、memory、aliases 等子节点;每个节点通过 compatible、reg、status 等属性描述自己;标签和别名让我们能灵活引用;而 dtsi 文件则把公共部分抽离出来,避免重复劳动。

嗯,这一章的内容就这些。你先把节点、属性、标签、别名、包含文件这五个概念吃透,后面写设备树就像搭积木一样简单了。