4. 设备树地址编码与映射:#address-cells、#size-cells、ranges属性、地址转换机制
设备树(Device Tree)是描述硬件拓扑与资源分布的关键数据结构。在驱动移植过程中,CPU总线与外围设备之间的地址空间往往存在差异,必须通过设备树中的地址编码属性进行精确描述与转换。本章深入剖析#address-cells、#size-cells、ranges三个核心属性的语义与用法,并揭示地址转换机制的底层原理。
4.1 地址编码基础:#address-cells 与 #size-cells
设备树中,每个总线节点或桥接节点都需要声明其子节点地址的编码格式。这两个属性共同决定了地址描述的长度:
- #address-cells:表示子节点
reg属性中地址字段所占的32位字(cell)数量。 - #size-cells:表示子节点
reg属性中长度字段所占的32位字(cell)数量。
常见取值:32位系统通常使用
#address-cells = <1>、#size-cells = <1>;64位系统或需要描述大地址空间时,使用#address-cells = <2>、#size-cells = <2>。
子节点的reg属性格式为:reg = <地址1 长度1 地址2 长度2 ...>。每个地址/长度对由父节点定义的cells数量决定。
| #address-cells | #size-cells | reg 示例 | 地址范围 |
|---|---|---|---|
| 1 | 1 | <0x10000000 0x1000> |
0x10000000 ~ 0x10000FFF |
| 2 | 1 | <0x0 0x40000000 0x1000> |
0x40000000 ~ 0x40000FFF |
| 2 | 2 | <0x0 0x80000000 0x0 0x10000> |
0x80000000 ~ 0x8000FFFF |
关键规则:子节点
reg中的地址是相对于父节点地址空间的本地地址,而非系统全局物理地址。地址转换由ranges属性完成。
4.2 地址映射桥梁:ranges 属性
ranges属性定义父总线地址空间与子总线地址空间之间的映射关系。它位于桥接节点(如PCIe控制器、内存控制器、总线桥)中,格式为:
ranges = <子地址 父地址 长度>
其中:
- 子地址:子总线域内的起始地址,长度由子节点的
#address-cells决定。 - 父地址:父总线域内的起始地址,长度由父节点的
#address-cells决定。 - 长度:映射窗口的大小,长度由父节点的
#size-cells决定。
常见陷阱:如果
ranges属性为空(ranges;),表示子地址空间与父地址空间完全相同(1:1映射)。如果ranges属性不存在,则子地址空间与父地址空间隔离,无法直接访问。
以下是一个典型的内存映射示例:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xE0000000 0x00100000>; /* 子地址0x0 ~ 0xFFFFF 映射到父地址0xE0000000 ~ 0xE00FFFFF */
uart0: serial@0 {
compatible = "ns16550";
reg = <0x0 0x1000>; /* 子总线地址0x0,长度0x1000 */
interrupts = <5>;
};
};
在此例中,UART设备在子总线上的地址为0x0,经过ranges转换后,CPU看到的物理地址为0xE0000000。
4.3 地址转换机制详解
设备树地址转换是一个递归向上查找的过程。当驱动通过of_iomap()或of_address_to_resource()获取设备地址时,内核执行以下步骤:
- 解析本地地址:从设备节点的
reg属性中提取本地地址和长度。 - 查找父节点ranges:向上遍历到父节点,读取其
ranges属性。 - 地址转换:将本地地址通过
ranges映射到父地址空间。 - 递归进行:如果父节点不是根节点,继续向上转换,直到到达根节点
/。 - 得到物理地址:最终转换结果即为CPU可直接访问的物理地址。
核心函数:
of_translate_address()是地址转换的核心实现,它递归遍历设备树节点,逐级应用ranges映射。
4.4 多级桥接与复杂映射
在实际嵌入式系统中,设备可能挂在多级总线桥下,地址转换需要经过多个ranges层级。以下是一个两级桥接的典型场景:
/ {
#address-cells = <2>;
#size-cells = <2>;
bus0: bus@0 {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges = <0x0 0x0 0x0 0x40000000 0x0 0x10000000>; /* 子地址0x0~0xFFFFFFF → 父地址0x40000000~0x4FFFFFFF */
bridge: bridge@0 {
compatible = "pci-bridge";
#address-cells = <3>;
#size-cells = <2>;
ranges = <0x01000000 0x0 0x00000000 0x0 0x50000000 0x0 0x00100000>; /* PCI I/O空间映射 */
device@0 {
reg = <0x00010000 0x0 0x00000000 0x0 0x00001000>; /* PCI配置空间地址 */
};
};
};
};
PCI地址编码特殊说明:PCI总线使用3个cell表示地址:第1个cell表示地址类型(配置空间、I/O空间、内存空间),后2个cell表示实际地址。转换时需先解析地址类型,再应用对应的
ranges映射。
4.5 地址转换常见问题与调试方法
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 驱动无法映射寄存器地址 | ranges属性缺失或配置错误 |
检查桥接节点的ranges是否覆盖设备地址范围 |
| 地址转换后访问崩溃 | #address-cells或#size-cells与reg不匹配 |
确认父子节点的cells定义一致,reg字段数量正确 |
| 设备树编译报地址错误 | 地址值超出父节点地址空间范围 | 检查ranges映射窗口大小是否足够 |
| 多级桥接后地址错乱 | 中间节点ranges映射偏移量计算错误 |
逐级验证每个桥接节点的地址转换结果 |
调试建议:使用
devicetree-unittest内核模块或dt-utils工具可以验证地址转换的正确性。在驱动中打印of_translate_address()的返回值,与硬件手册中的物理地址对比。
4.6 地址转换在驱动中的应用
驱动开发中,地址转换通常由内核框架自动完成,但理解其原理有助于排查硬件映射问题。以下是一个典型驱动中获取并映射设备地址的代码片段:
#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;
int ret;
/* 获取设备树中转换后的物理地址和长度 */
ret = of_address_to_resource(dev->of_node, 0, &res);
if (ret) {
dev_err(dev, "Failed to get address resource\n");
return ret;
}
dev_info(dev, "Physical address: 0x%llx, size: 0x%llx\n",
(unsigned long long)res.start,
(unsigned long long)(res.end - res.start + 1));
/* 映射到内核虚拟地址空间 */
base = devm_ioremap_resource(dev, &res);
if (IS_ERR(base))
return PTR_ERR(base);
/* 使用base访问寄存器... */
return 0;
}
关键点:
of_address_to_resource()内部调用了of_translate_address(),自动完成从设备树本地地址到CPU物理地址的转换。驱动开发者无需手动处理ranges映射逻辑。
4.7 本章小结
- #address-cells 和 #size-cells 定义了设备树中地址描述的粒度,是地址编码的基础。
- ranges 属性是地址转换的核心桥梁,实现子总线地址到父总线地址的映射。
- 地址转换是一个递归向上的过程,从设备节点逐级向上直到根节点。
- 驱动开发中,
of_address_to_resource()和of_iomap()封装了地址转换细节,但理解底层机制对调试硬件映射问题至关重要。 - 多级桥接场景下,每个桥接节点都必须正确配置
ranges,否则设备地址将无法被CPU访问。