第4章:设备树地址编码——#address-cells、#size-cells、ranges、dma-ranges

地址编码,说白了就是告诉内核:「我的设备挂在哪个地址上?地址有多长?」。很多新手写设备树,一碰到地址相关的属性就头大。我刚开始做车载项目时,也在这上面栽过跟头。

今天咱们就把这几个属性彻底讲明白。你想想看,如果地址都描述不清楚,CPU怎么访问外设?DMA怎么搬运数据?

4.1 #address-cells 和 #size-cells:地址的「计量单位」

这两个属性是地址编码的基石。它们定义了一个地址单元用几个32位来表示。

  • #address-cells:地址需要几个32位整数来描述
  • #size-cells:地址范围长度需要几个32位整数来描述

举个例子,32位系统里,一个地址就是0x00000000到0xFFFFFFFF,一个32位整数就够了。所以#address-cells = <1>。但到了64位系统,地址范围是0x0000000000000000到0xFFFFFFFFFFFFFFFF,得两个32位整数拼起来,所以#address-cells = <2>

核心规则:父节点的#address-cells和#size-cells决定了子节点reg属性的解析方式。

我在项目中遇到过一个问题:某款车机芯片的GIC控制器,地址范围超过了4GB。有人写成了#address-cells = <1>,结果中断控制器死活初始化失败。查了两天才发现是地址截断了。

// 正确写法:64位地址
gic: interrupt-controller@2c010000 {
    compatible = "arm,gic-400";
    #interrupt-cells = <3>;
    #address-cells = <2>;  // 地址用两个32位
    #size-cells = <2>;     // 长度也用两个32位
    reg = <0x0 0x2c010000 0x0 0x1000>,
          <0x0 0x2c020000 0x0 0x2000>;
};

4.2 reg属性:设备地址的「身份证」

reg属性是设备地址的直接描述。它的格式是:reg = <地址1 长度1 地址2 长度2 ...>

每个地址和长度占几个32位,完全由父节点的#address-cells和#size-cells决定。嗯,这里要注意:reg里的地址是相对于父节点总线域的。

// 一个简单的UART设备
uart0: serial@10000000 {
    compatible = "ns16550";
    reg = <0x10000000 0x1000>;  // 基址0x10000000,长度0x1000
    interrupts = <0 33 4>;
    clock-frequency = <1843200>;
};

我建议初学者先画个地址映射图。把每个设备的基址、长度标出来,再写reg就清晰多了。

4.3 ranges属性:地址翻译的「桥梁」

ranges属性是设备树里最容易让人迷糊的东西。它的作用是把子节点的地址空间映射到父节点的地址空间。

说白了,就是告诉CPU:「子总线上的地址A,对应父总线上的地址B,长度是L」。

ranges的格式:ranges = <子地址 父地址 长度 ...>

每个子地址、父地址、长度占几个32位,分别由子节点和父节点的#address-cells、#size-cells决定。

个人经验:如果ranges为空(ranges;),表示子地址和父地址是1:1映射。如果ranges不存在,子设备对CPU来说就是不可见的——除非你手动建立映射。

// 一个典型的PCIe桥接场景
pcie@40000000 {
    compatible = "pci-host-ecam-generic";
    #address-cells = <3>;   // PCIe地址需要3个32位
    #size-cells = <2>;
    ranges = <0x82000000 0x0 0x40000000   // 子地址:PCIe配置空间
              0x0 0x40000000              // 父地址:CPU物理地址
              0x0 0x10000000>;            // 长度:256MB
};

我曾经调试过一个车载摄像头驱动,图像数据死活读不对。后来发现是ranges写错了,子地址和父地址的偏移量算反了。嗯,这种错误排查起来特别费时间。

4.4 dma-ranges属性:DMA的「专属通道」

dma-ranges和ranges很像,但它是给DMA控制器用的。它描述了DMA设备看到的地址空间和CPU地址空间之间的映射关系。

为什么要单独搞一个dma-ranges?因为有些设备通过DMA访问内存时,看到的地址和CPU看到的地址不一样。比如某些SoC里,GPU或视频编解码器的DMA地址空间是独立的。

避坑指南:我曾经遇到一个项目,VPU(视频处理单元)的DMA总是访问到错误的内存区域。查了三天,发现是dma-ranges没配。VPU的DMA地址从0x80000000开始,但CPU的物理内存从0x40000000开始。没有dma-ranges做地址翻译,DMA就乱飞了。

// DMA地址映射示例
vpu: video-codec@1d000000 {
    compatible = "vendor,vpu";
    reg = <0x1d000000 0x100000>;
    #address-cells = <1>;
    #size-cells = <0>;
    dma-ranges = <0x80000000 0x40000000 0x20000000>;
    // 含义:VPU的DMA地址0x80000000对应CPU物理地址0x40000000
    // 映射范围0x20000000(512MB)
};

4.5 实战中的常见陷阱

说了这么多理论,咱们看看实际项目中容易踩的坑。

陷阱 现象 原因
#address-cells不匹配 设备无法probe 父节点和子节点的地址单元数不一致
ranges偏移量错误 访问设备时崩溃或数据错乱 子地址到父地址的映射关系写反了
dma-ranges缺失 DMA传输数据到错误地址 DMA控制器和CPU的地址空间不同
reg长度不足 设备只能访问部分寄存器 size-cells设置错误,地址范围被截断

我个人习惯是:拿到一个新平台的设备树,先看根节点的#address-cells和#size-cells。这俩值决定了整个地址体系的基调。然后逐级检查每个总线节点的ranges,确保地址映射链是完整的。

4.6 调试技巧:如何验证地址配置正确

写完了设备树,怎么知道地址配对了没有?我分享几个实用方法。

  1. 查看/proc/iomem:这个文件列出了所有已注册的内存区域。如果设备地址没出现在这里,说明reg解析有问题。
  2. 使用devicetree工具dtc -I dtb -O dts可以把编译好的DTB反编译成DTS,检查地址值是否和预期一致。
  3. 内核启动日志dmesg | grep -i "address\|range\|dma",内核会打印地址映射信息。

小技巧:我经常在设备树里加一个status = "disabled"的测试节点,用reg = <0xDEADBEEF 0x1000>这种明显错误的地址。如果内核启动时没有报地址冲突,说明地址解析逻辑可能有问题。

好了,地址编码这块内容就讲到这里。记住一句话:地址是设备的门牌号,门牌号错了,快递就送不到。下一章咱们聊聊中断控制器和中断映射,那也是设备树里容易出幺蛾子的地方。