4、设备树地址映射:reg属性、ranges属性、dma-ranges属性、地址转换
地址映射,说白了就是让CPU能找到外设。你写驱动时,总得知道寄存器在哪吧?设备树里就是靠reg、ranges、dma-ranges这几个属性来描述的。我刚开始接触时也觉得绕,后来调过几个板子就明白了——其实核心就三件事:描述地址、转换地址、告诉DMA怎么走。
4.1 reg属性:描述设备占用的地址空间
reg是最基础的属性。它告诉内核:这个设备用哪段地址空间,长度是多少。格式很简单:reg = <地址 长度, 地址 长度, ...>。每个地址和长度,在32位系统里是1个cell(32位),在64位系统里是2个cell(64位)。
核心规则:reg中的地址和长度,其cell数量由父节点的#address-cells和#size-cells决定。
举个例子,一个UART控制器挂在内存总线上:
soc {
#address-cells = <1>;
#size-cells = <1>;
uart@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <33>;
};
};
这里0x10000000是起始地址,0x1000是长度(4KB)。内核解析后,驱动就能通过platform_get_resource()拿到这段IO内存。
我的习惯:写reg时,长度尽量对齐页大小(4KB)。有些IP核只用了几个寄存器,但给整页映射能避免后续扩展时改设备树。
4.2 ranges属性:实现父子节点间的地址转换
这是很多人容易搞混的地方。ranges解决的是地址翻译问题——子节点看到的地址空间,和父节点看到的地址空间,往往不是一回事。
比如,一个PCIe控制器内部有独立的配置空间和IO空间,这些地址在CPU视角下可能被映射到不同的基地址。ranges就是做这个映射的。
格式:ranges = <子地址 父地址 长度, ...>
看个实际例子:
bus@20000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x20000000 0x10000000>;
ethernet@0 {
reg = <0x0 0x10000>;
};
};
这里ranges的意思是:子地址0x0映射到父地址0x20000000,映射长度0x10000000(256MB)。所以以太网控制器看到的reg = <0x0 0x10000>,实际物理地址是0x20000000 + 0x0 = 0x20000000。
我曾经踩过的坑:如果ranges属性为空(ranges;),表示父子地址空间是1:1映射。但如果父节点没有定义ranges,子节点的地址就无法被CPU直接访问。有一次我忘了给桥接总线加ranges,驱动ioremap死活不成功,查了两天才发现是设备树的问题。
4.3 dma-ranges属性:DMA地址转换
dma-ranges和ranges很像,但用途完全不同。ranges是给CPU访问外设用的,dma-ranges是给外设做DMA访问内存用的。
为什么需要这个?因为有些设备(比如老旧的32位DMA控制器)只能访问低32位地址空间,但系统内存可能超过4GB。或者,IOMMU/SMMU做了地址重映射,设备看到的DMA地址和物理地址不一致。
格式和ranges一样:dma-ranges = <设备地址 父地址 长度, ...>
举个例子:
pcie@40000000 {
#address-cells = <3>;
#size-cells = <2>;
dma-ranges = <0x02000000 0x0 0x00000000
0x0 0x80000000
0x0 0x80000000>;
};
这个配置告诉PCIe设备:你的DMA地址0x00000000到0x7FFFFFFF,对应物理地址0x80000000到0xFFFFFFFF。说白了,就是把DMA地址空间整体偏移了2GB。
我建议:如果你的系统有IOMMU,dma-ranges可以简化驱动逻辑。驱动不需要知道物理内存布局,只管用设备看到的DMA地址就行,IOMMU会帮你做转换。
4.4 地址转换的完整流程
内核解析设备树地址时,会从子节点向上逐级查找ranges,直到根节点。这个过程叫地址翻译(address translation)。
我画了张图,帮你理清这个流程:
内核里,of_translate_address()函数就是干这个活的。它会从当前节点开始,向上遍历父节点,每遇到一个ranges就做一次偏移累加,直到根节点。最终得到的就是CPU能直接用的物理地址。
4.5 实战对比:三种属性的差异
| 属性 | 方向 | 典型场景 | 不配置的后果 |
|---|---|---|---|
reg |
设备→CPU视角 | 描述设备自身寄存器地址 | 驱动无法访问硬件 |
ranges |
子节点→父节点 | 桥接总线地址映射 | 子设备地址不可达 |
dma-ranges |
设备→内存 | DMA地址空间转换 | DMA操作可能访问错误内存 |
特别注意:dma-ranges和ranges虽然格式一样,但内核处理它们的时机不同。ranges在of_translate_address()中处理,dma-ranges在of_dma_configure()中处理。别搞混了。
4.6 避坑指南
我这些年调过的板子,地址映射出问题的情况真不少。总结几个常见坑:
- address-cells和size-cells不匹配:父节点和子节点必须一致,否则解析出来的地址全是错的。我见过有人父节点用
<2>,子节点用<1>,结果地址被截断。 - ranges长度不够:子设备地址范围超出了ranges定义的映射长度,内核不会报错,但访问时会挂掉。
- dma-ranges忘记配置:64位系统上,如果DMA控制器只能访问32位地址,不配
dma-ranges的话,DMA可能写到高地址内存去,数据损坏。 - 空ranges的含义:
ranges;(空)表示1:1映射,但ranges不存在时,子地址完全不可翻译。这两个情况差别很大。
我的调试技巧:在驱动里加dev_info()打印res->start和res->end,对比设备树里的值。如果地址不对,八成是ranges解析出了问题。另外,/sys/firmware/devicetree/base/下可以查看解析后的设备树,非常方便。
地址映射这块,说白了就是搞清楚「谁看谁」的问题。CPU看外设用ranges,外设看内存用dma-ranges,设备自己用reg。理清这个关系,设备树地址部分就通了。