4. 设备树(DT)基础:设备树语法、节点属性、reg/interrupts/compatible 属性详解

设备树(Device Tree,DT)是嵌入式 Linux 系统中用于描述硬件拓扑结构的数据结构,它替代了传统 Bootloader 向内核传递硬件信息的“板级文件”方式。在 PCIe 驱动开发中,设备树用于描述 PCIe 控制器、总线范围、中断映射以及 Endpoint 设备的静态连接关系。本章将深入解析设备树的语法规则、核心节点属性,并重点讲解 reg、interrupts 和 compatible 三个关键属性在 PCIe 场景下的使用方式。

学习提示:设备树是 ACPI 的前置知识。在嵌入式平台(如 ARM、RISC-V)上,PCIe 控制器必须通过设备树描述才能被内核识别。理解 DT 语法是后续对比 ACPI 配置的基础。

4.1 设备树语法基础

设备树源文件(.dts)采用树形结构描述硬件,每个硬件单元对应一个节点(node),节点包含属性(property)用于描述配置信息。一个典型的设备树结构如下:

/dts-v1/;

/ {
    model = "Embedded PCIe Platform";
    compatible = "vendor,board-rev1";

    soc {
        compatible = "simple-bus";
        #address-cells = <2>;
        #size-cells = <2>;

        pcie@40000000 {
            compatible = "vendor,pcie-controller";
            reg = <0x0 0x40000000 0x0 0x10000000>;
            #address-cells = <3>;
            #size-cells = <2>;
            device_type = "pci";
            ranges = <0x02000000 0x0 0x50000000
                      0x0 0x50000000
                      0x0 0x10000000>;
            interrupt-map-mask = <0xf800 0x0 0x0 0x7>;
            interrupt-map = <...>;
        };
    };
};

设备树的基本语法规则如下表所示:

语法元素 格式 说明
节点 node-name@unit-address 节点名称 + 寄存器基地址(可选)
属性 property-name = <value>; 键值对,值可为 u32 数组、字符串、二进制数据
标签 label: node@addr 为节点定义引用标签,便于其他节点引用
包含 #include "file.dtsi" 引入公共设备树头文件(类似 C 语言 #include)
宏定义 #define NAME value 在 dts 文件中定义常量,提高可读性
关键点:设备树中的 #address-cells 和 #size-cells 决定了 reg 和 ranges 属性中地址和长度的编码宽度。对于 PCIe 控制器,#address-cells = <3> 表示使用 3 个 u32 编码 PCIe 地址空间(包含总线号、设备号、功能号信息)。

4.2 节点属性详解

设备树节点通过属性描述硬件的资源需求、兼容性和拓扑关系。以下是与 PCIe 驱动开发最相关的核心属性:

  • compatible:指定设备与驱动的匹配字符串,格式为 "vendor,device"。内核通过该属性查找对应的驱动。
  • reg:描述设备占用的地址空间,格式由 #address-cells 和 #size-cells 决定。
  • interrupts:描述设备的中断请求线,包含中断类型、触发方式等信息。
  • interrupt-parent:指定该设备的中断控制器父节点。
  • ranges:定义父节点地址空间与子节点地址空间的映射关系,PCIe 控制器必须通过 ranges 将 PCIe 总线地址转换为 CPU 地址。
  • device_type:PCIe 节点必须设置为 "pci",用于内核识别总线类型。
  • #address-cells / #size-cells:控制子节点 reg 属性的编码宽度。

下表对比了 PCIe 控制器节点与普通外设节点的属性差异:

属性 PCIe 控制器节点 普通外设节点(如 UART)
#address-cells 3(PCIe 地址编码) 1 或 2(CPU 地址编码)
#size-cells 2 1 或 2
reg 控制器内部寄存器基地址 外设寄存器基地址
ranges 必须定义,映射 PCIe 地址到 CPU 地址 可选,通常不定义
interrupt-map 必须定义,将 PCIe INTA~INTD 映射到中断控制器 直接使用 interrupts 属性
device_type "pci" 通常不设置

4.3 reg 属性详解

reg 属性用于描述设备占用的地址空间,其格式由父节点的 #address-cells 和 #size-cells 决定。对于 PCIe 控制器节点,reg 通常描述控制器内部的配置寄存器空间。

