第4章:设备树地址编码——#address-cells、#size-cells、ranges、dma-ranges
地址编码,说白了就是告诉内核:「我的设备挂在哪个地址上?地址有多长?」。很多新手写设备树,一碰到地址相关的属性就头大。我刚开始做车载项目时,也在这上面栽过跟头。
今天咱们就把这几个属性彻底讲明白。你想想看,如果地址都描述不清楚,CPU怎么访问外设?DMA怎么搬运数据?
4.1 #address-cells 和 #size-cells:地址的「计量单位」
这两个属性是地址编码的基石。它们定义了一个地址单元用几个32位来表示。
- #address-cells:地址需要几个32位整数来描述
- #size-cells:地址范围长度需要几个32位整数来描述
举个例子,32位系统里,一个地址就是0x00000000到0xFFFFFFFF,一个32位整数就够了。所以#address-cells = <1>。但到了64位系统,地址范围是0x0000000000000000到0xFFFFFFFFFFFFFFFF,得两个32位整数拼起来,所以#address-cells = <2>。
核心规则:父节点的#address-cells和#size-cells决定了子节点reg属性的解析方式。
我在项目中遇到过一个问题:某款车机芯片的GIC控制器,地址范围超过了4GB。有人写成了#address-cells = <1>,结果中断控制器死活初始化失败。查了两天才发现是地址截断了。
// 正确写法:64位地址
gic: interrupt-controller@2c010000 {
compatible = "arm,gic-400";
#interrupt-cells = <3>;
#address-cells = <2>; // 地址用两个32位
#size-cells = <2>; // 长度也用两个32位
reg = <0x0 0x2c010000 0x0 0x1000>,
<0x0 0x2c020000 0x0 0x2000>;
};
4.2 reg属性:设备地址的「身份证」
reg属性是设备地址的直接描述。它的格式是:reg = <地址1 长度1 地址2 长度2 ...>。
每个地址和长度占几个32位,完全由父节点的#address-cells和#size-cells决定。嗯,这里要注意:reg里的地址是相对于父节点总线域的。
// 一个简单的UART设备
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>; // 基址0x10000000,长度0x1000
interrupts = <0 33 4>;
clock-frequency = <1843200>;
};
我建议初学者先画个地址映射图。把每个设备的基址、长度标出来,再写reg就清晰多了。
4.3 ranges属性:地址翻译的「桥梁」
ranges属性是设备树里最容易让人迷糊的东西。它的作用是把子节点的地址空间映射到父节点的地址空间。
说白了,就是告诉CPU:「子总线上的地址A,对应父总线上的地址B,长度是L」。
ranges的格式:ranges = <子地址 父地址 长度 ...>。
每个子地址、父地址、长度占几个32位,分别由子节点和父节点的#address-cells、#size-cells决定。
个人经验:如果ranges为空(ranges;),表示子地址和父地址是1:1映射。如果ranges不存在,子设备对CPU来说就是不可见的——除非你手动建立映射。
// 一个典型的PCIe桥接场景
pcie@40000000 {
compatible = "pci-host-ecam-generic";
#address-cells = <3>; // PCIe地址需要3个32位
#size-cells = <2>;
ranges = <0x82000000 0x0 0x40000000 // 子地址:PCIe配置空间
0x0 0x40000000 // 父地址:CPU物理地址
0x0 0x10000000>; // 长度:256MB
};
我曾经调试过一个车载摄像头驱动,图像数据死活读不对。后来发现是ranges写错了,子地址和父地址的偏移量算反了。嗯,这种错误排查起来特别费时间。
4.4 dma-ranges属性:DMA的「专属通道」
dma-ranges和ranges很像,但它是给DMA控制器用的。它描述了DMA设备看到的地址空间和CPU地址空间之间的映射关系。
为什么要单独搞一个dma-ranges?因为有些设备通过DMA访问内存时,看到的地址和CPU看到的地址不一样。比如某些SoC里,GPU或视频编解码器的DMA地址空间是独立的。
避坑指南:我曾经遇到一个项目,VPU(视频处理单元)的DMA总是访问到错误的内存区域。查了三天,发现是dma-ranges没配。VPU的DMA地址从0x80000000开始,但CPU的物理内存从0x40000000开始。没有dma-ranges做地址翻译,DMA就乱飞了。
// DMA地址映射示例
vpu: video-codec@1d000000 {
compatible = "vendor,vpu";
reg = <0x1d000000 0x100000>;
#address-cells = <1>;
#size-cells = <0>;
dma-ranges = <0x80000000 0x40000000 0x20000000>;
// 含义:VPU的DMA地址0x80000000对应CPU物理地址0x40000000
// 映射范围0x20000000(512MB)
};
4.5 实战中的常见陷阱
说了这么多理论,咱们看看实际项目中容易踩的坑。
| 陷阱 | 现象 | 原因 |
|---|---|---|
| #address-cells不匹配 | 设备无法probe | 父节点和子节点的地址单元数不一致 |
| ranges偏移量错误 | 访问设备时崩溃或数据错乱 | 子地址到父地址的映射关系写反了 |
| dma-ranges缺失 | DMA传输数据到错误地址 | DMA控制器和CPU的地址空间不同 |
| reg长度不足 | 设备只能访问部分寄存器 | size-cells设置错误,地址范围被截断 |
我个人习惯是:拿到一个新平台的设备树,先看根节点的#address-cells和#size-cells。这俩值决定了整个地址体系的基调。然后逐级检查每个总线节点的ranges,确保地址映射链是完整的。
4.6 调试技巧:如何验证地址配置正确
写完了设备树,怎么知道地址配对了没有?我分享几个实用方法。
- 查看/proc/iomem:这个文件列出了所有已注册的内存区域。如果设备地址没出现在这里,说明reg解析有问题。
- 使用devicetree工具:
dtc -I dtb -O dts可以把编译好的DTB反编译成DTS,检查地址值是否和预期一致。 - 内核启动日志:
dmesg | grep -i "address\|range\|dma",内核会打印地址映射信息。
小技巧:我经常在设备树里加一个status = "disabled"的测试节点,用reg = <0xDEADBEEF 0x1000>这种明显错误的地址。如果内核启动时没有报地址冲突,说明地址解析逻辑可能有问题。
好了,地址编码这块内容就讲到这里。记住一句话:地址是设备的门牌号,门牌号错了,快递就送不到。下一章咱们聊聊中断控制器和中断映射,那也是设备树里容易出幺蛾子的地方。