3. MMIO 原理:内存映射 I/O 的工作机制

MMIO(Memory-Mapped I/O)是嵌入式 Linux 驱动与硬件交互的核心机制。它将外设的寄存器、FIFO、控制状态字等硬件资源映射到 CPU 的物理地址空间,使驱动能够通过标准的内存访问指令(如 ldr/str、readl/writel)直接读写硬件。本节深入剖析 MMIO 的地址转换、访问流程以及与标准内存访问的本质区别。

3.1 MMIO 的工作机制

MMIO 的核心思想是“统一编址”:CPU 将外设寄存器视为内存地址空间的一部分。当驱动访问某个 MMIO 地址时,硬件总线控制器(如 AXI、AHB、PCIe)根据地址范围判断目标为内存还是外设,并触发对应的总线事务。

关键流程: 驱动调用 ioremap() 将物理 MMIO 地址映射到内核虚拟地址空间 → 驱动通过虚拟地址发起读写 → CPU MMU 将虚拟地址转换为物理地址 → 总线控制器解码地址 → 选中目标外设 → 外设返回数据或执行控制操作。
MMIO 地址转换与访问流程 驱动 (readl/writel) 虚拟地址 (VA) MMU 地址转换 物理地址 (PA) 总线控制器 外设寄存器 ioremap() 建立页表映射,设置 MMIO 页属性 (非缓存、非推测、设备内存类型)

3.2 MMIO 与标准内存访问的区别

虽然 MMIO 在指令层面与内存访问相同,但二者在硬件行为、缓存策略、访问副作用和顺序保证上存在根本差异。下表从 6 个关键维度进行对比:

对比维度 标准内存访问 MMIO 访问
目标设备 DRAM / SRAM 外设寄存器、FIFO、控制逻辑
缓存策略 可缓存(Cacheable),允许写合并 强制非缓存(Non-Cacheable),禁止写合并
访问副作用 无副作用(读不改变状态) 读/写可能触发硬件动作(如清中断、启动 DMA)
访问顺序 CPU 和编译器可重排(弱序) 必须严格按程序顺序执行(强序)
访问宽度 任意宽度(字节/半字/字/双字) 必须匹配寄存器物理宽度(通常 32-bit 或 64-bit)
错误处理 触发缺页异常(可恢复) 触发总线错误(通常不可恢复,驱动需防错)
⚠️ 常见陷阱: 驱动开发者容易误将 MMIO 当作普通内存访问,导致使用 memcpy()、memset() 或直接解引用指针操作 MMIO 地址。这些函数会触发缓存、合并或非原子访问,轻则寄存器写入失败,重则导致硬件状态机紊乱。必须使用 readl()/writel() 等专用访问宏。

3.3 MMIO 的缓存与顺序语义

MMIO 区域在页表项中标记为设备内存类型(Device memory type),这向 CPU 和总线控制器传达了三条关键约束:

  • 非缓存(Non-Cacheable): 每次读写都直接穿透到总线,不经过 CPU 缓存,确保驱动看到的是外设的实时状态。
  • 非推测(Non-Speculative): CPU 不能提前执行 MMIO 读操作,避免因预取导致外设状态被意外读取。
  • 严格顺序(Strongly Ordered): 所有 MMIO 访问按程序顺序提交,CPU 和总线不得重排。
标准内存 vs MMIO 缓存策略对比 标准内存访问 CPU 核心 L1/L2 缓存 DRAM 可缓存、写合并 读可预取、可重排 MMIO 访问 CPU 核心 总线桥 外设寄存器 非缓存、直通总线 严格顺序、无预取 MMIO 绕过缓存,直接访问硬件

3.4 MMIO 的访问宏与屏障

Linux 内核提供了一套与架构无关的 MMIO 访问宏,驱动必须使用这些宏而非直接解引用指针:

/* 读操作 */
u32 readl(const volatile void __iomem *addr);   /* 32-bit 读 */
u16 readw(const volatile void __iomem *addr);   /* 16-bit 读 */
u8  readb(const volatile void __iomem *addr);   /* 8-bit 读 */

/* 写操作 */
void writel(u32 val, volatile void __iomem *addr);  /* 32-bit 写 */
void writew(u16 val, volatile void __iomem *addr);  /* 16-bit 写 */
void writeb(u8  val, volatile void __iomem *addr);  /* 8-bit 写 */

/* 带内存屏障的变体(确保顺序) */
u32 readl_relaxed(const volatile void __iomem *addr);
void writel_relaxed(u32 val, volatile void __iomem *addr);
💡 最佳实践: 在需要严格顺序的场景(如写寄存器后立即读状态),使用 readl()/writel() 而非 relaxed 版本。relaxed 变体去掉了额外的内存屏障,适用于对顺序不敏感的高吞吐路径(如 DMA 描述符写入)。

3.5 MMIO 映射的生命周期

驱动使用 MMIO 必须遵循“映射 → 访问 → 解除映射”的完整生命周期:

  1. 获取物理地址: 从设备树(Device Tree)、ACPI 或 PCI 配置空间获取外设寄存器的物理基地址。
  2. 建立映射: 调用 ioremap(phys_addr, size) 返回内核虚拟地址 void __iomem *。
  3. 访问寄存器: 使用 readl()/writel() 等宏通过虚拟地址操作硬件。
  4. 解除映射: 在驱动卸载或设备移除时调用 iounmap(addr) 释放映射。
#include <linux/io.h>

static void __iomem *reg_base;

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;

    /* 1. 从设备树获取 MMIO 物理地址 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENXIO;

    /* 2. 建立 MMIO 映射 */
    reg_base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
    if (!reg_base)
        return -ENOMEM;

    /* 3. 访问寄存器:使能外设 */
    writel(0x1, reg_base + 0x00);   /* 控制寄存器 */
    dev_info(&pdev->dev, "MMIO mapped at %p\n", reg_base);
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    /* 4. 解除映射(devm 版本自动处理,无需手动 iounmap) */
    return 0;
}
🔑 关键点: 使用 devm_ioremap() 系列资源管理版本,驱动在 remove 或 probe 失败时自动解除映射,避免内存泄漏。所有 MMIO 虚拟地址必须用 __iomem 标记,Sparse 工具会检查是否混用普通指针。

3.6 MMIO 的硬件交互验证

在调试 MMIO 驱动时,可以通过以下方法验证硬件交互是否正确:

  • 寄存器回读: 写入已知值后立即回读,确认总线事务完成且值正确。
  • 逻辑分析仪 / 示波器: 观察片选信号(CS)、读写使能(WE/RE)和数据总线上的时序。
  • /dev/mem 测试: 在用户态通过 mmap /dev/mem 直接访问物理地址,快速验证硬件行为(仅调试用)。
  • ftrace / tracepoints: 跟踪 readl/writel 的调用频率和参数。
MMIO 硬件交互验证流程 写寄存器 (writel) 读回验证 (readl) 值匹配? 交互正常 检查时序/映射 是 否 验证要点:写入值必须与回读值完全一致;检查总线信号片选、读写使能时序

3.7 本章小结

  • MMIO 通过统一编址将外设寄存器映射到内存空间,驱动使用 readl/writel 访问。
  • MMIO 与标准内存访问在缓存策略、访问副作用、顺序保证上存在本质区别。
  • MMIO 区域必须标记为设备内存类型:非缓存、非推测、严格顺序。
  • 驱动必须遵循 ioremap → 访问 → iounmap 的生命周期,优先使用 devm_ioremap。
  • 调试时通过寄存器回读、逻辑分析仪、/dev/mem 等方式验证硬件交互正确性。