问题引出和概述
示例代码如下:
#include
CCM_CCGR1 = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
GPIO5_GDIR = ioremap(0x020AC000 + 0x4, 4);
GPIO5_DR = ioremap(0x020AC000 + 0, 4);
- 1
- 2
- 3
- 4
- 5
- 6
这段示例代码来自于博文 http://iyenn.com/rec/1709453.html
注意:ioremap()使用时,要包含相应的头文件,在IMX6ULL开发时使用的ioremap
函数要包含的头文件是#include
这些代码显然就是把寄存器的物理地址映射到Linux的虚拟地址中,在前面的博文
IMX6ULL开发板基础实验:Framebuffer驱动程序的简单应用实例代码详细分析中,用到了函数mmap
,也是把地址映射到虚拟地址,不同的是mmap
映射的是设备的内存,而这里咱们需要映射的是CPU的物理地址。
对于函数mmap
的讲解详见我的另一篇博文
嵌入式Linux开发中的内存映射函数mmap详解?什么叫内存映射?什么叫对设备的显式和隐式读写操作?什么叫程序的虚拟内存?
ioremap()
函数详解
下面对ioremap()函数
进行详细解释:
以上面的第1行代码为例进行解释:
CCM_CCGR1 = ioremap(0x20C406C, 4);
- 1
在 Linux 内核中,ioremap
是一个重要的函数,用于将物理地址映射到内核虚拟地址空间,允许内核访问特定的硬件寄存器或设备的物理地址。
ioremap
的功能
-
目的:
物理地址不能直接被内核访问,尤其是在现代的虚拟内存系统中。ioremap
提供了一种方法将设备寄存器的物理地址映射到内核空间的虚拟地址。 -
应用场景:
- 访问硬件寄存器。
- 控制外设(如 GPIO、时钟控制、DMA 控制器等)。
- 映射内存映射的设备(如 FPGA 或 SoC 外设)。
-
返回值:
- 成功:返回映射后的内核虚拟地址(
void __iomem *
类型)。 - 失败:返回
NULL
。
- 成功:返回映射后的内核虚拟地址(
ioremap
函数原型
void __iomem *ioremap(resource_size_t phys_addr, unsigned long size);
- 1
参数说明:
-
phys_addr
:- 要映射的设备寄存器或外设的物理地址。
- 有些时候要求这个参数是页对齐的物理地址(什么是页对齐的物理地址,后面有详述,如果这里传入的不是页对齐地址而程序又能正常运行,那么说明ioremap函数在其内部进行了页对齐处理)。
-
size
:- 要映射的地址范围的大小,以字节为单位。
- 通常与外设的寄存器大小一致。
返回值:
- 虚拟地址:
一个可以在内核空间访问的虚拟地址,返回值需要用ioread
/iowrite
或直接操作进行访问。 - 如果映射失败,返回
NULL
。
代码中的解释
在你的代码中:
CCM_CCGR1 = ioremap(0x20C406C, 4);
- 1
这是将物理地址 0x20C406C
映射到一个 4 字节大小的内核虚拟地址区域中,并将虚拟地址赋值给指针 CCM_CCGR1
,以便后续通过 *CCM_CCGR1
来访问硬件寄存器。
使用步骤:
-
使用
ioremap
映射地址:volatile unsigned int *reg; reg = ioremap(0x20C406C, 4);
- 1
- 2
-
访问寄存器内容:
*reg |= (1 << 3); // 设置某一位
- 1
-
取消映射(释放资源):
iounmap(reg);
- 1
注意事项
-
映射范围大小:
必须指定正确的size
,避免超出设备寄存器范围。多映射可能导致访问无效区域。 -
对称性:
每次ioremap
调用后,必须确保在不再需要时调用iounmap
释放映射的虚拟地址空间。 -
地址对齐:
输入的phys_addr
应该是页对齐的,否则可能出现不可预期的问题。 -
读写时的同步问题:
- 对映射的寄存器地址进行操作时,需注意使用内存屏障(
mb()
/wmb()
/rmb()
)来确保操作顺序正确。 - 或者使用专门的
ioread
/iowrite
函数来确保访问的原子性。
- 对映射的寄存器地址进行操作时,需注意使用内存屏障(
小提示
ioremap
是专门为 设备内存 映射设计的。如果需要访问主内存中的地址或缓冲区,应使用普通的内存操作,而不是 ioremap
。
与相关函数的对比
-
ioremap_nocache
:
旧版本中专用于非缓存的映射,现在已经被合并到ioremap
中,ioremap
默认进行非缓存映射。 -
phys_to_virt
:
用于直接将物理地址转换为内核虚拟地址,但仅适用于内存区域,不能用于 I/O 寄存器或设备地址。 -
vmalloc
/kmalloc
:
分配内存而不是映射地址,与ioremap
用途完全不同。
小结
在代码中:
CCM_CCGR1 = ioremap(0x20C406C, 4);
- 1
这一行代码的作用是将硬件寄存器的物理地址 0x20C406C
映射到内核的虚拟地址空间,并将映射后的地址赋值给 CCM_CCGR1
,从而让程序可以通过普通的内存访问操作(如 *CCM_CCGR1
)直接操作寄存器的内容。
页对齐的地址详解
在Linxu系统中,虚拟地址空间被分成固定大小的页(例如 4KB)来进行管理,比如Linux的虚拟内存就是以固定大小的页来进行管理的,关于什么是Linux的虚拟内存,请参考我的另一篇博文(http://iyenn.com/rec/1709459.html)。
页的大小由系统架构决定,通常是 4KB、2MB 或 1GB 等(在 x86 和 ARM 系统中,默认页大小通常为 4KB)。如果页大小为 4KB,一个页对齐的地址可以是 0x0000
、0x1000
、0x2000
【即二进制表示下最低的12位必须为0,十六进制表示下最低3位必须为0】,但不能是 0x1234
或 0x1FFF
。
“页对齐的地址” 是指一个地址必须是内存页大小的整数倍。
在这里,我们使用函数ioremap()
把物理地址映射到Linux系统的虚拟地址空间。其第一个参数resource_size_t phys_addr
代表物理地址,这个物理地址在有时候要求是页对齐的,如果传入的不是页对齐的物理地址但程序又能跑通,那么说明函数ioremap()
内部进行了页对齐调整处理。
您肯定要问,不是虚拟地址空间才是以页为单位管理的吗?怎么要求被映射的物理地址也是页对齐的呢?
这是因为操作系统也会将物理内存(即物理地址空间)按页大小划分为物理内存块(Frame)。虽然物理地址本身不强制分页,但操作系统为了方便管理,也会以页为单位分配和管理物理地址空间,所以函数ioremap()
的第一个参数resource_size_t phys_addr
有时会严格要求这个物理地址也是页对齐的,如果传入的不是页对齐的物理地址但程序又能跑通,那么说明函数ioremap()
内部进行了页对齐调整处理。
在下面的代码中:
CCM_CCGR1 = ioremap(0x20C406C, 4);
- 1
地址0x20C406C
的十六进制表示的最后三位不是0,所以不是一个页对齐地址,但实测我们的程序是能跑通的,说明咱们这里遇到的ioremap()
函数的内部是作了对齐处理的。
如果你以后遇到了没有自动处理的ioremap()函数,那么你需要像下面这样作调整:
#define PHYS_ADDR 0x20C406C
#define SIZE 4
void __iomem *base;
unsigned int *reg;
unsigned int offset;
// 对齐地址
unsigned int aligned_addr = PHYS_ADDR & ~(0x1000 - 1);
offset = PHYS_ADDR - aligned_addr;
// 映射页对齐后的地址
base = ioremap(aligned_addr, offset + SIZE);
if (!base) {
printk("ioremap failed\n");
return -1;
}
// 访问寄存器时加上偏移
reg = (unsigned int *)(base + offset);
*reg |= (1 << 3); // 操作寄存器
// 释放映射
iounmap(base);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
上面的代码中, aligned_addr代表向下对齐的页的起始地址,PHYS_ADDR 代表传入的物理地址,注意为了保证能映射到想要的物理地址,ioremap
的第二个参数,即要映射的地址范围的大小也要调大,其具体的值为offset + SIZE



评论记录:
回复评论: