2. 外设内存映射基础:物理地址与虚拟地址、MMU的作用、ioremap与of_iomap函数详解

在嵌入式Linux驱动开发中,访问外设寄存器是驱动与硬件交互的核心操作。然而,CPU无法直接通过物理地址访问外设,必须经过内存管理单元(MMU)的地址转换。本章将深入剖析物理地址与虚拟地址的映射关系、MMU的核心作用,并详细讲解驱动开发中最常用的两个内存映射函数:ioremap 与 of_iomap。

物理地址 → 虚拟地址映射关系 物理地址空间 0x1000_0000 0x1000_1000 0x1000_2000 ... 外设寄存器区 MMU 页表转换 TLB 缓存 物理→虚拟 虚拟地址空间 0xC000_0000 0xC000_1000 0xC000_2000 ... 内核虚拟区 驱动只能操作虚拟地址,MMU 负责将虚拟地址转换为物理地址以访问外设寄存器

2.1 物理地址与虚拟地址

物理地址(Physical Address, PA) 是硬件外设在系统总线上的真实地址,由芯片设计时固定。CPU 或 DMA 控制器通过物理地址直接选中具体的存储单元或外设寄存器。

虚拟地址(Virtual Address, VA) 是 CPU 内核视角下看到的地址空间,每个进程拥有独立的虚拟地址空间。在 Linux 中,内核空间和用户空间使用不同的虚拟地址范围。

特性 物理地址 虚拟地址
定义 硬件总线上的真实地址 CPU 编程视角的逻辑地址
空间大小 由芯片地址线位数决定(如 32位/4GB) 由操作系统决定(如 32位/4GB,64位更大)
连续性 外设寄存器通常不连续 映射后可在内核中连续访问
驱动访问方式 不能直接访问 通过 ioremap/of_iomap 映射后访问
典型范围 如 0x1000_0000 ~ 0x1000_FFFF 如 0xF000_0000 ~ 0xF000_FFFF
实战要点: 在设备树中,reg 属性描述的是外设的物理地址范围。驱动通过解析设备树获取物理地址后,必须映射为虚拟地址才能使用 readl/writel 等函数进行寄存器读写。

2.2 MMU 的作用

内存管理单元(Memory Management Unit, MMU) 是 CPU 内部负责地址转换的硬件模块。在嵌入式 Linux 中,MMU 默认开启,其核心作用包括:

  • 地址转换: 将虚拟地址转换为物理地址,使驱动能够通过虚拟地址访问外设。
  • 内存保护: 为不同进程设置独立的页表,防止进程间非法访问。
  • 缓存控制: 对外设内存区域可配置为 非缓存(Non-cacheable),避免 CPU 缓存导致寄存器读写不一致。
  • 虚拟内存扩展: 支持大于物理内存的虚拟地址空间,通过页表映射实现。
MMU 地址转换流程 CPU 发出 VA 0xC000_1000 TLB 查找 页表缓存 页表遍历 多级页表 物理地址 PA 0x1000_1000 TLB 命中则直接返回 PA TLB 未命中则遍历页表 MMU 通过 TLB 加速地址转换;外设寄存器区域在页表中标记为 “设备内存(Device Memory)”,禁止 CPU 缓存和预取
关键注意: 外设寄存器必须使用 非缓存(Non-cacheable) 映射。如果 CPU 缓存了寄存器值,驱动读取的可能不是硬件最新状态,导致逻辑错误。ioremap 默认使用非缓存属性。

2.3 ioremap 函数详解

ioremap 是 Linux 内核提供的标准内存映射函数,用于将物理地址区域映射到内核虚拟地址空间。函数原型如下:

#include <linux/io.h>

void __iomem *ioremap(phys_addr_t phys_addr, unsigned long size);
参数/返回值 说明
phys_addr 外设的起始物理地址(如 0x1000_0000)
size 需要映射的地址范围大小(字节)
返回值 映射后的内核虚拟地址(void __iomem * 类型),失败返回 NULL

典型使用流程:

  1. 从设备树或平台数据中获取外设物理地址和大小。
  2. 调用 ioremap 建立映射,得到虚拟地址指针。
  3. 使用 readl() / writel() 等访问函数操作寄存器。
  4. 驱动卸载时调用 iounmap() 释放映射。

