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

好,咱们今天聊聊设备树里最绕、也最容易出bug的一块——地址编码。

说实话,我刚开始接触设备树那会儿,被这几个属性折腾得不轻。尤其是 ranges,看着像天书一样。后来调了几个月驱动,踩了无数坑,才算真正搞明白。

你想想看,一个CPU要访问外设的寄存器,总得知道地址在哪吧?设备树就是干这个的。但问题来了——不同总线、不同设备的地址长度不一样。有的用32位,有的用64位。怎么统一描述?

嗯,这就是 #address-cells#size-cells 要解决的问题。

4.1 #address-cells 和 #size-cells:地址的“计量单位”

这两个属性,说白了就是告诉解析器:一个地址占几个32位整数?一个长度占几个32位整数?

核心规则:

  • #address-cells:表示地址字段占用的 cell(32位) 数量
  • #size-cells:表示长度字段占用的 cell(32位) 数量
  • 这两个属性是 父节点 定义的,影响的是 子节点reg 属性解析

举个例子,你一看就明白了:

// 32位系统,地址和长度都是32位
soc {
    #address-cells = <1>;
    #size-cells = <1>;
    
    uart0: serial@101f0000 {
        reg = <0x101f0000 0x1000>;
        // 地址:0x101f0000,长度:0x1000(4KB)
    };
};

// 64位系统,地址64位,长度32位
pcie {
    #address-cells = <2>;  // 64位地址需要2个cell
    #size-cells = <1>;     // 32位长度
    
    pcie_device@0 {
        reg = <0x0 0x40000000 0x10000>;
        // 地址:0x0_40000000(64位),长度:0x10000(64KB)
    };
};

我在项目中遇到过一个问题:某款ARM芯片的DMA控制器,地址总线是40位的。结果有人写成了 #address-cells = <1>,导致高8位地址被截断,DMA死活访问不了高端内存。查了两天才发现是这里的问题。

我的习惯: 写设备树前,先确认CPU的物理地址宽度。ARMv7一般是32位,ARMv8通常是40位或48位。别想当然用默认值。

4.2 reg 属性:设备资源的“身份证”

reg 属性描述设备的地址空间资源。它的格式是:

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

每个 addresslength 的cell数量,由父节点的 #address-cells#size-cells 决定。

来看一个实际例子——我在调试某款网络芯片时遇到的:

// 父节点定义
bus@0 {
    #address-cells = <2>;  // 64位地址
    #size-cells = <2>;     // 64位长度
    compatible = "simple-bus";
    ranges;
    
    // 子节点
    ethernet@20000000 {
        reg = <0x0 0x20000000 0x0 0x10000>;
        // 地址:0x00000000_20000000
        // 长度:0x00000000_00010000
    };
    
    dma@30000000 {
        reg = <0x0 0x30000000 0x0 0x1000>;
        // 地址:0x00000000_30000000
        // 长度:0x00000000_00001000
    };
};

注意看,这里地址和长度都用了2个cell。为什么?因为芯片的地址空间确实需要64位来描述。虽然当前只用了低32位,但为了兼容性,还是按64位写的。

我曾经踩过的坑: 别以为 #address-cells#size-cells 可以随便写。它们必须和硬件实际地址宽度匹配。写小了,地址被截断;写大了,解析器会读错数据。我见过有人把32位地址写成 #address-cells = <2>,结果reg里的地址全变成了0x00000000_xxxxxxxx,虽然值没错,但浪费了空间,也容易让人困惑。

4.3 ranges:地址翻译的“桥梁”

这是设备树里最让人头疼的属性之一。我刚开始学的时候,看了三遍文档才搞明白。

ranges 的作用是:把子节点的地址空间,映射到父节点的地址空间

说白了,就是告诉CPU:子设备声称的地址,在CPU眼里是什么地址。

它的格式是:

ranges = <child_base parent_base size>;

