4. 设备树地址与DMA映射

在ARM嵌入式Linux系统中,设备树(Device Tree)不仅描述设备的存在,还必须精确描述设备的地址空间、总线拓扑以及DMA(直接内存访问)映射关系。本章深入剖析四个核心属性:#address-cells、#size-cells、ranges和dma-ranges,这些属性共同构成了设备树中地址翻译与DMA地址映射的基石。

核心概念:设备树中的地址描述采用“地址单元”和“长度单元”的抽象模型。每个总线节点通过 #address-cells 和 #size-cells 定义其子节点地址描述的格式,而 ranges 和 dma-ranges 则负责将子总线的地址空间翻译到父总线的地址空间。

4.1 地址描述基础:#address-cells 与 #size-cells

这两个属性定义了一个地址描述符的格式。地址描述符由两部分组成:地址(address)和长度(size),每个部分由若干个32位单元(cell)组成。

  • #address-cells:指定子节点中 reg 属性中地址部分占用的32位单元数量。
  • #size-cells:指定子节点中 reg 属性中长度部分占用的32位单元数量。

例如,一个32位地址总线的节点通常设置 #address-cells = <1> 和 #size-cells = <1>;而64位地址总线则可能使用 #address-cells = <2>。

#address-cells #size-cells reg 属性示例 含义
1 1 reg = <0x10000000 0x1000> 基地址 0x10000000,长度 0x1000 字节
2 1 reg = <0x0 0x40000000 0x1000> 64位基地址 0x0000000040000000,长度 0x1000 字节
1 0 reg = <0x38> 仅地址(如I2C设备地址),无长度
实战要点:在编写DTS时,必须确保子节点的 reg 属性格式与父节点声明的 #address-cells 和 #size-cells 严格一致。不一致会导致内核解析地址失败,设备无法正确探测。

4.2 地址翻译机制:ranges 属性

ranges 属性定义了子总线地址到父总线地址的映射关系。它是一个可选的属性,如果不存在,表示子地址空间与父地址空间完全相同(1:1映射);如果为空(ranges;),表示子地址空间与父地址空间之间没有直接映射,通常用于PCIe等需要动态映射的总线。

ranges 属性的格式为:

ranges = <子地址 父地址 长度>;

其中每个字段的单元数由子节点和父节点的 #address-cells 决定:

  • 子地址:子总线的起始地址,单元数由子节点的 #address-cells 决定。
  • 父地址:父总线的起始地址,单元数由父节点的 #address-cells 决定。
  • 长度:映射区域的大小,单元数由子节点的 #size-cells 决定。
常见错误:初学者容易混淆“子地址”和“父地址”的单元数。记住:子地址的格式由当前节点(子总线)的 #address-cells 定义,父地址的格式由父节点的 #address-cells 定义。
ranges 属性地址翻译流程 父总线节点 #address-cells = <2> #size-cells = <1> 子总线节点 #address-cells = <1> #size-cells = <1> 子设备 ranges 映射表 <子地址 父地址 长度> 子设备 reg = <0x1000 0x100> 通过 ranges 翻译为父总线地址 0x00000000_80001000 翻译公式:父地址 = 父基地址 + (子地址 - 子基地址)

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

dma-ranges 属性与 ranges 类似,但它专门用于描述DMA地址空间的映射关系。当设备通过DMA访问内存时,设备看到的地址(设备地址)可能与CPU看到的地址(物理地址)不同。dma-ranges 定义了从子总线设备地址到父总线CPU物理地址的映射。

其格式与 ranges 完全相同:

dma-ranges = <子地址 父地址 长度>;

关键区别在于语义:

  • 子地址:设备进行DMA操作时使用的地址(设备地址)。
  • 父地址:CPU物理地址空间中的地址。
  • 长度:映射区域的大小。
典型应用场景:在SoC中,某些外设(如GPU、VPU)只能访问特定的内存区域(如32位地址空间),或者需要通过IOMMU/SMMU进行地址翻译。此时,dma-ranges 告诉内核如何将设备的DMA地址映射到系统物理地址。

4.4 实战案例:完整的总线地址映射

以下是一个典型的ARM SoC设备树片段,展示了 #address-cells、#size-cells、ranges 和 dma-ranges 的综合使用:

/ {
    #address-cells = <2>;      // 64位地址
    #size-cells = <2>;         // 64位长度

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;  // 32位地址
        #size-cells = <1>;     // 32位长度
        ranges = <0x0 0x0 0x40000000 0x10000000>;  // 子地址0 -> 父地址0x40000000, 长度0x10000000

        dma-controller@0 {
            compatible = "my,dma";
            reg = <0x0 0x1000>;  // 基地址0x0, 长度0x1000
            #dma-cells = <1>;
        };

        display-controller@100000 {
            compatible = "my,display";
            reg = <0x100000 0x10000>;  // 基地址0x100000, 长度0x10000
            dma-ranges = <0x0 0x0 0x80000000 0x10000000>;  // DMA地址0 -> 物理地址0x80000000, 长度0x10000000
        };
    };
};

在这个例子中:

  • 根节点使用64位地址,soc 节点使用32位地址。
  • ranges 将 soc 的地址0x0映射到CPU物理地址0x40000000,映射大小为0x10000000。
  • dma-ranges 在 display-controller 节点中定义,将设备的DMA地址0x0映射到CPU物理地址0x80000000,映射大小为0x10000000。
设备树地址与DMA映射架构 CPU物理地址空间 0x40000000 - 0x4FFFFFFF (soc) 0x80000000 - 0x8FFFFFFF (DMA) 0x00000000 - 0x3FFFFFFF (内存) soc 总线地址空间 0x00000000 - 0x0FFFFFFF (通过 ranges 映射) 设备DMA地址空间 0x00000000 - 0x0FFFFFFF ranges dma-ranges ranges: 子总线地址 → CPU物理地址 (用于CPU访问外设寄存器) dma-ranges: 设备DMA地址 → CPU物理地址 (用于外设DMA访问内存) 两者可能映射到不同的物理地址区域,实现地址隔离或地址翻译

4.5 常见问题与避坑指南

问题 原因 解决方案
设备无法探测,内核报地址解析错误 reg 属性格式与父节点的 #address-cells/#size-cells 不匹配 检查父节点声明的cells数量,确保 reg 中地址和长度的单元数正确
DMA操作失败,数据写入错误地址 dma-ranges 缺失或配置错误 确认设备DMA地址范围,添加正确的 dma-ranges 映射
多个子设备地址冲突 ranges 映射区域重叠 规划好地址空间,确保每个子设备的地址范围在 ranges 中唯一映射
64位地址在32位系统中截断 #address-cells 设置为2但内核或驱动不支持 确认硬件和内核支持64位地址,或使用 #address-cells = <1> 限制在32位空间
调试技巧:使用 dtc -I dtb -O dts 反编译设备树二进制文件,检查 ranges 和 dma-ranges 是否被正确编译。在Linux内核启动日志中搜索 OF: translation 相关消息,可以查看地址翻译的详细过程。

4.6 本章小结

  • #address-cells 和 #size-cells 定义了地址描述符的格式,是设备树地址解析的基础。
  • ranges 用于将子总线的地址空间映射到父总线的地址空间,实现CPU对外设寄存器的访问。
  • dma-ranges 专门用于DMA地址映射,将设备的DMA地址翻译为CPU物理地址。
  • 正确配置这些属性是确保外设正常工作、DMA操作成功的关键,也是嵌入式Linux驱动开发中的常见难点。