4. 设备树地址映射:reg属性详解、#address-cells和#size-cells、ranges属性、dma-ranges
地址映射这块,说白了你得让CPU知道怎么找到外设。我早年刚接触设备树时,总觉得这些属性就是填几个数字,后来踩过坑才明白——地址映射要是写错了,驱动根本跑不起来。
咱们这一章就把地址映射的几个核心属性掰开揉碎讲清楚。你想想看,一个外设挂在总线上,CPU要访问它,总得知道它在哪里、占多大地方、中间有没有地址转换。这些信息,全藏在设备树的地址相关属性里。
4.1 reg属性:外设的“身份证”和“房产证”
reg属性是设备树里最基础的地址描述属性。它告诉内核:这个设备占用了哪段地址空间,长度是多少。
格式很简单:
reg = <address1 length1 address2 length2 ...>;
每个地址-长度对描述一段连续的物理地址区域。一个设备可以有多个区域,比如控制寄存器区和数据缓冲区区。
核心要点:reg属性中的地址和长度,其位数由父节点的#address-cells和#size-cells决定。
举个例子,我在一个ARM Cortex-A7项目里写过这样的节点:
uart0: serial@10000000 {
compatible = "my-company,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 31 4>;
};
这里0x10000000是基地址,0x1000是地址范围(4KB)。如果父节点的#address-cells = <1>,#size-cells = <1>,那每个地址和长度都是32位。
我的习惯:写reg属性时,我总会在注释里标明地址和长度的含义。比如reg = <0x10000000 0x1000>; /* base=0x10000000, size=4KB */。半年后回头看,你会感谢自己写了注释。
4.2 #address-cells和#size-cells:地址的“计量单位”
这两个属性定义在总线节点上,告诉子节点:地址用几个32位整数表示,长度用几个32位整数表示。
| 属性 | 含义 | 常见值 |
|---|---|---|
#address-cells |
地址字段占用的32位单元数 | 1(32位地址)、2(64位地址) |
#size-cells |
长度字段占用的32位单元数 | 0(无长度)、1(32位长度)、2(64位长度) |
为什么要有这个设计?我遇到过的情况是:一个SoC里既有32位外设又有64位外设,地址宽度不一样。设备树通过这两个属性,让每个总线自己定义地址格式,互不干扰。
看个实际例子:
soc {
compatible = "simple-bus";
#address-cells = <2>; /* 64位地址 */
#size-cells = <2>; /* 64位长度 */
dma@100000000 {
reg = <0x1 0x00000000 0x0 0x100000>;
/* 地址:0x1_00000000,长度:0x100000(1MB) */
};
};
这里地址用了两个32位单元拼成64位,长度也是两个32位单元。你想想看,如果#address-cells = <1>,那0x1_00000000这个地址根本装不下。
我曾经踩过的坑:有一次我写了一个PCIe控制器节点,#address-cells设成了2,但子设备的reg只填了一个32位地址。内核解析时直接读错了数据,驱动注册失败。查了两天才发现是cells数量不匹配。记住:子节点的reg格式必须和父节点的cells定义严格一致。
4.3 ranges属性:地址转换的“翻译官”
ranges属性定义了子总线地址到父总线地址的映射关系。说白了,就是告诉CPU:子设备声称的地址,在父总线上对应哪个物理地址。
格式:
ranges = <child-bus-address parent-bus-address length>;
每个条目包含三个字段:
- child-bus-address:子总线上的起始地址
- parent-bus-address:父总线上的起始地址
- length:映射的地址范围长度
每个字段的位数由相关节点的cells属性决定。具体来说:
- child-bus-address的位数由包含
ranges的节点(即子总线节点)的#address-cells决定 - parent-bus-address的位数由父总线节点的
#address-cells决定 - length的位数由包含
ranges的节点的#size-cells决定
看个典型例子:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
bridge@20000000 {
compatible = "my-bridge";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x20000000 0x100000>;
/* 子地址0x0~0xFFFFF映射到父地址0x20000000~0x200FFFFF */
device@0 {
reg = <0x0 0x1000>;
/* 实际物理地址:0x20000000 + 0x0 = 0x20000000 */
};
};
};
这里ranges只有一个条目,表示子地址0映射到父地址0x20000000,范围1MB。子设备device@0的reg地址0,经过转换后实际物理地址是0x20000000。
空ranges的含义:如果ranges;后面没有等号和数值,表示子地址和父地址是1:1映射,地址不变。如果ranges;属性不存在,表示子设备和父设备之间没有直接地址映射,CPU无法直接访问子设备。
我做过一个FPGA项目,外设通过AXI桥接挂在ARM总线上。桥接器把FPGA内部地址0x0000~0xFFFF映射到ARM物理地址0x40000000~0x4000FFFF。设备树里就写:
ranges = <0x0000 0x40000000 0x10000>;
驱动里直接访问0x40000000,实际上操作的是FPGA内部的0x0000。这个转换对驱动是透明的,驱动根本不用关心映射关系——设备树和内核帮你搞定了。
4.4 dma-ranges:DMA的“专属地图”
dma-ranges和ranges很像,但它描述的是DMA地址空间的映射关系。说白了,就是告诉DMA控制器:设备要访问的内存地址,在DMA总线上对应哪个地址。
为什么需要这个?我遇到过的情况是:有些设备只能访问32位地址空间,但系统内存可能挂在64位地址总线上。这时候就需要dma-ranges来做地址转换。
格式和ranges完全一样:
dma-ranges = <child-dma-address parent-dma-address length>;
看个实际场景:
usb@30000000 {
compatible = "my-usb";
reg = <0x30000000 0x10000>;
#address-cells = <1>;
#size-cells = <1>;
dma-ranges = <0x0 0x80000000 0x10000000>;
/* USB设备DMA地址0~0xFFFFFF映射到系统物理地址0x80000000~0x8FFFFFFF */
};
这里USB控制器只能发出32位DMA地址,但系统内存的高位地址超过了32位范围。通过dma-ranges,USB的DMA地址0映射到系统物理地址0x80000000,这样USB就能访问256MB的内存区域了。
| 属性 | 用途 | 转换方向 |
|---|---|---|
ranges |
CPU访问外设的地址映射 | 子总线地址 → 父总线地址 |
dma-ranges |
外设DMA访问内存的地址映射 | 设备DMA地址 → 系统物理地址 |
我的经验:调试DMA问题的时候,如果数据传着传着就飞了,先检查dma-ranges对不对。我曾经有个项目,FPGA通过DMA往内存写数据,写的位置总是不对。查了半天发现是dma-ranges写反了——把系统地址和DMA地址颠倒了。内核不会报错,但数据全写到了错误的位置。
4.5 地址映射的整体流程
咱们用一张图把整个地址映射的流程串起来:
从这张图能看出来:CPU访问外设时,地址经过桥接器的ranges转换,从子地址映射到父地址。而DMA方向正好相反,设备通过dma-ranges把DMA地址映射到系统物理地址。
4.6 实战中的常见问题
我总结几个实际项目中容易出问题的地方:
- cells数量不匹配:子节点的
reg字段数必须和父节点的#address-cells+#size-cells一致。多一个少一个都不行。 - ranges写反了:子地址和父地址的顺序不能搞错。我见过有人把
ranges = <0x20000000 0x0 0x100000>写成<0x0 0x20000000 0x100000>,结果地址映射全乱套。 - dma-ranges被忽略:有些驱动不检查
dma-ranges,直接用reg里的地址做DMA。这在地址空间简单时没问题,但遇到地址转换时就出错了。 - 空ranges的误解:
ranges;表示1:1映射,不是没有映射。如果不想让CPU直接访问子设备,应该不写ranges属性,而不是写ranges;。
重要提醒:调试地址映射问题时,可以在内核启动参数里加devicetree=debug,内核会打印设备树的解析过程。我每次遇到地址不对的问题,第一件事就是打开这个调试开关,看内核到底解析出了什么地址。
嗯,地址映射这块内容就这些。说白了就是三件事:用reg告诉内核设备在哪,用ranges做CPU视角的地址转换,用dma-ranges做DMA视角的地址转换。理解了这三个属性的关系,设备树的地址部分你就掌握了八成。