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设备地址),无长度 |
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 定义。
4.3 DMA地址映射:dma-ranges 属性
dma-ranges 属性与 ranges 类似,但它专门用于描述DMA地址空间的映射关系。当设备通过DMA访问内存时,设备看到的地址(设备地址)可能与CPU看到的地址(物理地址)不同。dma-ranges 定义了从子总线设备地址到父总线CPU物理地址的映射。
其格式与 ranges 完全相同:
dma-ranges = <子地址 父地址 长度>;
关键区别在于语义:
- 子地址:设备进行DMA操作时使用的地址(设备地址)。
- 父地址:CPU物理地址空间中的地址。
- 长度:映射区域的大小。
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。
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驱动开发中的常见难点。