设备地址与IO内存映射

时间:2024-04-06 21:43:14
在嵌入式编程中,绝大部分功能都是通过驱动外设实现的,这些外设不仅可以是CPU外部的某种功能模块,也可以是CPU芯片内部集成的某些器件。这些芯片内部的外设基本都是通过总线的方式与CPU核心相连,而对它们的控制也通过对这些总线上的外设寄存器的配置来实现。

外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续

地编址。但是外设寄存器与CPU核心寄存器不同,核心寄存器是有名字的,不同体系架构的寄存器名也不一样,这个体现在不同架构的编译器里;而外设寄存器是有地址的,不同CPU芯片会有不同总线连接方式,所以也会有不同的外设寄存器地址,有了地址以后,CPU核心寄存器就可以通过相应的外设寄存器地址去配置相应设备的动作了。

CPU对外设IO端口物理地址的编址方式 有两种:

一种是I/O映射方式(I/O-mapped)称为端口映射,另一种是存储空间映射方式(Memory-mapped),称为内存映射

而具体采用哪一种则取决于CPU的体系结构。

端口映射的典型代表是MCS-51系列单片机和x86体系,这种映射需要有独立的地址空间对应外设地址,而且还需要另外的汇编命令来控制。

如51单片机的sfr特殊功能寄存器就是一个特别的命令来控制外设,而sfr所管理的128B的地址也是与RAM地址独立的,有兴趣的同学可以搜索下sfr的相关介绍。

内存映射是嵌入式设备体系用的比较多的方式,我们熟知的ARM体系,PowerPC就是用了这种与物理RAM地址统一编址的方式。

如STM32里对各种外设寄存器的操作就使用了简单的指针方式:

#define rGPACON    (*(volatile unsigned *)0x7F008000) //Port Acontrol

相应的外设寄存器地址可以通过芯片的datasheet找到。  下图是s3c6410 datasheet的截图

设备地址与IO内存映射

通过指针配置外设想到的方便,在STM32的编程中你完全可以体会到它的便利。但是这仅仅是在裸机程序上,到操作系统层面上,原本简单的东西也会变得复杂。。。

linux在物理地址的管理上也是考虑周到。

首先介绍下linux中的物理地址和虚拟地址:

在支持MMU的32位处理器平台上,Linux系统中的物理存储空间和虚拟存储空间的地址范围分别都是从0x00000000到0xFFFFFFFF,共4GB,但物理存储空间与虚拟存储空间布局完全不同。Linux运行在虚拟存储空间,并负责把系统中实际存在的远小于4GB的物理内存根据不同需求映射到整个4GB的虚拟存储空间中。

在虚拟地址空间中,linux又将这4G分为用户空间(0-0xbfffffff)和内核空间(0xc0000000-0xffffffff),为了让用户空间没机会直接接触物理地址,linux的物理地址映射都是在内核空间完成的。

在32位的CPU中,内核地址区域就1G大小,对于现在的硬件来说这么点大的地址确实不够直接映射物理地址,所以linux对这内核区域又进行了划分:

设备地址与IO内存映射

内核区开始的896M区域被划分为物理内存映射区

接下去的8M区域是隔离区

--------------------------------分界线以上称为低端内存地址,以下称为高端内存地址-------------------------------------------

后面的约120M区域是Vmalloc虚拟内存分配区

接下去是8k是隔离区

然后4M区域是KMAP高端内存映射区(永久内存映射区)

后面4M区域是固定映射区

最后4k是保留区

物理内存映射区会将实际的物理内存的起始地址到偏移896M的地址直接映射过来(注意这里直接映射的是SDRAM(内存)的物理地址)

但是如果物理内存的总量超过了896M,那就要用到KMAP区了,这个区域会临时映射896M以后的内存区域,这样不管物理内存有多大都能映射到这里。

物理内存都分配完了,那么其他的IO外设地址该如何映射呢?  在linux中并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中

void *ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

  iounmap函数用于取消ioremap()所做的映射,原型如下:

voidiounmap(void * addr);

  这两个函数都是实现在mm/ioremap.c文件中。

  在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。如在x86平台上,读写I/O的函数如下所示:

#definereadb(addr) (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))

#definewriteb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))

#definememset_io(a,b,c) memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))

最后,我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问,这样就实现了虚拟地址到物理地址的映射与操作。