您的位置 首页 资料

IO端口和IO内存的差异及别离运用的函数接口

IO端口和IO内存的区别及分别使用的函数接口每个外设都是通过读写其寄存器来控制的。外设寄存器也称为IO端口,通常包括:控制寄存器、状态

IO端口和IO内存的差异及别离运用的函数接口

每个外设都是经过读写其寄存器来操控的。外设寄存器也称为I/O端口,一般包括:操控寄存器、状况寄存器和数据寄存器三大类。依据拜访外设寄存器的不同办法,能够把CPU分红两大类。一类CPU(如M68K,Power PC等)把这些寄存器看作内存的一部分,寄存器参加内存共同编址,拜访寄存器就经过拜访一般的内存指令进行,所以,这种CPU没有专门用于设备I/O的指令。这便是所谓的“I/O内存”办法。另一类CPU(典型的如X86),将外设的寄存器当作一个独立的地址空间,所以拜访内存的指令不能用来拜访这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这便是所谓的“I/O端口”办法。可是,用于I/O指令的“地址空间”相对来说是很小的,如x86 CPU的I/O空间就只有64KB(0-0xffff)。

结合下图,咱们完全叙述IO端口和IO内存以及内存之间的联系。主存16M字节的SDRAM,外设是个视频采集卡,上面有16M字节的SDRAM作为缓冲区。

1.CPU是i386架构的状况

在i386系列的处理中,内存和外部IO是独立编址,也是独立寻址的。MEM的内存空间是32位能够寻址到4G,IO空间是16位能够寻址到64K。

在Linux内核中,拜访外设上的IO Port有必要经过IO Port的寻址办法。而拜访IO Mem就比较罗嗦,外部MEM不能和主存相同拜访,虽然巨细上平起平坐,可是外部MEM是没有在系统中注册的。拜访外部IO MEM有必要经过remap映射到内核的MEM空间后才干拜访。为了到达接口的同一性,内核供给了IO Port到IO Mem的映射函数。映射后IO Port就能够看作是IO Mem,依照IO Mem的拜访办法即可。

3. CPU是ARM或PPC架构的状况

在这一类的嵌入式处理器中,IO Port的寻址办法是选用内存映射,也便是IO bus便是Mem bus。系统的寻址才能假如是32位,IO Port+Mem(包括IO Mem)能够到达4G。

1.运用I/O端口

I/O端口是驱动用来和许多设备通讯的办法。

1.1、分配I/O端口

在驱动还没独占设备之前,不应对端口进行操作。内核供给了一个注册接口,以答应驱动声明其需求的端口:

#include/* request_region告知内核:要运用first开端的n个端口。参数name为设备名。假如分配成功回来值对错NULL;否则无法运用需求的端口(/proc/ioports包括了系统当时一切端口的分配信息,若request_region分配失利时,能够查看该文件,看谁先用了你要的端口) */
structresource*request_region(unsignedlongfirst,unsignedlongn,constchar*name);
/* 用完I/O端口后(或许在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region共同 */
voidrelease_region(unsignedlongstart,unsignedlongn);
/*check_region用于查看一个给定的I/O端口集是否可用。假如给定的端口不行用,check_region回来一个错误码。不引荐运用该函数,由于即使它回来0(端口可用),它也不能确保后边的端口分配操作会成功,由于查看和后边的端口分配并不是一个原子操作。而request_region经过加锁来确保操作的原子性,因而是安全的 */
intcheck_region(unsignedlongfirst,unsignedlongn);

1.2、操作I/O端口

在驱动成功恳求到I/O端口后,就能够读写这些端口了。大部分硬件会将8位、16位和32位端口区分隔,无法像拜访内存那样混杂运用。驱动程序有必要调用不同的函数来拜访不同巨细的端口。

好像前面所讲的,仅支撑单地址空间的计算机系统经过将I/O端口地址从头映射到内存地址来假装端口I/O。为了进步移植性,内核对驱动躲藏了这些细节。Linux内核头文件(系统依靠的头文件)界说了下列内联函数来存取I/O端口:

