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物理地址。

转换流程核心步骤:
  1. 从目标节点的 reg 属性中提取子总线地址。
  2. 检查当前节点的父节点是否定义了 ranges 属性。
  3. 如果存在 ranges,则根据映射表将子地址转换为父地址。
  4. 如果父节点是根节点(/),则转换完成,得到CPU物理地址。
  5. 否则,将父地址作为新的子地址,继续向上递归。
设备树地址转换递归流程 目标节点 reg 地址 检查父节点 ranges 属性 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、内存等),不要与普通地址混淆。
设备树地址映射层级关系 根节点 / CPU总线 (soc) 外设总线 A ranges = <0 0x10000000 0x1000> 外设总线 B ranges = <0 0x20000000 0x2000> UART@0 TIMER@100 GPIO@0 DMA@200 UART实际CPU地址 = 0x10000000 + 0x0 = 0x10000000 | GPIO实际CPU地址 = 0x20000000 + 0x0 = 0x20000000

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()。