4. DTS地址编码与ranges属性

在嵌入式系统中,CPU 通过总线层次结构访问外部设备。设备树(DTS)通过 地址编码 和 ranges 属性 来描述这种层次化的地址空间映射。本章深入剖析 DTS 中的地址单元定义、大小端表示以及 ranges 属性在总线桥接中的核心作用,帮助你在 Platform 驱动开发中准确配置地址映射。

4.1 地址编码基础:reg 属性与 #address-cells / #size-cells

每个设备节点通过 reg 属性描述其占用地址空间。而 #address-cells 和 #size-cells 则定义了地址和长度字段的 32 位字数量。

属性 含义 常见值
#address-cells 地址字段占用的 32 位单元数 1(32位地址)、2(64位地址)
#size-cells 长度字段占用的 32 位单元数 1(32位长度)、2(64位长度)
reg 设备地址空间列表,格式为 (address, length) 对 <0x10000000 0x1000>
关键规则: 子节点的 reg 属性中的地址和长度字段,由父节点的 #address-cells 和 #size-cells 决定,而非子节点自身定义。

示例:一个 64 位地址空间的设备节点定义:

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

    uart0: serial@10000000 {
        reg = <0x0 0x10000000 0x0 0x1000>;
        // 地址: 0x00000000_10000000
        // 长度: 0x00000000_00001000
    };
};

4.2 大小端编码:DTS 中的字节序处理

设备树规范默认使用 大端(Big-Endian) 编码,但实际硬件可能采用小端(Little-Endian)。DTS 通过以下方式处理大小端问题:

  • 默认规则: 所有 reg、ranges、interrupts 等属性中的数值按大端存储。
  • 显式指定: 使用 big-endian 或 little-endian 布尔属性覆盖设备默认字节序。
  • 驱动适配: 驱动代码通过 of_device_is_big_endian() 等 API 动态判断。
常见陷阱: 当 CPU 是小端架构(如 ARM 默认小端)而 DTS 按大端编码时,直接读取 reg 属性中的 32 位值会得到错误结果。必须使用 of_property_read_u32() 等 API 自动转换字节序。

大小端处理流程示意图:

DTS 大小端处理流程 DTS 源文件 (大端编码) dtc 编译 (保持大端) DTB 二进制 (大端存储) 内核解析 DTB (自动字节序转换) 驱动 API 读取 (of_property_read_u32) 运行时判断 (big-endian 属性) 说明:内核在解析 DTB 时自动将大端数据转换为 CPU 本地字节序,驱动无需手动转换

4.3 ranges 属性:总线桥接地址映射

ranges 属性是设备树中实现 地址空间转换 的核心机制。它定义了子总线地址空间到父总线地址空间的映射关系。

4.3.1 ranges 属性格式

ranges = <child_base parent_base length>;

其中每个字段的含义由父节点和子节点的 #address-cells / #size-cells 共同决定:

字段 含义 长度决定者
child_base 子总线中的起始地址 子节点的 #address-cells
parent_base 父总线中的起始地址 父节点的 #address-cells
length 映射区域的长度 子节点的 #size-cells
特殊值: 当 ranges 属性为空(ranges;)时,表示子地址空间与父地址空间完全一致(1:1 映射)。当 ranges 属性不存在时,表示子地址空间与父地址空间不直接映射,需要通过驱动或 MMU 进行转换。

4.3.2 典型桥接场景示例

考虑一个 SoC 内部总线桥接场景:CPU 通过 AXI 总线访问外设,而外设挂载在 APB 总线上。

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

    axi_bus: axi@0 {
        compatible = "simple-bus";
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        apb_bus: apb@10000000 {
            compatible = "simple-bus";
            #address-cells = <1>;
            #size-cells = <1>;
            ranges = <0x0 0x0 0x10000000 0x100000>;
            // 子地址 0x0 ~ 0xFFFFF 映射到父地址 0x10000000 ~ 0x10FFFFFF

            uart1: serial@1000 {
                reg = <0x1000 0x100>;
                // 在 APB 总线中的地址为 0x1000
                // 在 AXI 总线中的地址为 0x10001000
                // 在 CPU 视角中的地址为 0x00000000_10001000
            };
        };
    };
};

4.4 地址映射的层级计算

当驱动通过 of_iomap() 或 platform_get_resource() 获取设备地址时,内核会自动进行 层级地址转换。转换过程如下:

  1. 获取子节点 reg: 读取设备节点中的 reg 属性值。
  2. 向上遍历 ranges: 逐级查找父节点的 ranges 属性,进行地址映射。
  3. 累加偏移: 每级映射将子地址转换为父地址,直到到达根节点。
  4. 返回 CPU 物理地址: 最终得到 CPU 可直接访问的物理地址。
层级地址映射计算过程 CPU 物理地址空间 AXI 总线 (ranges) APB 总线 (ranges) UART 设备 (reg) 地址转换示例 UART reg: 0x1000 APB ranges: 0x0 → 0x10000000 APB 转换后: 0x10001000 AXI ranges: 1:1 AXI 转换后: 0x10001000 CPU 最终地址: 0x00000000_10001000

4.5 实战:ranges 配置常见问题与调试

4.5.1 常见配置错误

错误类型 现象 解决方案
#address-cells 不匹配 设备地址解析错误,驱动无法 probe 确保父子节点 cells 定义一致
ranges 长度溢出 部分地址无法访问,系统 hang 检查 length 是否覆盖所有子设备
ranges 缺失 of_iomap 返回 NULL 添加 ranges 或使用 of_translate_address
大小端混淆 寄存器值读取异常 添加 big-endian / little-endian 属性

4.5.2 调试方法

内核调试接口:
  • /proc/device-tree/:查看设备树节点属性
  • of_iomap() 返回值检查:确认地址映射成功
  • devm_ioremap_resource():自动检查地址冲突
  • printk 打印 struct resource 的 start/end

驱动中打印地址映射信息的示例:

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "No memory resource\n");
        return -ENXIO;
    }

    dev_info(&pdev->dev, "phys addr: 0x%llx, size: 0x%llx\n",
             (unsigned long long)res->start,
             (unsigned long long)(res->end - res->start + 1));

    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 检查大小端
    if (of_device_is_big_endian(pdev->dev.of_node))
        dev_info(&pdev->dev, "Device is big-endian\n");

    return 0;
}

4.6 总结

  • 地址编码: #address-cells 和 #size-cells 决定地址空间的粒度,父子节点必须协调一致。
  • 大小端: DTS 默认大端编码,内核自动转换;驱动可通过 big-endian 属性显式指定。
  • ranges 属性: 实现总线桥接地址映射,支持 1:1 映射和偏移映射两种模式。
  • 层级计算: 内核自动逐级转换地址,驱动只需通过标准 API 获取最终物理地址。
  • 调试要点: 利用 /proc/device-tree 和驱动日志验证地址映射正确性。