4. DTS 地址编码与 ranges 属性:地址映射概念、属性格式与转换示例
在设备树中,地址编码与映射是描述硬件总线层级与地址空间转换的核心机制。本章将深入解析 #address-cells、#size-cells 以及 ranges 属性的工作原理,并通过实际示例演示地址转换过程。
4.1 地址编码基础:address-cells 与 size-cells
每个设备节点在描述其寄存器地址或内存范围时,必须明确地址和长度的编码宽度。这两个属性由父节点定义,子节点继承并使用。
| 属性 | 含义 | 典型值 | 说明 |
|---|---|---|---|
#address-cells |
地址字段占用的 32 位单元数 | 1 或 2 | 1 表示 32 位地址,2 表示 64 位地址 |
#size-cells |
长度字段占用的 32 位单元数 | 0 或 1 | 0 表示无长度(仅地址),1 表示 32 位长度 |
编码规则: 子节点的
reg 属性中,每个地址范围由 #address-cells 个单元 + #size-cells 个单元组成。例如 #address-cells = <2>; #size-cells = <1>; 时,reg = <0x0 0x10000000 0x1000>; 表示起始地址 0x10000000,长度 0x1000。
4.2 ranges 属性:地址映射的核心
ranges 属性定义了父节点地址空间与子节点地址空间之间的转换关系。它允许子节点使用本地地址,而父节点通过映射将其转换为全局地址。
4.2.1 ranges 属性格式
ranges = <子地址 父地址 长度>;
// 或
ranges; // 空 ranges 表示 1:1 映射
每个映射条目包含三个字段:
- 子地址: 子节点地址空间中的起始地址,宽度由子节点的
#address-cells决定。 - 父地址: 父节点地址空间中的起始地址,宽度由父节点的
#address-cells决定。 - 长度: 映射范围的大小,宽度由父节点的
#size-cells决定。
注意: 如果子节点与父节点地址空间完全相同(1:1 映射),可以写
ranges;(空属性)。如果子节点地址空间不可被父节点访问,则省略 ranges 属性。
4.3 地址转换示例:从子设备到 CPU 视角
以下是一个典型的嵌入式系统设备树片段,展示了通过 ranges 将外设总线地址映射到系统总线地址的过程。
/ {
#address-cells = <2>;
#size-cells = <1>;
soc@0 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x1e000000 0x1000000>;
// 子地址 0x0 ~ 0xFFFFFF 映射到父地址 0x1e000000 ~ 0x1effffff
uart@1000 {
compatible = "ns16550";
reg = <0x1000 0x100>;
// 子地址 0x1000,长度 0x100
// 转换后物理地址 = 0x1e000000 + 0x1000 = 0x1e001000
};
gpio@2000 {
compatible = "my-gpio";
reg = <0x2000 0x20>;
// 转换后物理地址 = 0x1e000000 + 0x2000 = 0x1e002000
};
};
};
转换过程: 当内核访问 uart 设备时,从
reg 读取子地址 0x1000,然后查找父节点 soc 的 ranges 表,找到子地址 0x0 对应的父地址 0x1e000000,最终物理地址 = 0x1e000000 + (0x1000 - 0x0) = 0x1e001000。
4.4 地址映射结构图
下图展示了从子设备地址到 CPU 物理地址的完整映射流程:
4.5 多级总线与级联映射
在实际系统中,设备可能挂在多层总线下,每层总线都有自己的 ranges 映射。内核会递归地向上查找,直到到达根节点。
/ {
#address-cells = <1>;
#size-cells = <1>;
bus@0 {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x10000000 0x100000>; // 子 0x0~0xFFFFF -> 父 0x10000000~0x100FFFFF
bridge@100 {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x10000>; // 子 0x0~0xFFFF -> 父 0x0~0xFFFF
ethernet@0 {
reg = <0x0 0x1000>;
// 最终物理地址 = 0x10000000 + 0x0 + 0x0 = 0x10000000
};
};
};
};
级联转换规则: 从最内层子节点开始,逐级向上累加偏移。每层
ranges 将子地址转换为父地址,父地址再作为上一层的子地址继续转换,直到根节点。
4.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 设备无法 probe | ranges 缺失或映射错误,导致地址转换失败 |
检查 ranges 条目中的子地址、父地址、长度是否匹配 |
| 地址空间重叠 | 多个子节点映射到同一父地址范围 | 确保每个子节点的地址范围在父空间中唯一 |
| address-cells 不匹配 | 父子节点 #address-cells 不一致导致解析错误 |
严格遵循父节点定义,子节点 reg 按父节点格式编写 |
| 空 ranges 误用 | 子节点地址空间与父节点不同,却使用了空 ranges |
只有 1:1 映射时才使用空 ranges,否则必须显式指定 |
4.7 本章小结
- 地址编码:
#address-cells和#size-cells定义了地址和长度的编码宽度,由父节点决定。 - ranges 属性: 实现子地址到父地址的映射,支持 1:1 映射(空 ranges)和偏移映射。
- 转换流程: 从子节点
reg出发,逐级向上查找ranges,累加偏移得到最终物理地址。 - 多级总线: 每层总线独立维护自己的
ranges,内核递归完成级联转换。
实践建议: 在编写设备树时,先画出总线层级图,明确每层总线的地址宽度和映射关系,再填写
ranges 属性。使用 dtc -I dtb -O dts 反编译确认最终地址是否正确。