在 PCIe 子设备(Endpoint)中,reg 属性用于描述设备的 PCIe 配置空间地址,格式为:

reg = <bus dev func reg_offset size>;

其中:

  • bus:PCIe 总线号(1 个 u32)
  • dev:设备号(1 个 u32)
  • func:功能号(1 个 u32)
  • reg_offset:配置空间寄存器偏移(1 个 u32)
  • size:地址空间长度(1 个 u32)

实际编码时,#address-cells = <3> 将 bus、dev、func 编码为第一个 u32,reg_offset 为第二个 u32,size 由 #size-cells 决定。例如:

pcie-ep@0 {
    reg = <0x0000 0x0 0x0 0x0 0x1000>;
    /* 解释:bus=0, dev=0, func=0, offset=0x0, size=0x1000 */
};
常见错误:在 PCIe 子节点中,reg 的第一个 u32 的高 16 位是总线号,中间 5 位是设备号,低 11 位是功能号。直接使用十进制数字而不进行位运算编码会导致地址解析错误。

4.4 interrupts 属性详解

interrupts 属性描述设备的中断请求线。对于 PCIe 设备,中断类型包括 INTA、INTB、INTC、INTD 以及 MSI/MSI-X。在设备树中,PCIe 子节点的 interrupts 属性通常由 interrupt-map 动态映射,而不是直接指定。

对于 PCIe 控制器本身,interrupts 属性描述控制器内部中断(如 MSI 中断控制器中断):

pcie@40000000 {
    compatible = "vendor,pcie-controller";
    reg = <0x0 0x40000000 0x0 0x10000000>;
    interrupts = <0 0 4>;  /* GIC SPI 中断号 0,触发方式 4(高电平) */
    interrupt-parent = <&gic>;
};

中断属性值的格式由中断控制器的 #interrupt-cells 决定。对于 ARM GIC(Generic Interrupt Controller),通常使用 3 个 u32:

  • 第一个 u32:中断类型(0=SPI,1=PPI)
  • 第二个 u32:中断号
  • 第三个 u32:触发方式(1=上升沿,2=下降沿,4=高电平,8=低电平)

对于 PCIe Endpoint 设备,中断映射通过 interrupt-map 和 interrupt-map-mask 实现:

interrupt-map-mask = <0xf800 0x0 0x0 0x7>;
interrupt-map = <
    /* 设备号 0, INTA -> GIC SPI 32 */
    0x0000 0x0 0x0 0x1 &gic 0 32 4
    /* 设备号 0, INTB -> GIC SPI 33 */
    0x0000 0x0 0x0 0x2 &gic 0 33 4
    /* 设备号 0, INTC -> GIC SPI 34 */
    0x0000 0x0 0x0 0x3 &gic 0 34 4
    /* 设备号 0, INTD -> GIC SPI 35 */
    0x0000 0x0 0x0 0x4 &gic 0 35 4
>;

4.5 compatible 属性详解

compatible 属性是设备树中最关键的匹配属性,内核通过它找到对应的设备驱动。其格式为 "vendor,device",vendor 是厂商名称,device 是设备型号。内核驱动通过 of_match_table 注册支持的 compatible 字符串。

对于 PCIe 控制器驱动,compatible 字符串通常包含 SoC 厂商和控制器版本:

static const struct of_device_id pcie_driver_match[] = {
    { .compatible = "vendor,pcie-controller-v1", .data = &v1_data },
    { .compatible = "vendor,pcie-controller-v2", .data = &v2_data },
    { /* end */ }
};
MODULE_DEVICE_TABLE(of, pcie_driver_match);

compatible 属性支持多个字符串,内核会按优先级依次匹配:

compatible = "vendor,pcie-controller-v2", "vendor,pcie-controller-v1", "simple-bus";

匹配规则:内核优先使用第一个字符串查找驱动,如果找不到则尝试第二个,以此类推。这种机制允许新硬件兼容旧驱动。

最佳实践:compatible 字符串应遵循“厂商,设备型号”的命名规范,避免使用通用名称。对于 PCIe 控制器,建议包含 SoC 名称和控制器版本号,例如 "ti,am654-pcie-rc" 表示 TI AM654 平台的 Root Complex 模式。

