4. 设备树基础语法:DTS/DTC/DTB 关系、节点与属性定义、reg/interrupts 属性详解

设备树,说白了就是一套描述硬件信息的“说明书”。Linux 内核通过它来知道你的板子上接了哪些外设、内存多大、中断怎么连。我刚开始接触时也觉得这东西有点绕,但摸清 DTS、DTC、DTB 这三者的关系后,就豁然开朗了。

4.1 DTS / DTC / DTB:三者的“前世今生”

这三个缩写,你几乎每天都会碰到。它们的关系其实很简单:

  • DTS(Device Tree Source):文本文件,人类可读。你用编辑器写的就是它。
  • DTC(Device Tree Compiler):编译器,把 DTS 转成 DTB。
  • DTB(Device Tree Blob):二进制文件,内核启动时解析的就是它。

流程就是:你写 .dts → DTC 编译 → 生成 .dtb → Bootloader 加载 → 内核解析。

核心流程: DTS(源文件) → DTC(编译器) → DTB(二进制) → 内核解析

我个人习惯在 Makefile 里加一句 dtbs 目标,一键编译所有设备树。省得每次手动敲命令。

DTS → DTC → DTB 编译流程 DTS 源文件 .dts / .dtsi DTC 编译器 dtc 工具 DTB 二进制 .dtb Bootloader 将 DTB 加载到内存,内核通过解析 DTB 获取硬件信息

小技巧: 用 dtc -I dtb -O dts -o output.dts input.dtb 可以把 DTB 反编译回 DTS。调试时很有用,我经常这么干。

4.2 节点与属性定义:设备树的“骨架”

设备树的结构,说白了就是一棵树。根节点是 /,下面挂各种子节点。每个节点代表一个硬件设备或总线。

4.2.1 节点定义

基本语法很简单:

/dts-v1/;

/ {
    compatible = "acme,coyote";
    #address-cells = <1>;
    #size-cells = <1>;

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

    serial@101f0000 {
        compatible = "ns16550";
        reg = <0x101f0000 0x1000>;
        interrupts = <1 0>;
    };
};

节点名格式:节点名@地址。地址部分通常写的是寄存器基址,用来区分同名节点。

注意: 节点名里的 @地址 只是标识,真正决定地址的是 reg 属性。两者不一致时,以 reg 为准。我曾经见过一个同事把地址写错,排查了半天。

4.2.2 属性定义

属性就是键值对。常见的有:

属性名 值格式 说明
compatible 字符串 驱动匹配的关键字,格式 "厂商,型号"
reg 32位整数数组 描述地址空间:基址 + 长度
interrupts 32位整数数组 中断号、触发类型等
status 字符串 "okay" 或 "disabled"
#address-cells 整数 reg 中地址字段占几个 cell(32位)
#size-cells 整数 reg 中长度字段占几个 cell

你想想看,#address-cells 和 #size-cells 这两个属性,决定了子节点 reg 的解析方式。如果父节点写 #address-cells = <2>,那子节点的 reg 里地址部分就要写两个 32 位数。

4.3 reg 属性详解:地址空间的“身份证”

reg 属性描述设备占用的地址范围。格式是:

reg = <address1 length1 address2 length2 ...>;

每个 address 和 length 占几个 32 位 cell,由父节点的 #address-cells 和 #size-cells 决定。

举个例子:

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  // 基址 0x80000000,长度 1GB
    };

    gpio@101e4000 {
        compatible = "arm,pl061";
        reg = <0x101e4000 0x1000>;      // 基址 0x101e4000,长度 4KB
    };
};

如果地址是 64 位的,#address-cells 就要设为 2:

/ {
    #address-cells = <2>;
    #size-cells = <2>;

    memory@800000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x40000000>;
        // 高32位 低32位    高32位 低32位
        // 地址 = 0x80000000,长度 = 0x40000000
    };
};

关键点: reg 的 cell 数量 = #address-cells + #size-cells。多段地址就重复这个组合。

4.4 interrupts 属性详解:中断的“接线图”

中断属性描述设备的中断请求线。它依赖 interrupt-parent 和 #interrupt-cells 来解析。

4.4.1 基本用法

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

    gic: interrupt-controller@fff01000 {
        compatible = "arm,gic-400";
        reg = <0xfff01000 0x1000>;
        interrupt-controller;
        #interrupt-cells = <3>;  // GIC 每个中断用 3 个 cell
    };

    serial@101f0000 {
        compatible = "ns16550";
        reg = <0x101f0000 0x1000>;
        interrupts = <0 29 4>;   // 类型 0(SPI),中断号 29,触发方式 4(高电平)
    };
};

对于 ARM GIC,#interrupt-cells = <3> 的三个 cell 含义:

Cell 位置 含义 取值
第 1 个 中断类型 0 = SPI(共享外设中断),1 = PPI(私有外设中断)
第 2 个 中断号 SPI 从 32 开始,PPI 从 16 开始
第 3 个 触发方式 1 = 上升沿,2 = 下降沿,4 = 高电平,8 = 低电平

经验之谈: 中断号在设备树里是硬件中断号,不是 Linux IRQ 号。内核会帮你映射。我曾经在移植时直接写了 Linux IRQ 号,结果中断死活不触发。嗯,这个坑我替你们踩过了。

4.4.2 中断级联

如果板子上有多个中断控制器,可以用 interrupt-map 做级联:

/ {
    interrupt-parent = <&gic>;

    gpio@101e4000 {
        interrupt-controller;
        #interrupt-cells = <2>;

        interrupt-map = <
            0 0 &gic 0 42 4
            1 0 &gic 0 43 4
        >;
    };
};

这里 GPIO 控制器把它的中断 0 映射到 GIC 的 SPI 42,中断 1 映射到 SPI 43。

4.5 避坑指南:我踩过的几个坑

  • reg 地址写错: 我遇到过把 #address-cells 写成 2,但 reg 里只写了一个 32 位数的情况。内核解析出来的地址完全不对。
  • 中断号偏移: 有些 SoC 的手册写的是 Linux IRQ 号,不是硬件中断号。设备树里必须用硬件号。我建议你对照 TRM 确认。
  • 忘记加 interrupt-controller;: 如果一个节点要作为中断控制器,必须显式声明这个属性。否则内核不认。
  • DTC 编译报错: 最常见的是括号不匹配或分号漏了。用 dtc -I dts -O dtb -o /dev/null test.dts 只做语法检查,不生成文件。

重要提醒: 设备树语法错误不会导致内核编译失败,但会导致启动时解析失败,板子直接挂掉。每次修改后务必用 DTC 检查一遍。

好了,设备树的基础语法就这些。DTS 是源文件,DTC 是编译器,DTB 是内核吃的东西。节点和属性是骨架,reg 管地址,interrupts 管中断。把这些搞明白,后面写驱动移植时就顺手多了。