4. DTS地址编码
设备树(Device Tree Source, DTS)的核心任务之一是描述硬件设备的 地址空间 与 总线拓扑。本章深入解析 DTS 中用于地址编码的三个关键要素:#address-cells、#size-cells 以及 reg 和 ranges 属性。理解这些概念是编写正确设备树、实现设备驱动地址映射的基础。
4.1 地址编码基础:address-cells 与 size-cells
在设备树中,每个可寻址的设备节点都需要描述其占用地址空间的位置和大小。由于不同总线(如 32 位 ARM 平台、64 位 RISC-V 平台、PCIe 总线)的地址宽度不同,DTS 使用 #address-cells 和 #size-cells 这两个属性来定义地址和长度字段的 单元宽度(每个单元为 32 位)。
| 属性名 | 作用 | 值含义 | 典型值 |
|---|---|---|---|
#address-cells |
定义子节点 reg 属性中 地址 字段占用的 32 位单元个数 |
1 表示 32 位地址;2 表示 64 位地址 | 1(32位SoC)、2(64位SoC/PCIe) |
#size-cells |
定义子节点 reg 属性中 长度 字段占用的 32 位单元个数 |
0 表示无长度(仅地址);1 表示 32 位长度;2 表示 64 位长度 | 1(常规外设)、0(中断控制器) |
关键规则:
#address-cells 和 #size-cells 是 总线节点 的属性,它们作用于该总线下的 直接子节点。根节点(/)本身也是一个总线节点,需要显式声明这两个属性。
4.2 reg 属性:描述设备地址资源
reg 属性用于描述设备在总线上的 地址空间(寄存器、内存、I/O 端口等)。其格式为:
reg = <address1 length1 [address2 length2] ...>;
每个 address 和 length 的单元数由父节点的 #address-cells 和 #size-cells 决定。
4.2.1 32 位地址示例
/* 父节点(根节点)定义地址单元 */
/ {
#address-cells = <1>; /* 地址为 32 位 */
#size-cells = <1>; /* 长度为 32 位 */
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x1000>; /* 基址 0x10000000,长度 4KB */
interrupts = <33>;
};
};
4.2.2 64 位地址示例(如 ARM64 或 PCIe)
/* PCIe 总线节点 */
pcie@40000000 {
#address-cells = <3>; /* PCIe 地址域:Bus/Dev/Func + 偏移 */
#size-cells = <2>; /* 64 位长度 */
pci_device@0,0 {
reg = <0x0000 0x0000 0x0000 0x0000 0x1000>;
/* 解释:3个单元地址 + 2个单元长度 = 5个32位值 */
};
};
常见错误: 忘记在父节点设置
#address-cells 和 #size-cells,或者子节点 reg 中的单元数与父节点声明不匹配,会导致内核解析失败,设备无法正确映射地址。
4.3 ranges 属性:地址映射与转换
ranges 属性用于定义 父总线地址空间 与 子总线地址空间 之间的映射关系。它允许子节点使用本地地址,而内核通过 ranges 自动转换为 CPU 可见的物理地址。
4.3.1 ranges 语法
ranges = <child_bus_addr parent_bus_addr length>;
每个字段的单元数由以下规则决定:
- child_bus_addr:子节点地址单元数,由当前节点(声明 ranges 的节点)的
#address-cells决定。 - parent_bus_addr:父节点地址单元数,由父节点的
#address-cells决定。 - length:映射长度单元数,由当前节点的
#size-cells决定。
4.3.2 典型应用:桥接芯片地址映射
/* 根节点:CPU 地址空间 */
/ {
#address-cells = <1>;
#size-cells = <1>;
/* 外部总线桥接器 */
bridge@20000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x00000000 0x20000000 0x10000000>;
/* 子地址 0x0 ~ 0xFFFFFFF 映射到父地址 0x20000000 ~ 0x2FFFFFFF */
ethernet@0 {
reg = <0x00000000 0x1000>;
/* 实际物理地址 = 0x20000000 + 0x0 = 0x20000000 */
};
usb@100000 {
reg = <0x00100000 0x10000>;
/* 实际物理地址 = 0x20000000 + 0x100000 = 0x20100000 */
};
};
};
映射规则: 当
ranges 属性存在时,子节点 reg 中的地址是 本地总线地址,内核通过 ranges 表将其转换为 父总线地址(最终为 CPU 物理地址)。如果 ranges 为空(ranges;),表示子地址与父地址 一一对应(1:1 映射)。
4.4 地址映射流程 SVG 图
下图展示了从设备树节点 reg 属性到 CPU 物理地址的完整解析流程:
4.5 地址单元与 ranges 实战对比
| 场景 | #address-cells | #size-cells | ranges 示例 | 说明 |
|---|---|---|---|---|
| 32位 SoC 片上外设 | 1 | 1 | ranges = <0 0x40000000 0x10000000>; |
子地址 0 映射到 CPU 地址 0x40000000 |
| 64位 SoC(ARM64) | 2 | 2 | ranges = <0x0 0x0 0x0 0x80000000 0x0 0x10000000>; |
64位地址映射,每个字段占 2 个单元 |
| PCIe 总线 | 3 | 2 | ranges = <0x02000000 0x0 0x40000000 0x0 0x40000000 0x0 0x20000000>; |
PCIe 地址域包含类型、Bus/Dev/Func |
| 无地址映射(1:1) | 1 | 1 | ranges;(空属性) |
子地址等于父地址,无需转换 |
| 无地址空间设备 | 1 | 0 | 不需要 ranges | 如中断控制器,仅地址无长度 |
4.6 驱动中获取地址信息:of 函数族
内核驱动通过以下 OF API 从设备树节点读取地址信息:
of_n_addr_cells(node):获取节点#address-cells值。of_n_size_cells(node):获取节点#size-cells值。of_get_address(node, index, &size, &flags):获取第index个reg条目的地址。of_translate_address(node, addr):将本地总线地址转换为 CPU 物理地址(自动遍历 ranges)。of_iomap(node, index):直接映射第index个reg条目到内核虚拟地址(等价于ioremap(of_translate_address(...), size))。
驱动最佳实践: 推荐使用
devm_platform_ioremap_resource() 或 of_iomap(),它们自动处理地址转换和映射,无需手动调用 of_translate_address() 和 ioremap(),减少出错概率。
4.7 常见陷阱与调试技巧
- 陷阱 1: 子节点
reg中的地址单元数必须严格等于父节点的#address-cells+#size-cells。多一个或少一个都会导致解析偏移。 - 陷阱 2:
ranges中的child_bus_addr字段数由声明ranges的节点自身的#address-cells决定,而不是子节点的。 - 陷阱 3: 对于
simple-bus类型节点,如果ranges缺失,子节点地址将无法被内核正确翻译,导致驱动 probe 失败。 - 调试技巧: 使用
devicetree/unittest或内核启动参数of_debug打印地址解析日志;在驱动中通过dev_info(dev, "phys addr: %pa\n", &res->start);输出实际物理地址。
4.8 地址编码与 ranges 关系 SVG 图
下图展示了多级总线桥接下,地址如何通过 ranges 逐级转换:
4.9 本章小结
- #address-cells / #size-cells 是总线节点的属性,定义了地址和长度的编码宽度,直接影响
reg和ranges的解析。 - reg 属性描述设备占用的地址资源,格式为
<地址 长度>,单元数由父节点决定。 - ranges 属性实现子总线地址到父总线地址的映射,支持多级桥接拓扑,是设备树地址翻译的核心机制。
- 驱动开发中应使用
of_iomap()或devm_platform_ioremap_resource()简化地址映射操作,避免手动处理地址转换。