4.6 设备树 PCIe 节点完整示例

以下是一个完整的 PCIe 控制器设备树节点示例,展示了上述属性的综合使用:

pcie0: pcie@40000000 {
    compatible = "vendor,pcie-controller";
    reg = <0x0 0x40000000 0x0 0x10000000>;
    reg-names = "config";
    #address-cells = <3>;
    #size-cells = <2>;
    device_type = "pci";
    ranges = <0x02000000 0x0 0x50000000
              0x0 0x50000000
              0x0 0x10000000>;
    interrupts = <0 0 4>;
    interrupt-parent = <&gic>;
    interrupt-map-mask = <0xf800 0x0 0x0 0x7>;
    interrupt-map = <
        0x0000 0x0 0x0 0x1 &gic 0 32 4
        0x0000 0x0 0x0 0x2 &gic 0 33 4
        0x0000 0x0 0x0 0x3 &gic 0 34 4
        0x0000 0x0 0x0 0x4 &gic 0 35 4
    >;
    msi-parent = <&msi_controller>;
    status = "okay";

    pcie-ep@0 {
        reg = <0x0000 0x0 0x0 0x0 0x1000>;
        /* 子节点继承父节点的 interrupt-map */
    };
};

4.7 设备树 PCIe 地址映射流程

下图展示了设备树中 PCIe 地址映射的完整流程,从 CPU 地址到 PCIe 总线地址的转换关系:

设备树 PCIe 地址映射流程 CPU 地址空间 0x5000_0000 ~ 0x5FFF_FFFF ranges 映射 PCIe 总线地址空间 0x0200_0000_5000_0000 reg 解析 PCIe Endpoint bus=0, dev=0, func=0 PCIe INTA~INTD 中断请求线 interrupt-map 中断控制器 (GIC) SPI 32~35 MSI/MSI-X 中断 写寄存器触发中断 msi-parent MSI 控制器 中断号分配 说明 ranges 将 CPU 地址 转换为 PCIe 总线地址

4.8 设备树与 PCIe 驱动匹配流程

内核通过设备树匹配 PCIe 驱动的完整流程如下:

  1. 解析设备树:内核启动时,Bootloader 将设备树二进制(DTB)传递给内核,内核解析所有节点。
  2. 创建 platform_device:对于 compatible 匹配的节点,内核创建 platform_device 并注册到平台总线。
  3. 驱动匹配:PCIe 控制器驱动注册到平台总线,通过 of_match_table 与设备树节点匹配。
  4. 资源获取:驱动通过 platform_get_resource() 获取 reg 定义的地址空间,通过 irq_of_parse_and_map() 获取中断号。
  5. PCIe 总线扫描:驱动初始化控制器后,扫描 PCIe 总线,发现 Endpoint 设备。
  6. Endpoint 驱动匹配:Endpoint 设备通过 PCIe 配置空间的 Vendor ID / Device ID 匹配 PCIe 驱动。
关键理解:设备树只描述 PCIe 控制器本身,不描述 PCIe 总线上的 Endpoint 设备。Endpoint 设备通过 PCIe 标准枚举机制发现,其驱动匹配基于 PCIe 配置空间的 VID/PID,而非设备树 compatible 属性。设备树中的 PCIe 子节点仅用于静态配置(如固定总线号、中断映射)。

4.9 常见问题与避坑指南

问题 原因 解决方案
PCIe 控制器无法匹配驱动 compatible 字符串拼写错误或与驱动不匹配 检查 of_match_table 中的字符串,确保与 dts 完全一致
中断无法触发 interrupt-map 配置错误或中断号冲突 使用 cat /proc/interrupts 检查中断分配,验证 interrupt-map-mask 是否正确
地址访问异常 ranges 属性未正确映射 PCIe 地址到 CPU 地址 检查 ranges 的地址类型编码(0x02000000 表示 32 位非预取内存空间)
MSI 中断不工作 msi-parent 未指定或 MSI 控制器驱动未加载 确保 MSI 控制器节点存在且驱动已注册,检查 msi-parent 引用是否正确
设备树编译错误 语法错误或属性值格式不正确 使用 dtc -I dts -O dtb -o output.dtb input.dts 编译并检查错误信息