第四节 设备树语法入门:节点、属性、值、字符串、整数、数组、引用等基本语法元素

设备树(Device Tree)是一种描述硬件资源的数据结构,它通过一种树状的层级关系,将CPU、内存、外设控制器、总线、板级器件等硬件信息以节点(Node)和属性(Property)的形式组织起来。对于嵌入式Linux开发者而言,掌握设备树的基本语法是进行驱动开发、内核移植和板级调试的前提。本节将围绕设备树的语法核心,深入剖析节点、属性、值、字符串、整数、数组以及引用等基本元素,帮助读者建立扎实的语法基础。

根节点 "/" cpus { ... } memory { ... } soc { ... } leds { ... } i2c@1000 uart@2000 gpio@3000 spi@4000 compatible = "vendor,soc-device"; reg = <0x1000 0x100>; interrupts = <0 29 4>; 根节点 父节点 子节点 包含关系

上图展示了设备树的基本层级结构。根节点“/”是所有节点的祖先,其下可以挂载多个子节点,如 cpus、memory、soc 和板级设备节点。每个节点又可以包含子节点,从而形成一棵描述完整硬件拓扑的树。设备树的核心价值在于将硬件配置信息从内核代码中剥离出来,通过 Bootloader(如 U-Boot)传递给内核,实现“硬件描述与内核代码分离”的设计思想。

4.1 节点(Node)与属性(Property)的核心原理

节点是设备树的基本组成单元,用于表示一个硬件设备、总线、控制器或简单的逻辑分组。节点通过路径唯一标识,例如 /soc/i2c@1000 表示根节点下的 soc 节点中的 i2c@1000 节点。节点名称通常包含设备类型和单元地址(如 i2c@1000),其中 @1000 用于区分同一类型的不同实例。

属性是附着在节点上的键值对,用于描述节点的具体特征。属性值可以是空、字符串、整数、字节串、数组等。常见的属性包括 compatible(兼容性标识)、reg(寄存器地址和长度)、interrupts(中断信息)、status(设备状态)等。内核通过解析这些属性来初始化对应的驱动程序。

核心原理:设备树语法遵循“节点-属性”模型。节点代表硬件对象,属性代表对象的参数。内核在启动时通过 of_* API(如 of_find_node_by_path、of_property_read_u32)遍历设备树,获取硬件资源信息并绑定驱动。这种机制使得同一份内核镜像可以支持多种硬件平台,只需更换设备树源文件(.dts)即可。

4.2 值类型与编码规则

设备树中的属性值有多种数据类型,理解这些类型是正确编写和解析设备树的关键。下表总结了常见的数据类型及其编码方式:

数据类型语法表示说明示例
字符串"string"以双引号括起的文本,用于描述设备名称、兼容性等compatible = "arm,cortex-a9";
整数(32位)<0x1234>尖括号内为32位无符号整数,支持十进制或十六进制reg = <0x1000 0x100>;
64位整数<0x00000000 0x80000000>使用两个32位整数拼接表示64位值reg = <0x0 0x80000000 0x0 0x10000000>;
字节串[0x01 0x02 0x03]方括号内为十六进制字节序列,用于MAC地址等local-mac-address = [00 11 22 33 44 55];
数组<0x1 0x2 0x3>尖括号内多个整数,表示一组值clock-frequency = <1000000 2000000>;
布尔值空属性属性存在即为真,不存在为假status = "disabled"; 或省略该属性
引用(phandle)<&node>引用其他节点的句柄,用于建立节点间关联interrupt-parent = <&intc>;

在编写设备树时,需要注意整数默认是32位,若需要64位地址或大小,必须拆分为两个32位整数。此外,#address-cells 和 #size-cells 属性用于定义 reg 属性中地址和长度的单元长度,这是解析 reg 的关键。

4.3 引用(Reference)与标签(Label)

设备树中的引用机制允许一个节点通过 &label 语法引用另一个节点,从而实现节点间的关联。标签(Label)是在节点定义时附加的标识符,例如:

