地址映射
MMU全称叫做Memory Manage Unit,也就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU主要完成的功能如下:
- 完成虚拟空间到物理空间的映射。
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第1点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于32位的处理器来说,虚拟地址范围是2^32=4GB,例如CPU搭配的是512MB的DDR3,这512MB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间,如下图所示:
物理内存只有512MB,虚拟内存有4GB,那么肯定 存在多个虚拟地址映射到同一个物理地址上去,虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究,因为MMU是很复杂的一个东西,后续有时间的话正点原子Linux团队会专门做MMU专题教程。
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。比如ZYNQ的GPIO模块寄存器基地址为 0xE000A000。如果没有开启MMU的话直接读写0xE000A000这个地址是没有问题的,现在开启了MMU,并且设置了内存映射,因此就不能直接向0xE000A000这个地址写入数据了。我们必须得到0xE000A000这个物理地址在Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。
-
ioremap 函数
ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h
文件中,定义如下:#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE) void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype) { return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0)); }
ioremap
是个宏,有两个参数:cookie
和size
,真正起作用的是函数__arm_ioremap
,此函数有三个参数和一个返回值,这些参数和返回值的含义如下:phys_addr
:要映射的物理起始地址。size
:要映射的内存空间大小。mtype
:ioremap
的类型,可以选择MT_DEVICE
、MT_DEVICE_NONSHARED
、MT_DEVICE_CACHED
和MT_DEVICE_WC
,ioremap
函数选择MT_DEVICE
。- 返回值:
__iomem
类型的指针,指向映射后的虚拟空间首地址。
假如我们要获取ZYNQ的APER_CLK_CTRL寄存器对应的虚拟地址,使用如下代码即可:
#define APER_CLK_CTRL 0xF800012C static void __iomem *aper_clk_ctrl_addr; aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
宏定义
APER_CLK_CTRL
是寄存器物理地址,aper_clk_ctrl_addr
是该物理地址映射后的虚拟地址。对于ZYNQ来说一个寄存器是 4字节(32位)的,因此映射的内存长度为4。映射完成以后直接对aper_clk_ctrl_addr
进行读写操作即可。 -
iounmap 函数
卸载驱动的时候需要使用iounmap
函数释放掉ioremap
函数所做的映射,iounmap
函数原型如下:
void iounmap (volatile void __iomem *addr)
iounmap只有一个参数addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉APER_CLK_CTRL寄存器的地址映射,使用如下代码即可:
iounmap(aper_clk_ctrl_addr);
I/O 内存访问函数
这里说的I/O是输入/输出的意思,并不是我们学习单片机的时候讲的 GPIO引脚。这里涉及到两个概念:I/O端口和I/O内存。当外部寄存器或内存映射到IO空间时,称为I/O 端口。当外部寄存器或内存映射到内存空间时,称为I/O内存。但是对于ARM来说没有I/O空间这个概念,因此ARM体系下只有I/O内存(可以直接理解为内存)。
使用ioremap
函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
-
读操作函数
读操作函数有如下几个:
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb
、readw
和readl
这三个函数分别对应8bit、16bit和32bit读操作,参数addr
就是要读取的内存地址,返回值就是读取到的数据。 -
写操作函数
写操作函数有如下几个:
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb
、writew
和writel
这三个函数分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr
是要写入的地址。