IO地址空间访问.docx
《IO地址空间访问.docx》由会员分享,可在线阅读,更多相关《IO地址空间访问.docx(10页珍藏版)》请在冰豆网上搜索。
IO地址空间访问
驱动开发中的I/O地址空间
节点的孩子被收集在一个链表中,其第一个元素由child指向。
sibling字段指向链表中的下一个节点。
为什么使用树?
例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000到0xf00f。
那么,start字段为0xf000且end字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。
但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘使用0xf000到0xf007的子范围,从盘使用0xf008到0xf00f的子范围。
为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000到0xf00f的整个范围对应的资源下。
一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。
I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。
任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:
request_resource()
把一个给定范围分配给一个I/O设备。
allocate_resource()
在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。
release_resource()request_region()分配I/O端口的给定范围,release_region()释放以前分配给I/O端口的范围。
当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。
3.把I/O端口映射到内存空间-访问I/O端口的另一种方式
映射函数的原型为:
void*ioport_map(unsignedlongport,unsignedintcount);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。
然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。
但请注意,在进行映射前,还必须通过request_region()分配I/O端口。
当不再需要这种映射时,需要调用下面的函数来撤消:
voidioport_unmap(void*addr);
在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用Linux内核的如下一组函数来完成访问I/O内存:
·读I/O内存
unsignedintioread8(void*addr);
unsignedintioread16(void*addr);
unsignedintioread32(void*addr);
与上述函数对应的较早版本的函数为(这些函数在Linux2.6中仍然被支持):
unsignedreadb(address);
unsignedreadw(address);
unsignedreadl(address);
·写I/O内存
voidiowrite8(u8value,void*addr);
voidiowrite16(u16value,void*addr);
voidiowrite32(u32value,void*addr);
与上述函数对应的较早版本的函数为(这些函数在Linux2.6中仍然被支持):
voidwriteb(unsignedvalue,address);
voidwritew(unsignedvalue,address);
voidwritel(unsignedvalue,address);
4.访问I/O内存
Linux内核也提供了一组函数申请和释放某一范围的I/O内存:
structresource*requset_mem_region(unsignedlongstart,unsignedlonglen,char*name);
这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为,name为设备的名称。
注意,。
如果分配成功,则返回非NULL,否则,返回NULL。
另外,可以通过/proc/iomem查看系统给各种设备的内存范围。
要释放所申请的I/O内存,应当使用release_mem_region()函数:
voidrelease_mem_region(unsignedlongstart,unsignedlonglen)
申请一组I/O内存后,调用ioremap()函数:
void*ioremap(unsignedlongphys_addr,unsignedlongsize,unsignedlongflags);
其中三个参数的含义为:
phys_addr:
与requset_mem_region函数中参数start相同的I/O物理地址;
size:
要映射的空间的大小;
flags:
要映射的IO空间的和权限有关的标志;
功能:
将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)
为什么要申请虚拟内存然后才进行映射?
————————————————————————————————————
留给读者的思考:
直接访问I/O端口、把I/O端口映射到内存进行访问,以及访问I/O内存,三者之间有什么区别,在驱动程序开发中如何具体应用?
IO端口和IO内存
默认分类2010-03-2222:
06:
57阅读67评论0 字号:
大中小 订阅
驱动程序编写过程中,很少会注意到IOPort和IOMem的区别。
虽然使用一些不符合规范的代码可以达到最终目的,这是极其不推荐使用的。
结合下图,我们彻底讲述IO端口和IO内存以及内存之间的关系。
主存16M字节的SDRAM,外设是个视频采集卡,上面有16M字节的SDRAM作为缓冲区。
1. CPU是i386架构的情况在i386系列的处理中,内存和外部IO是独立编址,也是独立寻址的。
MEM的内存空间是32位可以寻址到4G,IO空间是16位可以寻址到64K。
2. 在Linux内核中,访问外设上的IOPort必须通过IOPort的寻址方式。
而访问IOMem就比较罗嗦,外部MEM不能和主存一样访问,虽然大小上不相上下,可是外部MEM是没有在系统中注册的。
访问外部IOMEM必须通过remap映射到内核的MEM空间后才能访问。
为了达到接口的同一性,内核提供了IOPort到IOMem的映射函数。
映射后IOPort就可以看作是IOMem,按照IOMem的访问方式即可。
3. CPU是ARM或PPC架构的情况
在这一类的嵌入式处理器中,IOPort的寻址方式是采用内存映射,也就是IObus就是Membus。
系统的寻址能力如果是32位,IOPort+Mem(包括IOMem)可以达到4G。
访问这类IOPort时,我们也可以用IOPort专用寻址方式。
至于在对IOPort寻址时,内核是具体如何完成的,这个在内核移植时就已经完成。
在这种架构的处理器中,仍然保持对IOPort的支持,完全是i386架构遗留下来的问题,在此不多讨论。
而访问IOMem的方式和i386一致。
注意:
linux内核给我提供了完全对IOPort和IOMem的支持,然而具体去看看driver目录下的驱动程序,很少按照这个规范去组织IOPort和IOMem资源。
对这二者访问最关键问题就是地址的定位,在C语言中,使用volatile就可以实现。
很多的代码访问IOPort中的寄存器时,就使用volatile关键字,虽然功能可以实现,我们还是不推荐使用。
就像最简单的延时莫过于while,可是在多任务的系统中是坚决避免的!
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。
此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。
但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:
void*ioremap(unsignedlongphys_addr,unsignedlongsize,unsignedlongflags);
iounmap函数用于取消ioremap()所做的映射,原型如下:
voidiounmap(void*addr);
这两个函数都是实现在mm/ioremap.c文件中。
在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。
为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。
如在x86平台上,读写I/O的函数如下所示:
#definereadb(addr) (*(volatileunsignedchar*)__io_virt(addr))
#definereadw(addr) (*(volatileunsignedshort*)__io_virt(addr))
#definereadl(addr) (*(volatileunsignedint*)__io_virt(addr))
#definewriteb(b,addr) (*(volatileunsignedchar*)__io_virt(addr)=(b))
#definewritew(b,addr) (*(volatileunsignedshort*)__io_virt(addr)=(b))
#definewritel(b,addr) (*(volatileunsignedint*)__io_virt(addr)=(b))
#definememset_io(a,b,c) memset(__io_virt(a),(b),(c))
#definememcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
#definememcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
最后,我们要特别强调驱动程序中mmap函数的实现方法。
用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。
笔者在Linux源代码中进行包含"ioremap"文本的搜索,发现真正出现的ioremap的地方相当少。
所以笔者追根索源地寻找I/O操作的物理地址转换到虚拟地址的真实所在,发现Linux有替代ioremap的语句,但是这个转换过程却是不可或缺的。
CPU对外设端口物理地址的编址方式有两种:
一种是IO映射方式,另一种是内存映射方式。
Linux将基于IO映射方式的和内存映射方式的IO端口统称为IO区域(IOregion)。
IOregion仍然是一种IO资源,因此它仍然可以用resource结构类型来描述。
Linux管理IOregion:
1)request_region()
把一个给定区间的IO端口分配给一个IO设备。
2)check_region()
检查一个给定区间的IO端口是否空闲,或者其中一些是否已经分配给某个IO设备。
3)release_region()
释放以前分配给一个IO设备的给定区间的IO端口。
Linux中可以通过以下辅助函数来访问IO端口:
inb(),inw(),inl(),outb(),outw(),outl()
“b”“w”“l”分别代表8位,16位,32位。
对IO内存资源的访问
1)request_mem_region()
请求分配指定的IO内存资源。
2)check_mem_region()
检查指定的IO内存资源是否已被占用。
3)release_mem_region()
释放指定的IO内存资源。
其中传给函数的startaddress参数是内存区的物理地址(以上函数参数表已省略)。
驱动开发人员可以将内存映射方式的IO端口和外设内存统一看作是IO内存资源。
ioremap()用来将IO资源的物理地址映射到内核虚地址空间(3GB-4GB)中,参数addr是指向内核虚地址的指针。
Linux中可以通过以下辅助函数来访问IO内存资源:
readb(),readw(),readl(),writeb(),writew(),writel()。
Linux在kernel/resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于IO映射方式的整个IO端口空间和基于内存映射方式的IO内存资源空间(包括IO端口和外设内存)。
内存映射(IO地址和内存地址)
ARM体系结构下面内存和i/o映射区别
(1)关于IO与内存空间:
在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。
端口号标识了外设的寄存器地址。
Intel语法的in、out指令格式为:
IN累加器,{端口号│DX}
OUT{端口号│DX},累加器
目前,大多数嵌入式微控制器如ARM、PowerPC等中并不提供I/O空间,而仅存在内存空间。
内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。
即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。
此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。
因此,内存空间是必须的,而I/O空间是可选的。
(2)inb和outb:
在Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
· 读写字节端口(8位宽)
unsignedinb(unsignedport);
voidoutb(unsignedcharbyte,unsignedport);
· 读写字端口(16位宽)
unsignedinw(unsignedport);
voidoutw(unsignedshortword,unsignedport);
· 读写长字端口(32位宽)
unsignedinl(unsignedport);
voidoutl(unsignedlongword,unsignedport);
· 读写一串字节
voidinsb(unsignedport,void*addr,unsignedlongcount);
voidoutsb(unsignedport,void*addr,unsignedlongcount);
· insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存的count个字节连续地写入port开始的端口。
· 读写一串字
voidinsw(unsignedport,void*addr,unsignedlongcount);
voidoutsw(unsignedport,void*addr,unsignedlongcount);
· 读写一串长字
voidinsl(unsignedport,void*addr,unsignedlongcount);
voidoutsl(unsignedport,void*addr,unsignedlongcount);
上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned。
(3)readb和writeb:
在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用Linux内核的如下一组函数来完成设备内存映射的虚拟地址的读写,这些函数包括:
· 读I/O内存
unsignedintioread8(void*addr);
unsignedintioread16(void*addr);
unsignedintioread32(void*addr);
与上述函数对应的较早版本的函数为(这些函数在Linux2.6中仍然被支持):
unsignedreadb(address);
unsignedreadw(address);
unsignedreadl(address);
· 写I/O内存
voidiowrite8(u8value,void*addr);
voidiowrite16(u16value,void*addr);
voidiowrite32(u32value,void*addr);
与上述函数对应的较早版本的函数为(这些函数在Linux2.6中仍然被支持):
voidwriteb(unsignedvalue,address);
voidwritew(unsignedvalue,address);
voidwritel(unsignedvalue,address);
(4)把I/O端口映射到“内存空间”:
void*ioport_map(unsignedlongport,unsignedintcount);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。
然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。
当不再需要这种映射时,需要调用下面的函数来撤消:
voidioport_unmap(void*addr);
实际上,分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口访问I/O端口。
11.2.7I/O空间的映射
很多硬件设备都有自己的内存,通常称之为I/O空间。
例如,所有比较新的图形卡都有几MB的RAM,称为显存,用它来存放要在屏幕上显示的屏幕影像。
1.地址映射
根据设备和总线类型的不同,PC体系结构中的I/O空间可以在三个不同的物理地址范围之间进行映射:
(1)对于连接到ISA总线上的大多数设备
I/O空间通常被映射到从0xa0000到0xfffff的物理地址范围,这就在640K和1MB之间留出了一段空间,这就是所谓的“洞”。
(2)对于使用VESA本地总线(VLB)的一些老设备
这是主要由图形卡使用的一条专用总线:
I/O空间被映射到从0xe00000到0xffffff的地址范围中,也就是14MB到16MB之间。
因为这些设备使页表的初始化更加复杂,因此已经不生产这种设备。
(3)对于连接到PCI总线的设备
I/O空间被映射到很大的物理地址区间,位于RAM物理地址的顶端。
这种设备的处理比较简单。
2.访问I/O空间
内核如何访问一个I/O空间单元?
让我们从PC体系结构开始入手,这个问题很容易就可以解决,之后我们再进一步讨论其他体系结构。
不要忘了内核程序作用于虚拟地址,因此I/O空间单元必须表示成大于PAGE_OFFSET的地址。
在后面的讨论中,我们假设PAGE_OFFSET等于0xc0000000,也就是说,内核虚拟地址是在第4G。
内核驱动程序必须把I/O空间单元的物理地址转换成内核空间的虚拟地址。
在PC体系结构中,这可以简单地把32位的物理地址和0xc0000000常量进行或运算得到。
例如,假设内核需要把物理地址为0x000b0fe4的I/O单元的值存放在t1中,把物理地址为0xfc000000的I/O单元的值存放在t2中,就可以使用下面的表达式来完成这项功能:
t1=*((unsignedchar*)(0xc00b0fe4));
t2=*((unsignedchar*)(0xfc000000));
在第六章我们已经介绍过,在初始化阶段,内核已经把可用的RAM物理地址映射到虚拟地址空间第4G的最初部分。
因此,分页机制把出现在第一个语句中的虚拟地址0xc00b0fe4映射回到原来的I/O物理地址0x000b0fe4,这正好落在从640K到1MB的这段“ISA洞”中。
这正是我们所期望的。
但是,对于第二个语句来说,这里有一个问题,因为其I/O物理地址超过了系统RAM的最大物理地址。
因此,虚拟地址0xfc000000就不需要与物理地址0xfc000000相对应。
在这种情况下,为了在内核页表中包括对这个I/O物理地址进行映射的虚拟地址,必须对页表进行修改:
这可以通过调用ioremap()函数来实现。
ioremap()和vmalloc()函数类似,都调用get_vm_area()建立一个新的vm_struct描述符,其描述的虚拟地址区间为所请求I/O空间区的大小。
然后,ioremap()函数适当地更新所有进程的对应页表项。
因此,第二个语句的正确形式应该为:
io_mem=ioremap(0xfb000000,0x200000);
t2=*((unsignedchar*)(io_mem+0x100000));
第一条语句建立一个2MB的虚拟地址区间,从0xfb000000开始;第二条语句读取地址0xfc000000的内存单元。
驱动程序以后要取消这种映射,就必须使用iounmap()函数。
现在让我们考虑一下除PC之外的体系结构。
在这种情况下,把I/O物理地址加上0xc0000000常量所得到的相应虚拟地址并不总是正确的。
为了提高内核的可移植性,Linux特意包含了下面这些宏来访问I/O空间:
readb,readw,readl
分别从一个I/O空间单元读取1、2或者4个字节
wr