4. 设备树地址与DMA映射:#address-cells、#size-cells、ranges、dma-ranges属性解析

设备树(Device Tree)是描述硬件拓扑与资源分布的核心数据结构。在嵌入式Linux系统中,CPU、内存、外设之间的地址映射关系以及DMA(直接内存访问)传输的地址空间转换,完全由设备树中的地址相关属性定义。本章深入解析 #address-cells、#size-cells、ranges 和 dma-ranges 四个关键属性,帮助开发者准确理解地址编码规则与映射机制。

4.1 地址编码基础:#address-cells 与 #size-cells

设备树中,每个总线节点或设备节点都需要声明其子节点地址域的编码宽度。#address-cells 定义地址字段占用的32位单元数,#size-cells 定义长度字段占用的32位单元数。这两个属性共同决定了 reg 属性中地址与长度的解析方式。

属性 含义 常见取值 示例
#address-cells 子节点地址字段的32位字数量 1(32位地址)、2(64位地址) #address-cells = <1>
#size-cells 子节点长度字段的32位字数量 0(无长度)、1(32位长度) #size-cells = <1>
关键规则:父节点的 #address-cells 和 #size-cells 决定了其直接子节点中 reg 属性的解析格式。若父节点未显式声明,则默认值为 #address-cells = <2>、#size-cells = <1>。

以下是一个典型的设备树片段,展示地址编码的层级关系:

soc {
    compatible = "simple-bus";
    #address-cells = <2>;   /* 64位地址 */
    #size-cells = <2>;      /* 64位长度 */
    ranges;

    uart0: serial@101f0000 {
        reg = <0x0 0x101f0000 0x0 0x1000>;
        /* 地址: 0x101f0000, 长度: 0x1000 */
    };

    gpio0: gpio@101e0000 {
        reg = <0x0 0x101e0000 0x0 0x100>;
        /* 地址: 0x101e0000, 长度: 0x100 */
    };
};
调试技巧:使用 dtc -I dtb -O dts 反编译设备树二进制文件,检查 #address-cells 和 #size-cells 是否与硬件手册的地址宽度一致。不一致会导致内核解析 reg 时出现偏移错误。

4.2 地址转换桥梁:ranges 属性

ranges 属性定义了父总线地址空间与子总线地址空间之间的映射关系。它允许子节点使用本地地址(local address),而父节点通过 ranges 将其转换为全局地址(global address)。ranges 是一个可选的地址转换表,每个条目包含三个字段:子地址、父地址、映射长度。

字段 含义 宽度
子地址(child address) 子总线域内的起始地址 子节点的 #address-cells
父地址(parent address) 父总线域内的起始地址 父节点的 #address-cells
映射长度(length) 地址映射的连续范围大小 子节点的 #size-cells

当 ranges 属性为空(ranges;)时,表示子地址空间与父地址空间是1:1直接映射,即子地址等于父地址。若节点不包含 ranges 属性,则子地址空间与父地址空间完全隔离,内核无法自动进行地址转换。

常见错误:忘记在桥接总线节点中声明 ranges,导致挂载在该总线下的外设无法被内核正确访问。调试时可通过 cat /proc/iomem 检查物理地址映射是否完整。

以下是一个包含 ranges 映射的完整示例:

pcie@40000000 {
    compatible = "pci-host-ecam-generic";
    #address-cells = <3>;   /* PCIe 地址域:3个32位单元 */
    #size-cells = <2>;      /* PCIe 长度域:2个32位单元 */
    reg = <0x0 0x40000000 0x0 0x10000000>;

    /* 将PCIe地址空间映射到CPU地址空间 */
    ranges = <0x01000000 0x0 0x00000000   /* PCIe I/O 空间 */
             0x0 0x3eff0000
             0x0 0x00010000>,

             <0x02000000 0x0 0x50000000   /* PCIe 内存空间 */
             0x0 0x50000000
             0x0 0x20000000>;
};
ranges 属性地址映射示意图 CPU 地址空间 0x5000_0000 - 0x6FFF_FFFF 0x3EFF_0000 - 0x3EFF_FFFF (父地址域) PCIe 地址空间 0x0200_0000_5000_0000 0x0100_0000_0000_0000 (子地址域) ranges 映射条目解析 条目1: PCIe I/O 空间 → CPU 地址 子地址: 0x01000000 0x0 0x00000000 父地址: 0x0 0x3EFF0000 长度: 0x10000 条目2: PCIe 内存空间 → CPU 地址 子地址: 0x02000000 0x0 0x50000000 父地址: 0x0 0x50000000 长度: 0x20000000

4.3 DMA地址映射:dma-ranges 属性

