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()获取设备地址时,内核执行以下步骤:

  1. 解析本地地址:从设备节点的reg属性中提取本地地址和长度。
  2. 查找父节点ranges:向上遍历到父节点,读取其ranges属性。
  3. 地址转换:将本地地址通过ranges映射到父地址空间。
  4. 递归进行:如果父节点不是根节点,继续向上转换,直到到达根节点/。
  5. 得到物理地址:最终转换结果即为CPU可直接访问的物理地址。
核心函数:of_translate_address()是地址转换的核心实现,它递归遍历设备树节点,逐级应用ranges映射。
设备树地址转换递归流程 设备节点 (uart@0) reg = <0x0 0x1000> 父节点 soc (simple-bus) ranges = <0x0 0xE0000000 0x100000> 地址转换:子地址 0x0 → 父地址 0xE0000000 偏移量 = 0xE0000000 - 0x0 = 0xE0000000 根节点 / 最终物理地址:0xE0000000 递归终止条件 当父节点为根节点 / 时, 地址转换完成。 根节点无 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访问。