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 典型使用流程
- 获取物理地址和大小: 从设备树、平台数据或硬件手册中获取外设寄存器的基地址和映射范围。
- 调用 ioremap: 将物理地址映射为内核虚拟地址,保存返回的
void __iomem *指针。 - 寄存器读写: 使用
ioread32/iowrite32或readl/writel通过映射后的虚拟地址操作寄存器。 - 调用 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 映射与访问流程总览
下图展示了从物理地址到虚拟地址的映射过程,以及驱动通过映射后的地址访问寄存器的完整数据流。
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() 输出。如果读写寄存器后设备无响应,首先检查映射地址是否正确,其次用示波器或逻辑分析仪确认物理总线上是否有对应访问。