2. 硬件基础:SoC内存映射与外设寄存器物理地址空间布局
在嵌入式Linux驱动开发中,理解SoC(System on Chip)的内存映射是进行外设寄存器地址映射的前提。本章将深入剖析物理地址空间的布局规则、外设寄存器的编址方式,以及如何从芯片手册中提取关键的地址信息。
2.1 SoC内存映射的基本概念
SoC内部集成了CPU、内存控制器、各种外设控制器(如UART、I2C、SPI、GPIO、DMA等)。为了统一管理这些硬件资源,SoC设计者将物理地址空间划分为多个区域,每个区域映射到特定的硬件模块。CPU通过访问这些物理地址来读写外设的寄存器,从而控制外设的行为。
2.2 典型SoC物理地址空间布局
以广泛使用的ARM Cortex-A系列SoC为例,其物理地址空间通常按以下方式划分(具体地址范围因厂商而异):
| 地址范围(起始 - 结束) | 区域名称 | 典型用途 | 访问属性 |
|---|---|---|---|
0x0000_0000 - 0x3FFF_FFFF |
DDR内存区域 | 系统主内存(DRAM),存放代码、堆栈、数据 | 可缓存、可执行 |
0x4000_0000 - 0x4FFF_FFFF |
片上SRAM / 共享内存 | 快速数据交换、低延迟存储 | 可缓存(部分不可缓存) |
0x5000_0000 - 0x5FFF_FFFF |
外设寄存器区域(Peripheral) | UART、I2C、SPI、GPIO、Timer等控制寄存器 | 不可缓存、不可预测(Device memory) |
0x6000_0000 - 0x6FFF_FFFF |
显示控制器 / GPU区域 | Framebuffer、显示引擎寄存器 | 不可缓存或写合并 |
0x7000_0000 - 0x7FFF_FFFF |
系统控制 / 调试区域 | 中断控制器(GIC)、系统定时器、调试接口 | 不可缓存 |
2.3 外设寄存器地址空间结构
每个外设控制器内部包含一组连续的寄存器,这些寄存器在物理地址空间中占据一个基地址(Base Address)加上偏移量(Offset)的区间。驱动开发者需要从芯片手册中获取以下信息:
- 基地址(Base Address): 外设寄存器区域的起始物理地址,由SoC设计时固定。
- 寄存器偏移(Register Offset): 每个功能寄存器相对于基地址的偏移量。
- 寄存器宽度(Register Width): 通常是32位(4字节),也可能是8位或16位。
- 地址对齐要求: 外设寄存器通常要求按宽度对齐访问(如32位寄存器必须4字节对齐)。
以下是一个典型的UART外设寄存器布局示例:
| 偏移地址 | 寄存器名称 | 宽度 | 功能描述 |
|---|---|---|---|
0x000 |
UART_DR | 32位 | 数据寄存器(发送/接收) |
0x004 |
UART_RSR | 32位 | 接收状态寄存器 |
0x008 |
UART_FR | 32位 | 标志寄存器(TX/RX FIFO状态) |
0x00C |
UART_IBRD | 32位 | 整数波特率除数 |
0x010 |
UART_FBRD | 32位 | 小数波特率除数 |
0x014 |
UART_LCR_H | 32位 | 线路控制寄存器(数据位、停止位、校验) |
0x018 |
UART_CR | 32位 | 控制寄存器(使能、中断、DMA) |
2.4 外设寄存器地址映射流程(SVG流程图)
从芯片手册到设备树配置,外设寄存器地址映射遵循以下标准化流程:
2.5 设备树中reg属性的编码规则
在设备树(Device Tree)中,外设的物理地址信息通过reg属性描述。reg属性是一个地址/长度对的数组,每个对描述一个连续的物理地址区域。
// 设备树reg属性语法
uart0: serial@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 0x1000>;
// 解释:基地址 = 0x101F1000,区域大小 = 0x1000(4KB)
interrupts = <0 12 4>;
clock-frequency = <24000000>;
};
- 第一个值: 外设寄存器的起始物理地址(基地址)。
- 第二个值: 该外设占用的地址空间大小(以字节为单位)。通常设置为寄存器覆盖范围,或按页对齐(4KB)。
- 多区域外设: 如果外设包含多个不连续的地址块(如控制寄存器区和数据缓冲区),可以包含多对地址/长度。
2.6 多区域外设的reg属性示例
某些复杂外设(如DMA控制器、GPU)可能拥有多个物理地址区域,例如控制寄存器区和描述符表区。设备树通过多对reg值来描述:
// 多区域外设示例:DMA控制器
dma0: dma-controller@30000000 {
compatible = "vendor,dma-controller";
reg = <0x30000000 0x1000>, // 控制寄存器区(4KB)
<0x30001000 0x2000>; // 描述符表区(8KB)
interrupts = <0 23 4>;
#dma-cells = <1>;
};
驱动中通过platform_get_resource()或of_iomap()按索引获取对应的物理地址区域:
// 内核驱动中获取reg区域
struct resource *res;
void __iomem *ctrl_base;
void __iomem *desc_base;
// 获取第一个reg区域(索引0)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
ctrl_base = devm_ioremap_resource(&pdev->dev, res);
// 获取第二个reg区域(索引1)
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
desc_base = devm_ioremap_resource(&pdev->dev, res);
2.7 地址映射中的对齐与页边界
内核的ioremap()函数以页(Page)为单位进行地址映射。这意味着即使外设寄存器只占用4字节,内核也会映射至少一个完整的页(通常是4KB)。因此,设备树中reg的大小应至少覆盖到下一个页边界,以避免访问越界。
| 外设寄存器范围 | 推荐reg大小 | 映射后虚拟地址范围 | 说明 |
|---|---|---|---|
0x101F1000 - 0x101F100C |
0x1000(4KB) |
0x101F1000 - 0x101F1FFF |
按页对齐,避免后续扩展越界 |
0x30000000 - 0x30000FFF |
0x1000(4KB) |
0x30000000 - 0x30000FFF |
正好一个页 |
0x40000000 - 0x400000FF |
0x1000(4KB) |
0x40000000 - 0x40000FFF |
即使只有256字节,也映射4KB |
reg的大小设置过小(例如只写0x10),而实际外设寄存器访问范围超过该大小,驱动在访问高偏移寄存器时可能触发页错误(Page Fault)或访问到其他外设的地址空间。始终将大小设置为覆盖所有寄存器并向上对齐到页边界。
2.8 从芯片手册到设备树的实战映射
以下是一个从芯片手册提取信息并编写设备树节点的完整示例:
- 查阅手册: 找到UART外设的基地址为
0x101F1000,寄存器偏移范围0x000 - 0x0FF。 - 确定大小: 寄存器覆盖256字节,按4KB页对齐,取
0x1000。 - 编写设备树:
// 最终设备树节点
uart0: serial@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 0x1000>;
interrupts = <0 12 4>;
clocks = <&uart_clk>;
status = "okay";
};
- 驱动解析: 内核驱动通过
of_iomap()将物理地址映射到内核虚拟地址空间,然后通过readl()/writel()访问寄存器。
// 驱动中映射并访问寄存器
static int uart_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
void __iomem *base;
// 从设备树获取reg并映射
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
// 通过偏移访问寄存器
writel(0x01, base + UART_CR); // 使能UART
val = readl(base + UART_FR); // 读取状态标志
return 0;
}
2.9 本章小结
- SoC物理地址空间将外设寄存器映射到特定的MMIO区域,CPU通过访存指令操作外设。
- 外设寄存器区域必须使用Device内存属性映射,禁止缓存和重排。
- 设备树
reg属性由基地址 + 大小组成,大小应覆盖所有寄存器并对齐到页边界。 - 多区域外设使用多对
reg值,驱动按索引分别映射。 - 从芯片手册到设备树的映射流程是驱动移植的第一步,准确获取基地址和偏移是后续工作的基础。