第四节:设备树地址编码的核心三剑客

各位同学,今天我们来聊聊设备树里最容易让人头晕的三个属性——#address-cells#size-cellsranges。说实话,我当年刚接触设备树时,被这三个东西绕得晕头转向。后来在项目中踩了几次坑,才真正搞明白它们的工作原理。

一、地址编码的基本概念

设备树里描述硬件,最核心的就是两件事:设备在哪占多大地方。你想想看,CPU要访问一个外设,总得知道它的地址范围吧?

#address-cells#size-cells就是干这个的。它们定义了一个地址描述需要几个32位整数。

  • #address-cells:表示地址需要几个32位单元
  • #size-cells:表示大小需要几个32位单元

举个例子,32位系统里地址范围是0~4GB,一个32位整数就够了。但64位系统呢?地址范围大到离谱,得两个32位整数拼起来才行。

重要原则:这两个属性是父节点定义的,影响的是子节点的reg属性解析方式。

二、#address-cells 和 #size-cells 实战

我在项目中遇到过这样一个场景:一个简单的SPI控制器,挂接了多个外设。设备树是这样写的:

spi@10000000 {
    compatible = "vendor,spi-controller";
    reg = <0x10000000 0x1000>;
    #address-cells = <1>;
    #size-cells = <0>;

    flash@0 {
        reg = <0>;  // 片选0
    };

    sensor@1 {
        reg = <1>;  // 片选1
    };
};

看到没?父节点SPI控制器自己的reg用了两个值(地址+大小),但子节点的reg只有一个值。为什么?因为父节点声明了#size-cells = <0>,子节点就不需要描述大小了。

嗯,这里要注意:父节点的reg和子节点的reg是两套规则。父节点自己的reg由它自己的父节点决定,子节点的reg才受#address-cells#size-cells影响。

三、64位系统的地址编码

现在很多芯片都是64位的,比如ARM Cortex-A系列。这时候地址编码就变成这样了:

soc {
    compatible = "simple-bus";
    #address-cells = <2>;  // 64位地址
    #size-cells = <2>;     // 64位大小

    dma@100000000 {
        reg = <0x0 0x10000000 0x0 0x1000>;
        // 地址:0x10000000(高32位0,低32位0x10000000)
        // 大小:0x1000(高32位0,低32位0x1000)
    };
};

说白了,就是地址和大小各拆成两个32位整数。我第一次看到这种写法时还纳闷,为什么地址要写两段?后来才明白,这是为了兼容32位和64位系统的统一描述方式。

四、ranges属性的工作原理

ranges属性,我个人觉得是设备树里最巧妙的设计。它解决的是地址映射问题。

你想想看,一个外设挂在总线上,它在CPU视角的地址和它在总线视角的地址,往往是不一样的。ranges就是用来建立这种映射关系的。

基本语法:

ranges = <子地址 父地址 大小>;

来看一个实际例子:

soc {
    compatible = "simple-bus";
    #address-cells = <1>;
    #size-cells = <1>;
    ranges = <0x0 0xe0000000 0x00100000>;
    // 子地址0~1MB映射到父地址0xe0000000~0xe0100000

    ethernet@0 {
        reg = <0x0 0x1000>;
        // 在子空间地址0x0,大小0x1000
        // 实际物理地址:0xe0000000 + 0x0 = 0xe0000000
    };
};

我的经验:调试ranges映射时,最好先确认父节点的地址范围。我曾经遇到一个bug,ranges写对了,但父节点自己的reg范围没覆盖到映射区域,结果驱动死活访问不到外设。

五、ranges的几种特殊形式

ranges不是非得写满的。它有三种形态:

形式 含义 使用场景
ranges = <...>; 有映射关系 地址需要转换
ranges; 空属性,表示1:1映射 子地址和父地址相同
没有ranges 子节点不能直接访问 需要驱动自己处理映射

我记得有一次调试PCIe设备,发现子设备地址怎么都访问不到。查了半天,原来是父节点没写ranges属性。PCIe的配置空间和内存空间映射关系复杂,必须显式声明ranges才行。

避坑指南:我曾经犯过一个错误——在桥接设备上忘了写ranges。结果子设备的中断和DMA都工作不正常。因为CPU无法通过桥接设备正确翻译子设备的地址。记住:只要地址空间有变化,就必须用ranges

六、多级地址映射的实战

实际项目中,地址映射往往是多级的。比如:

root {
    #address-cells = <1>;
    #size-cells = <1>;

    bridge@a0000000 {
        reg = <0xa0000000 0x100000>;
        #address-cells = <1>;
        #size-cells = <1>;
        ranges = <0x0 0xa0000000 0x100000>;

        sub_bridge@0 {
            reg = <0x0 0x10000>;
            #address-cells = <1>;
            #size-cells = <1>;
            ranges = <0x0 0x0 0x10000>;

            uart@0 {
                reg = <0x0 0x1000>;
                // 最终物理地址:0xa0000000 + 0x0 + 0x0 = 0xa0000000
            };
        };
    };
};

这种嵌套结构,说白了就是一层层地址翻译。CPU访问UART时,内核会沿着设备树逐级解析ranges,最终得到物理地址。

七、总结与建议

讲到这里,我想强调几点:

  1. #address-cells和#size-cells是父子节点之间的契约,子节点必须遵守父节点的定义
  2. ranges是地址翻译的桥梁,没有它,子设备在CPU眼里就是"不存在"的
  3. 调试时多用devicetree dump,查看内核解析后的地址是否正确

我个人习惯在写设备树时,先画一张地址映射图。把每个节点的地址范围、映射关系都标清楚。这样写出来的设备树,基本不会出地址相关的bug。

下一节我们会讲中断控制器,那是另一个容易踩坑的地方。各位先把地址编码搞明白,后面学起来就轻松多了。