/ {
    intc: interrupt-controller@1000 {
        compatible = "arm,cortex-a9-gic";
        reg = <0x1000 0x1000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    uart0: serial@2000 {
        compatible = "ns16550a";
        reg = <0x2000 0x100>;
        interrupts = <0 5 4>;
        interrupt-parent = <&intc>;
    };
};

在上述代码中,intc: 和 uart0: 是标签,&intc 是对中断控制器节点的引用。这种机制在中断映射、时钟引用、GPIO控制等场景中极为常见。引用不仅简化了设备树的书写,还避免了硬编码路径带来的维护困难。

经验提醒:在编写设备树时,建议为所有重要节点添加有意义的标签(如 uart0、i2c1),这样在驱动代码中可以通过 of_find_node_by_label() 快速定位节点,提高代码可读性。

4.4 案例拆解:从零构建一个简单的设备树

为了将上述语法知识融会贯通,我们以一个典型的嵌入式板卡为例,构建一个包含CPU、内存、UART和GPIO控制器的设备树片段。假设硬件环境如下:

  • CPU:ARM Cortex-A9 双核,主频1GHz
  • 内存:512MB DDR3,起始地址0x80000000
  • UART0:地址0x10000000,中断号29(SPI类型)
  • GPIO控制器:地址0x10001000,支持32个GPIO

对应的设备树源文件(.dts)片段如下:

/dts-v1/;

/ {
    model = "ExampleBoard Rev.1";
    compatible = "vendor,example-board", "arm,cortex-a9";
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a9";
            reg = <0>;
            clock-frequency = <1000000000>;
        };
    };

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

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart0: serial@10000000 {
            compatible = "ns16550a";
            reg = <0x10000000 0x100>;
            interrupts = <0 29 4>;
            interrupt-parent = <&intc>;
            clock-frequency = <24000000>;
            status = "okay";
        };

        intc: interrupt-controller@10002000 {
            compatible = "arm,cortex-a9-gic";
            reg = <0x10002000 0x1000>;
            interrupt-controller;
            #interrupt-cells = <3>;
        };

        gpio0: gpio@10001000 {
            compatible = "vendor,gpio-controller";
            reg = <0x10001000 0x200>;
            gpio-controller;
            #gpio-cells = <2>;
        };
    };
};

在这个案例中,我们定义了根节点及其子节点。关键点包括:

  1. 根节点使用 model 和 compatible 描述板卡型号和兼容性。
  2. CPU节点通过 reg = <0> 表示CPU0,clock-frequency 描述主频。
  3. 内存节点使用 reg 描述起始地址和大小,注意 #address-cells 和 #size-cells 均为1,因此地址和长度各占一个32位整数。
  4. UART节点通过 interrupt-parent 引用中断控制器,interrupts 描述中断类型(0表示SPI)、中断号和触发方式(4表示高电平触发)。
  5. GPIO节点定义了 gpio-controller 属性和 #gpio-cells,为其他设备引用GPIO提供接口。

通过这个案例,读者可以直观地看到节点、属性、值、引用等语法元素如何协同工作,构建出内核可识别的硬件描述。

4.5 常见误区与总结复盘

在设备树语法学习过程中,初学者容易陷入以下误区:

风险提示:

  • 忽略 #address-cells 和 #size-cells:这两个属性决定了 reg 中地址和长度的位数。若父节点和子节点设置不一致,会导致地址解析错误,驱动无法正确获取资源。
  • 字符串与整数混淆:例如将 compatible 写成 <"vendor,device">,这是错误的,字符串应直接使用双引号。
  • 引用未定义标签:使用 &label 时,若标签未定义,编译会报错。务必确保标签在设备树中唯一且已声明。
  • 忘记 status = "disabled" 的语义:当属性值为 "disabled" 时,内核不会激活该设备;若省略该属性,默认视为 "okay"。但某些驱动可能要求显式声明。

总结复盘:设备树语法虽然简单,但细节决定成败。掌握节点、属性、值类型和引用机制,是编写高质量设备树的基础。建议读者在实战中多阅读内核自带的 .dts 文件(如 arch/arm/boot/dts/ 目录下的文件),模仿其写法,并利用 dtc 工具(Device Tree Compiler)进行编译和反编译,验证语法的正确性。通过不断实践,逐步建立起对设备树语法的直觉和熟练度。