/*inb/outb:读/写字节端口(8位宽)。有些系统将port参数界说为unsigned long;而有些渠道则将它界说为unsigned short。inb的回来类型也是依靠系统的 */
unsignedinb(unsignedport);
voidoutb(unsignedcharbyte,unsignedport);
/*inw/outw:读/写字端口(16位宽)*/
unsignedinw(unsignedport);
voidoutw(unsignedshortword,unsignedport);
/*inl/outl:读/写32位端口。longword也是依靠系统的,有的系统为unsigned long;而有的为unsigned int */
unsignedinl(unsignedport);
voidoutl(unsignedlongword,unsignedport);

从现在开端,当咱们运用unsigned没有进一步指定类型时,表明是一个依靠系统的界说。

留意,没有64位的I/O端口操作函数。即使在64位系统中,端口地址空间运用一个32位(最大)的数据通路。

1.3、从用户空间拜访I/O端口

1.2节介绍的函数首要是供给给驱动运用,但它们也可在用户空间运用,至少在PC机上能够。GNUC库在中界说它们。假如在用户空间运用这些函数,有必要满意下列条件:

1)、程序有必要运用-O选项编译来强制扩展内联函数

2)、有必要运用ioperm和iopl系统调用(#include )来取得进行操作I/O端口的权限。ioperm为获取单个端口的操作答应,iopl为获取整个I/O空间答应。这2个函数都是x86特有的

3)、程序有必要以root来调用ioperm或许iopl,或许其父进程(先人)有必要以root取得的端口操作权限

假如渠道不支撑ioperm和iopl系统调用,经过运用/dev/prot设备文件,用户空间依然能够存取I/O端口。可是要留意的是,这个文件的界说也是依靠渠道的。

1.4、字串操作

除了一次传递一个数据的I/O操作,某些处理器完成了一次传递一序列数据(单位能够是字节、字和双字)的特别指令。这些所谓的字串指令,它们完成任务比一个C言语循环更快。下列宏界说完成字串操作,在某些系统上,它们经过运用单个机器指令完成;但假如方针处理器没有进行字串I/O指令,则经过履行一个紧凑的循环完成。

字串函数的原型是:

/* insb:从I/O端口port读取count个数据(单位字节)到以内存地址addr为开端的内存空间*/
voidinsb(unsignedport,void*addr,unsignedlongcount);
/* outsb:将内存地址addr开端的count个数据(单位字节)写到I/O端口port*/
voidoutsb(unsignedport,void*addr,unsignedlongcount);
/* insw:从I/O端口port读取count个数据(单位字)到以内存地址addr为开端的内存空间*/
voidinsw(unsignedport,void*addr,unsignedlongcount);
/* outsw:将内存地址addr开端的count个数据(单位字)写到I/O端口port*/
voidoutsw(unsignedport,void*addr,unsignedlongcount);
/* insl:从I/O端口port读取count个数据(单位双字)到以内存地址addr为开端的内存空间*/
voidinsl(unsignedport,void*addr,unsignedlongcount);
/* outsl:将内存地址addr开端的count个数据(单位双字)写到I/O端口port*/
voidoutsl(unsignedport,void*addr,unsignedlongcount);

留意:运用字串函数时,它们直接将字节省从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不行预期的成果。运用inw读取端口应在必要时自行转化字节序,以匹配主机字节序。

1.5、暂停式I/O操作函数

由于处理器的速率或许与外设(尤其是低速设备)的并不匹配,当处理器过快地传送数据到或自总线时,这时或许就会引起问题。解决办法是:假如在I/O指令后边紧跟着另一个相似的I/O指令,就有必要刺进一个小的延时。为此,Linux供给了暂停式I/O操作函数,这些函数的名子仅仅在非暂停式I/O操作函数(前面说到的那些I/O操作函数都对错暂停式的)名后加上_p,如inb_p、outb_p等。大部分系统都支撑这些函数,虽然它们常常被扩展为与非暂停I/O相同的代码,由于假如系统运用一个合理的现代外设总线,没有必要额定暂停。

以下是ARM系统暂停式I/O宏的界说:

#defineoutb_p(val,port)outb((val),(port))
#defineoutw_p(val,port)outw((val),(port))
#defineoutl_p(val,port)outl((val),(port))
#defineinb_p(port)inb((port))
#defineinw_p(port)inw((port))
#defineinl_p(port)inl((port))
#defineoutsb_p(port,from,len)outsb(port,from,len)
#defineoutsw_p(port,from,len)outsw(port,from,len)
#defineoutsl_p(port,from,len)outsl(port,from,len)
#defineinsb_p(port,to,len)insb(port,to,len)
#defineinsw_p(port,to,len)insw(port,to,len)
#defineinsl_p(port,to,len)insl(port,to,len)

