4、设备树地址编码与映射:#address-cells、#size-cells、ranges属性、地址转换机制、64位地址处理
设备树(Device Tree)是嵌入式Linux系统中描述硬件拓扑与资源分布的核心数据结构。地址编码与映射是设备树中最关键、也最容易出错的环节之一。本章将深入剖析 #address-cells、#size-cells、ranges 属性的语义与用法,揭示地址转换的底层机制,并给出64位地址场景下的处理方案。
4.1 地址编码基础:#address-cells 与 #size-cells
在设备树中,每个总线节点(bus node)或桥接节点(bridge node)必须通过 #address-cells 和 #size-cells 属性声明其子节点地址域的编码格式。这两个属性共同决定了 reg 属性中地址与长度的字长。
| 属性 | 含义 | 取值 | 典型场景 |
|---|---|---|---|
#address-cells |
子节点 reg 中地址字段占用的32位字个数 |
1、2 | 32位地址空间用1,64位地址空间用2 |
#size-cells |
子节点 reg 中长度字段占用的32位字个数 |
0、1、2 | 无大小需求用0,32位长度用1,64位长度用2 |
关键规则: 子节点的
reg 属性格式为 reg = <addr1 len1 addr2 len2 ...>,其中每个 addr 占用 #address-cells 个32位字,每个 len 占用 #size-cells 个32位字。
// 示例1:32位地址空间,1个地址单元,1个长度单元
soc {
#address-cells = <1>;
#size-cells = <1>;
uart@101f0000 {
reg = <0x101f0000 0x1000>; // 基址0x101f0000,长度0x1000
};
};
// 示例2:64位地址空间,2个地址单元,2个长度单元
pcie@40000000 {
#address-cells = <2>;
#size-cells = <2>;
pci-ep@0,0 {
reg = <0x0 0x40000000 0x0 0x100000>; // 64位基址,64位长度
};
};
4.2 ranges 属性:地址映射的桥梁
ranges 属性定义了父总线地址空间与子总线地址空间之间的映射关系。它使得子节点中的地址能够被父节点正确解析,是设备树地址转换的核心机制。
4.2.1 ranges 属性格式
ranges = <child-bus-address parent-bus-address length>;
其中每个字段的字长由以下规则决定:
- child-bus-address:子节点地址域中的起始地址,字长由子节点的
#address-cells决定。 - parent-bus-address:父节点地址域中的起始地址,字长由父节点的
#address-cells决定。 - length:映射区域的大小,字长由子节点的
#size-cells决定。
实战提示: 当
ranges 属性为空(ranges;)时,表示子地址空间与父地址空间是1:1直接映射,即子节点地址无需转换即可被父节点理解。当 ranges 属性不存在时,表示子地址空间与父地址空间完全隔离,子节点不能被父节点直接访问。
4.2.2 典型映射场景
// 示例:CPU总线 → 外设总线映射
cpu-bus {
#address-cells = <1>;
#size-cells = <1>;
// 外设总线在CPU地址空间中的基址为0x10000000
// 外设总线内部地址0x0 ~ 0xFFFFFF 映射到 CPU 地址 0x10000000 ~ 0x10FFFFFF
peripheral-bus@10000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x10000000 0x1000000>; // child=0x0, parent=0x10000000, len=0x1000000
uart@0 {
reg = <0x0 0x1000>; // 外设总线内部地址0x0,实际CPU地址为0x10000000
};
timer@1000 {
reg = <0x1000 0x100>; // 外设总线内部地址0x1000,实际CPU地址为0x10001000
};
};
};
4.3 地址转换机制:从设备树到CPU物理地址
设备树地址转换是一个递归解析过程,由内核的 of_translate_address() 函数实现。转换路径从目标节点开始,逐级向上遍历父节点,应用每一级的 ranges 映射,最终得到CPU物理地址。
转换流程核心步骤:
- 从目标节点的
reg属性中提取子总线地址。 - 检查当前节点的父节点是否定义了
ranges属性。 - 如果存在
ranges,则根据映射表将子地址转换为父地址。 - 如果父节点是根节点(
/),则转换完成,得到CPU物理地址。 - 否则,将父地址作为新的子地址,继续向上递归。
4.4 64位地址处理
随着嵌入式系统对内存和外设地址空间需求的增长,64位地址处理已成为常态。设备树通过 #address-cells = <2> 和 #size-cells = <2> 来支持64位地址。
4.4.1 64位地址编码规则
// 64位地址空间中的reg属性
memory@0 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>; // 基址0x80000000,长度0x80000000
};
// 64位外设地址映射
pcie@0x4000000000 {
#address-cells = <2>;
#size-cells = <2>;
ranges = <0x0 0x0 0x0 0x4000000000 0x0 0x10000000>;
// child: 0x0_0x0, parent: 0x0_0x4000000000, len: 0x0_0x10000000
};
常见陷阱: 在64位地址场景中,
ranges 属性的每个字段都占用2个32位字。开发者容易混淆字段边界,导致地址映射错误。建议在编写时使用显式的64位常量注释,如 0x0_0x4000000000 表示完整的64位地址。
4.4.2 64位地址转换示例
以下是一个完整的64位地址转换案例,展示从PCIe端点设备到CPU物理地址的映射过程:
// 根节点
/ {
#address-cells = <2>;
#size-cells = <2>;
// PCIe控制器节点
pcie@0x4000000000 {
compatible = "pci-host-ecam-generic";
#address-cells = <3>; // PCIe规范:3个地址单元
#size-cells = <2>;
reg = <0x0 0x4000000000 0x0 0x10000000>; // ECAM配置空间
// 将PCIe总线地址0x0_0x0 ~ 0x0_0xFFFFFF 映射到CPU地址0x0_0x50000000 ~ 0x0_0x50FFFFFF
ranges = <0x02000000 0x0 0x0 0x0 0x50000000 0x0 0x1000000>;
// PCIe地址格式:type(1字) + bus_addr(2字) → CPU_addr(2字) → len(2字)
// PCIe端点设备
pci-ep@0,0 {
reg = <0x02000000 0x0 0x0 0x0 0x0 0x0>;
// type=0x02000000(非预取内存), bus_addr=0x0_0x0
};
};
};
4.5 地址转换调试技巧
| 调试方法 | 命令/工具 | 说明 |
|---|---|---|
| 查看设备树 | dtc -I fs -O dts /proc/device-tree |
导出当前系统使用的设备树源文件 |
| 检查地址映射 | ls /proc/device-tree/*/ranges |
查看各级总线的地址映射表 |
| 验证转换结果 | cat /proc/iomem |
对比设备树地址与内核实际映射的物理地址 |
| 驱动调试 | devm_ioremap_resource() |
驱动中通过此API获取已转换的物理地址并映射 |
| 内核日志 | dmesg | grep -i "of_translate" |
查看地址转换过程中的错误信息 |
调试黄金法则: 当驱动无法正确访问外设时,首先检查
/proc/iomem 中该设备的物理地址范围是否与预期一致。如果不一致,则问题出在设备树的 ranges 映射链上。使用 dtc 反编译设备树,逐级验证每个 ranges 的映射计算是否正确。
4.6 常见错误与避坑指南
- #address-cells / #size-cells 不匹配:父子节点之间的地址单元数不一致,导致
ranges解析错位。务必确保每个字段的字长符合声明。 - ranges 缺失导致地址隔离:如果子节点需要被父节点访问但未定义
ranges,驱动将无法通过of_iomap()获取有效地址。 - 64位地址截断:在32位系统中处理64位地址时,内核可能截断高32位。使用
phys_addr_t类型确保完整存储。 - PCIe地址类型混淆:PCIe的
#address-cells = <3>中第一个字是地址类型标识(配置空间、I/O、内存等),不要与普通地址混淆。
4.7 驱动接口中的地址获取
在Linux驱动中,通过以下API获取经过设备树转换的物理地址和映射后的虚拟地址:
#include <linux/of_address.h>
#include <linux/of_platform.h>
static int my_driver_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct resource *res;
void __iomem *base;
// 方法1:通过资源获取(推荐)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(dev, "Failed to get memory resource\n");
return -ENXIO;
}
dev_info(dev, "Physical address: 0x%llx, size: 0x%llx\n",
(unsigned long long)res->start,
(unsigned long long)resource_size(res));
// 方法2:直接映射
base = devm_ioremap_resource(dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 方法3:通过设备树节点直接获取(底层调试用)
struct device_node *np = pdev->dev.of_node;
const __be32 *addr;
u64 phys_addr;
addr = of_get_address(np, 0, NULL, NULL);
if (addr)
phys_addr = of_translate_address(np, addr);
return 0;
}
最佳实践: 驱动开发中优先使用
platform_get_resource() 和 devm_ioremap_resource() 组合,这两个API内部已经完成了地址转换和错误处理。仅在需要调试或特殊场景下才直接调用 of_translate_address()。