4. DTS地址编码

设备树(Device Tree Source, DTS)的核心任务之一是描述硬件设备的 地址空间 与 总线拓扑。本章深入解析 DTS 中用于地址编码的三个关键要素:#address-cells、#size-cells 以及 reg 和 ranges 属性。理解这些概念是编写正确设备树、实现设备驱动地址映射的基础。

4.1 地址编码基础:address-cells 与 size-cells

在设备树中,每个可寻址的设备节点都需要描述其占用地址空间的位置和大小。由于不同总线(如 32 位 ARM 平台、64 位 RISC-V 平台、PCIe 总线)的地址宽度不同,DTS 使用 #address-cells 和 #size-cells 这两个属性来定义地址和长度字段的 单元宽度(每个单元为 32 位)。

属性名 作用 值含义 典型值
#address-cells 定义子节点 reg 属性中 地址 字段占用的 32 位单元个数 1 表示 32 位地址;2 表示 64 位地址 1(32位SoC)、2(64位SoC/PCIe)
#size-cells 定义子节点 reg 属性中 长度 字段占用的 32 位单元个数 0 表示无长度(仅地址);1 表示 32 位长度;2 表示 64 位长度 1(常规外设)、0(中断控制器)
关键规则: #address-cells 和 #size-cells 是 总线节点 的属性,它们作用于该总线下的 直接子节点。根节点(/)本身也是一个总线节点,需要显式声明这两个属性。

4.2 reg 属性:描述设备地址资源

reg 属性用于描述设备在总线上的 地址空间(寄存器、内存、I/O 端口等)。其格式为:

reg = <address1 length1 [address2 length2] ...>;

每个 address 和 length 的单元数由父节点的 #address-cells 和 #size-cells 决定。

4.2.1 32 位地址示例

/* 父节点(根节点)定义地址单元 */
/ {
    #address-cells = <1>;   /* 地址为 32 位 */
    #size-cells    = <1>;   /* 长度为 32 位 */

    uart0: serial@10000000 {
        compatible = "ns16550a";
        reg = <0x10000000 0x1000>;  /* 基址 0x10000000,长度 4KB */
        interrupts = <33>;
    };
};

4.2.2 64 位地址示例(如 ARM64 或 PCIe)

/* PCIe 总线节点 */
pcie@40000000 {
    #address-cells = <3>;   /* PCIe 地址域:Bus/Dev/Func + 偏移 */
    #size-cells    = <2>;   /* 64 位长度 */

    pci_device@0,0 {
        reg = <0x0000 0x0000 0x0000 0x0000 0x1000>;
        /* 解释:3个单元地址 + 2个单元长度 = 5个32位值 */
    };
};
常见错误: 忘记在父节点设置 #address-cells 和 #size-cells,或者子节点 reg 中的单元数与父节点声明不匹配,会导致内核解析失败,设备无法正确映射地址。

4.3 ranges 属性:地址映射与转换

ranges 属性用于定义 父总线地址空间 与 子总线地址空间 之间的映射关系。它允许子节点使用本地地址,而内核通过 ranges 自动转换为 CPU 可见的物理地址。

4.3.1 ranges 语法

ranges = <child_bus_addr parent_bus_addr length>;

每个字段的单元数由以下规则决定:

  • child_bus_addr:子节点地址单元数,由当前节点(声明 ranges 的节点)的 #address-cells 决定。
  • parent_bus_addr:父节点地址单元数,由父节点的 #address-cells 决定。
  • length:映射长度单元数,由当前节点的 #size-cells 决定。

4.3.2 典型应用:桥接芯片地址映射

/* 根节点:CPU 地址空间 */
/ {
    #address-cells = <1>;
    #size-cells    = <1>;

    /* 外部总线桥接器 */
    bridge@20000000 {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells    = <1>;
        ranges = <0x00000000 0x20000000 0x10000000>;
        /* 子地址 0x0 ~ 0xFFFFFFF 映射到父地址 0x20000000 ~ 0x2FFFFFFF */

        ethernet@0 {
            reg = <0x00000000 0x1000>;
            /* 实际物理地址 = 0x20000000 + 0x0 = 0x20000000 */
        };

        usb@100000 {
            reg = <0x00100000 0x10000>;
            /* 实际物理地址 = 0x20000000 + 0x100000 = 0x20100000 */
        };
    };
};
映射规则: 当 ranges 属性存在时,子节点 reg 中的地址是 本地总线地址,内核通过 ranges 表将其转换为 父总线地址(最终为 CPU 物理地址)。如果 ranges 为空(ranges;),表示子地址与父地址 一一对应(1:1 映射)。

