4、ioremap 核心API:ioremap、iounmap、ioread32/iowrite32、readl/writel 函数族详解

在嵌入式 Linux 驱动开发中,CPU 访问外设寄存器通常有两种地址空间:物理地址(由硬件设计决定)和虚拟地址(由操作系统管理)。由于内核运行在虚拟地址空间,驱动无法直接使用物理地址操作寄存器。ioremap 系列 API 正是解决这一映射问题的核心工具。

核心原则: 外设寄存器必须通过 ioremap 映射为内核虚拟地址后,才能使用 readl/writel 或 ioread32/iowrite32 进行访问。直接解引用物理地址会导致崩溃或不可预期的行为。

4.1 ioremap / iounmap:建立与销毁映射

ioremap 将一段物理地址区域映射到内核虚拟地址空间,返回一个可用于寄存器访问的虚拟地址指针。iounmap 在驱动卸载或不再需要访问时释放该映射。

函数 原型 说明
ioremap void __iomem *ioremap(phys_addr_t offset, unsigned long size) 将物理地址 offset 开始的 size 字节映射到内核虚拟地址空间,返回 __iomem 标记的虚拟地址。
iounmap void iounmap(void __iomem *addr) 销毁由 ioremap 建立的映射,释放内核虚拟地址空间。
注意: 返回值类型为 void __iomem *,这是一个特殊的指针类型,用于告诉编译器该地址指向 I/O 内存,不应直接解引用。必须通过专门的访问函数(如 readl/writel)来操作。

4.2 寄存器访问函数族:readl/writel 与 ioread32/iowrite32

这两组函数用于在映射后的虚拟地址上执行实际的寄存器读写操作。它们确保在访问 I/O 内存时使用正确的内存屏障和字节序处理。

函数 作用 典型使用场景
readl(addr) 从 addr 读取一个 32 位无符号整数 读取状态寄存器、版本寄存器
writel(val, addr) 向 addr 写入一个 32 位无符号整数 val 配置控制寄存器、启动 DMA 传输
ioread32(addr) 从 addr 读取一个 32 位无符号整数 与 readl 功能相同,语义更明确
iowrite32(val, addr) 向 addr 写入一个 32 位无符号整数 val 与 writel 功能相同,语义更明确
关键区别: readl/writel 是传统 include/asm/io.h 中的函数,而 ioread32/iowrite32 是 include/linux/io.h 中更现代的封装。两者在 ARM/x86 等主流架构上行为一致,但 ioread32 系列支持更丰富的字节序变体(如 ioread32be)。推荐新驱动优先使用 ioread32/iowrite32。

4.3 典型使用流程

  1. 获取物理地址和大小: 从设备树、平台数据或硬件手册中获取外设寄存器的基地址和映射范围。
  2. 调用 ioremap: 将物理地址映射为内核虚拟地址,保存返回的 void __iomem * 指针。
  3. 寄存器读写: 使用 ioread32/iowrite32 或 readl/writel 通过映射后的虚拟地址操作寄存器。
  4. 调用 iounmap: 在驱动移除或错误处理路径中释放映射。
#include <linux/io.h>
#include <linux/module.h>

#define MY_DEV_BASE_PHYS  0x01C20800
#define MY_DEV_REG_SIZE   0x1000

static void __iomem *reg_base;

static int __init my_driver_init(void)
{
    // 步骤1: 映射物理地址到内核虚拟地址空间
    reg_base = ioremap(MY_DEV_BASE_PHYS, MY_DEV_REG_SIZE);
    if (!reg_base) {
        pr_err("ioremap failed\n");
        return -ENOMEM;
    }

    // 步骤2: 读取控制寄存器(偏移 0x00)
    u32 ctrl = ioread32(reg_base + 0x00);
    pr_info("Control register value: 0x%08x\n", ctrl);

    // 步骤3: 写入配置(使能位)
    ctrl |= BIT(0);
    iowrite32(ctrl, reg_base + 0x00);

    return 0;
}

static void __exit my_driver_exit(void)
{
    // 步骤4: 释放映射
    if (reg_base)
        iounmap(reg_base);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

4.4 映射与访问流程总览

下图展示了从物理地址到虚拟地址的映射过程,以及驱动通过映射后的地址访问寄存器的完整数据流。

ioremap 映射与寄存器访问流程 物理地址空间 0x01C20800 外设寄存器 ioremap() 内核虚拟地址空间 0xF0008000 (示例) void __iomem *reg_base iounmap() 驱动代码 ioread32() iowrite32() 关键说明: 1. ioremap 不分配物理内存,只建立页表映射,使 CPU 能通过虚拟地址访问物理寄存器。 2. 映射粒度通常为页(4KB),但 size 参数可以小于页大小。 3. 必须使用 ioread32/iowrite32 或 readl/writel 访问,不能直接解引用 __iomem 指针。 4. 每次 ioremap 必须对应一次 iounmap,否则造成虚拟地址泄漏。

4.5 常见错误与避坑指南

  • 直接解引用 __iomem 指针: 如 u32 val = *reg_base; 在大多数架构上会导致编译警告或运行时错误。必须使用 ioread32(reg_base)。
  • 映射大小不足: 如果寄存器分布在多个页上,必须映射完整范围。通常映射 0x1000(4KB)对齐的页大小。
  • 忘记调用 iounmap: 在驱动卸载或 probe 失败路径中必须释放映射,否则内核虚拟地址空间逐渐耗尽。
  • 字节序问题: 某些外设使用大端字节序,此时应使用 ioread32be / iowrite32be 或 readl_be / writel_be。
  • 内存屏障缺失: readl/writel 和 ioread32/iowrite32 已经包含必要的内存屏障,无需额外添加 mb()。但如果使用 __raw_readl 等原始函数,则需要手动管理屏障。
调试建议: 在开发阶段,可以在 ioremap 前后打印物理地址和返回的虚拟地址,使用 dev_info() 输出。如果读写寄存器后设备无响应,首先检查映射地址是否正确,其次用示波器或逻辑分析仪确认物理总线上是否有对应访问。