4. 设备树地址编码:#address-cells、#size-cells、ranges属性、地址映射机制
设备树的核心任务之一是描述硬件系统中各种设备的地址空间及其映射关系。CPU、内存、外设控制器、桥接器、总线等不同组件各自拥有独立的地址域,设备树通过 #address-cells、#size-cells 和 ranges 三个关键属性,精确地描述这些地址域的宽度、大小以及跨域映射规则。理解这套地址编码机制,是正确配置设备树、确保内核能正确访问硬件资源的前提。
4.1 地址编码基础:#address-cells 与 #size-cells
在设备树中,每个总线节点(如 soc、pci、axi)都需要声明其子节点地址的编码格式。这通过两个属性完成:
#address-cells:指定子节点reg属性中 地址 部分占用的 32 位单元(cell)个数。#size-cells:指定子节点reg属性中 长度(size)部分占用的 32 位单元个数。
这两个属性是 继承性 的:子节点如果没有显式声明,则继承父节点的值。它们共同决定了 reg 属性中每个地址区域的编码格式:reg = 。
#address-cells = <1>,地址为 32 位;若 #address-cells = <2>,地址为 64 位(高 32 位 + 低 32 位)。#size-cells 同理,决定区域长度的位数。
4.1.1 典型配置示例
soc {
compatible = "simple-bus";
#address-cells = <1>; // 32位地址
#size-cells = <1>; // 32位长度
uart0: serial@101f0000 {
reg = <0x101f0000 0x1000>; // 基地址0x101f0000,长度0x1000
};
gpio0: gpio@101e0000 {
reg = <0x101e0000 0x1000>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; // 1GB内存,起始0x80000000
};
对于 64 位地址空间(如某些 ARM64 SoC),#address-cells 通常设为 <2>:
soc {
#address-cells = <2>;
#size-cells = <2>;
dma@5000000000 {
reg = <0x00000005 0x00000000 0x0 0x100000>; // 地址: 0x5_00000000, 长度: 0x100000
};
};
4.2 地址映射核心:ranges 属性
ranges 属性是设备树中实现 地址域转换 的关键。它定义了子总线(子节点)的地址空间如何映射到父总线(父节点)的地址空间。没有 ranges 属性,子节点的地址在父节点视角下是无效的。
4.2.1 ranges 语法
ranges = <child_base parent_base length [child_base parent_base length ...]>;
- child_base:子总线地址空间中的起始地址(占
#address-cells个 cell)。 - parent_base:父总线地址空间中的起始地址(占父节点的
#address-cells个 cell)。 - length:映射区域的大小(占
#size-cells个 cell)。
ranges; 为空(无参数),表示子地址空间与父地址空间是 1:1 直接映射,即子地址等于父地址。若 ranges 属性不存在,则表示子地址空间与父地址空间 完全隔离,子设备无法被父节点直接访问(常见于 PCIe 桥)。
4.2.2 映射示例:桥接器地址转换
下图展示了一个典型 SoC 中,通过桥接器将外设总线地址映射到系统总线的过程:
4.3 地址映射机制详解
内核在解析设备树时,会遍历每个节点的 ranges 属性,构建从子节点地址到根节点(CPU 地址空间)的 完整映射链。这个过程称为 地址翻译(Address Translation)。
4.3.1 映射链构建流程
- 从根节点开始:根节点的地址空间即为 CPU 的物理地址空间。
- 逐级向下:对于每个总线节点,读取其
ranges属性,得到子地址到父地址的映射表。 - 递归计算:子节点的
reg地址,通过其父节点的ranges转换后,再传递给祖父节点,直到根节点。 - 最终地址:得到设备在 CPU 物理地址空间中的实际基地址。
ranges 属性,且非 1:1 映射,则其所有子设备的地址将无法被 CPU 直接访问,导致驱动 probe 失败或访问异常。此时内核日志会报 "no ranges" 或 "translation failed" 错误。
4.3.2 多级映射示例
考虑一个三层结构:CPU → AXI 总线 → APB 桥 → UART。设备树配置如下:
axi@40000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x40000000 0x20000000>; // AXI子地址0~0x1FFFFFFF映射到CPU地址0x40000000~0x5FFFFFFF
apb@10000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x10000000 0x100000>; // APB子地址0~0xFFFFF映射到AXI地址0x10000000~0x10FFFFF
uart@2000 {
reg = <0x2000 0x100>; // APB子地址0x2000
// 最终CPU地址 = 0x40000000 + 0x10000000 + 0x2000 = 0x50002000
};
};
};
4.4 特殊场景:ranges 为空与 PCIe 地址映射
4.4.1 空 ranges(1:1 映射)
当子总线地址与父总线地址完全一致时,可以声明 ranges;(空属性)。这常见于某些 SoC 的内部总线,子设备地址直接就是系统地址:
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges; // 子地址 == 父地址
timer@1c20000 {
reg = <0x1c20000 0x1000>; // CPU直接访问0x1c20000
};
};
4.4.2 PCIe 地址映射
PCIe 总线使用 非透明桥,其地址空间与 CPU 地址空间完全隔离,因此 PCIe 控制器节点通常 不包含 ranges 属性,而是使用 dma-ranges 描述 DMA 映射,并使用 bus-range 描述 PCIe 总线号。PCIe 设备的 reg 属性中的地址是 PCIe 域内的配置空间地址,需要由 PCIe 驱动通过枚举和 BAR 分配来转换为 CPU 地址。
| 场景 | ranges 设置 |
地址映射行为 | 典型应用 |
|---|---|---|---|
| 1:1 直接映射 | ranges;(空) |
子地址 = 父地址,无需转换 | SoC 内部高速总线 |
| 偏移映射 | ranges = <child parent len> |
子地址 + 偏移 = 父地址 | 桥接器、外设总线 |
| 完全隔离 | 无 ranges 属性 |
子地址在父域中不可见 | PCIe 控制器、非透明桥 |
4.5 实战调优建议
- 检查地址宽度一致性:确保
#address-cells和#size-cells在父子节点间逻辑一致。若父节点为 64 位地址,子节点却使用 32 位地址,会导致地址截断。 - 避免重叠映射:同一父总线下的不同子总线映射区域不应重叠,否则会导致地址冲突,内核可能无法正确分配资源。
- 使用
dma-ranges区分 DMA 映射:对于支持 DMA 的设备,若其 DMA 地址空间与 CPU 地址空间不同,应使用dma-ranges属性(语法与ranges相同),而不是复用ranges。 - 验证映射正确性:内核启动时,可通过
/proc/device-tree查看设备树展开后的实际地址。使用ls -l /proc/device-tree/和cat /proc/device-tree/*/reg验证地址值是否符合预期。
ranges 中定义大块连续映射(如整个外设总线映射到一个连续区域),可以减少内核地址翻译时的查找次数,提升外设访问性能。避免将同一外设的寄存器分散映射到多个不连续的 ranges 条目中。
4.6 本章小结
#address-cells 和 #size-cells 定义了设备树中地址和长度的编码格式,是正确解析 reg 属性的基础。ranges 属性实现了子总线到父总线的地址映射,是连接不同地址域的桥梁。理解这三者的协同工作方式,能够帮助开发者准确描述硬件地址拓扑,避免因地址配置错误导致的驱动加载失败或系统崩溃。在实际调优中,应重点关注地址宽度一致性、映射区域不重叠以及 DMA 地址空间的独立配置。