4、设备树地址映射:地址范围描述与转换

地址映射这个话题,说简单也简单,说复杂也复杂。我刚开始接触设备树时,就被 #address-cells#size-cells 这两个属性搞得晕头转向。后来在项目中调一个 PCIe 的地址映射问题,折腾了两天才发现是 ranges 写错了。嗯,今天咱们就把这块彻底讲清楚。

4.1 地址范围描述:#address-cells 和 #size-cells

这两个属性是设备树地址映射的基石。它们定义了一个节点中,地址和长度分别用几个 32 位整数来表示。

  • #address-cells:地址字段占几个 cell(一个 cell = 32 位)
  • #size-cells:长度字段占几个 cell

举个例子,32 位系统通常这样写:

soc {
    #address-cells = <1>;  // 地址用 32 位
    #size-cells = <1>;     // 长度用 32 位

    uart@10000000 {
        reg = <0x10000000 0x1000>;  // 基址 0x10000000,长度 0x1000
    };
};

64 位系统就不一样了:

soc {
    #address-cells = <2>;  // 地址用 64 位
    #size-cells = <2>;     // 长度用 64 位

    memory@80000000 {
        reg = <0x0 0x80000000 0x0 0x80000000>;
        // 地址:0x00000000_80000000
        // 长度:0x00000000_80000000
    };
};

关键点:子节点中的 reg 属性,必须按照父节点的 #address-cells 和 #size-cells 来解析。这是很多人容易搞混的地方。

我的习惯:写设备树时,我总会在每个总线节点上显式声明这两个属性。哪怕默认值就是 1,也写出来。这样代码更清晰,别人看的时候不会猜。

4.2 ranges 属性详解

ranges 属性,说白了就是地址转换表。它告诉内核:子节点的地址空间,怎么映射到父节点的地址空间。

ranges 的格式是这样的:

ranges = <子地址 父地址 长度>;

每个字段的 cell 数由对应的 #address-cells 和 #size-cells 决定:

  • 子地址:子节点的 #address-cells 个 cell
  • 父地址:父节点的 #address-cells 个 cell
  • 长度:子节点的 #size-cells 个 cell

来看一个实际例子:

soc {
    #address-cells = <1>;
    #size-cells = <1>;

    bus@80000000 {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges = <0x0 0x80000000 0x10000000>;
        // 子地址 0x0 映射到父地址 0x80000000,长度 0x10000000

        device@0 {
            reg = <0x0 0x1000>;  // 子地址 0x0
        };
    };
};

这里 device 的物理地址是 0x80000000 + 0x0 = 0x80000000。内核看到 ranges 后,会自动做这个加法。

我曾经踩过的坑:有一次写 FPGA 桥接的 ranges,把父地址和子地址写反了。结果驱动加载时,ioremap 返回的地址完全不对,读出来的寄存器值全是 0xdeadbeef。查了两天才发现是 ranges 顺序搞反了。记住:子地址在前,父地址在后

ranges 还有一个特殊用法:空 ranges。如果写成 ranges;(没有等号),表示子地址和父地址是 1:1 映射,地址完全一样。

soc {
    #address-cells = <1>;
    #size-cells = <1>;

    bus@0 {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;  // 1:1 映射

        device@1000 {
            reg = <0x1000 0x100>;
        };
    };
};

这种情况下,device 的物理地址就是 0x1000,不需要转换。

4.3 dma-ranges 属性

dma-ranges 和 ranges 很像,但用途完全不同。它描述的是 DMA 地址映射,也就是设备做 DMA 时,设备看到的地址怎么映射到系统物理地址。

为什么需要这个?你想想看,有些设备只能访问 32 位地址空间,但系统内存可能在 64 位地址空间。这时候就需要 dma-ranges 来做地址转换。

格式和 ranges 完全一样:

dma-ranges = <设备地址 父地址 长度>;

来看一个典型场景:

pcie-controller {
    #address-cells = <3>;
    #size-cells = <2>;
    dma-ranges = <0x02000000 0x0 0x00000000
                  0x0 0x80000000
                  0x0 0x10000000>;
    // 设备 DMA 地址 0x00000000 映射到
    // 系统物理地址 0x80000000,长度 0x10000000
};

核心区别:ranges 用于 CPU 访问设备(MMIO),dma-ranges 用于设备访问内存(DMA)。这两个千万别搞混。

我在项目中遇到过一个问题:一个老旧的 DMA 控制器只能寻址 32 位空间,但系统有 4GB 内存。如果不配 dma-ranges,DMA 写到高地址时就会出错。后来在设备树里加了 dma-ranges,把设备地址 0-1GB 映射到系统物理地址 3-4GB,问题就解决了。

4.4 实战:地址映射的完整示例

咱们把前面讲的知识点串起来,看一个完整的例子:

/dts-v1/;

/ {
    #address-cells = <2>;
    #size-cells = <2>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x80000000>;
    };

    soc {
        compatible = "simple-bus";
        #address-cells = <2>;
        #size-cells = <2>;
        ranges = <0x0 0x0 0x0 0x80000000 0x0 0x10000000>;
        // 子地址 0x0 映射到父地址 0x80000000,长度 0x10000000

        dma-ranges = <0x0 0x0 0x0 0x80000000 0x0 0x80000000>;
        // DMA 地址 0x0 映射到物理地址 0x80000000,长度 0x80000000

        ethernet@10000000 {
            reg = <0x0 0x10000000 0x0 0x1000>;
            interrupts = <0 33 4>;
        };
    };
};

这个例子中:

  • ethernet 的 MMIO 基址是 0x80000000 + 0x10000000 = 0x90000000
  • ethernet 做 DMA 时,设备地址 0x0 对应物理地址 0x80000000
  • DMA 可访问范围是 0x80000000 到 0x100000000(4GB)

调试技巧:如果地址映射出了问题,可以在内核启动参数中加 devicetree=debug,内核会打印设备树的解析过程。我经常用这招来验证 ranges 是否写对了。

4.5 常见错误与避坑指南

  1. cell 数不匹配:子节点的 reg 必须和父节点的 #address-cells/#size-cells 一致。我曾经见过一个设备树,父节点是 2 个 cell,子节点却只写了 1 个 cell 的地址,内核直接报错。
  2. ranges 顺序搞反:再次强调,子地址在前,父地址在后。这个顺序和 reg 属性不同,容易记混。
  3. 忘记声明 #address-cells 和 #size-cells:如果不写,内核会使用默认值 1。但如果你实际用的是 64 位地址,那就全乱了。
  4. dma-ranges 和 ranges 混用:这两个属性服务于不同的地址空间,不能互相替代。

特别提醒:在写 PCIe 设备的设备树时,dma-ranges 尤其重要。PCIe 规范中,设备看到的地址空间和 CPU 看到的地址空间是独立的。如果不配 dma-ranges,很多 PCIe 设备无法正常工作。

好了,地址映射这块就讲到这里。记住三个核心点:#address-cells 和 #size-cells 定义地址格式,ranges 做 MMIO 地址转换,dma-ranges 做 DMA 地址转换。下一章咱们聊聊中断控制器,那个也是设备树里容易出问题的地方。