4. 地址映射与reg属性:#address-cells、#size-cells、reg属性格式、地址映射实战
在设备树中,描述设备在总线上的地址空间和内存映射范围是核心功能之一。本章将深入解析用于地址映射的三个关键属性:#address-cells、#size-cells 和 reg,并通过实战案例演示如何正确描述外设的寄存器基址与地址范围。
核心概念:设备树通过
reg 属性描述设备的地址资源,而 #address-cells 和 #size-cells 则定义了地址和长度字段的编码宽度(以32位字为单位)。这三者共同构成了设备树地址映射的基石。
4.1 地址映射三要素
每个总线节点(如 soc、axi、amba)都需要声明其子节点地址描述的格式。这通过两个属性完成:
| 属性 | 含义 | 值类型 | 典型值 |
|---|---|---|---|
#address-cells |
子节点 reg 属性中地址字段占用的32位字数量 |
u32 | 1(32位地址)、2(64位地址) |
#size-cells |
子节点 reg 属性中长度字段占用的32位字数量 |
u32 | 1(32位长度)、0(无长度,如中断控制器) |
reg |
描述设备的地址资源,格式为 (address, length) 对 |
prop-encoded-array | <0x10000000 0x1000> |
记忆口诀:“父节点定格式,子节点填数据”。父总线节点通过
#address-cells 和 #size-cells 告诉内核如何解析子节点的 reg 值。
4.2 reg属性格式详解
reg 属性是一个数组,由一组或多组 (address, length) 对组成。每组描述一个连续的地址区域。其格式完全由父节点的 #address-cells 和 #size-cells 决定。
// 格式:reg = < address1 length1 [address2 length2 ...] >
// 其中 address 占用 #address-cells 个32位字
// 其中 length 占用 #size-cells 个32位字
// 示例1:32位地址,32位长度(最常见)
// #address-cells = <1>;
// #size-cells = <1>;
reg = <0x10000000 0x1000>; // 基址0x10000000,长度4KB
// 示例2:64位地址,32位长度
// #address-cells = <2>;
// #size-cells = <1>;
reg = <0x00000000 0x80000000 0x1000000>; // 基址0x0000000080000000,长度16MB
// 示例3:多段地址空间
reg = <0x10000000 0x1000 // 第一段:寄存器空间
0x20000000 0x200>; // 第二段:控制寄存器空间
4.3 地址映射的层级结构
设备树的地址映射是层级化的。每个总线节点可以有自己的地址转换规则,通过 ranges 属性将子地址空间映射到父地址空间。下图展示了典型的SoC地址映射层级:
4.4 地址映射实战:描述一个UART控制器
下面通过一个完整的实战案例,展示如何为一个典型的UART控制器编写正确的设备树节点,包括地址映射、中断和时钟信息。
// 实战:描述一个ARM SoC中的UART控制器
// 该UART位于APB总线,基址0x10000000,地址范围4KB
/dts-v1/;
/ {
model = "Example ARM SoC";
compatible = "arm,example-soc";
#address-cells = <1>;
#size-cells = <1>;
// 系统内存
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; // 1GB RAM
};
// 片上系统总线
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges; // 直接映射,子地址等于父地址
// UART控制器
uart@10000000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x10000000 0x1000>; // 基址0x10000000,长度4KB
interrupts = <0 33 4>; // GIC SPI 33, 高电平触发
clocks = <&uart_clk 0>;
clock-names = "apb_pclk";
status = "okay";
};
// GPIO控制器
gpio@20000000 {
compatible = "arm,pl061", "arm,primecell";
reg = <0x20000000 0x1000>;
interrupts = <0 34 4>;
gpio-controller;
#gpio-cells = <2>;
status = "okay";
};
};
};
常见错误:忘记在总线节点中声明
#address-cells 和 #size-cells,或者声明的值与子节点 reg 的实际格式不匹配。例如,如果父节点声明 #address-cells = <2>,但子节点 reg 只写了一个32位地址,内核将无法正确解析。
4.5 多段地址空间与ranges映射
当设备需要多个不连续的地址区域时,reg 属性可以包含多组地址-长度对。此外,ranges 属性用于将子总线的地址空间映射到父总线的地址空间。
// 多段地址空间示例:一个包含寄存器空间和DMA空间的设备
// #address-cells = <1>; #size-cells = <1>;
reg = <0x10000000 0x1000 // 寄存器空间:0x10000000 ~ 0x10000FFF
0x20000000 0x10000>; // DMA描述符空间:0x20000000 ~ 0x2000FFFF
// ranges映射示例:将子总线地址0映射到父总线地址0x40000000
// 子地址0 ~ 0x100000 映射到父地址0x40000000 ~ 0x40100000
ranges = <0x0 0x40000000 0x100000>; // 格式:<子地址 父地址 长度>
4.6 地址映射的解析流程
内核在解析设备树时,会按照以下流程处理地址映射:
- 定位父总线:从当前节点向上查找第一个包含
#address-cells和#size-cells的父节点。 - 读取格式:获取父节点的
#address-cells和#size-cells值。 - 解析reg:按照格式从
reg属性中提取地址和长度值。 - 地址转换:如果父节点有
ranges属性,将子地址转换为父地址空间中的地址。 - 资源注册:将最终地址资源注册到内核的IO资源树中,供驱动使用。
4.7 常见问题与避坑指南
| 问题 | 现象 | 解决方案 |
|---|---|---|
#address-cells 未声明 |
内核报错 "Bad cell count" | 在父总线节点中显式声明 #address-cells 和 #size-cells |
| reg 长度与 cells 不匹配 | 设备无法正确探测,资源注册失败 | 确保 reg 中的地址和长度字段数量与父节点声明一致 |
| 64位地址使用错误 | 高32位地址被截断 | 设置 #address-cells = <2>,reg 中地址写两个32位字 |
| ranges 映射错误 | 驱动访问的地址与实际物理地址不符 | 检查 ranges 的格式:<子地址 父地址 长度>,确保三个字段的cells数正确 |
调试技巧:使用
ls /sys/devices/platform/ 查看已注册的平台设备,使用 cat /proc/iomem 查看IO内存映射,确认地址是否正确注册。
4.8 实战练习:编写一个DMA控制器的设备树节点
假设有一个DMA控制器,具有以下特性:
- 基址:0x30000000,地址范围:0x1000
- 中断:SPI 45,高电平触发
- 时钟:dma_clk
- 支持8个DMA通道
// 参考答案
dma@30000000 {
compatible = "example,dma-controller";
reg = <0x30000000 0x1000>;
interrupts = <0 45 4>;
clocks = <&dma_clk 0>;
clock-names = "dma";
#dma-cells = <2>;
dma-channels = <8>;
status = "okay";
};
通过本章的学习,你已经掌握了设备树地址映射的核心机制。正确理解 #address-cells、#size-cells 和 reg 的配合使用,是编写正确设备树文件的基础,也是后续学习pinctrl配置的前提。