4.4 地址映射流程 SVG 图

下图展示了从设备树节点 reg 属性到 CPU 物理地址的完整解析流程:

设备树地址映射流程 DTS 设备节点 reg = <addr len> 父节点 ranges 表 子地址 → 父地址映射 地址转换单元 of_translate_address() 读取父节点 应用映射 CPU 物理地址 ioremap() / of_iomap() 输出物理地址 驱动寄存器访问 readl() / writel() 虚拟地址映射 核心要点 1. #address-cells / #size-cells 决定 reg 和 ranges 的字段宽度 2. ranges 实现子总线地址到父总线地址的转换

4.5 地址单元与 ranges 实战对比

场景 #address-cells #size-cells ranges 示例 说明
32位 SoC 片上外设 1 1 ranges = <0 0x40000000 0x10000000>; 子地址 0 映射到 CPU 地址 0x40000000
64位 SoC(ARM64) 2 2 ranges = <0x0 0x0 0x0 0x80000000 0x0 0x10000000>; 64位地址映射,每个字段占 2 个单元
PCIe 总线 3 2 ranges = <0x02000000 0x0 0x40000000 0x0 0x40000000 0x0 0x20000000>; PCIe 地址域包含类型、Bus/Dev/Func
无地址映射(1:1) 1 1 ranges;(空属性) 子地址等于父地址,无需转换
无地址空间设备 1 0 不需要 ranges 如中断控制器,仅地址无长度

4.6 驱动中获取地址信息:of 函数族

内核驱动通过以下 OF API 从设备树节点读取地址信息:

  • of_n_addr_cells(node):获取节点 #address-cells 值。
  • of_n_size_cells(node):获取节点 #size-cells 值。
  • of_get_address(node, index, &size, &flags):获取第 index 个 reg 条目的地址。
  • of_translate_address(node, addr):将本地总线地址转换为 CPU 物理地址(自动遍历 ranges)。
  • of_iomap(node, index):直接映射第 index 个 reg 条目到内核虚拟地址(等价于 ioremap(of_translate_address(...), size))。
驱动最佳实践: 推荐使用 devm_platform_ioremap_resource() 或 of_iomap(),它们自动处理地址转换和映射,无需手动调用 of_translate_address() 和 ioremap(),减少出错概率。

4.7 常见陷阱与调试技巧

  • 陷阱 1: 子节点 reg 中的地址单元数必须严格等于父节点的 #address-cells + #size-cells。多一个或少一个都会导致解析偏移。
  • 陷阱 2: ranges 中的 child_bus_addr 字段数由声明 ranges 的节点自身的 #address-cells 决定,而不是子节点的。
  • 陷阱 3: 对于 simple-bus 类型节点,如果 ranges 缺失,子节点地址将无法被内核正确翻译,导致驱动 probe 失败。
  • 调试技巧: 使用 devicetree/unittest 或内核启动参数 of_debug 打印地址解析日志;在驱动中通过 dev_info(dev, "phys addr: %pa\n", &res->start); 输出实际物理地址。

4.8 地址编码与 ranges 关系 SVG 图

下图展示了多级总线桥接下,地址如何通过 ranges 逐级转换:

多级总线地址转换示例 CPU (0x00000000) 一级总线 (0x20000000) ranges = <0 0x20000000 0x10000000> 二级总线 (0x30000000) ranges = <0 0x30000000 0x10000000> CPU 地址空间 CPU 地址空间 UART (本地 0x1000) 物理: 0x20001000 Ethernet (本地 0x100) 物理: 0x30000100 一级映射 二级映射 地址转换计算 UART 物理地址 = 一级总线基址(0x20000000) + 本地偏移(0x1000) = 0x20001000 Ethernet 物理地址 = 二级总线基址(0x30000000) + 本地偏移(0x100) = 0x30000100 内核通过 ranges 自动完成逐级转换,驱动无需手动计算

4.9 本章小结

  • #address-cells / #size-cells 是总线节点的属性,定义了地址和长度的编码宽度,直接影响 reg 和 ranges 的解析。
  • reg 属性描述设备占用的地址资源,格式为 <地址 长度>,单元数由父节点决定。
  • ranges 属性实现子总线地址到父总线地址的映射,支持多级桥接拓扑,是设备树地址翻译的核心机制。
  • 驱动开发中应使用 of_iomap() 或 devm_platform_ioremap_resource() 简化地址映射操作,避免手动处理地址转换。