4. 设备树中的地址编码:reg属性、#address-cells、#size-cells、ranges属性的使用

在嵌入式Linux系统中,设备树(Device Tree)负责描述硬件资源的布局与映射关系。地址编码是设备树中最核心的机制之一,它通过 reg、#address-cells、#size-cells 和 ranges 属性,精确描述设备在总线或系统中的地址空间。本章将深入解析这些属性的语法、语义及实战用法。

核心概念速览: 地址编码三要素 = 地址单元宽度 + 大小单元宽度 + 地址描述列表。父节点定义编址规则,子节点通过 reg 属性声明自己的地址资源。

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

这两个属性定义在总线节点(如 soc、axi、i2c)中,用于声明该总线下的地址描述格式。

属性名 含义 取值 典型值
#address-cells 描述地址(reg中的address部分)占用的32位单元数 正整数 1(32位地址)、2(64位地址)
#size-cells 描述大小(reg中的size部分)占用的32位单元数 正整数 1(32位长度)、0(无长度,如中断控制器)
语法规则: 每个 reg 条目由 #address-cells 个单元(address) + #size-cells 个单元(size)组成。多个条目用空格分隔。
// 示例:32位地址空间,32位长度
soc {
    #address-cells = <1>;
    #size-cells = <1>;

    uart0: serial@10000000 {
        reg = <0x10000000 0x1000>;
        // 地址:0x10000000,大小:0x1000(4KB)
    };

    gpio0: gpio@10001000 {
        reg = <0x10001000 0x100>;
        // 地址:0x10001000,大小:0x100(256字节)
    };
};
// 示例:64位地址空间,32位长度
bus@0 {
    #address-cells = <2>;
    #size-cells = <1>;

    dma@0x4000000000 {
        reg = <0x40 0x00000000 0x1000>;
        // 地址高32位:0x40,地址低32位:0x00000000,大小:0x1000
    };
};
常见错误: 忘记在父节点声明 #address-cells 和 #size-cells,或子节点 reg 的单元数与父节点声明不匹配,会导致内核解析失败或地址错乱。
reg 属性编码结构 父节点 (soc) #address-cells = <1> #size-cells = <1> reg 属性条目分解 address 部分 1 个单元 (32位) size 部分 1 个单元 (32位) reg = <0x10000000 0x1000> 地址: 0x10000000 | 大小: 0x1000 (4KB)

4.2 reg 属性:设备地址资源的声明

reg 属性是设备节点中最基本的地址描述属性,用于声明设备占用的地址空间(如内存映射寄存器、IO端口、DMA通道等)。其格式为:

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

每个 address 和 length 的单元数由父节点的 #address-cells 和 #size-cells 决定。当设备有多个不连续的地址段时,可以声明多个条目。

场景 #address-cells #size-cells reg 示例 含义
32位内存映射 1 1 <0x1000 0x200> 基址0x1000,长度0x200
64位内存映射 2 1 <0x0 0x40000000 0x1000> 基址0x40000000,长度0x1000
无大小(中断控制器) 1 0 <0x2000> 仅地址0x2000,无长度
多段地址 1 1 <0x1000 0x100 0x2000 0x50> 两段:0x1000-0x10FF, 0x2000-0x204F
实战建议: 对于内存映射外设,reg 中的地址通常是该外设的基地址,长度应覆盖所有可访问的寄存器空间。对于 GPIO 控制器,长度通常为 0x100(256字节)或更少。
// 多段地址示例:网卡有两个独立的寄存器区域
ethernet@20000000 {
    reg = <0x20000000 0x1000    // 主寄存器区
           0x30000000 0x200>;   // DMA描述符区
};

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

ranges 属性定义在总线节点中,用于将子节点的地址空间映射到父节点的地址空间。它实现了地址域的转换,是设备树中实现“地址翻译”的关键机制。

ranges 的格式为:

ranges = <child-bus-address parent-bus-address length>;

