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

大家好,我是你们的老朋友。今天咱们来聊聊设备树语法的基础。说实话,我刚接触设备树那会儿,看着那一堆花括号和分号,心里直犯嘀咕——这玩意儿跟C语言长得也太像了吧?但用久了你会发现,它其实比C语言简单多了。

设备树说白了就是一套描述硬件信息的“说明书”。它告诉内核:你有哪些外设、它们挂在哪条总线上、中断怎么接、GPIO怎么用。没有它,内核就像瞎子摸象,啥也干不了。

设备树语法核心 节点 (Node) 属性 (Property) 标签/别名/包含 根节点 / 子节点 compatible reg interrupts 标签 label: 别名 aliases #include "xxx.dtsi" — 包含文件

1. 节点——设备树的基本单元

节点就是设备树里的“文件夹”。每个节点代表一个硬件设备或总线。根节点用斜杠 / 表示,所有设备都挂在它下面。

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
    
    cpu@0 {
        compatible = "arm,cortex-a8";
        reg = <0>;
    };
    
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };
};

看到没?每个节点都有名字,比如 cpu@0。@后面的数字是地址,用来区分同类型设备。我刚开始写的时候老忘记加@,结果内核死活认不出第二个CPU——嗯,这个坑我踩过。

节点命名规则:

  • 节点名格式:名称@地址
  • 名称不能有特殊字符,一般用字母、数字、短横线
  • 地址部分通常用十六进制,比如 uart@44e09000
  • 根节点没有名字,直接用 /

2. 属性——描述节点的“身份证”

属性就是节点的特征。它告诉内核这个设备是什么、怎么用。常见的属性类型有字符串、整数数组、二进制数据等。

属性类型 写法示例 说明
字符串 compatible = "ti,am335x-uart"; 用于匹配驱动
32位整数 reg = <0x44e09000 0x2000>; 地址和大小
64位整数 reg = <0x00000000 0x80000000 0x00000000 0x20000000>; 高32位在前
布尔值 status = "disabled"; 存在即true
数组 interrupts = <0 56 4>; 中断描述

我个人习惯把 compatible 属性放在每个节点的第一行。为什么?因为内核匹配驱动时第一个就看它。你想想看,如果驱动和设备树里的compatible对不上,那设备根本没法工作。

小技巧:compatible 的命名格式一般是 "厂商,设备型号"。比如 "ti,am335x-uart" 表示TI公司的AM335x系列UART。这样能避免不同厂商的同名设备冲突。

3. 标签——给节点起个“外号”

标签就是节点的别名。用冒号加在节点名前。有了标签,其他地方引用这个节点就方便多了。

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

这里的 &uart0 就是标签。以后想引用这个串口节点,直接用 &uart0 就行。我在项目中经常用标签来覆盖节点属性,比如在板级dts里修改某个外设的status:

&uart0 {
    status = "okay";
};

这样就不用把整个节点重新写一遍了。省事吧?

4. 别名——全局的“快捷方式”

别名和标签有点像,但它是放在根节点的 aliases 子节点里的。别名的作用是给一些常用设备起个简短的名字,方便bootloader或内核代码引用。

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

我记得有一次调试SD卡启动,bootloader就是通过 mmc0 这个别名找到SD卡控制器的。如果别名写错了,系统根本起不来。所以别名这东西,看着简单,但千万别写错。

注意:别名和标签的区别:

  • 标签(label)是节点的一部分,写在节点定义里
  • 别名(alias)是独立的节点,放在 /aliases 下
  • 标签用于设备树内部引用,别名更多用于bootloader和内核代码

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

设备树支持 #include 指令,就像C语言一样。这样可以把通用的硬件定义放在一个文件里,不同板子只需要包含它,再修改差异部分就行。

// am335x-boneblack.dts
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"

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

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;
};

你看,板级dts文件通常只有几十行。大部分硬件定义都在 .dtsi 文件里。这样维护起来特别方便——SoC升级了,只需要改 .dtsi 文件,所有板子自动受益。

我曾经接手过一个项目,前任工程师把所有硬件定义都写在一个dts文件里,足足3000多行。每次改个GPIO都要翻半天。后来我把它拆成多个 .dtsi 文件,清爽多了。所以我的建议是:能用 #include 就别硬编码。

包含文件的最佳实践:

  • .dtsi 文件放SoC通用定义,比如CPU、中断控制器、时钟
  • .dts 文件放板级特有定义,比如外设使能、引脚复用
  • 头文件(.h)放宏定义,比如GPIO号、中断号
  • 用 #include 而不是 /include/(后者是旧语法)

6. 实战:一个完整的设备树片段

说了这么多,咱们来看个完整的例子。这是一个I2C控制器节点的定义:

/* i2c0: 位于 0x44e0b000,中断号 70 */
&i2c0: i2c@44e0b000 {
    compatible = "ti,omap4-i2c";
    reg = <0x44e0b000 0x1000>;
    interrupts = <70>;
    status = "disabled";
    
    #address-cells = <1>;
    #size-cells = <0>;
    
    /* 子节点:挂载在I2C总线上的设备 */
    eeprom@50 {
        compatible = "atmel,24c32";
        reg = <0x50>;
        pagesize = <32>;
    };
    
    rtc@68 {
        compatible = "dallas,ds1307";
        reg = <0x68>;
    };
};

这里有几个关键点:

  • #address-cells 和 #size-cells 告诉内核子节点的地址和大小用几个32位整数表示
  • 子节点的 reg 属性就是I2C从设备地址
  • 每个子节点也有自己的 compatible,用于匹配对应的驱动

说实话,我刚学设备树时最困惑的就是 #address-cells 和 #size-cells。后来才明白,这俩属性决定了 reg 的解析方式。比如 #address-cells = <1> 表示地址用1个32位数,#size-cells = <0> 表示没有大小字段。你想想看,I2C设备只需要一个地址,不需要大小,所以size-cells为0。

避坑指南:我曾经在写SPI设备节点时,把 #address-cells 设成了2,结果reg怎么都对不上。后来查了半天才发现,SPI从设备只需要片选号,一个32位就够了。所以记住:地址和大小字段的数量一定要和总线协议匹配。

好了,设备树的基本语法就这些。节点、属性、标签、别名、包含文件——这五个概念搞懂了,你就能看懂大部分设备树文件了。下一节咱们会深入中断和GPIO的配置,那才是真正考验功力的时候。

可可精品资料库