dma-ranges 属性与 ranges 结构完全相同,但语义不同:它描述的是设备DMA引擎看到的地址空间(设备地址域)与CPU地址空间之间的映射关系。当设备通过DMA访问内存时,硬件DMA控制器使用设备地址,而CPU使用物理地址。dma-ranges 定义了这两者之间的转换规则。

属性 作用域 转换方向 典型应用场景
ranges CPU访问外设 子地址 → 父地址 MMIO、配置空间访问
dma-ranges 外设DMA访问内存 设备地址 → CPU地址 DMA传输、SMMU/IOMMU配置
核心区别:ranges 用于CPU发起的访问(如 readl/writel),而 dma-ranges 用于设备发起的访问(如DMA写内存)。在存在IOMMU/SMMU的系统中,dma-ranges 通常描述经过IOMMU转换后的地址映射。

以下是一个包含 dma-ranges 的典型设备树片段:

pcie@40000000 {
    compatible = "pci-host-ecam-generic";
    #address-cells = <3>;
    #size-cells = <2>;
    reg = <0x0 0x40000000 0x0 0x10000000>;

    /* CPU访问PCIe配置空间和MMIO */
    ranges = <0x01000000 0x0 0x00000000
              0x0 0x3eff0000
              0x0 0x00010000>,
             <0x02000000 0x0 0x50000000
              0x0 0x50000000
              0x0 0x20000000>;

    /* PCIe设备DMA访问内存的地址映射 */
    dma-ranges = <0x02000000 0x0 0x00000000
                  0x0 0x80000000
                  0x0 0x40000000>;
    /* 设备地址 0x00000000-0x3FFFFFFF
       映射到 CPU 物理地址 0x80000000-0xBFFFFFFF */
};
dma-ranges 与 ranges 对比示意图 CPU 视角 (ranges) CPU 发起访问:读/写外设寄存器 CPU 地址 → 外设地址 通过 ranges 表转换 DMA 视角 (dma-ranges) 外设发起访问:DMA 读写内存 设备地址 → CPU 物理地址 通过 dma-ranges 表转换 实际硬件映射示例 PCIe 设备 DMA 引擎看到的地址空间:0x0000_0000 - 0x3FFF_FFFF CPU 物理内存地址空间:0x8000_0000 - 0xBFFF_FFFF dma-ranges 映射:设备地址 0x0000_0000 → CPU 地址 0x8000_0000,长度 0x4000_0000 内核 DMA API 自动使用此映射进行地址转换,驱动无需手动处理

4.4 内核启动流程中的地址解析

在Linux内核启动过程中,设备树地址属性的解析发生在 unflatten_device_tree() 之后,由 of_address 子系统完成。关键流程如下:

  1. 设备树展开:内核将扁平设备树(FDT)展开为树形结构,建立 device_node 链表。
  2. 地址域初始化:遍历每个总线节点,读取 #address-cells 和 #size-cells,缓存到 struct device_node 的 _flags 字段。
  3. reg 属性解析:调用 of_address_to_resource() 将 reg 属性转换为 struct resource,包含起始地址和长度。
  4. ranges 映射计算:通过 of_translate_address() 沿父节点链向上查找 ranges,逐级计算全局物理地址。
  5. dma-ranges 映射注册:调用 of_dma_configure() 解析 dma-ranges,设置设备的 dma_pfn_offset 和 dma_mask。
调试方法:在 drivers/of/address.c 中添加 printk 或使用 trace_printk 跟踪 of_translate_address() 的调用栈,观察地址转换的中间结果。内核启动参数 of_debug 可启用设备树调试信息。

4.5 常见问题与调试技巧

问题现象 可能原因 调试方法
外设寄存器读写无响应 ranges 缺失或映射错误 检查 /proc/iomem 确认物理地址范围
DMA传输数据损坏 dma-ranges 未正确配置 使用 dma_debug 内核参数跟踪DMA映射
内核崩溃在 of_translate_address #address-cells 与 reg 字段数不匹配 反编译DTB,核对地址字段宽度
PCIe设备无法枚举 PCIe域 ranges 条目格式错误 使用 lspci -vvv 查看桥接窗口
避坑指南:在编写设备树时,务必保持 #address-cells 和 #size-cells 在父子节点间的一致性。父节点声明了 #address-cells = <2>,则子节点的 reg 属性中地址字段必须包含两个32位单元,否则内核解析会错位,导致地址值完全错误。

4.6 本章小结

  • #address-cells 和 #size-cells 定义了设备树地址编码的宽度,是解析 reg 属性的基础。
  • ranges 属性实现CPU地址空间到外设地址空间的映射,是CPU访问外设的桥梁。
  • dma-ranges 属性实现设备DMA地址空间到CPU物理地址空间的映射,是DMA传输正确性的保障。
  • 内核启动时通过 of_address_to_resource() 和 of_dma_configure() 自动解析这些属性,驱动开发者应理解其底层机制以便调试。