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。
reg属性地址编码结构解析 父节点:soc { #address-cells = <1>; #size-cells = <1>; } 子节点 reg = <0x10000000 0x1000> 地址字段:0x10000000 (1个cell,32位) 长度字段:0x1000 (1个cell,32位) 解析规则:reg中的每个地址-长度对,地址占#address-cells个cell,长度占#size-cells个cell

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 地址映射的完整流程

当内核解析设备树并访问外设寄存器时,地址翻译遵循以下步骤:

  1. 读取子节点reg:获取子节点声明的本地地址和长度。
  2. 查找父节点ranges:从当前节点的父节点开始,向上查找 ranges 属性。
  3. 地址转换:如果找到 ranges,则根据映射表将子地址转换为父地址。
  4. 递归向上:将转换后的地址作为新的“子地址”,继续向上查找更高级别的 ranges,直到到达根节点。
  5. 得到CPU物理地址:根节点下的地址即为CPU可直接访问的物理地址。
设备树地址映射完整流程 根节点 (CPU物理地址空间) soc 总线 (simple-bus) #address-cells=<2> #size-cells=<2> peripheral-bridge (外设总线桥) #address-cells=<1> #size-cells=<1> ranges = <0x0 0x0 0x40000000 0x10000000> ethernet@0 reg = <0x00000000 0x1000> → CPU物理地址: 0x40000000 usb@10000000 reg = <0x10000000 0x10000> → CPU物理地址: 0x50000000 地址翻译过程: 1. 读取子节点reg中的本地地址(如0x00000000) 2. 查找父节点ranges映射表,计算偏移:0x00000000 + 0x40000000 = 0x40000000 3. 递归向上直到根节点,得到最终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。对于复杂桥接,建议先在草稿纸上画出地址映射层级图,再编写设备树源码。
多级地址映射层级关系 CPU 物理地址空间 (64位) AXI 系统总线 (64位, ranges=1:1) APB 外设总线桥 (32位, ranges=0x0→0x40000000) I2C桥 (ranges=0x0→0x0) SPI控制器 (直接映射) 地址翻译路径:I2C设备reg=0x00000000 → I2C桥ranges → 0x00000000 → APB桥ranges → 0x40000000

4.8 总结:地址编码核心要点

  • #address-cells 和 #size-cells 是父节点为子节点定义的地址编码规则,决定了 reg 和 ranges 的解析方式。
  • reg 描述设备占用的本地地址空间,由地址和长度对组成,每对字段宽度由父节点属性决定。
  • ranges 实现子地址到父地址的映射,支持多级桥接场景下的地址翻译。
  • 空ranges 表示1:1映射,无ranges 表示地址空间隔离,两者含义完全不同。
  • 内核通过 of_translate_address() 递归向上查找 ranges,最终得到CPU可直接访问的物理地址。