代码示例:

#include <linux/io.h>
#include <linux/module.h>

#define UART_PHYS_BASE  0x1000_0000
#define UART_REG_SIZE   0x1000

static void __iomem *uart_base;

static int __init my_driver_init(void)
{
    uart_base = ioremap(UART_PHYS_BASE, UART_REG_SIZE);
    if (!uart_base) {
        pr_err("ioremap failed\n");
        return -ENOMEM;
    }

    // 写寄存器示例:向偏移 0x00 写入 0x01
    writel(0x01, uart_base + 0x00);

    // 读寄存器示例:读取偏移 0x04 的值
    u32 val = readl(uart_base + 0x04);
    pr_info("Register value: 0x%x\n", val);

    return 0;
}

static void __exit my_driver_exit(void)
{
    if (uart_base)
        iounmap(uart_base);
}

module_init(my_driver_init);
module_exit(my_driver_exit);
实战建议: 使用 ioremap 时,务必检查返回值。映射失败通常是因为物理地址无效或内存不足。另外,size 参数应精确匹配外设寄存器区域大小,避免映射过多或过少。

2.4 of_iomap 函数详解

of_iomap 是设备树(Device Tree)驱动中更便捷的映射函数,它直接从设备节点中解析 reg 属性并完成映射,省去了手动获取物理地址的步骤。函数原型如下:

#include <linux/of_address.h>

void __iomem *of_iomap(struct device_node *np, int index);
参数/返回值 说明
np 设备节点指针(从设备树中获取)
index reg 属性中的地址索引(通常为 0)
返回值 映射后的内核虚拟地址,失败返回 NULL

与 ioremap 的对比:

对比项 ioremap of_iomap
数据来源 手动传入物理地址 自动从设备树节点解析
设备树依赖 不依赖 必须使用设备树
代码简洁性 需要额外获取地址 一行代码完成映射
适用场景 传统平台数据、PCI 等 设备树驱动的标准做法
错误处理 需手动检查物理地址有效性 自动校验 reg 属性

代码示例(设备树驱动):

#include <linux/of_address.h>
#include <linux/of_platform.h>
#include <linux/module.h>

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-uart" },
    { /* end */ }
};

static int my_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    void __iomem *base;

    // 直接映射 reg 属性中索引为 0 的地址段
    base = of_iomap(np, 0);
    if (!base) {
        dev_err(&pdev->dev, "of_iomap failed\n");
        return -ENOMEM;
    }

    // 使用映射后的虚拟地址访问寄存器
    writel(0x55, base + 0x10);
    dev_info(&pdev->dev, "UART mapped at %p\n", base);

    // 保存 base 到驱动私有数据
    platform_set_drvdata(pdev, base);
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    void __iomem *base = platform_get_drvdata(pdev);
    if (base)
        iounmap(base);
    return 0;
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_uart",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);
ioremap vs of_iomap 流程对比 ioremap 流程 1. 手动获取物理地址(硬编码/平台数据) 2. 调用 ioremap(phys_addr, size) 3. 检查返回值,使用虚拟地址 4. 卸载时 iounmap() of_iomap 流程 1. 获取设备节点 np(probe 时传入) 2. 调用 of_iomap(np, index) 3. 自动解析 reg 属性并映射 4. 卸载时 iounmap() 更简洁
重要提醒: 无论使用 ioremap 还是 of_iomap,映射后的虚拟地址都必须通过 readl/writel 等 I/O 访问函数操作,不能直接解引用。直接解引用可能导致编译优化或缓存一致性问题。

2.5 本章小结

  • 物理地址 是硬件真实地址,驱动不能直接访问;虚拟地址 是 CPU 编程视角,通过 MMU 转换。
  • MMU 负责地址转换、内存保护和缓存控制,外设寄存器必须使用非缓存映射。
  • ioremap 是通用映射函数,需要手动传入物理地址;of_iomap 是设备树专用函数,自动解析 reg 属性,更简洁。
  • 映射后必须使用 iounmap 释放,避免内存泄漏。
  • 所有寄存器访问必须通过 readl/writel 等 I/O 函数,确保操作的正确性和一致性。