4. DTS地址编码与ranges属性
在嵌入式系统中,CPU 通过总线层次结构访问外部设备。设备树(DTS)通过 地址编码 和 ranges 属性 来描述这种层次化的地址空间映射。本章深入剖析 DTS 中的地址单元定义、大小端表示以及 ranges 属性在总线桥接中的核心作用,帮助你在 Platform 驱动开发中准确配置地址映射。
4.1 地址编码基础:reg 属性与 #address-cells / #size-cells
每个设备节点通过 reg 属性描述其占用地址空间。而 #address-cells 和 #size-cells 则定义了地址和长度字段的 32 位字数量。
| 属性 | 含义 | 常见值 |
|---|---|---|
#address-cells |
地址字段占用的 32 位单元数 | 1(32位地址)、2(64位地址) |
#size-cells |
长度字段占用的 32 位单元数 | 1(32位长度)、2(64位长度) |
reg |
设备地址空间列表,格式为 (address, length) 对 | <0x10000000 0x1000> |
关键规则: 子节点的
reg 属性中的地址和长度字段,由父节点的 #address-cells 和 #size-cells 决定,而非子节点自身定义。
示例:一个 64 位地址空间的设备节点定义:
soc {
#address-cells = <2>; // 64位地址
#size-cells = <2>; // 64位长度
uart0: serial@10000000 {
reg = <0x0 0x10000000 0x0 0x1000>;
// 地址: 0x00000000_10000000
// 长度: 0x00000000_00001000
};
};
4.2 大小端编码:DTS 中的字节序处理
设备树规范默认使用 大端(Big-Endian) 编码,但实际硬件可能采用小端(Little-Endian)。DTS 通过以下方式处理大小端问题:
- 默认规则: 所有
reg、ranges、interrupts等属性中的数值按大端存储。 - 显式指定: 使用
big-endian或little-endian布尔属性覆盖设备默认字节序。 - 驱动适配: 驱动代码通过
of_device_is_big_endian()等 API 动态判断。
常见陷阱: 当 CPU 是小端架构(如 ARM 默认小端)而 DTS 按大端编码时,直接读取
reg 属性中的 32 位值会得到错误结果。必须使用 of_property_read_u32() 等 API 自动转换字节序。
大小端处理流程示意图:
4.3 ranges 属性:总线桥接地址映射
ranges 属性是设备树中实现 地址空间转换 的核心机制。它定义了子总线地址空间到父总线地址空间的映射关系。
4.3.1 ranges 属性格式
ranges = <child_base parent_base length>;
其中每个字段的含义由父节点和子节点的 #address-cells / #size-cells 共同决定:
| 字段 | 含义 | 长度决定者 |
|---|---|---|
child_base |
子总线中的起始地址 | 子节点的 #address-cells |
parent_base |
父总线中的起始地址 | 父节点的 #address-cells |
length |
映射区域的长度 | 子节点的 #size-cells |
特殊值: 当
ranges 属性为空(ranges;)时,表示子地址空间与父地址空间完全一致(1:1 映射)。当 ranges 属性不存在时,表示子地址空间与父地址空间不直接映射,需要通过驱动或 MMU 进行转换。
4.3.2 典型桥接场景示例
考虑一个 SoC 内部总线桥接场景:CPU 通过 AXI 总线访问外设,而外设挂载在 APB 总线上。
soc {
#address-cells = <2>;
#size-cells = <2>;
axi_bus: axi@0 {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges;
apb_bus: apb@10000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x10000000 0x100000>;
// 子地址 0x0 ~ 0xFFFFF 映射到父地址 0x10000000 ~ 0x10FFFFFF
uart1: serial@1000 {
reg = <0x1000 0x100>;
// 在 APB 总线中的地址为 0x1000
// 在 AXI 总线中的地址为 0x10001000
// 在 CPU 视角中的地址为 0x00000000_10001000
};
};
};
};
4.4 地址映射的层级计算
当驱动通过 of_iomap() 或 platform_get_resource() 获取设备地址时,内核会自动进行 层级地址转换。转换过程如下:
- 获取子节点 reg: 读取设备节点中的
reg属性值。 - 向上遍历 ranges: 逐级查找父节点的
ranges属性,进行地址映射。 - 累加偏移: 每级映射将子地址转换为父地址,直到到达根节点。
- 返回 CPU 物理地址: 最终得到 CPU 可直接访问的物理地址。
4.5 实战:ranges 配置常见问题与调试
4.5.1 常见配置错误
| 错误类型 | 现象 | 解决方案 |
|---|---|---|
| #address-cells 不匹配 | 设备地址解析错误,驱动无法 probe | 确保父子节点 cells 定义一致 |
| ranges 长度溢出 | 部分地址无法访问,系统 hang | 检查 length 是否覆盖所有子设备 |
| ranges 缺失 | of_iomap 返回 NULL | 添加 ranges 或使用 of_translate_address |
| 大小端混淆 | 寄存器值读取异常 | 添加 big-endian / little-endian 属性 |
4.5.2 调试方法
内核调试接口:
/proc/device-tree/:查看设备树节点属性of_iomap()返回值检查:确认地址映射成功devm_ioremap_resource():自动检查地址冲突printk打印struct resource的 start/end
驱动中打印地址映射信息的示例:
static int my_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
dev_info(&pdev->dev, "phys addr: 0x%llx, size: 0x%llx\n",
(unsigned long long)res->start,
(unsigned long long)(res->end - res->start + 1));
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 检查大小端
if (of_device_is_big_endian(pdev->dev.of_node))
dev_info(&pdev->dev, "Device is big-endian\n");
return 0;
}
4.6 总结
- 地址编码:
#address-cells和#size-cells决定地址空间的粒度,父子节点必须协调一致。 - 大小端: DTS 默认大端编码,内核自动转换;驱动可通过
big-endian属性显式指定。 - ranges 属性: 实现总线桥接地址映射,支持 1:1 映射和偏移映射两种模式。
- 层级计算: 内核自动逐级转换地址,驱动只需通过标准 API 获取最终物理地址。
- 调试要点: 利用
/proc/device-tree和驱动日志验证地址映射正确性。