3. 设备树地址编码:reg属性、ranges属性、地址映射与DMA地址空间配置

设备树(Device Tree)是描述硬件拓扑与资源分配的核心数据结构。地址编码是设备树中最关键也最容易出错的部分,它决定了CPU如何访问外设寄存器、外设如何访问系统内存,以及DMA引擎如何正确寻址。本章深入剖析reg属性、ranges属性、地址映射机制以及DMA地址空间的配置方法。

3.1 reg属性:设备资源的基础描述

reg属性用于描述设备在总线上的地址空间范围,其格式为:

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

每个address length对称为一个地址区域(region)。地址和长度的位数由父节点的#address-cells和#size-cells决定。

属性 含义 典型值
#address-cells 地址字段占用的32位单元数 1(32位地址)、2(64位地址)
#size-cells 长度字段占用的32位单元数 1(32位长度)、2(64位长度)
reg 地址与长度交替排列的数组 <0x10000000 0x1000>
示例:一个UART控制器在32位地址空间中的寄存器基址为0x10000000,大小为4KB。
uart@10000000 {
    compatible = "ns16550";
    reg = <0x10000000 0x1000>;
    interrupts = <33>;
};

3.2 ranges属性:地址转换的桥梁

ranges属性定义了子总线地址到父总线地址的映射关系。当子节点使用不同于父节点的地址空间时,必须通过ranges进行转换。

格式:

ranges = <child-bus-address parent-bus-address length>;

每个条目包含三个字段,其宽度由#address-cells(子节点)、父节点的#address-cells和#size-cells共同决定。

关键理解:如果ranges属性为空(ranges;),表示子地址与父地址是1:1直接映射。如果ranges属性不存在,则子节点无法被父总线访问。

下面是一个典型的PCIe桥接设备树片段,展示了地址映射关系:

pcie@40000000 {
    compatible = "pci-host-ecam-generic";
    reg = <0x40000000 0x10000000>;  // ECAM配置空间
    #address-cells = <3>;
    #size-cells = <2>;
    device_type = "pci";
    ranges = <0x02000000 0x00 0x50000000
              0x00 0x50000000
              0x00 0x10000000>;  // 非预取内存窗口
};
ranges属性地址映射流程 CPU / 父总线 桥接器 / 总线节点 子设备 父地址空间 子地址空间 ranges = <子地址 父地址 长度> 将子设备地址0x50000000映射到CPU可见的0x50000000 子设备访问 0x50000000 → 桥接器转换 → CPU访问 0x50000000 当 ranges 为空时,子地址 = 父地址(1:1映射)

3.3 地址映射的层级与解析过程

设备树地址解析是一个自底向上的过程。内核从设备节点出发,逐级向上查找ranges属性,直到到达根节点(CPU地址空间)。

  1. 读取节点自身的reg:获取设备在本地总线上的地址。
  2. 查找父节点的ranges:将本地地址转换为父总线地址。
  3. 递归向上:重复步骤2,直到根节点。
  4. 最终映射:得到CPU可直接访问的物理地址。
常见错误:忘记在桥接节点中定义ranges属性,导致子设备地址无法被CPU访问。内核会报错“OF: translation failed”,驱动probe失败。

3.4 DMA地址空间配置

DMA(直接内存访问)设备需要独立于CPU视角的地址空间。在设备树中,通过dma-ranges属性描述DMA引擎的地址映射能力。

属性 作用 示例
dma-ranges 定义DMA地址到CPU物理地址的映射 <0x00 0x80000000 0x00 0x80000000 0x00 0x10000000>
dma-coherent 声明设备支持DMA一致性(无需缓存刷新) 空属性
dma-noncoherent 声明设备不支持DMA一致性 空属性
典型场景:一个64位DMA控制器只能访问低32位地址空间,需要配置dma-ranges进行地址窗口映射。
dma-controller@30000000 {
    compatible = "my-dma-engine";
    reg = <0x30000000 0x1000>;
    #dma-cells = <3>;
    dma-ranges = <0x00 0x80000000   // DMA地址起始
                  0x00 0x80000000   // CPU物理地址起始
                  0x00 0x10000000>; // 映射大小256MB
    dma-coherent;
};
DMA地址空间映射架构 CPU 物理地址空间 0x80000000 ~ 0x8FFFFFFF (系统内存区域) DMA 引擎 DMA地址: 0x80000000 (外设视角) dma-ranges 映射 dma-ranges 配置详解 DMA地址 0x80000000 → CPU物理地址 0x80000000 映射大小: 256MB (0x10000000) | 支持DMA一致性

3.5 地址编码避坑指南

  • #address-cells / #size-cells 不匹配:父子节点之间的地址单元数必须一致,否则解析结果错误。
  • ranges 缺失导致地址不可达:桥接节点必须显式提供ranges,否则子设备无法被CPU访问。
  • DMA地址越界:DMA引擎的物理地址窗口必须与dma-ranges定义一致,否则DMA操作会访问无效内存。
  • 64位地址使用2个cell:对于64位地址空间,#address-cells = <2>,#size-cells = <2>,地址和长度各占64位。
  • dma-coherent 与缓存一致性:如果设备声明dma-coherent,内核不会对DMA缓冲区执行缓存刷新操作;如果硬件不支持,会导致数据不一致。
调试技巧:使用of_dump或devicetree.dump内核参数可以打印完整的设备树地址解析过程。在驱动中调用of_address_to_resource()和of_dma_configure()可以验证地址映射是否正确。

3.6 实战:配置一个带DMA的网卡设备树节点

以下是一个完整的设备树节点示例,集成了reg、ranges和dma-ranges配置:

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

    eth@1c00000 {
        compatible = "my-gmac";
        reg = <0x00 0x1c00000 0x00 0x10000>;  // 寄存器基址
        interrupts = <0 45 4>;
        dma-ranges = <0x00 0x40000000   // DMA地址
                      0x00 0x40000000   // CPU物理地址
                      0x00 0x20000000>; // 512MB映射
        dma-coherent;
        phy-mode = "rgmii";
        phy-handle = <&phy0>;

        mdio {
            #address-cells = <1>;
            #size-cells = <0>;
            phy0: ethernet-phy@0 {
                reg = <0>;
            };
        };
    };
};

该节点中:

  • reg定义了网卡寄存器在SoC总线上的位置(0x1c00000,64KB)。
  • ranges为空,表示SoC总线地址与CPU地址1:1映射。
  • dma-ranges将DMA地址0x40000000映射到CPU物理地址0x40000000,大小512MB。
  • dma-coherent声明网卡硬件保证DMA一致性。

通过正确配置地址编码,驱动可以无缝访问寄存器、发起DMA传输,并确保数据一致性。这是嵌入式Linux设备驱动开发中最基础也最关键的一步。