设备树基础语法:从DTS文件到中断绑定

各位同学,今天我们来啃设备树语法这块硬骨头。说实话,我刚接触设备树那会儿,看着满屏的#address-cells和interrupt-parent也是一头雾水。但后来在移植U-Boot时被它折腾过几回,慢慢就摸出门道了。

设备树说白了就是一套描述硬件信息的"说明书"。U-Boot启动时把它读进来,内核启动时也用它。你想想看,如果没有这套标准,每个板子都得写死代码,那移植工作得多痛苦?

2.1 DTS文件结构:一棵倒着长的树

DTS(Device Tree Source)是设备树的源文件,编译后生成DTB。它的结构很简单——就是一个树形结构,根节点是/,下面挂各种子节点。

/dts-v1/;

/ {
    compatible = "vendor,board-name";
    #address-cells = <1>;
    #size-cells = <1>;

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

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

嗯,这里要注意几点:

  • /dts-v1/; 是版本声明,必须写在最前面
  • 根节点用 / 表示,整个文件只有一个根
  • 节点名后面可以跟 @地址,比如 cpu@0
  • 每个节点用花括号 { } 包裹,最后加分号
我的习惯:写DTS文件时,我会先把根节点下的 #address-cells 和 #size-cells 写好。这两个属性决定了地址和长度用几个32位整数表示,后面所有子节点都继承这个规则。忘了写?编译不会报错,但运行时地址解析全乱套——我踩过这个坑。

2.2 节点与属性:硬件的"身份证"

每个节点代表一个硬件设备,属性就是描述这个设备的各种参数。常用的属性我列个表:

属性名 作用 示例
compatible 设备兼容性标识,驱动匹配用 "ti,am335x-uart"
reg 寄存器地址和长度 <0x48020000 0x1000>
interrupts 中断号、触发类型 <0 56 4>
gpios GPIO引脚绑定 <&gpio1 15 GPIO_ACTIVE_HIGH>
status 设备状态(okay/disabled) "okay"

节点之间可以嵌套,也可以引用。比如:

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

这里的 &uart0 就是引用其他地方定义的节点标签。我在移植时经常用这种方式——先在根节点下定义好所有外设,然后在板级文件里通过引用来使能或配置它们,代码复用率很高。

2.3 reg属性与地址编码:告诉CPU设备在哪

reg 属性是设备树里最重要的属性之一。它告诉系统:这个设备的寄存器在哪个地址,占多大空间。

uart@48020000 {
    compatible = "ti,am335x-uart";
    reg = <0x48020000 0x1000>;
    interrupts = <72>;
};

这里 0x48020000 是基地址,0x1000 是地址范围(4KB)。但为什么有时候 reg 里只有两个数,有时候却有四个?

答案就在 #address-cells 和 #size-cells 里:

  • #address-cells 表示地址占几个32位整数
  • #size-cells 表示长度占几个32位整数

比如64位地址的SoC:

/ {
    #address-cells = <2>;  // 地址用两个32位,即64位
    #size-cells = <2>;     // 长度也用两个32位

    memory@800000000 {
        device_type = "memory";
        reg = <0x00000008 0x00000000 0x00000000 0x80000000>;
        // 地址: 0x00000008_00000000
        // 长度: 0x00000000_80000000 (2GB)
    };
};
我曾经踩过的坑:有一次移植U-Boot到一块新板子,内存始终只识别到1GB,查了两天才发现是 #size-cells 写成了1,导致高32位地址被截断。所以写 reg 之前,一定先确认父节点的 #address-cells 和 #size-cells 对不对。

2.4 中断绑定:设备怎么"喊"CPU

中断绑定的核心是三个要素:中断控制器、中断号和触发类型。看个例子:

/ {
    interrupt-parent = <&gic>;  // 全局中断控制器

    gic: interrupt-controller@48241000 {
        compatible = "arm,cortex-a7-gic";
        reg = <0x48241000 0x1000>;
        interrupt-controller;
        #interrupt-cells = <3>;  // 每个中断用3个cell描述
    };

    uart@48020000 {
        compatible = "ti,am335x-uart";
        reg = <0x48020000 0x1000>;
        interrupts = <0 56 4>;
        // 0: 中断类型(SPI)
        // 56: 中断号
        // 4: 触发类型(高电平触发)
    };
};

这里 #interrupt-cells = <3> 表示每个中断描述需要3个32位整数。具体含义由中断控制器自己定义,GIC的标准是:

  • 第一个cell:中断类型(0=PPI,1=SPI)
  • 第二个cell:中断号(SPI从32开始)
  • 第三个cell:触发类型(1=上升沿,4=高电平)

如果设备有多个中断:

interrupts = <0 56 4>, <0 57 4>;  // 两个中断

2.5 GPIO绑定:控制引脚的"开关"

GPIO绑定比中断稍微复杂一点,因为涉及引脚号和电气特性。标准写法:

gpio1: gpio@4804c000 {
    compatible = "ti,omap4-gpio";
    reg = <0x4804c000 0x1000>;
    gpio-controller;
    #gpio-cells = <2>;  // 每个GPIO用2个cell
};

led {
    compatible = "gpio-leds";
    led-red {
        gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
        label = "red-led";
    };
};

这里 gpios 属性的含义:

  • &gpio1:引用GPIO控制器
  • 15:引脚号(GPIO1_15)
  • GPIO_ACTIVE_HIGH:高电平有效

如果引脚需要配置上下拉或驱动能力,可以在 pinctrl 里定义:

&gpio1 {
    pinctrl-0 = <&led_pins>;
    pinctrl-names = "default";
};

led_pins: pinmux_led_pins {
    pinctrl-single,pins = <
        0x0c 0x07  // 引脚复用配置
    >;
};

核心要点:设备树的GPIO绑定本质上是把"哪个控制器、哪个引脚、什么电平"这三个信息打包成一个属性。U-Boot在解析时,会根据 #gpio-cells 的值来拆分这些信息。我建议你在写GPIO绑定时,先确认GPIO控制器的 #gpio-cells 是2还是3——有些SoC需要3个cell来指定引脚复用功能。

2.6 知识体系总览

下面这张图把本章的核心知识点串起来了,你可以对照着看:

设备树基础语法知识体系 DTS文件结构 节点与属性 reg与地址编码 中断与GPIO绑定 compatible status #address-cells #size-cells interrupts gpios 核心:每个节点通过compatible匹配驱动,通过reg找到寄存器地址 通过interrupts/gpios与中断控制器和GPIO控制器建立连接 U-Boot解析DTB → 初始化硬件

这张图把设备树语法的核心脉络理清楚了。从DTS文件结构出发,三个分支分别对应节点属性、地址编码和中断GPIO绑定,最终汇聚到U-Boot解析DTB并初始化硬件这个终点。你在写设备树时,脑子里要有这张图——每个节点、每个属性都不是孤立的,它们共同构成了一套完整的硬件描述语言。