第四节:设备树地址编码的核心三剑客
各位同学,今天我们来聊聊设备树里最容易让人头晕的三个属性——#address-cells、#size-cells和ranges。说实话,我当年刚接触设备树时,被这三个东西绕得晕头转向。后来在项目中踩了几次坑,才真正搞明白它们的工作原理。
一、地址编码的基本概念
设备树里描述硬件,最核心的就是两件事:设备在哪和占多大地方。你想想看,CPU要访问一个外设,总得知道它的地址范围吧?
#address-cells和#size-cells就是干这个的。它们定义了一个地址描述需要几个32位整数。
#address-cells:表示地址需要几个32位单元#size-cells:表示大小需要几个32位单元
举个例子,32位系统里地址范围是0~4GB,一个32位整数就够了。但64位系统呢?地址范围大到离谱,得两个32位整数拼起来才行。
重要原则:这两个属性是父节点定义的,影响的是子节点的reg属性解析方式。
二、#address-cells 和 #size-cells 实战
我在项目中遇到过这样一个场景:一个简单的SPI控制器,挂接了多个外设。设备树是这样写的:
spi@10000000 {
compatible = "vendor,spi-controller";
reg = <0x10000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
flash@0 {
reg = <0>; // 片选0
};
sensor@1 {
reg = <1>; // 片选1
};
};
看到没?父节点SPI控制器自己的reg用了两个值(地址+大小),但子节点的reg只有一个值。为什么?因为父节点声明了#size-cells = <0>,子节点就不需要描述大小了。
嗯,这里要注意:父节点的reg和子节点的reg是两套规则。父节点自己的reg由它自己的父节点决定,子节点的reg才受#address-cells和#size-cells影响。
三、64位系统的地址编码
现在很多芯片都是64位的,比如ARM Cortex-A系列。这时候地址编码就变成这样了:
soc {
compatible = "simple-bus";
#address-cells = <2>; // 64位地址
#size-cells = <2>; // 64位大小
dma@100000000 {
reg = <0x0 0x10000000 0x0 0x1000>;
// 地址:0x10000000(高32位0,低32位0x10000000)
// 大小:0x1000(高32位0,低32位0x1000)
};
};
说白了,就是地址和大小各拆成两个32位整数。我第一次看到这种写法时还纳闷,为什么地址要写两段?后来才明白,这是为了兼容32位和64位系统的统一描述方式。
四、ranges属性的工作原理
ranges属性,我个人觉得是设备树里最巧妙的设计。它解决的是地址映射问题。
你想想看,一个外设挂在总线上,它在CPU视角的地址和它在总线视角的地址,往往是不一样的。ranges就是用来建立这种映射关系的。
基本语法:
ranges = <子地址 父地址 大小>;
来看一个实际例子:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
// 子地址0~1MB映射到父地址0xe0000000~0xe0100000
ethernet@0 {
reg = <0x0 0x1000>;
// 在子空间地址0x0,大小0x1000
// 实际物理地址:0xe0000000 + 0x0 = 0xe0000000
};
};
我的经验:调试ranges映射时,最好先确认父节点的地址范围。我曾经遇到一个bug,ranges写对了,但父节点自己的reg范围没覆盖到映射区域,结果驱动死活访问不到外设。
五、ranges的几种特殊形式
ranges不是非得写满的。它有三种形态:
| 形式 | 含义 | 使用场景 |
|---|---|---|
| ranges = <...>; | 有映射关系 | 地址需要转换 |
| ranges; | 空属性,表示1:1映射 | 子地址和父地址相同 |
| 没有ranges | 子节点不能直接访问 | 需要驱动自己处理映射 |
我记得有一次调试PCIe设备,发现子设备地址怎么都访问不到。查了半天,原来是父节点没写ranges属性。PCIe的配置空间和内存空间映射关系复杂,必须显式声明ranges才行。
避坑指南:我曾经犯过一个错误——在桥接设备上忘了写ranges。结果子设备的中断和DMA都工作不正常。因为CPU无法通过桥接设备正确翻译子设备的地址。记住:只要地址空间有变化,就必须用ranges。
六、多级地址映射的实战
实际项目中,地址映射往往是多级的。比如:
root {
#address-cells = <1>;
#size-cells = <1>;
bridge@a0000000 {
reg = <0xa0000000 0x100000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xa0000000 0x100000>;
sub_bridge@0 {
reg = <0x0 0x10000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x10000>;
uart@0 {
reg = <0x0 0x1000>;
// 最终物理地址:0xa0000000 + 0x0 + 0x0 = 0xa0000000
};
};
};
};
这种嵌套结构,说白了就是一层层地址翻译。CPU访问UART时,内核会沿着设备树逐级解析ranges,最终得到物理地址。
七、总结与建议
讲到这里,我想强调几点:
- #address-cells和#size-cells是父子节点之间的契约,子节点必须遵守父节点的定义
- ranges是地址翻译的桥梁,没有它,子设备在CPU眼里就是"不存在"的
- 调试时多用devicetree dump,查看内核解析后的地址是否正确
我个人习惯在写设备树时,先画一张地址映射图。把每个节点的地址范围、映射关系都标清楚。这样写出来的设备树,基本不会出地址相关的bug。
下一节我们会讲中断控制器,那是另一个容易踩坑的地方。各位先把地址编码搞明白,后面学起来就轻松多了。