为什么Linux驱动开发中必须要用到ioremap来访问硬件?
在驱动程序里面为什么不能直接用物理地址来访问寄存器?
这是一个非常深刻且本质的问题,触及了 操作系统(OS) 与 裸机(Bare Metal)开发 最核心的区别。
一句话回答:因为 Linux 开启了 MMU(内存管理单元),CPU 发出的所有地址都会被当做"虚拟地址"处理。如果你直接用物理地址,CPU 会迷路,甚至导致系统崩溃。
1. "裸机"思维 vs "操作系统"思维
在单片机(STM32 / 51 / Arduino)上:
确实是直接用物理地址的。
C
// STM32 代码
#define GPIO_BASE 0x40020000
*(volatile int *)GPIO_BASE = 1; // 这是一个物理地址,CPU 直接把数据写到了物理线路
这是因为单片机通常没有 MMU,或者没开启 MMU。CPU 发出的地址直接对应内存条或外设的物理引脚。也就是:所见即所得。
在 Linux 系统上(I.MX6ULL):
Linux 启动后,做的第一件大事就是 开启 MMU。
一旦 MMU 开启,CPU 的"眼睛"就被换了一副眼镜。CPU 再也看不到真实的物理地址了,它只能看到虚拟地址。
2. 为什么要开启 MMU?(把事情搞这么复杂)
Linux 作为一个多任务系统,必须开启 MMU,主要为了两个目的:
- 权限保护(安全) :
- 防止 APP A 修改 APP B 的内存。
- 防止 APP 直接修改内核的代码。
- 如果直接用物理地址,恶意程序知道你的密码存在内存条的第 100 号单元,它直接去读第 100 号单元就窃取了。有了 MMU,APP 只能看到虚拟地址,根本接触不到物理 RAM。
- 地址隔离(多任务) :
- 每个 APP 都觉得自己拥有 4GB 的内存空间(虚拟的)。
- MMU 负责把这些碎片化的物理内存拼凑成连续的虚拟内存给 APP 用。
3. 如果你强行用物理地址会发生什么?
假设寄存器的 物理地址 是 0x0209C000。
你在驱动代码里写:
C
unsigned int *p = (unsigned int *)0x0209C000;
*p = 0x1;
CPU 的执行过程如下:
- 指令发出 :CPU 看到指令要访问地址
0x0209C000。 - MMU 拦截 :由于 MMU 开启了,CPU 硬件会自动把这个地址当做 虚拟地址。
- 查表失败 :MMU 去查"页表"(Page Table,也就是映射表),想看看虚拟地址
0x0209C000对应哪个物理地址。- 但是,你并没有建立过这个映射!
- 或者,这个虚拟地址范围可能已经被分配给内核代码段或者其他地方了。
- 系统崩溃 :
- MMU 发现查不到,或者权限不对。
- MMU 抛出一个 Page Fault(缺页异常)。
- 内核捕获异常,发现是非法访问,直接触发 Kernel Panic,系统死机,打印 "Unable to handle kernel paging request"。
4. ioremap 到底做了什么?
ioremap 就是 "给物理地址办签证" 的过程。
-
输入 :物理地址
0x0209C000。 -
动作:
-
内核在 内核虚拟地址空间 (通常是 3GB~4GB 的高端内存区域)里,找一段没用的空闲范围(比如
0xF0001000)。 -
内核修改 页表(Page Table),写入一条记录:
"虚拟地址
0xF0001000对应 物理地址0x0209C000"
-
-
返回 :把这个虚拟地址
0xF0001000返回给你。
之后你操作 0xF0001000:
- 你写
*virt_addr = 1。 - CPU 拿到虚拟地址
0xF0001000。 - MMU 查表,发现对应物理地址
0x0209C000。 - 硬件信号准确传达到 LED 寄存器。
5. 图解对比
裸机模式 (Physical Access):
CPU (地址 0x2000) ------------> 物理总线 (0x2000) ----> 硬件寄存器
Linux/MMU 模式 (Virtual Access):
CPU (写 0xF000) ----> [MMU] ----(查表: 0xF000->0x2000)----> 物理总线 (0x2000) ----> 硬件寄存器
总结
- 不能直接用的原因:开启 MMU 后,CPU 硬件机制决定了所有指令中的地址都会被解读为虚拟地址。直接用物理地址会被指引到错误的内存位置或导致异常。
ioremap的作用:它是连接"虚拟世界"(代码)和"物理世界"(硬件)的桥梁,负责建立映射关系。
这就是为什么嵌入式 Linux 驱动开发比单片机麻烦一点的原因------你必须遵守操作系统的内存管理规则。