4. 设备树地址编码

地址编码这块,说实在的,是设备树里最容易让人头晕的部分之一。我记得刚接触设备树那会儿,看到 reg 属性里一堆数字,完全搞不清哪个是地址哪个是大小。后来在 STM32 项目里踩了几次坑,才算真正搞明白。

今天咱们就把这个硬骨头啃下来。你想想看,CPU 要访问外设,总得知道外设的寄存器在哪个地址、占多大空间吧?这就是 reg 属性干的事。

4.1 地址编码的核心概念

设备树里描述地址信息,靠的是三个东西:

  • #address-cells:表示地址需要几个 32 位整数来描述
  • #size-cells:表示大小需要几个 32 位整数来描述
  • reg:具体的地址和大小信息,格式是 (address, size) 的数组

说白了,address-cells 和 size-cells 就是告诉解析器:你读 reg 的时候,前几个数算地址,后几个数算大小。

重要规则:每个总线节点都必须声明 #address-cells 和 #size-cells,子节点则使用父节点的这些属性来解析自己的 reg。

4.2 常见配置场景

场景 #address-cells #size-cells 说明
32位系统,外设地址空间 1 1 地址和大小各占1个32位
64位系统,大地址空间 2 1 地址用2个32位拼成64位
简单内存映射 1 0 只有地址,没有大小信息
复杂总线桥接 2 2 地址和大小都是64位

4.3 实战:STM32F4 的地址编码

咱们拿 STM32F4 系列来举例。我在做 STM32F429 项目时,写过这样的设备树片段:

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

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;

        usart1: serial@40011000 {
            compatible = "st,stm32-usart", "st,stm32-uart";
            reg = <0x40011000 0x400>;
            interrupts = <37>;
            clocks = <&rcc 0 164>;
            status = "disabled";
        };

        gpioa: gpio@40020000 {
            compatible = "st,stm32-gpio";
            reg = <0x40020000 0x400>;
            interrupts = <0 16 4>;
            gpio-controller;
            #gpio-cells = <2>;
            status = "disabled";
        };
    };
};

你看,这里 #address-cells = <1> 表示地址用 1 个 32 位数就够了。STM32F4 的地址空间是 4GB,32 位刚好能覆盖。

reg = <0x40011000 0x400> 的意思是:USART1 的基地址是 0x40011000,地址范围是 0x400 字节(也就是 1KB)。

我的习惯:写 reg 的时候,我通常会先查芯片手册,确认外设的基地址和寄存器空间大小。比如 STM32F4 的 USART,寄存器空间就是 0x400,因为它的寄存器偏移最大到 0x3FC,加上对齐就是 0x400。

4.4 地址映射与 ranges 属性

ranges 属性是地址翻译的关键。它告诉系统:子总线的地址怎么映射到父总线的地址空间。

格式是:ranges = <子地址 父地址 长度>;

举个例子:

amba {
    #address-cells = <2>;
    #size-cells = <1>;
    ranges = <0 0 0xe0000000 0x00100000>;

    uart@0 {
        reg = <0 0x1000>;
    };
};

这里 ranges 的意思是:子地址 0 映射到父地址 0xe0000000,映射长度 0x100000(1MB)。所以 uart 的 reg 地址 0,实际物理地址是 0xe0000000 + 0 = 0xe0000000。

我曾经踩过的坑:有一次我写 ranges 时,忘了考虑 #address-cells 和 #size-cells 的位数。子地址用了 2 个 cell,父地址用了 2 个 cell,长度用了 1 个 cell,结果 ranges 里数字个数对不上,内核直接报错。后来我养成了习惯:写 ranges 之前先数清楚 cell 的个数。

4.5 多地址空间的 reg

有些外设不止一块地址空间。比如 DMA 控制器,可能有多个通道,每个通道有自己的寄存器区域。这时候 reg 可以写多组:

dma: dma@40026000 {
    compatible = "st,stm32-dma";
    reg = <0x40026000 0x400>,
          <0x40026400 0x200>;
    interrupts = <11>,
                 <12>;
    #dma-cells = <4>;
    status = "disabled";
};

第一组 reg 是 DMA 的主寄存器区,第二组是另一个功能块。每组 reg 都包含地址和大小,用逗号分隔。

4.4 知识体系结构图

设备树地址编码知识体系 地址编码核心 #address-cells #size-cells reg 属性 32位系统:1个cell 64位系统:2个cell 有大小信息:1个cell 无大小信息:0个cell 单地址空间:一组 (addr, size) 多地址空间:多组 (addr, size) ranges 属性:子地址 → 父地址映射 子节点使用父节点的 #address-cells 和 #size-cells 解析 reg

4.7 避坑指南

我这些年做设备树,总结了几条血泪教训:

  • cell 数量必须匹配:父节点声明了 #address-cells = <2>,子节点的 reg 里地址部分就必须写 2 个数,多一个少一个都不行。
  • ranges 为空表示直接映射:如果 ranges 属性存在但内容为空(ranges;),表示子地址和父地址是 1:1 映射,不需要翻译。
  • 没有 ranges 表示隔离:如果节点没有 ranges 属性,子节点不能直接被父节点访问,需要驱动自己处理地址翻译。
  • 大小要合理:reg 里的大小不要随便写。我见过有人把 USART 的大小写成 0x1000,结果和后面的外设地址重叠了,调试了好久才发现。

一个小技巧:写 reg 之前,先打开芯片手册的 memory map 章节,把外设的基地址和范围列个表。这样写出来的 reg 既准确又不会冲突。

嗯,地址编码这块内容就这些。说白了就是三个属性互相配合,把硬件地址信息描述清楚。你只要记住 #address-cells 和 #size-cells 是告诉解析器怎么读 reg 的,ranges 是告诉系统怎么做地址翻译的,基本就不会出大问题。