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 将虚拟地址转换为物理地址 → 总线控制器解码地址 → 选中目标外设 → 外设返回数据或执行控制操作。
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 和总线不得重排。
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 必须遵循“映射 → 访问 → 解除映射”的完整生命周期:
- 获取物理地址: 从设备树(Device Tree)、ACPI 或 PCI 配置空间获取外设寄存器的物理基地址。
- 建立映射: 调用
ioremap(phys_addr, size)返回内核虚拟地址void __iomem *。 - 访问寄存器: 使用
readl()/writel()等宏通过虚拟地址操作硬件。 - 解除映射: 在驱动卸载或设备移除时调用
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的调用频率和参数。
3.7 本章小结
- MMIO 通过统一编址将外设寄存器映射到内存空间,驱动使用
readl/writel访问。 - MMIO 与标准内存访问在缓存策略、访问副作用、顺序保证上存在本质区别。
- MMIO 区域必须标记为设备内存类型:非缓存、非推测、严格顺序。
- 驱动必须遵循
ioremap→ 访问 →iounmap的生命周期,优先使用devm_ioremap。 - 调试时通过寄存器回读、逻辑分析仪、
/dev/mem等方式验证硬件交互正确性。