由于ARM运用内部总线,就没有必要额定暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O相同的代码。

1.6、渠道依靠性

由于本身的特性,I/O指令高度依靠于处理器,十分难以躲藏各系统间的不同。因而,大部分的关于端口I/O的源码是渠道依靠的。以下是x86和ARM所运用函数的总结:

IA-32(x86)

x86_64

这个系统支撑本章介绍的一切函数;port参数的类型为unsignedshort。

ARM

端口映射到内存,并且支撑本章介绍的一切函数;port参数的类型为unsignedint;字串函数用C言语完成。

2、运用I/O内存

虽然I/O端口在x86国际中十分盛行,可是用来和设备通讯的首要机制是经过内存映射的寄存器和设备内存,两者都称为I/O内存,由于寄存器和内存之间的差异对软件是通明的。

I/O内存仅仅是一个相似于RAM的区域,处理器经过总线拜访该区域,以完成对设备的拜访。相同,读写这个区域是有边际效应。

依据计算机系统和总线不同,I/O内存可分为能够或许不能够经过页表来存取。若经过页表存取,内核有必要先从头编列物理地址,使其对驱动程序可见,这就意味着在进行任何I/O操作之前,你有必要调用ioremap;假如不需求页表,I/O内存区域就相似于I/O端口,你能够直接运用恰当的I/O函数读写它们。

由于边际效应的原因,不论是否需求ioremap,都不鼓舞直接运用I/O内存指针,而应运用专门的I/O内存操作函数。这些I/O内存操作函数不只在一切渠道上是安全,并且对直接运用指针操作I/O内存的状况进行了优化。

2.1、I/O内存分配和映射

I/O内存区在运用前有必要先分配。分配内存区的函数接口在界说中:

/* request_mem_region分配一个开端于start,len字节的I/O内存区。分配成功,回来一个非NULL指针;否则回来NULL。系统当时一切I/O内存分配信息都在/proc/iomem文件中列出,你分配失利时,能够看看该文件,看谁先占用了该内存区 */
structresource*request_mem_region(unsignedlongstart,unsignedlonglen,char*name);
/* release_mem_region用于开释不再需求的I/O内存区*/
voidrelease_mem_region(unsignedlongstart,unsignedlonglen);
/* check_mem_region用于查看I/O内存区的可用性。相同,该函数不安全,不引荐运用 */
intcheck_mem_region(unsignedlongstart,unsignedlonglen);

在拜访I/O内存之前,分配I/O内存并不是仅有要求的过程,你还有必要确保内核可存取该I/O内存。拜访I/O内存并不仅仅简略解引证指针,在许多系统中,I/O内存无法以这种办法直接存取。因而,还有必要经过ioremap函数设置一个映射。

#include
/*ioremap用于将I/O内存区映射到虚拟地址。参数phys_addr为要映射的I/O内存开始地址,参数size为要映射的I/O内存的巨细,回来值为被映射到的虚拟地址 */
void*ioremap(unsignedlongphys_addr,unsignedlongsize);

/* ioremap_nocache为ioremap的无缓存版别。实践上,在大部分系统中,ioremap与ioremap_nocache的完成相同的,由于一切 I/O 内存都是在无缓存的内存地址空间中 */
void*ioremap_nocache(unsignedlongphys_addr,unsignedlongsize);
/* iounmap用于开释不再需求的映射 */
voidiounmap(void*addr);

经过ioremap(和iounmap)之后,设备驱动就能够存取任何I/O内存地址。留意,ioremap回来的地址不能够直接解引证;相反,应当运用内核供给的拜访函数。

2.2、拜访I/O内存

拜访I/O内存的正确办法是经过一系列专门用于完成此意图的函数:

