4. 设备树地址编码:#address-cells、#size-cells、ranges、dma-ranges
好,咱们今天聊聊设备树里最绕、也最容易出bug的一块——地址编码。
说实话,我刚开始接触设备树那会儿,被这几个属性折腾得不轻。尤其是 ranges,看着像天书一样。后来调了几个月驱动,踩了无数坑,才算真正搞明白。
你想想看,一个CPU要访问外设的寄存器,总得知道地址在哪吧?设备树就是干这个的。但问题来了——不同总线、不同设备的地址长度不一样。有的用32位,有的用64位。怎么统一描述?
嗯,这就是 #address-cells 和 #size-cells 要解决的问题。
4.1 #address-cells 和 #size-cells:地址的“计量单位”
这两个属性,说白了就是告诉解析器:一个地址占几个32位整数?一个长度占几个32位整数?
核心规则:
#address-cells:表示地址字段占用的 cell(32位) 数量#size-cells:表示长度字段占用的 cell(32位) 数量- 这两个属性是 父节点 定义的,影响的是 子节点 的
reg属性解析
举个例子,你一看就明白了:
// 32位系统,地址和长度都是32位
soc {
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@101f0000 {
reg = <0x101f0000 0x1000>;
// 地址:0x101f0000,长度:0x1000(4KB)
};
};
// 64位系统,地址64位,长度32位
pcie {
#address-cells = <2>; // 64位地址需要2个cell
#size-cells = <1>; // 32位长度
pcie_device@0 {
reg = <0x0 0x40000000 0x10000>;
// 地址:0x0_40000000(64位),长度:0x10000(64KB)
};
};
我在项目中遇到过一个问题:某款ARM芯片的DMA控制器,地址总线是40位的。结果有人写成了 #address-cells = <1>,导致高8位地址被截断,DMA死活访问不了高端内存。查了两天才发现是这里的问题。
我的习惯: 写设备树前,先确认CPU的物理地址宽度。ARMv7一般是32位,ARMv8通常是40位或48位。别想当然用默认值。
4.2 reg 属性:设备资源的“身份证”
reg 属性描述设备的地址空间资源。它的格式是:
reg = <address1 length1 [address2 length2] ...>;
每个 address 和 length 的cell数量,由父节点的 #address-cells 和 #size-cells 决定。
来看一个实际例子——我在调试某款网络芯片时遇到的:
// 父节点定义
bus@0 {
#address-cells = <2>; // 64位地址
#size-cells = <2>; // 64位长度
compatible = "simple-bus";
ranges;
// 子节点
ethernet@20000000 {
reg = <0x0 0x20000000 0x0 0x10000>;
// 地址:0x00000000_20000000
// 长度:0x00000000_00010000
};
dma@30000000 {
reg = <0x0 0x30000000 0x0 0x1000>;
// 地址:0x00000000_30000000
// 长度:0x00000000_00001000
};
};
注意看,这里地址和长度都用了2个cell。为什么?因为芯片的地址空间确实需要64位来描述。虽然当前只用了低32位,但为了兼容性,还是按64位写的。
我曾经踩过的坑: 别以为 #address-cells 和 #size-cells 可以随便写。它们必须和硬件实际地址宽度匹配。写小了,地址被截断;写大了,解析器会读错数据。我见过有人把32位地址写成 #address-cells = <2>,结果reg里的地址全变成了0x00000000_xxxxxxxx,虽然值没错,但浪费了空间,也容易让人困惑。
4.3 ranges:地址翻译的“桥梁”
这是设备树里最让人头疼的属性之一。我刚开始学的时候,看了三遍文档才搞明白。
ranges 的作用是:把子节点的地址空间,映射到父节点的地址空间。
说白了,就是告诉CPU:子设备声称的地址,在CPU眼里是什么地址。
它的格式是:
ranges = <child_base parent_base size>;
其中:
child_base:子节点地址空间的基地址(cell数由子节点的#address-cells决定)parent_base:父节点地址空间的基地址(cell数由父节点的#address-cells决定)size:映射范围的大小(cell数由父节点的#size-cells决定)
来看一个经典例子——我在做某款ARM SoC的PCIe控制器移植时遇到的:
soc {
#address-cells = <2>; // CPU视角:64位地址
#size-cells = <2>;
pcie: pcie@40000000 {
#address-cells = <3>; // PCIe规范:3个cell(bus+dev+func)
#size-cells = <2>;
reg = <0x0 0x40000000 0x0 0x10000000>;
// 关键在这里:地址翻译
ranges = <
// PCIe地址空间 -> CPU地址空间
0x02000000 0x0 0x50000000 // PCIe 32位非预取内存
0x0 0x50000000 // CPU地址
0x0 0x10000000 // 大小:256MB
0x43000000 0x0 0x60000000 // PCIe 64位预取内存
0x0 0x60000000 // CPU地址
0x0 0x20000000 // 大小:512MB
>;
pci_device@0 {
reg = <0x0000 0x0 0x0 0x0 0x0>;
// 这个设备在PCIe总线上的地址是0x50000000
// 经过ranges翻译,CPU看到的是0x50000000
};
};
};
说实话,我第一次看到3个cell的地址时也懵了。后来查了PCIe规范才知道,PCIe地址的第一个cell是空间类型标识:
| 类型值 | 含义 |
|---|---|
| 0x01000000 | I/O空间 |
| 0x02000000 | 32位内存空间(非预取) |
| 0x42000000 | 32位内存空间(预取) |
| 0x43000000 | 64位内存空间(预取) |
我的经验: 写 ranges 时,先画一张地址映射图。把CPU地址空间、总线地址空间、设备地址空间都画出来。然后一条一条对应着写。我每次写复杂的ranges,都会先在纸上画一遍,确认无误再写到dts里。
4.4 dma-ranges:DMA的“专属地图”
dma-ranges 和 ranges 很像,但它是给DMA用的。
你想想看,CPU访问设备和DMA访问设备,走的路径可能不一样。有些设备有独立的DMA地址空间,和CPU看到的地址不一样。
举个例子——我在调试某款GPU时遇到的:
gpu: gpu@18000000 {
compatible = "arm,mali-400";
reg = <0x18000000 0x10000>;
// GPU的DMA引擎看到的地址空间
dma-ranges = <
// GPU DMA地址 -> 系统内存地址
0x0 0x80000000 // GPU DMA基地址
0x0 0x80000000 // 系统内存基地址
0x0 0x40000000 // 映射大小:1GB
>;
};
这里的意思是:GPU的DMA引擎访问地址0x80000000时,实际上访问的是系统内存的0x80000000。为什么需要这个?因为有些GPU的DMA地址空间和CPU地址空间不是一一对应的,需要做地址翻译。
我曾经犯过的错: 有次做视频编解码芯片的驱动,忘了写 dma-ranges。结果DMA传输总是报错,数据写到错误的内存地址。查了三天,最后发现是DMA引擎的地址空间和CPU地址空间不一致。加上 dma-ranges 后,问题立刻解决。
4.5 实战中的常见问题
说了这么多,我总结几个实际开发中容易踩的坑:
- #address-cells 和 #size-cells 不匹配:父节点和子节点的定义不一致,导致reg解析错误。我建议你在写dts时,每个节点都显式声明这两个属性,不要依赖默认值。
- ranges 写反了顺序:记住,永远是
子地址 父地址 大小。我见过有人写反了,结果设备地址全乱套了。 - 忘记处理地址对齐:有些总线要求地址必须对齐到特定边界。比如PCIe要求64位地址对齐到64位边界。不满足对齐条件,设备可能无法正常工作。
- dma-ranges 和 ranges 混淆:记住,
ranges是给CPU访问用的,dma-ranges是给DMA访问用的。两者可能完全不同。
最后说一句: 地址编码是设备树里最基础也最容易出错的部分。我的建议是——写之前先画图,写完之后用 dtc -I dtb -O dts 反编译看看,确认地址解析是否正确。别像我当年那样,等到硬件调不通了才回头查设备树。