原文地址:http://blog.csdn.net/darennet/article/details/40521015,在原文基础上进行了一些整理,加入了一些自己的理解。
在嵌入式编程中,绝大部分功能都是通过外设实现的,这些外设不仅可以是CPU外部的某种功能模块,也可以是CPU芯片内部集成的某些片内外设。这些芯片内部的外设基本都是通过总线的方式与CPU核心相连,而对它们的控制也通过对这些总线上的外设寄存器的配置来实现。
外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址(通常一个外设寄存器会占好几个字节)。
但是外设寄存器与CPU核心寄存器不同,核心寄存器是有名字的,不同体系架构的寄存器名也不一样,这个体现在不同架构的编译器里;而外设寄存器是有地址的,不同CPU芯片会有不同总线连接方式,所以也会有不同的外设寄存器地址,知道地址之后就可以通过给外设对应的寄存器地址赋值,来控制外设。
CPU对外设IO端口物理地址的编址方式 有两种:
一种是I/O映射方式(I/O-mapped)称为端口映射,另一种是存储空间映射方式(Memory-mapped),称为内存映射。而具体采用哪一种则取决于CPU的体系结构。
一类CPU(如Power PC、ARM等)把这些外设寄存器看做是内存的一部分、寄存器参与内存统一编址,通过一般的内存指令来访问这些外设寄存器,称为“I/O内存”。
另一类CPU(如X86等)把这些外设寄存器看做是一个独立的地址空间,访问内存的指令不能用来访问这些外设寄存器,而需要用专用的指令(如IN、OUT指令),称为“I/O端口”。
端口映射的典型代表是MCS-51系列单片机和x86体系,这种映射需要有独立的地址空间对应外设地址,而且还需要另外的汇编命令来控制。
如51单片机的sfr特殊功能寄存器就是一个特别的命令来控制外设,而sfr所管理的128B的地址也是与RAM地址独立的,有兴趣的同学可以搜索下sfr的相关介绍。
内存映射是嵌入式设备体系用的比较多的方式,我们熟知的 ARM 体系,PowerPC 就是用了这种与物理RAM地址统一编址的方式。
比如在单片机编程中经常看到下面的外设寄存器的宏定义:
#define rGPACON (*(volatile unsigned int *)0x7F008000) //Port Acontrol
通过强制类型转换,将一个立即数(0x7F008000)转换成一个指向无符号整型的指针(指针的值为0x7F008000),然后通过*
运算符取得该指针所指向的(地址为0x7F008000)数据,这样在编程时就可以直接使用宏rGPACON
来代表对应外设寄存器的值。类似下面这种方式则可以直接给P1OUT
赋值(也就是给指定的内存地址赋值),来控制P1端口的输出电平。
#define P1OUT (volatile unsigned int *)0x80008000 //Port 1 output register
关于上面出现的地址值,在ARM或MCU的芯片手册(datasheet)上一般是有描述的。
通过指针来配置外设的确很方便,但是这种用法一般只用在裸机程序或者小型的RTOS上;对于很多大型的OS系统而言(比如Linux),考虑到安全问题,一般是不允许用户直接访问物理地址的,这样在实际编程中用到的都是虚拟地址,然后由操作系统完成虚拟地址到物理地址的映射过程。
首先介绍内存管理单元MMU,主要提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持,操作系统通过MMU进行内存管理。
介绍下linux中的物理地址和虚拟地址:
在支持MMU的32位处理器平台上,Linux系统中的物理存储空间和虚拟存储空间的地址范围分别都是从0x00000000到0xFFFFFFFF,共4GB,Linux系统负责把4GB的物理内存根据不同需求映射到整个4GB的虚拟存储空间中。
在虚拟地址空间中,linux又将这4G分为用户空间(0-0xbfffffff)和内核空间(0xc0000000-0xffffffff),为了让用户空间没机会直接接触物理地址,linux的物理地址映射都是在内核空间完成的。
在32位的CPU中,内核地址区域就1G大小,对于现在的硬件来说这么点大的地址确实不够直接映射物理地址,所以linux对这内核区域又进行了划分:
内核区开始的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映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问,这样就实现了虚拟地址到物理地址的映射与操作。