4. 地址映射概念:物理地址、虚拟地址与 MMU 的作用

在嵌入式 Linux 驱动开发中,理解地址映射是操作外设寄存器的前提。CPU 通过 虚拟地址 访问内存和外设,而硬件外设的寄存器则位于 物理地址 空间。MMU(内存管理单元)负责在这两者之间建立转换规则。本章将系统阐述这三个核心概念及其在驱动移植中的实际意义。

核心观点: 驱动代码中不能直接使用物理地址操作外设寄存器,必须通过 MMU 建立页表映射,将物理地址转换为内核虚拟地址后,才能安全、高效地访问。

4.1 物理地址(Physical Address)

物理地址是硬件系统真实的地址空间,由 CPU 地址总线直接输出,对应内存芯片的存储单元或外设寄存器的编址。在嵌入式 SoC 中,物理地址空间通常划分为多个区域:

  • DDR 内存区域: 系统主存,用于运行程序和数据。
  • 外设寄存器区域: 各外设模块(GPIO、UART、I2C、SPI、DMA 等)的控制/状态/数据寄存器。
  • Boot ROM / SRAM 区域: 芯片内部固件或快速暂存区。
典型 ARM SoC 物理地址空间划分示例
地址范围 区域名称 用途
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 的核心功能包括:

  1. 地址转换: 将虚拟地址通过多级页表查找,得到对应的物理地址。
  2. 权限检查: 检查访问类型(读/写/执行)是否与页表项中定义的权限匹配。
  3. 缓存控制: 控制该地址区域的缓存策略(Cacheable / Non-Cacheable、Bufferable / Non-Bufferable)。
  4. 异常处理: 当虚拟地址未映射或权限不足时,触发缺页异常(Page Fault)或权限异常。
MMU 地址转换流程 MMU 地址转换流程 CPU 核心 虚拟地址 MMU 物理地址 页表(Page Table) 由内核维护的映射规则 权限检查 · 缓存策略

4.4 外设寄存器映射:从物理地址到虚拟地址

在 Linux 驱动中,访问外设寄存器的标准流程如下:

  1. 获取物理地址: 从设备树或平台数据中获取外设寄存器的物理基地址和范围。
  2. 请求内存区域: 使用 request_mem_region() 声明对该物理地址区域的占用,防止其他驱动冲突。
  3. 建立映射: 使用 ioremap() 或 of_iomap() 将物理地址映射到内核虚拟地址空间。
  4. 通过虚拟地址操作: 使用 readl() / writel() 等访问函数读写寄存器。
  5. 解除映射: 在驱动卸载时使用 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 的页表项中需要为寄存器区域设置特殊的属性:

外设寄存器 vs 普通内存的 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;
}
设备树到寄存器访问的映射流程 设备树到寄存器访问的映射流程 设备树 (DTS/DTB) reg = <0x1000A000 0x1000> of_iomap(np, 0) request_mem_region() ioremap() 建立页表 虚拟地址 (void __iomem *) readl() / writel() 物理寄存器

4.7 本章小结

物理地址、虚拟地址与 MMU 核心概念总结
概念 定义 驱动开发中的意义
物理地址 硬件真实地址,由芯片手册定义 设备树 reg 属性中填写,驱动不能直接使用
虚拟地址 CPU 在 MMU 开启后看到的地址 驱动通过 ioremap() 获得,用于实际读写操作
MMU 硬件地址转换单元,管理页表 提供地址映射、权限检查、缓存策略控制
ioremap() 物理地址 → 内核虚拟地址的映射函数 自动设置 Non-Cacheable、Non-Bufferable 属性
of_iomap() 设备树节点 → 内核虚拟地址的快捷映射 集成资源请求和映射,简化设备树驱动代码
驱动移植要点: 在移植驱动到新平台时,必须核对芯片手册中的外设物理地址与设备树中的 reg 属性是否一致。同时确认 ioremap() 或 of_iomap() 返回的虚拟地址非空,且使用正确的访问函数(readl/writel)操作寄存器,避免缓存一致性和访问顺序问题。