#include
/*I/O内存读函数。参数addr应当是从ioremap取得的地址(或许包括一个整型偏移); 回来值是从给定I/O内存读取到的值 */
unsignedintioread8(void*addr);
unsignedintioread16(void*addr);
unsignedintioread32(void*addr);
/*I/O内存写函数。参数addr同I/O内存读函数,参数value为要写的值 */
voidiowrite8(u8 value,void*addr);
voidiowrite16(u16 value,void*addr);
voidiowrite32(u32 value,void*addr);
/* 以下这些函数读和写一系列值到一个给定的 I/O 内存地址,从给定的buf读或写count个值到给定的addr。参数count表明要读写的数据个数,而不是字节巨细 */
voidioread8_rep(void*addr,void*buf,unsignedlongcount);
voidioread16_rep(void*addr,void*buf,unsignedlongcount);
voidioread32_rep(void*addr,void*buf,unsignedlongcount);
voidiowrite8_rep(void*addr,constvoid*buf,unsignedlongcount);
voidiowrite16_rep(void*addr,constvoid*buf,unsignedlongcount);
voidiowrite32_rep(void*addr,,onstvoid*buf,,nsignedlongcount);
/* 需求操作一块I/O地址时,运用下列函数(这些函数的行为相似于它们的C库相似函数): */
voidmemset_io(void*addr,u8 value,unsignedintcount);
voidmemcpy_fromio(void*dest,void*source,unsignedintcount);
voidmemcpy_toio(void*dest,void*source,unsignedintcount);
/* 旧的I/O内存读写函数,不引荐运用 */
unsignedreadb(address);
unsignedreadw(address);
unsignedreadl(address);
voidwriteb(unsignedvalue,address);
voidwritew(unsignedvalue,address);
voidwritel(unsignedvalue,address);

2.3、像I/O内存相同运用端口

一些硬件有一个风趣的特性:有些版别运用I/O端口;而有些版别则运用I/O内存。不论是I/O端口仍是I/O内存,处理器见到的设备寄存器都是相同的,仅仅拜访办法不同。为了共同编程接口,使驱动程序易于编写,2.6内核供给了一个ioport_map函数:

/*ioport_map从头映射count个I/O端口,使它们看起来I/O内存。尔后,驱动程序能够在ioport_map回来的地址上运用ioread8和同类函数。这样,就能够在编程时,消除了I/O端口和I/O 内存的差异*/
void*ioport_map(unsignedlongport,unsignedintcount);
/* ioport_unmap用于开释不再需求的映射 */
voidioport_unmap(void*addr);

留意,I/O端口在从头映射前有必要运用request_region分配所需的I/O端口。

3、ARM系统的I/O操作接口

s3c24x0处理器运用的是I/O内存,也便是说:s3c24x0处理器运用共同编址办法,I/O寄存器和内存运用的是单一地址空间,并且读写I/O寄存器和读写内存的指令是相同的。所以引荐运用I/O内存的相关指令和函数。但这并不表明I/O端口的指令在s3c24x0中不行用。假如你留意过s3c24x0关于I/O方面的内核源码,你就会发现:其实I/O端口的指令仅仅一个外壳,内部仍是运用和I/O内存相同的代码。

下面是ARM系统原始的I/O操作函数。其实后边I/O端口和I/O内存操作函数,仅仅对这些函数进行再封装。从这儿也能够看出为什么咱们不引荐直接运用I/O端口和I/O内存地址指针,而是要求运用专门的I/O操作函数——专门的I/O操作函数会查看地址指针是否有用是否为IO地址(经过__iomem或__chk_io_ptr)

#include

/*
* Generic IO read/write. These perform native-endian accesses. Note
* that some architectures will want to re-define __raw_{read,write}w.
*/
externvoid__raw_writesb(void__iomem*addr,constvoid*data,intbytelen);
externvoid__raw_writesw(void__iomem*addr,constvoid*data,intwordlen);
externvoid__raw_writesl(void__iomem*addr,constvoid*data,intlonglen);
externvoid__raw_readsb(constvoid__iomem*addr,void*data,intbytelen);
externvoid__raw_readsw(constvoid__iomem*addr,void*data,intwordlen);
externvoid__raw_readsl(constvoid__iomem*addr,void*data,intlonglen);
#define__raw_writeb(v,a)(__chk_io_ptr(a),*(volatileunsignedchar__force*)(a)=(v))
#define__raw_writew(v,a)(__chk_io_ptr(a),*(volatileunsignedshort__force*)(a)=(v))
#define__raw_writel(v,a)(__chk_io_ptr(a),*(volatileunsignedint__force*)(a)=(v))
#define__raw_readb(a)(__chk_io_ptr(a),*(volatileunsignedchar__force*)(a))
#define__raw_readw(a)(__chk_io_ptr(a),*(volatileunsignedshort__force*)(a))
#define__raw_readl(a)(__chk_io_ptr(a),*(volatileunsignedint__force*)(a))