其中:

  • child_base:子节点地址空间的基地址(cell数由子节点的 #address-cells 决定)
  • parent_base:父节点地址空间的基地址(cell数由父节点的 #address-cells 决定)
  • size:映射范围的大小(cell数由父节点的 #size-cells 决定)

来看一个经典例子——我在做某款ARM SoC的PCIe控制器移植时遇到的:

soc {
    #address-cells = <2>;  // CPU视角:64位地址
    #size-cells = <2>;
    
    pcie: pcie@40000000 {
        #address-cells = <3>;  // PCIe规范:3个cell(bus+dev+func)
        #size-cells = <2>;
        reg = <0x0 0x40000000 0x0 0x10000000>;
        
        // 关键在这里:地址翻译
        ranges = <
            // PCIe地址空间 -> CPU地址空间
            0x02000000 0x0 0x50000000  // PCIe 32位非预取内存
            0x0 0x50000000             // CPU地址
            0x0 0x10000000             // 大小:256MB
            
            0x43000000 0x0 0x60000000  // PCIe 64位预取内存
            0x0 0x60000000             // CPU地址
            0x0 0x20000000             // 大小:512MB
        >;
        
        pci_device@0 {
            reg = <0x0000 0x0 0x0 0x0 0x0>;
            // 这个设备在PCIe总线上的地址是0x50000000
            // 经过ranges翻译,CPU看到的是0x50000000
        };
    };
};

说实话,我第一次看到3个cell的地址时也懵了。后来查了PCIe规范才知道,PCIe地址的第一个cell是空间类型标识:

类型值 含义
0x01000000 I/O空间
0x02000000 32位内存空间(非预取)
0x42000000 32位内存空间(预取)
0x43000000 64位内存空间(预取)

我的经验:ranges 时,先画一张地址映射图。把CPU地址空间、总线地址空间、设备地址空间都画出来。然后一条一条对应着写。我每次写复杂的ranges,都会先在纸上画一遍,确认无误再写到dts里。

4.4 dma-ranges:DMA的“专属地图”

dma-rangesranges 很像,但它是给DMA用的。

你想想看,CPU访问设备和DMA访问设备,走的路径可能不一样。有些设备有独立的DMA地址空间,和CPU看到的地址不一样。

举个例子——我在调试某款GPU时遇到的:

gpu: gpu@18000000 {
    compatible = "arm,mali-400";
    reg = <0x18000000 0x10000>;
    
    // GPU的DMA引擎看到的地址空间
    dma-ranges = <
        // GPU DMA地址 -> 系统内存地址
        0x0 0x80000000  // GPU DMA基地址
        0x0 0x80000000  // 系统内存基地址
        0x0 0x40000000  // 映射大小:1GB
    >;
};

这里的意思是:GPU的DMA引擎访问地址0x80000000时,实际上访问的是系统内存的0x80000000。为什么需要这个?因为有些GPU的DMA地址空间和CPU地址空间不是一一对应的,需要做地址翻译。

我曾经犯过的错: 有次做视频编解码芯片的驱动,忘了写 dma-ranges。结果DMA传输总是报错,数据写到错误的内存地址。查了三天,最后发现是DMA引擎的地址空间和CPU地址空间不一致。加上 dma-ranges 后,问题立刻解决。

4.5 实战中的常见问题

说了这么多,我总结几个实际开发中容易踩的坑:

  1. #address-cells 和 #size-cells 不匹配:父节点和子节点的定义不一致,导致reg解析错误。我建议你在写dts时,每个节点都显式声明这两个属性,不要依赖默认值。
  2. ranges 写反了顺序:记住,永远是 子地址 父地址 大小。我见过有人写反了,结果设备地址全乱套了。
  3. 忘记处理地址对齐:有些总线要求地址必须对齐到特定边界。比如PCIe要求64位地址对齐到64位边界。不满足对齐条件,设备可能无法正常工作。
  4. dma-ranges 和 ranges 混淆:记住,ranges 是给CPU访问用的,dma-ranges 是给DMA访问用的。两者可能完全不同。

最后说一句: 地址编码是设备树里最基础也最容易出错的部分。我的建议是——写之前先画图,写完之后用 dtc -I dtb -O dts 反编译看看,确认地址解析是否正确。别像我当年那样,等到硬件调不通了才回头查设备树。