4. 设备树中的地址编码:reg属性、#address-cells、#size-cells、ranges属性的使用
在嵌入式Linux系统中,设备树(Device Tree)负责描述硬件资源的布局与映射关系。地址编码是设备树中最核心的机制之一,它通过 reg、#address-cells、#size-cells 和 ranges 属性,精确描述设备在总线或系统中的地址空间。本章将深入解析这些属性的语法、语义及实战用法。
核心概念速览: 地址编码三要素 = 地址单元宽度 + 大小单元宽度 + 地址描述列表。父节点定义编址规则,子节点通过
reg 属性声明自己的地址资源。
4.1 地址编码基础:#address-cells 与 #size-cells
这两个属性定义在总线节点(如 soc、axi、i2c)中,用于声明该总线下的地址描述格式。
| 属性名 | 含义 | 取值 | 典型值 |
|---|---|---|---|
#address-cells |
描述地址(reg中的address部分)占用的32位单元数 | 正整数 | 1(32位地址)、2(64位地址) |
#size-cells |
描述大小(reg中的size部分)占用的32位单元数 | 正整数 | 1(32位长度)、0(无长度,如中断控制器) |
语法规则: 每个
reg 条目由 #address-cells 个单元(address) + #size-cells 个单元(size)组成。多个条目用空格分隔。
// 示例:32位地址空间,32位长度
soc {
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@10000000 {
reg = <0x10000000 0x1000>;
// 地址:0x10000000,大小:0x1000(4KB)
};
gpio0: gpio@10001000 {
reg = <0x10001000 0x100>;
// 地址:0x10001000,大小:0x100(256字节)
};
};
// 示例:64位地址空间,32位长度
bus@0 {
#address-cells = <2>;
#size-cells = <1>;
dma@0x4000000000 {
reg = <0x40 0x00000000 0x1000>;
// 地址高32位:0x40,地址低32位:0x00000000,大小:0x1000
};
};
常见错误: 忘记在父节点声明
#address-cells 和 #size-cells,或子节点 reg 的单元数与父节点声明不匹配,会导致内核解析失败或地址错乱。
4.2 reg 属性:设备地址资源的声明
reg 属性是设备节点中最基本的地址描述属性,用于声明设备占用的地址空间(如内存映射寄存器、IO端口、DMA通道等)。其格式为:
reg = <address1 length1 [address2 length2] ...>;
每个 address 和 length 的单元数由父节点的 #address-cells 和 #size-cells 决定。当设备有多个不连续的地址段时,可以声明多个条目。
| 场景 | #address-cells | #size-cells | reg 示例 | 含义 |
|---|---|---|---|---|
| 32位内存映射 | 1 | 1 | <0x1000 0x200> |
基址0x1000,长度0x200 |
| 64位内存映射 | 2 | 1 | <0x0 0x40000000 0x1000> |
基址0x40000000,长度0x1000 |
| 无大小(中断控制器) | 1 | 0 | <0x2000> |
仅地址0x2000,无长度 |
| 多段地址 | 1 | 1 | <0x1000 0x100 0x2000 0x50> |
两段:0x1000-0x10FF, 0x2000-0x204F |
实战建议: 对于内存映射外设,
reg 中的地址通常是该外设的基地址,长度应覆盖所有可访问的寄存器空间。对于 GPIO 控制器,长度通常为 0x100(256字节)或更少。
// 多段地址示例:网卡有两个独立的寄存器区域
ethernet@20000000 {
reg = <0x20000000 0x1000 // 主寄存器区
0x30000000 0x200>; // DMA描述符区
};
4.3 ranges 属性:地址转换与映射
ranges 属性定义在总线节点中,用于将子节点的地址空间映射到父节点的地址空间。它实现了地址域的转换,是设备树中实现“地址翻译”的关键机制。
ranges 的格式为:
ranges = <child-bus-address parent-bus-address length>;
其中每个字段的单元数由当前节点(子总线)的 #address-cells、父节点的 #address-cells 和当前节点的 #size-cells 共同决定。
| 属性值 | 含义 | 典型场景 |
|---|---|---|
ranges;(空) |
1:1 直接映射,子地址等于父地址 | 简单总线,地址无需转换 |
ranges = <...>; |
定义地址转换表 | 桥接、PCIe、外部总线 |
无 ranges |
子节点地址对父节点不可见 | 非内存映射设备(如I2C子设备) |
// 示例:外部总线地址转换
// 子总线地址 0x0 ~ 0x3FFFFFF 映射到父总线地址 0x80000000 ~ 0x83FFFFFF
bus@80000000 {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x80000000 0x4000000>;
// 子地址0 → 父地址0x80000000,长度0x4000000(64MB)
ethernet@0 {
reg = <0x0 0x1000>;
// 子地址0x0 → 实际物理地址 0x80000000
};
display@100000 {
reg = <0x100000 0x100000>;
// 子地址0x100000 → 实际物理地址 0x80100000
};
};
关键理解:
ranges 只影响内存映射地址的翻译。对于非内存映射设备(如 I2C 从设备、SPI 设备),子节点地址是总线上的逻辑地址(如从机地址),不需要 ranges 转换。
4.4 实战案例:GPIO 控制器节点的地址编码
结合 GPIO 控制器节点,展示完整的地址编码配置。以下是一个典型的 SoC 内部 GPIO 控制器设备树节点:
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
gpio0: gpio@ff310000 {
compatible = "snps,dw-apb-gpio";
reg = <0xff310000 0x1000>;
interrupts = <0 40 4>;
#gpio-cells = <2>;
gpio-controller;
ngpios = <32>;
};
gpio1: gpio@ff320000 {
compatible = "snps,dw-apb-gpio";
reg = <0xff320000 0x1000>;
interrupts = <0 41 4>;
#gpio-cells = <2>;
gpio-controller;
ngpios = <16>;
};
};
| 属性 | 值 | 说明 |
|---|---|---|
#address-cells |
1 | 32位地址空间 |
#size-cells |
1 | 32位长度描述 |
ranges |
空 | 1:1 直接映射,子地址等于父地址 |
reg (gpio0) |
<0xff310000 0x1000> |
基址 0xff310000,寄存器空间 4KB |
reg (gpio1) |
<0xff320000 0x1000> |
基址 0xff320000,寄存器空间 4KB |
关键点: 当父节点使用
ranges;(空)时,子节点的 reg 地址直接对应 CPU 物理地址。这是 SoC 内部设备最常见的配置方式。
4.5 地址编码常见问题与避坑指南
- 单元数不匹配: 子节点
reg的单元数必须严格等于#address-cells + #size-cells。多一个或少一个都会导致解析错误。 - 忘记声明父节点属性: 如果父节点没有
#address-cells和#size-cells,内核会使用默认值(通常为 2 和 1),可能导致地址解析错误。 - ranges 长度溢出: 转换长度不能超过父总线地址空间范围,否则会导致地址映射重叠或越界。
- 64位地址的高低位顺序: 在
#address-cells = <2>时,地址顺序为<高32位 低32位>,不要写反。 - 非内存映射设备不要用 ranges: I2C、SPI 等总线上的设备地址是从机地址,不是内存地址,不需要
ranges转换。
调试建议: 使用
dtc -I dtb -O dts 反编译设备树,检查 reg 属性是否被正确解析。内核启动时可通过 of_address_to_resource() 函数验证地址映射是否正确。