关于__force和__iomem

#include

/* __force表明所界说的变量类型是能够做强制类型转化的 */
#define__force __attribute__((force))
/* __iomem是用来润饰一个变量的,这个变量有必要对错解引证(no dereference)的,即这个变量地址有必要是有用的,并且变量地点的地址空间有必要是2,即设备地址映射空间。0表明normal space,即一般地址空间,对内核代码来说,当然便是内核空间地址了。1表明用户地址空间,2表明是设备地址映射空间 */
#define__iomem __attribute__((noderef,address_space(2)))

I/O端口

#include

#defineoutb(v,p)__raw_writeb(v,__io(p))
#defineoutw(v,p)__raw_writew((__force __u16)
cpu_to_le16(v),__io(p))
#defineoutl(v,p)__raw_writel((__force __u32)
cpu_to_le32(v),__io(p))
#defineinb(p)({__u8 __v=__raw_readb(__io(p));__v;})
#defineinw(p)({__u16 __v=le16_to_cpu((__force __le16)
__raw_readw(__io(p)));__v;})
#defineinl(p)({__u32 __v=le32_to_cpu((__force __le32)
__raw_readl(__io(p)));__v;})
#defineoutsb(p,d,l)__raw_writesb(__io(p),d,l)
#defineoutsw(p,d,l)__raw_writesw(__io(p),d,l)
#defineoutsl(p,d,l)__raw_writesl(__io(p),d,l)
#defineinsb(p,d,l)__raw_readsb(__io(p),d,l)
#defineinsw(p,d,l)__raw_readsw(__io(p),d,l)
#defineinsl(p,d,l)__raw_readsl(__io(p),d,l)

I/O内存

#include

#defineioread8(p)({unsignedint__v=__raw_readb(p);__v;})
#defineioread16(p)({unsignedint__v=le16_to_cpu((__force __le16)__raw_readw(p));__v;})
#defineioread32(p)({unsignedint__v=le32_to_cpu((__force __le32)__raw_readl(p));__v;})
#defineiowrite8(v,p)__raw_writeb(v,p)
#defineiowrite16(v,p)__raw_writew((__force __u16)cpu_to_le16(v),p)
#defineiowrite32(v,p)__raw_writel((__force __u32)cpu_to_le32(v),p)
#defineioread8_rep(p,d,c)__raw_readsb(p,d,c)
#defineioread16_rep(p,d,c)__raw_readsw(p,d,c)
#defineioread32_rep(p,d,c)__raw_readsl(p,d,c)
#defineiowrite8_rep(p,s,c)__raw_writesb(p,s,c)
#defineiowrite16_rep(p,s,c)__raw_writesw(p,s,c)
#defineiowrite32_rep(p,s,c)__raw_writesl(p,s,c)

留意:

1)、一切的读写指令(I/O操作函数)所赋的地址有必要都是虚拟地址,你有两种挑选:运用内核现已界说好的地址,如在include/asm-arm/arch-s3c2410/regs-xxx.h中界说了s3c2410处理器各外设寄存器地址(其他处理器芯片也可在相似途径找到内核界说好的外设寄存器的虚拟地址;另一种办法便是运用自己用ioremap映射的虚拟地址。肯定不能运用实践的物理地址,否则会由于内核无法处理地址而呈现oops。

2)、在运用I/O指令时,能够不运用request_region和request_mem_region,而直接运用outb、ioread等指令。由于request的功用仅仅告知内核端口被谁占用了,如再次request,内核会阻止(资源busy)。可是不引荐这么做,这样的代码也不标准,或许会引起并发问题(许多时分咱们都需求独占设备)。

3)、在运用I/O指令时,所赋的地址数据有时有必要经过强制类型转化为unsigned long,否则会有正告。

4)、在includeasm-armarch-s3c2410hardware.h中界说了许多io口的操作函数,有需求能够在驱动中直接运用,很便利。

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/ziliao/261368.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部