2、设备树语法精讲:节点、属性、标签、包含机制

设备树这东西,说白了就是一块描述硬件的数据结构。我刚开始接触Linux驱动时,总觉得它很神秘——为什么非得用这种格式来描述硬件?后来做多了才发现,它其实就是个「硬件说明书」,告诉内核你的板子上有什么、怎么访问。

今天咱们就把设备树的四个核心概念掰开揉碎讲清楚。嗯,我保证不绕弯子。

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

节点是设备树最基本的组成单元。你可以把它想象成一颗树上的分叉点,每个节点代表一个硬件设备或总线。

节点长这样:

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

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

根节点是 /,它下面挂载了 cpu 和 memory 两个子节点。每个节点用花括号 { } 包裹,最后用分号结束。

关键点:节点名可以重复,但路径必须唯一。比如 /soc/i2c@1234 和 /soc/i2c@5678 是两个不同的节点。

我在项目中遇到过一个问题:两个外设的节点名写成了 uart 和 uart@1234,结果内核解析时把后者当成了前者的别名,导致驱动加载失败。嗯,节点名后面加 @地址 是规范写法,别偷懒。

2.2 属性(Property)——节点的灵魂

属性就是节点的「配置项」。它告诉驱动:这个设备用什么协议、地址是多少、中断号是什么。

常见的属性类型有四种:

类型 写法 示例
字符串 "string" compatible = "ti,am335x-uart";
32位整数 <value> reg = <0x44e09000 0x2000>;
64位整数 <0x00000000 0x80000000> 用两个32位拼一个64位
字节数组 [hex bytes] local-mac-address = [00 11 22 33 44 55];

我个人习惯把 compatible 属性放在每个节点的第一行。为什么?因为内核匹配驱动时第一个就看它。你想想看,如果驱动和设备树的 compatible 字符串对不上,后面写再多属性都是白搭。

避坑指南:我曾经在 reg 属性里少写了一个地址单元,导致驱动读到的内存地址完全错位。检查时发现 #address-cells 和 #size-cells 没配对。记住:父节点定义地址长度,子节点按这个长度写 reg。

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

标签就是节点的别名。它让你在别处引用节点时不用写一长串路径。

uart0: serial@44e09000 {
    compatible = "ti,am335x-uart";
    reg = <0x44e09000 0x2000>;
    interrupts = <72>;
};

这里的 uart0 就是标签。在其他地方你可以直接用 &uart0 来引用它:

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

标签的作用说白了就两个:一是让引用更简洁,二是方便在多个文件中覆盖节点属性。我在做板级支持包时,经常把基础配置写在 .dtsi 文件里,然后在 .dts 中用标签覆盖某些属性——这样既保留了通用配置,又能针对具体板子做调整。

注意:标签名不能重复,也不能以数字开头。我见过有人用 0uart 做标签,结果编译直接报错。标签的命名规范和C语言变量名一样。

2.4 包含机制(Include)——模块化的基石

设备树支持 #include 指令,作用和C语言的头文件类似。它让你把通用的硬件描述拆分成多个文件,避免重复劳动。

举个例子,一个典型的设备树文件结构:

// am335x.dtsi
#include "skeleton.dtsi"

/ {
    compatible = "ti,am335x";
    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
        };
    };
};
// myboard.dts
#include "am335x.dtsi"

/ {
    model = "My Custom Board";
    memory {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB
    };
};

编译时,myboard.dts 会先展开 am335x.dtsi 的内容,然后再叠加自己的配置。最终生成一个完整的设备树。

这里有个细节:.dtsi 文件通常不包含根节点的闭合花括号,因为它只是「片段」,需要被 .dts 文件包含后才能形成完整的树结构。

实用技巧:我习惯把 SoC 公共部分、外设控制器、时钟配置等放到 .dtsi 里,只把板级特有的东西(如内存大小、GPIO 复用、外设使能)放到 .dts 中。这样换板子时只需要改一个文件,省心不少。

2.5 知识体系总览

下面这张图把四个概念的关系串起来了。你看一眼就能明白:节点是骨架,属性是血肉,标签是名字,包含是拼图。

根节点 / cpu 节点 memory 节点 uart 节点 属性 (Property) compatible = "arm,cortex-a8"; reg = <0x00>; 属性 (Property) device_type = "memory"; reg = <0x80000000 0x40000000>; 标签 (Label) uart0: serial@44e09000 { interrupts = <72>; 包含机制 (Include) #include "skeleton.dtsi" 根节点 子节点 属性/标签 包含机制

你看,设备树的语法其实不复杂。节点搭框架,属性填细节,标签方便引用,包含实现复用。这四个概念搞明白了,写设备树就跟搭积木一样简单。

最后说一句:别怕报错。我刚开始写设备树时,编译错误能堆满一屏幕。但每次报错都是学习的机会——你想想看,编译器其实在帮你检查语法,比你自己肉眼检查靠谱多了。

个人经验:写设备树时,先把 compatible 和 reg 写对,这两个对了,驱动基本就能跑起来。其他属性可以慢慢加。我曾经为了调一个I2C设备,花了三天才发现是 reg 的地址偏移写错了——嗯,从那以后我每次写 reg 都会拿数据手册对照三遍。

可可精品资料库