其中每个字段的单元数由当前节点(子总线)的 #address-cells、父节点的 #address-cells 和当前节点的 #size-cells 共同决定。

属性值 含义 典型场景
ranges;(空) 1:1 直接映射,子地址等于父地址 简单总线,地址无需转换
ranges = <...>; 定义地址转换表 桥接、PCIe、外部总线
无 ranges 子节点地址对父节点不可见 非内存映射设备(如I2C子设备)
// 示例:外部总线地址转换
// 子总线地址 0x0 ~ 0x3FFFFFF 映射到父总线地址 0x80000000 ~ 0x83FFFFFF
bus@80000000 {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges = <0x0 0x80000000 0x4000000>;
    // 子地址0 → 父地址0x80000000,长度0x4000000(64MB)

    ethernet@0 {
        reg = <0x0 0x1000>;
        // 子地址0x0 → 实际物理地址 0x80000000
    };

    display@100000 {
        reg = <0x100000 0x100000>;
        // 子地址0x100000 → 实际物理地址 0x80100000
    };
};
关键理解: ranges 只影响内存映射地址的翻译。对于非内存映射设备(如 I2C 从设备、SPI 设备),子节点地址是总线上的逻辑地址(如从机地址),不需要 ranges 转换。
ranges 属性地址转换流程 父总线 (CPU 地址空间) 0x80000000 ~ 0x83FFFFFF 子总线 (外设总线) 0x0 ~ 0x3FFFFFF ranges = <0x0 0x80000000 0x4000000> 子地址 → 父地址,长度 64MB ethernet@0 reg = <0x0 0x1000> display@100000 reg = <0x100000 0x100000> 转换结果:ethernet → 0x80000000 | display → 0x80100000

4.4 实战案例:GPIO 控制器节点的地址编码

结合 GPIO 控制器节点,展示完整的地址编码配置。以下是一个典型的 SoC 内部 GPIO 控制器设备树节点:

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

    gpio0: gpio@ff310000 {
        compatible = "snps,dw-apb-gpio";
        reg = <0xff310000 0x1000>;
        interrupts = <0 40 4>;
        #gpio-cells = <2>;
        gpio-controller;
        ngpios = <32>;
    };

    gpio1: gpio@ff320000 {
        compatible = "snps,dw-apb-gpio";
        reg = <0xff320000 0x1000>;
        interrupts = <0 41 4>;
        #gpio-cells = <2>;
        gpio-controller;
        ngpios = <16>;
    };
};
属性 值 说明
#address-cells 1 32位地址空间
#size-cells 1 32位长度描述
ranges 空 1:1 直接映射,子地址等于父地址
reg (gpio0) <0xff310000 0x1000> 基址 0xff310000,寄存器空间 4KB
reg (gpio1) <0xff320000 0x1000> 基址 0xff320000,寄存器空间 4KB
关键点: 当父节点使用 ranges;(空)时,子节点的 reg 地址直接对应 CPU 物理地址。这是 SoC 内部设备最常见的配置方式。

4.5 地址编码常见问题与避坑指南

  1. 单元数不匹配: 子节点 reg 的单元数必须严格等于 #address-cells + #size-cells。多一个或少一个都会导致解析错误。
  2. 忘记声明父节点属性: 如果父节点没有 #address-cells 和 #size-cells,内核会使用默认值(通常为 2 和 1),可能导致地址解析错误。
  3. ranges 长度溢出: 转换长度不能超过父总线地址空间范围,否则会导致地址映射重叠或越界。
  4. 64位地址的高低位顺序: 在 #address-cells = <2> 时,地址顺序为 <高32位 低32位>,不要写反。
  5. 非内存映射设备不要用 ranges: I2C、SPI 等总线上的设备地址是从机地址,不是内存地址,不需要 ranges 转换。
调试建议: 使用 dtc -I dtb -O dts 反编译设备树,检查 reg 属性是否被正确解析。内核启动时可通过 of_address_to_resource() 函数验证地址映射是否正确。

4.6 本章小结