4. 地址映射与reg属性:#address-cells、#size-cells、reg属性格式、地址映射实战

在设备树中,描述设备在总线上的地址空间和内存映射范围是核心功能之一。本章将深入解析用于地址映射的三个关键属性:#address-cells、#size-cells 和 reg,并通过实战案例演示如何正确描述外设的寄存器基址与地址范围。

核心概念:设备树通过 reg 属性描述设备的地址资源,而 #address-cells 和 #size-cells 则定义了地址和长度字段的编码宽度(以32位字为单位)。这三者共同构成了设备树地址映射的基石。

4.1 地址映射三要素

每个总线节点(如 soc、axi、amba)都需要声明其子节点地址描述的格式。这通过两个属性完成:

属性 含义 值类型 典型值
#address-cells 子节点 reg 属性中地址字段占用的32位字数量 u32 1(32位地址)、2(64位地址)
#size-cells 子节点 reg 属性中长度字段占用的32位字数量 u32 1(32位长度)、0(无长度,如中断控制器)
reg 描述设备的地址资源,格式为 (address, length) 对 prop-encoded-array <0x10000000 0x1000>
记忆口诀:“父节点定格式,子节点填数据”。父总线节点通过 #address-cells 和 #size-cells 告诉内核如何解析子节点的 reg 值。

4.2 reg属性格式详解

reg 属性是一个数组,由一组或多组 (address, length) 对组成。每组描述一个连续的地址区域。其格式完全由父节点的 #address-cells 和 #size-cells 决定。

// 格式:reg = < address1 length1 [address2 length2 ...] >
// 其中 address 占用 #address-cells 个32位字
// 其中 length  占用 #size-cells 个32位字

// 示例1:32位地址,32位长度(最常见)
// #address-cells = <1>;
// #size-cells = <1>;
reg = <0x10000000 0x1000>;  // 基址0x10000000,长度4KB

// 示例2:64位地址,32位长度
// #address-cells = <2>;
// #size-cells = <1>;
reg = <0x00000000 0x80000000 0x1000000>;  // 基址0x0000000080000000,长度16MB

// 示例3:多段地址空间
reg = <0x10000000 0x1000    // 第一段:寄存器空间
       0x20000000 0x200>;   // 第二段:控制寄存器空间

4.3 地址映射的层级结构

设备树的地址映射是层级化的。每个总线节点可以有自己的地址转换规则,通过 ranges 属性将子地址空间映射到父地址空间。下图展示了典型的SoC地址映射层级:

设备树地址映射层级结构 根节点 / soc 节点 #address-cells = <1>; #size-cells = <1>; uart@10000000 reg = <0x10000000 0x1000>; i2c@20000000 reg = <0x20000000 0x100>; spi@30000000 reg = <0x30000000 0x400>; rtc@68 reg = <0x68 1>; // I2C地址 说明: 每个总线节点定义其子节点的地址格式。soc节点使用32位地址和32位长度,而i2c总线使用7位地址和1字节长度。

4.4 地址映射实战:描述一个UART控制器

下面通过一个完整的实战案例,展示如何为一个典型的UART控制器编写正确的设备树节点,包括地址映射、中断和时钟信息。

// 实战:描述一个ARM SoC中的UART控制器
// 该UART位于APB总线,基址0x10000000,地址范围4KB

/dts-v1/;

/ {
    model = "Example ARM SoC";
    compatible = "arm,example-soc";
    #address-cells = <1>;
    #size-cells = <1>;

    // 系统内存
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  // 1GB RAM
    };

    // 片上系统总线
    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;  // 直接映射,子地址等于父地址

        // UART控制器
        uart@10000000 {
            compatible = "arm,pl011", "arm,primecell";
            reg = <0x10000000 0x1000>;  // 基址0x10000000,长度4KB
            interrupts = <0 33 4>;       // GIC SPI 33, 高电平触发
            clocks = <&uart_clk 0>;
            clock-names = "apb_pclk";
            status = "okay";
        };

        // GPIO控制器
        gpio@20000000 {
            compatible = "arm,pl061", "arm,primecell";
            reg = <0x20000000 0x1000>;
            interrupts = <0 34 4>;
            gpio-controller;
            #gpio-cells = <2>;
            status = "okay";
        };
    };
};
常见错误:忘记在总线节点中声明 #address-cells 和 #size-cells,或者声明的值与子节点 reg 的实际格式不匹配。例如,如果父节点声明 #address-cells = <2>,但子节点 reg 只写了一个32位地址,内核将无法正确解析。

