4、设备树地址编码:#address-cells、#size-cells、ranges、dma-ranges
地址编码,说白了就是告诉内核:「我这个设备,在总线上怎么找到它?它占多大地方?」
我刚开始学设备树时,最头疼的就是这四个属性。尤其是 ranges,绕来绕去,总搞不清谁映射到谁。后来调了几个板子,踩了几次坑,才算真正弄明白。
4.1 #address-cells 和 #size-cells:地址的「度量衡」
这两个属性,决定了子节点里 reg 属性的写法。
#address-cells:用几个 32 位整数表示地址。#size-cells:用几个 32 位整数表示长度。
举个例子。如果父节点写的是:
#address-cells = <1>;
#size-cells = <1>;
那么子节点的 reg 就是 <地址 长度>,比如:
reg = <0x10000000 0x1000>;
表示基地址 0x10000000,长度 0x1000(4KB)。
如果改成:
#address-cells = <2>;
#size-cells = <1>;
那地址就要写两个 32 位数,比如:
reg = <0x0 0x10000000 0x1000>;
高 32 位是 0,低 32 位是 0x10000000。长度还是 0x1000。
核心原则:子节点的 reg 格式,完全由父节点的 #address-cells 和 #size-cells 决定。不是子节点自己说了算。
我遇到过有人把这两个属性写反了。地址写成了长度,长度写成了地址。结果内核解析出来的资源完全错位,驱动一加载就崩。嗯,这种错很难查,因为编译不报错。
4.2 ranges:地址翻译的「桥梁」
ranges 是设备树里最绕的属性。它的作用,是把子节点的地址空间,映射到父节点的地址空间。
格式长这样:
ranges = <子地址 父地址 长度>;
每个字段的含义:
- 子地址:子节点内部的地址(用子节点的 #address-cells 表示)。
- 父地址:在父节点总线上的地址(用父节点的 #address-cells 表示)。
- 长度:映射范围的大小(用父节点的 #size-cells 表示)。
举个例子。假设我们有一个外部总线,挂了一个 SPI 控制器:
external-bus {
#address-cells = <2>;
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000>;
spi@1,0 {
reg = <1 0 0x1000>;
...
};
};
这里 ranges 的意思是:子地址 0 0(片选 0,偏移 0),映射到父地址 0x10100000,长度 0x10000。
而 SPI 节点的 reg = <1 0 0x1000>,表示它位于片选 1,偏移 0,长度 0x1000。内核会自动算出它在 CPU 视角的物理地址:0x10100000 + 0x10000 * 1 + 0 = 0x10200000。
我的经验:写 ranges 时,先画一张地址映射图。把子地址、父地址、长度都标清楚。别凭感觉写,很容易算错偏移。
如果 ranges 属性为空(ranges;),表示子地址和父地址是 1:1 映射,地址相同。如果 ranges 不存在,表示子节点地址对父节点不可见,内核不会自动做地址翻译。
4.3 dma-ranges:DMA 的「专属通道」
dma-ranges 和 ranges 很像,但它是给 DMA 用的。
有些设备做 DMA 时,看到的地址空间和 CPU 不一样。比如,设备只能访问 32 位地址,但 CPU 有 64 位地址空间。这时候就需要 dma-ranges 来告诉内核:设备 DMA 看到的地址,对应到 CPU 的哪个物理地址。
格式完全一样:
dma-ranges = <设备地址 CPU地址 长度>;
举个例子:
pcie-controller {
#address-cells = <3>;
#size-cells = <2>;
dma-ranges = <0x02000000 0x0 0x00000000 0x0 0x80000000 0x0 0x10000000>;
};
这表示:PCIe 设备看到的地址 0x00000000,对应 CPU 的 0x80000000,长度 0x10000000(256MB)。设备做 DMA 时,内核会帮它做地址转换。
注意:dma-ranges 和 ranges 不要搞混。ranges 是 CPU 访问设备用的,dma-ranges 是设备做 DMA 用的。两个完全不同的方向。
我曾经调试一个网卡驱动,死活收不到数据。查了两天,发现是 dma-ranges 写错了。设备 DMA 地址映射到了 CPU 的保留内存区域,数据写进去就被系统覆盖了。嗯,从那以后,我每次写 dma-ranges 都会反复核对地址范围。
4.4 一张图看懂地址编码
下面这张 SVG 图,展示了地址编码的核心逻辑:
4.5 避坑指南
我总结几个常见的坑,你写设备树时多留个心眼:
- #address-cells 和 #size-cells 不匹配:父节点和子节点的 cells 数不一致,导致 reg 解析错误。检查每个总线节点的定义。
- ranges 长度写错:长度单位是字节,不是字。我见过有人把 0x1000 写成 0x100,结果只映射了 256 字节。
- dma-ranges 和 ranges 混淆:两个属性方向不同,别混用。我曾经在调试时把 dma-ranges 写成了 ranges,设备 DMA 直接访问了错误的地址。
- 空 ranges 的含义:
ranges;表示 1:1 映射,不是「没有映射」。如果不想让子地址暴露,直接不写 ranges 属性。
我的调试技巧:在驱动里打印 dev->resource 和 dma_set_mask 后的地址,对比设备树里的值。如果对不上,八成是 ranges 或 dma-ranges 写错了。
地址编码这块,说白了就是「告诉内核,我的设备在哪,怎么访问它」。多写几次,多调几个板子,自然就熟了。嗯,今天就聊到这。