4. 设备树(DT)基础:设备树语法、节点属性、reg/interrupts/compatible 属性详解
设备树(Device Tree,DT)是嵌入式 Linux 系统中用于描述硬件拓扑结构的数据结构,它替代了传统 Bootloader 向内核传递硬件信息的“板级文件”方式。在 PCIe 驱动开发中,设备树用于描述 PCIe 控制器、总线范围、中断映射以及 Endpoint 设备的静态连接关系。本章将深入解析设备树的语法规则、核心节点属性,并重点讲解 reg、interrupts 和 compatible 三个关键属性在 PCIe 场景下的使用方式。
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 */
};
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";
匹配规则:内核优先使用第一个字符串查找驱动,如果找不到则尝试第二个,以此类推。这种机制允许新硬件兼容旧驱动。
"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 总线地址的转换关系:
4.8 设备树与 PCIe 驱动匹配流程
内核通过设备树匹配 PCIe 驱动的完整流程如下:
- 解析设备树:内核启动时,Bootloader 将设备树二进制(DTB)传递给内核,内核解析所有节点。
- 创建 platform_device:对于 compatible 匹配的节点,内核创建 platform_device 并注册到平台总线。
- 驱动匹配:PCIe 控制器驱动注册到平台总线,通过
of_match_table与设备树节点匹配。 - 资源获取:驱动通过
platform_get_resource()获取reg定义的地址空间,通过irq_of_parse_and_map()获取中断号。 - PCIe 总线扫描:驱动初始化控制器后,扫描 PCIe 总线,发现 Endpoint 设备。
- Endpoint 驱动匹配:Endpoint 设备通过 PCIe 配置空间的 Vendor ID / Device ID 匹配 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 编译并检查错误信息 |