4. 设备树地址编码:#address-cells、#size-cells、ranges属性、地址映射机制
地址编码这块,说白了就是告诉内核:你的外设挂在哪个地址上?地址范围有多大?中间有没有经过地址转换?
我刚开始接触设备树时,总觉得这些属性可有可无。直到有一次调试一个PCIe桥片,死活枚举不到下游设备……折腾了两天,最后发现是 ranges 写错了。嗯,从那以后我再也不敢小看这几个属性了。
4.1 地址编码的三要素
每个可寻址的设备节点,都需要回答三个问题:
- 地址有多宽? —— 由
#address-cells定义 - 大小有多宽? —— 由
#size-cells定义 - 父总线怎么翻译? —— 由
ranges定义
你想想看,一个32位ARM芯片,地址总线是32根线,那地址值用1个32位整数就能表示。但如果是64位PowerPC,地址就得拆成两个32位整数。这就是 #address-cells 存在的意义。
核心规则:
#address-cells表示地址字段占几个32位单元#size-cells表示长度字段占几个32位单元- 这两个属性定义在父节点中,影响子节点的
reg属性解析
4.2 reg 属性的编码规则
reg 是设备节点的核心属性,它告诉内核:这个设备的寄存器空间在哪。
格式很简单:reg = <地址1 长度1 地址2 长度2 ...>
但具体怎么拆分,取决于父节点的 #address-cells 和 #size-cells。
| 父节点配置 | 地址字段宽度 | 长度字段宽度 | reg 示例 | 实际含义 |
|---|---|---|---|---|
| #address-cells = <1>; #size-cells = <1> | 1个32位 | 1个32位 | <0x10000000 0x1000> | 基址0x10000000,大小0x1000 |
| #address-cells = <2>; #size-cells = <1> | 2个32位 | 1个32位 | <0x0 0x10000000 0x1000> | 基址0x00000000_10000000,大小0x1000 |
| #address-cells = <1>; #size-cells = <0> | 1个32位 | 无 | <0x10000000> | 仅地址,无长度(常用于中断控制器) |
// 实际案例:一个典型的ARM SoC设备树片段
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
uart0: serial@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 0x1000>;
interrupts = <0 5 4>;
};
gpio0: gpio@101e0000 {
compatible = "arm,pl061";
reg = <0x101e0000 0x1000>;
interrupts = <0 8 4>;
};
};
我的习惯:写 reg 时,我总会在注释里把地址和长度拆开写清楚。比如 reg = <0x101f1000 0x1000>; /* 基址 0x101f1000,长度 4KB */。半年后你自己回来看代码,绝对感谢这个注释。
4.3 ranges 属性:地址翻译的桥梁
ranges 是设备树里最容易写错、也最难排查的属性之一。它的作用很简单:告诉内核,子节点的地址空间怎么映射到父节点的地址空间。
格式:ranges = <子地址 父地址 长度>;
每个字段的宽度由对应的 #address-cells 决定:
- 子地址:子节点的
#address-cells宽度 - 父地址:父节点的
#address-cells宽度 - 长度:子节点的
#size-cells宽度
如果 ranges 属性为空(ranges;),表示子地址和父地址是一一对应的,不需要翻译。
如果节点没有 ranges 属性,表示子节点不在父节点的地址空间内,内核无法直接访问。
我曾经踩过的坑:有一次给FPGA挂一个自定义IP,ranges 写成了 ranges = <0x0 0x43c00000 0x10000>;,但FPGA桥的 #address-cells 是2,父节点是1。结果内核解析时把地址拆错了,驱动读到的寄存器全是0xFF。排查了整整一天……
教训:写 ranges 之前,先确认父子和自己的 #address-cells 和 #size-cells 各是多少。
4.4 地址映射机制:从设备树到物理地址
内核解析设备树地址的流程,其实就三步:
- 解析 reg:根据父节点的
#address-cells和#size-cells,从reg中提取出子地址和长度 - 逐级翻译:从当前节点向上遍历,每经过一个带
ranges的节点,就做一次地址转换 - 得到物理地址:最终翻译到根节点,得到CPU可以直接访问的物理地址
举个例子,假设有这样的设备树层级:
/ {
#address-cells = <1>;
#size-cells = <1>;
pcie@40000000 {
#address-cells = <3>; // PCIe地址格式:总线号+设备号+偏移
#size-cells = <2>; // PCIe长度格式:高32位+低32位
ranges = <0x02000000 0x0 0x40000000
0x40000000
0x0 0x10000000>;
ethernet@0,0 {
reg = <0x02000000 0x0 0x0
0x0 0x1000>;
};
};
};
内核解析 ethernet 的地址时:
- 先看父节点
pcie的#address-cells=<3>,#size-cells=<2>,从reg中提取出子地址0x02000000 0x0 0x0,长度0x0 0x1000 - 然后看
pcie的ranges,发现子地址0x02000000 0x0 0x0落在第一个映射范围内,翻译成父地址0x40000000 - 继续向上到根节点,根节点没有
ranges(或者ranges;空映射),最终物理地址就是0x40000000
一句话总结:设备树的地址映射,就是一层层剥洋葱。每层 ranges 告诉你这一层怎么翻译,直到剥到根节点得到物理地址。
4.5 知识体系总览
下面这张图,把地址编码的核心逻辑串起来了:
4.6 实战中的避坑指南
这些年调过的板卡,地址编码出问题的情况我见得多了。总结几个高频坑:
- 坑一:#address-cells 写错位置 —— 记住,这个属性是定义在父节点上的,不是设备节点自己。我见过有人把
#address-cells写在uart节点里,内核直接报错。 - 坑二:ranges 的字段宽度不匹配 —— 子地址、父地址、长度三个字段的宽度必须严格对应各自的
#address-cells和#size-cells。少写一个32位,整个映射就偏了。 - 坑三:64位地址只用了一个cell —— 有些SoC的地址空间超过4GB,但设备树里只写了一个32位地址。内核解析时高位被截断,驱动访问的地址完全不对。
- 坑四:忘记写 ranges 导致设备不可见 —— 如果子节点在父节点的地址空间内,但父节点没写
ranges,内核会认为子设备不可直接访问,驱动 probe 直接失败。
我的调试小技巧:遇到地址相关的问题,先在内核启动日志里搜 of_address_to_resource 或者 of_translate_address 的输出。内核会把每一步地址翻译的结果打印出来,对照设备树一看,问题基本就现形了。
地址编码这块,说白了就是约定和翻译。只要把 #address-cells、#size-cells、ranges 这三个属性的关系理清楚,再复杂的地址映射也能拆解明白。嗯,下次调板卡遇到地址问题,不妨先从这三个属性查起。