4. 设备树地址编码:reg属性、#address-cells、#size-cells、ranges属性的地址映射机制
在嵌入式Linux系统中,设备树(Device Tree)是描述硬件拓扑结构的核心机制。CPU与各类外设(如UART、I2C、SPI、GPIO控制器)之间的地址空间映射关系,完全通过设备树中的地址编码属性来定义。本章深入剖析 reg、#address-cells、#size-cells 和 ranges 四个关键属性的工作原理与地址映射机制。
核心概念:设备树的地址编码本质上是将硬件总线上的物理地址空间,通过树形结构逐级映射到CPU的地址域中。每个总线节点都定义了自己的地址宽度和地址转换规则。
4.1 地址编码基础:address-cells 与 size-cells
每个总线节点(bus node)必须声明两个关键属性,用于定义其子节点如何编码地址:
#address-cells:指定子节点reg属性中 地址字段 所占的32位字(cell)数量。#size-cells:指定子节点reg属性中 长度字段 所占的32位字(cell)数量。
这两个属性是地址解析的“坐标系”定义,决定了后续 reg 和 ranges 的解析方式。
| #address-cells 值 | 地址宽度 | 典型场景 |
|---|---|---|
| 1 | 32位地址 | 32位ARM SoC内部总线、简单外设 |
| 2 | 64位地址 | 64位ARM/x86 SoC、PCIe总线、DMA控制器 |
| 3 | 96位地址 | 某些PowerPC或特殊总线(如PCI三段式编码) |
| #size-cells 值 | 长度宽度 | 典型场景 |
|---|---|---|
| 0 | 无长度字段 | 仅表示地址存在,不关心大小(如中断控制器) |
| 1 | 32位长度 | 大多数内存映射外设(寄存器区域) |
| 2 | 64位长度 | 大块内存区域、PCIe BAR空间 |
避坑指南:子节点
reg 的解析完全依赖父节点的 #address-cells 和 #size-cells,而不是子节点自身的属性。如果父节点未声明这两个属性,内核默认使用 #address-cells = <2> 和 #size-cells = <1>(PowerPC ePAPR标准)。
4.2 reg属性:设备地址空间的直接描述
reg 属性是设备节点描述其占用地址空间的核心属性。其格式为:
reg = <address1 length1 [address2 length2] ...>;
每个 地址-长度对 描述一段连续的物理地址空间。地址字段和长度字段的宽度由父节点的 #address-cells 和 #size-cells 决定。
示例1:32位系统,单段地址空间
/* 父节点定义地址编码规则 */
soc {
compatible = "simple-bus";
#address-cells = <1>; /* 32位地址 */
#size-cells = <1>; /* 32位长度 */
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>; /* 基址0x10000000,长度0x1000(4KB) */
interrupts = <33>;
};
};
示例2:64位系统,多段地址空间
/* 64位ARM SoC */
soc {
compatible = "simple-bus";
#address-cells = <2>; /* 64位地址,高32位+低32位 */
#size-cells = <2>; /* 64位长度 */
dma: dma-controller@3000000000 {
compatible = "my-dma";
reg = <0x0 0x30000000 0x0 0x1000>, /* 第一段:地址0x30000000,长度0x1000 */
<0x0 0x30001000 0x0 0x2000>; /* 第二段:地址0x30001000,长度0x2000 */
};
};
注意:当
#address-cells = <2> 时,地址字段占用两个32位字,即高32位在前,低32位在后。例如 <0x0 0x30000000> 表示64位地址 0x00000000_30000000。
4.3 ranges属性:父子地址空间的桥梁
ranges 属性定义了 子总线地址空间 到 父总线地址空间 的映射关系。它是设备树地址翻译的核心机制,使得子节点可以使用本地地址,而内核自动将其转换为CPU可见的物理地址。
ranges属性格式:
ranges = <child-bus-address parent-bus-address length>;
其中每个字段的宽度由不同节点决定:
- child-bus-address:子节点地址宽度,由当前节点(声明ranges的节点)的
#address-cells决定。 - parent-bus-address:父节点地址宽度,由父节点的
#address-cells决定。 - length:映射区域长度,由当前节点的
#size-cells决定。
典型示例:桥接芯片地址映射
/* CPU总线:64位地址空间 */
cpu-bus {
compatible = "simple-bus";
#address-cells = <2>; /* 64位CPU地址 */
#size-cells = <2>;
/* 外设总线桥:将32位外设地址映射到64位CPU地址空间 */
peripheral-bridge {
compatible = "simple-bus";
#address-cells = <1>; /* 外设总线使用32位地址 */
#size-cells = <1>;
ranges = <0x00000000 0x0 0x40000000 0x10000000>;
/* 含义:外设地址0x00000000 ~ 0x0FFFFFFF 映射到 CPU地址0x40000000 ~ 0x4FFFFFFF */
ethernet@0 {
reg = <0x00000000 0x1000>; /* 外设本地地址0x00000000 */
/* 内核自动翻译为CPU物理地址:0x40000000 */
};
usb@10000000 {
reg = <0x10000000 0x10000>; /* 外设本地地址0x10000000 */
/* 内核自动翻译为CPU物理地址:0x50000000 */
};
};
};
关键理解:子节点
reg 中填写的是 子总线本地地址,内核通过逐级向上查找 ranges 映射,最终计算出CPU物理地址。如果某个中间节点没有 ranges 属性,则子地址与父地址直接相等(1:1映射)。
4.4 地址映射的完整流程
当内核解析设备树并访问外设寄存器时,地址翻译遵循以下步骤:
- 读取子节点reg:获取子节点声明的本地地址和长度。
- 查找父节点ranges:从当前节点的父节点开始,向上查找
ranges属性。 - 地址转换:如果找到
ranges,则根据映射表将子地址转换为父地址。 - 递归向上:将转换后的地址作为新的“子地址”,继续向上查找更高级别的
ranges,直到到达根节点。 - 得到CPU物理地址:根节点下的地址即为CPU可直接访问的物理地址。
4.5 特殊场景:ranges为空与无ranges
ranges 属性有两种特殊形式,含义截然不同:
| ranges形式 | 含义 | 典型场景 |
|---|---|---|
ranges; (空属性) |
子地址空间与父地址空间完全相同(1:1映射),子节点地址直接作为父地址使用 | 同地址宽度的总线桥接,如simple-bus级联 |
无 ranges 属性 |
子地址空间与父地址空间不直接映射,子设备不能被CPU直接访问(需通过IOMMU或特殊驱动) | PCIe总线、IOMMU管理的设备、DMA引擎 |
示例:ranges为空(1:1映射)
/* 父节点和子节点使用相同的地址空间 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges; /* 子地址 = 父地址,1:1映射 */
gpio0: gpio@10010000 {
reg = <0x10010000 0x1000>; /* 直接使用CPU物理地址 */
};
};
示例:无ranges(非直接映射)
/* PCIe总线:子设备地址需要通过配置空间访问 */
pcie {
compatible = "pci-host-ecam-generic";
#address-cells = <3>; /* PCI三段式地址:bus+dev+func + 空间类型 */
#size-cells = <2>;
/* 没有ranges属性,PCI设备不能通过CPU直接内存访问 */
/* 需要通过PCI配置空间读写来访问设备 */
pci-device@0,0 {
reg = <0x0000 0x0000 0x0000 0x0 0x0>;
/* 地址由PCI驱动通过配置周期访问,非直接内存映射 */
};
};
重要区别:
ranges;(空)表示“地址透明传递”,子节点地址就是父节点地址;而缺失 ranges 属性表示“地址空间隔离”,子设备不能通过简单地址转换被CPU直接访问,通常需要总线控制器驱动参与。
4.6 多级地址映射实战分析
在复杂SoC中,地址映射可能涉及多级桥接。以下是一个三级地址映射的完整示例:
/* 根节点:CPU地址空间 */
/ {
#address-cells = <2>; /* 64位CPU地址 */
#size-cells = <2>;
/* 第一级:主系统总线 */
axi-bus {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges; /* 1:1映射到CPU地址空间 */
/* 第二级:外设总线桥,将32位外设地址映射到64位AXI地址 */
apb-bridge {
compatible = "simple-bus";
#address-cells = <1>; /* APB总线32位地址 */
#size-cells = <1>;
ranges = <0x00000000 0x0 0x40000000 0x10000000>;
/* APB地址0x00000000~0x0FFFFFFF → AXI地址0x40000000~0x4FFFFFFF */
/* 第三级:子功能模块桥接 */
i2c-bridge {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x00000000 0x00000000 0x10000>;
/* I2C子模块地址0x00000000~0x0000FFFF → APB地址0x00000000~0x0000FFFF */
i2c-controller@0 {
reg = <0x00000000 0x1000>;
/* 最终CPU物理地址:0x40000000 */
/* 翻译路径:0x00000000 → I2C桥ranges → 0x00000000 → APB桥ranges → 0x40000000 */
};
};
};
};
};
实战要点:多级映射时,每个桥接节点都独立定义自己的
#address-cells、#size-cells 和 ranges。内核的 of_translate_address() 函数会递归遍历所有父节点,逐级完成地址转换。调试时可通过 /sys/devices/platform/.../of_node/ 下的设备树文件验证地址映射是否正确。
4.7 常见错误与调试方法
| 错误类型 | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| address-cells不匹配 | 驱动读取reg地址为0或异常大值 | 子节点reg的地址字段数量与父节点#address-cells不一致 | 检查父节点#address-cells值,确保子节点reg地址字段数量匹配 |
| ranges缺失 | ioremap返回NULL或驱动挂死 | 桥接节点缺少ranges属性,导致地址无法翻译 | 添加正确的ranges映射,或确认设备确实不需要直接内存映射 |
| ranges长度溢出 | 部分子设备地址映射正确,部分错误 | ranges定义的映射范围不足以覆盖所有子设备地址 | 扩大ranges映射范围,或为不同子设备添加多个映射条目 |
| size-cells=0误用 | reg属性解析异常 | 父节点#size-cells=0时,reg中不应包含长度字段 | 确认设备类型,中断控制器等无长度需求的设备才使用size-cells=0 |
调试命令:
# 查看设备树完整结构(包含地址映射)
dtc -I fs -O dts /sys/firmware/devicetree/base
# 查看特定节点的地址映射信息
cat /sys/devices/platform/soc/xxxx/of_node/reg
# 使用of_translate_address调试(内核编译需开启DEBUG_DRIVER)
echo 'file drivers/of/address.c +p' > /sys/kernel/debug/dynamic_debug/control
最佳实践:编写设备树时,始终遵循“父节点定义规则,子节点填写数据”的原则。先确认父节点的
#address-cells 和 #size-cells,再填写子节点的 reg 和 ranges。对于复杂桥接,建议先在草稿纸上画出地址映射层级图,再编写设备树源码。
4.8 总结:地址编码核心要点
- #address-cells 和 #size-cells 是父节点为子节点定义的地址编码规则,决定了
reg和ranges的解析方式。 - reg 描述设备占用的本地地址空间,由地址和长度对组成,每对字段宽度由父节点属性决定。
- ranges 实现子地址到父地址的映射,支持多级桥接场景下的地址翻译。
- 空ranges 表示1:1映射,无ranges 表示地址空间隔离,两者含义完全不同。
- 内核通过
of_translate_address()递归向上查找ranges,最终得到CPU可直接访问的物理地址。