4. 地址映射概念:物理地址、虚拟地址与 MMU 的作用
在嵌入式 Linux 驱动开发中,理解地址映射是操作外设寄存器的前提。CPU 通过 虚拟地址 访问内存和外设,而硬件外设的寄存器则位于 物理地址 空间。MMU(内存管理单元)负责在这两者之间建立转换规则。本章将系统阐述这三个核心概念及其在驱动移植中的实际意义。
核心观点: 驱动代码中不能直接使用物理地址操作外设寄存器,必须通过 MMU 建立页表映射,将物理地址转换为内核虚拟地址后,才能安全、高效地访问。
4.1 物理地址(Physical Address)
物理地址是硬件系统真实的地址空间,由 CPU 地址总线直接输出,对应内存芯片的存储单元或外设寄存器的编址。在嵌入式 SoC 中,物理地址空间通常划分为多个区域:
- DDR 内存区域: 系统主存,用于运行程序和数据。
- 外设寄存器区域: 各外设模块(GPIO、UART、I2C、SPI、DMA 等)的控制/状态/数据寄存器。
- Boot ROM / SRAM 区域: 芯片内部固件或快速暂存区。
| 地址范围 | 区域名称 | 用途 |
|---|---|---|
| 0x0000_0000 – 0x0FFF_FFFF | DDR 内存 | Linux 内核、用户空间程序、堆栈 |
| 0x1000_0000 – 0x1FFF_FFFF | 外设寄存器区 | GPIO、UART、I2C、SPI 等控制寄存器 |
| 0x2000_0000 – 0x2FFF_FFFF | Boot ROM / SRAM | 一级启动代码、安全固件 |
| 0x3000_0000 – 0x3FFF_FFFF | 保留 / 未使用 | 预留或芯片特定功能 |
驱动移植提示: 外设寄存器的物理基地址由芯片 数据手册(Datasheet) 或 参考手册(Reference Manual) 定义。在设备树中,
reg = <0x10000000 0x1000>; 即表示该外设的物理起始地址为 0x10000000,地址范围为 0x1000 字节。
4.2 虚拟地址(Virtual Address)
虚拟地址是 CPU 在启用 MMU 后,程序(包括内核)所看到的地址空间。每个进程拥有独立的 32 位或 64 位虚拟地址空间,通过 MMU 映射到物理地址。虚拟地址带来的核心优势:
- 隔离性: 每个进程的虚拟地址空间相互独立,一个进程的崩溃不会直接破坏其他进程或内核。
- 连续性: 物理内存可能碎片化,但虚拟地址可以为程序提供连续的地址空间。
- 安全性: 用户态程序无法直接访问内核虚拟地址空间,防止越权操作。
- 简化编程: 驱动开发者只需操作虚拟地址,无需关心物理内存的碎片分布。
常见误区: 在裸机或 RTOS 环境下,CPU 通常直接使用物理地址。但在 Linux 内核中,所有代码(包括驱动)都运行在虚拟地址空间。直接对物理地址解引用会导致 段错误(Segmentation Fault) 或 Oops。
4.3 MMU(内存管理单元)的作用
MMU 是 CPU 内部的一个硬件模块,负责将虚拟地址转换为物理地址。转换过程基于 页表(Page Table),页表由操作系统内核维护。MMU 的核心功能包括:
- 地址转换: 将虚拟地址通过多级页表查找,得到对应的物理地址。
- 权限检查: 检查访问类型(读/写/执行)是否与页表项中定义的权限匹配。
- 缓存控制: 控制该地址区域的缓存策略(Cacheable / Non-Cacheable、Bufferable / Non-Bufferable)。
- 异常处理: 当虚拟地址未映射或权限不足时,触发缺页异常(Page Fault)或权限异常。
4.4 外设寄存器映射:从物理地址到虚拟地址
在 Linux 驱动中,访问外设寄存器的标准流程如下:
- 获取物理地址: 从设备树或平台数据中获取外设寄存器的物理基地址和范围。
- 请求内存区域: 使用
request_mem_region()声明对该物理地址区域的占用,防止其他驱动冲突。 - 建立映射: 使用
ioremap()或of_iomap()将物理地址映射到内核虚拟地址空间。 - 通过虚拟地址操作: 使用
readl()/writel()等访问函数读写寄存器。 - 解除映射: 在驱动卸载时使用
iounmap()释放映射,并使用release_mem_region()释放内存区域。
/* 典型的外设寄存器映射代码片段 */
#include <linux/io.h>
#include <linux/ioport.h>
#define UART_BASE_PHYS 0x1000A000 /* 物理基地址 */
#define UART_REG_SIZE 0x1000 /* 地址范围 */
static void __iomem *uart_base_virt;
static int uart_probe(struct platform_device *pdev)
{
struct resource *res;
/* 1. 获取物理地址资源 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
/* 2. 请求内存区域 */
if (!request_mem_region(res->start, resource_size(res), "my_uart"))
return -EBUSY;
/* 3. 建立虚拟地址映射 */
uart_base_virt = ioremap(res->start, resource_size(res));
if (!uart_base_virt) {
release_mem_region(res->start, resource_size(res));
return -ENOMEM;
}
/* 4. 通过虚拟地址读写寄存器 */
writel(0x01, uart_base_virt + 0x00); /* 控制寄存器 */
val = readl(uart_base_virt + 0x04); /* 状态寄存器 */
return 0;
}
static int uart_remove(struct platform_device *pdev)
{
/* 5. 解除映射并释放资源 */
iounmap(uart_base_virt);
release_mem_region(UART_BASE_PHYS, UART_REG_SIZE);
return 0;
}
关键函数说明:
ioremap(phys_addr, size):将物理地址区域映射到内核虚拟地址空间,返回void __iomem *指针。readl(addr)/writel(val, addr):从映射后的虚拟地址读取/写入 32 位数据,包含内存屏障保证顺序。of_iomap(node, index):直接从设备树节点映射第 index 个reg属性,简化设备树驱动代码。
4.5 缓存一致性:外设寄存器映射的特殊要求
外设寄存器与普通内存不同,对访问顺序和缓存策略有严格要求。MMU 的页表项中需要为寄存器区域设置特殊的属性:
| 属性 | 普通内存 | 外设寄存器 |
|---|---|---|
| Cacheable | 是(允许 CPU 缓存) | 否(必须 Non-Cacheable) |
| Bufferable | 是(允许写缓冲) | 否(必须 Non-Bufferable) |
| 访问顺序 | 允许 CPU 重排 | 严格顺序(必须保持编程顺序) |
| 共享属性 | 可配置 | 强制共享(多核访问一致性) |
ioremap() 在建立映射时,会自动为返回的虚拟地址区域设置 Non-Cacheable、Non-Bufferable、强序 的页表属性,确保每次读写都直接作用于物理寄存器,不会被 CPU 缓存或写缓冲延迟。
避坑指南: 切勿使用
memcpy()、memset() 或直接指针解引用来操作 ioremap() 返回的地址。必须使用 readl() / writel() 系列函数,这些函数内嵌了必要的内存屏障指令,保证访问顺序和宽度正确。
4.6 设备树中的地址信息与映射流程
设备树(Device Tree)是描述硬件拓扑和地址信息的标准方式。驱动通过设备树节点获取物理地址,再通过 of_iomap() 一步完成映射:
/* 设备树节点示例 */
uart0: serial@1000A000 {
compatible = "vendor,my-uart";
reg = <0x1000A000 0x1000>; /* 物理基地址 + 地址长度 */
interrupts = <0 33 4>;
clocks = &uart_clk;
status = "okay";
};
/* 驱动中直接映射 */
static int uart_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
/* of_iomap 自动完成 request_mem_region + ioremap */
uart_base_virt = of_iomap(np, 0);
if (!uart_base_virt)
return -ENOMEM;
/* 后续操作与 ioremap 方式一致 */
writel(0x01, uart_base_virt + 0x00);
return 0;
}
4.7 本章小结
| 概念 | 定义 | 驱动开发中的意义 |
|---|---|---|
| 物理地址 | 硬件真实地址,由芯片手册定义 | 设备树 reg 属性中填写,驱动不能直接使用 |
| 虚拟地址 | CPU 在 MMU 开启后看到的地址 | 驱动通过 ioremap() 获得,用于实际读写操作 |
| MMU | 硬件地址转换单元,管理页表 | 提供地址映射、权限检查、缓存策略控制 |
| ioremap() | 物理地址 → 内核虚拟地址的映射函数 | 自动设置 Non-Cacheable、Non-Bufferable 属性 |
| of_iomap() | 设备树节点 → 内核虚拟地址的快捷映射 | 集成资源请求和映射,简化设备树驱动代码 |
驱动移植要点: 在移植驱动到新平台时,必须核对芯片手册中的外设物理地址与设备树中的
reg 属性是否一致。同时确认 ioremap() 或 of_iomap() 返回的虚拟地址非空,且使用正确的访问函数(readl/writel)操作寄存器,避免缓存一致性和访问顺序问题。