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 知识体系结构图
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 是告诉系统怎么做地址翻译的,基本就不会出大问题。