4.5 多段地址空间与ranges映射

当设备需要多个不连续的地址区域时,reg 属性可以包含多组地址-长度对。此外,ranges 属性用于将子总线的地址空间映射到父总线的地址空间。

// 多段地址空间示例:一个包含寄存器空间和DMA空间的设备
// #address-cells = <1>; #size-cells = <1>;
reg = <0x10000000 0x1000    // 寄存器空间:0x10000000 ~ 0x10000FFF
       0x20000000 0x10000>; // DMA描述符空间:0x20000000 ~ 0x2000FFFF

// ranges映射示例:将子总线地址0映射到父总线地址0x40000000
// 子地址0 ~ 0x100000 映射到父地址0x40000000 ~ 0x40100000
ranges = <0x0 0x40000000 0x100000>;  // 格式:<子地址 父地址 长度>

4.6 地址映射的解析流程

内核在解析设备树时,会按照以下流程处理地址映射:

  1. 定位父总线:从当前节点向上查找第一个包含 #address-cells 和 #size-cells 的父节点。
  2. 读取格式:获取父节点的 #address-cells 和 #size-cells 值。
  3. 解析reg:按照格式从 reg 属性中提取地址和长度值。
  4. 地址转换:如果父节点有 ranges 属性,将子地址转换为父地址空间中的地址。
  5. 资源注册:将最终地址资源注册到内核的IO资源树中,供驱动使用。
内核地址映射解析流程 1. 定位父总线 查找#address-cells 2. 读取格式 获取cells值 3. 解析reg 提取地址/长度 4. 转换 ranges映射 5. 资源注册 注册到IO资源树 关键点: • 地址转换仅在父节点存在 ranges 属性时进行,否则子地址直接使用 • 驱动通过 platform_get_resource() 获取解析后的地址资源

4.7 常见问题与避坑指南

问题 现象 解决方案
#address-cells 未声明 内核报错 "Bad cell count" 在父总线节点中显式声明 #address-cells 和 #size-cells
reg 长度与 cells 不匹配 设备无法正确探测,资源注册失败 确保 reg 中的地址和长度字段数量与父节点声明一致
64位地址使用错误 高32位地址被截断 设置 #address-cells = <2>,reg 中地址写两个32位字
ranges 映射错误 驱动访问的地址与实际物理地址不符 检查 ranges 的格式:<子地址 父地址 长度>,确保三个字段的cells数正确
调试技巧:使用 ls /sys/devices/platform/ 查看已注册的平台设备,使用 cat /proc/iomem 查看IO内存映射,确认地址是否正确注册。

4.8 实战练习:编写一个DMA控制器的设备树节点

假设有一个DMA控制器,具有以下特性:

  • 基址:0x30000000,地址范围:0x1000
  • 中断:SPI 45,高电平触发
  • 时钟:dma_clk
  • 支持8个DMA通道
// 参考答案
dma@30000000 {
    compatible = "example,dma-controller";
    reg = <0x30000000 0x1000>;
    interrupts = <0 45 4>;
    clocks = <&dma_clk 0>;
    clock-names = "dma";
    #dma-cells = <2>;
    dma-channels = <8>;
    status = "okay";
};

通过本章的学习,你已经掌握了设备树地址映射的核心机制。正确理解 #address-cells、#size-cells 和 reg 的配合使用,是编写正确设备树文件的基础,也是后续学习pinctrl配置的前提。