IO端口和IO内存的区别及分别使用的函数接口 Posted 2020-08-02 小米拍客光
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 <linux/ioport.h> /* request_region告诉内核:要使用first开始的n个端口。参数name为设备名。如果分配成功返回值是非NULL;否则无法使用需要的端口(/proc/ioports包含了系统当前所有端口的分配信息,若request_region分配失败时,可以查看该文件,看谁先用了你要的端口) */ struct resource *request_region (unsigned long first, unsigned long n, const char *name); /* 用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 */ void release_region (unsigned long start, unsigned long n); /* check_region 用于检查一个给定的I/O 端口集是否可用。如果给定的端口不可用,check_region返回一个错误码。不推荐使用该函数,因为即便它返回0(端口可用),它也不能保证后面的端口分配操作会成功,因为检查和后面的端口分配并不是一个原子操作。而request_region通过加锁来保证操作的原子性,因此是安全的 */ int check_region (unsigned long first, unsigned long n);
1.2 、操作 I/O 端口
在驱动成功请求到 I/O 端口后,就可以读写这些端口了。 大部分硬件会将 8 位、 16 位和 32 位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。
如同前面所讲的,仅支持单地址空间的计算机体系通过将 I/O 端口地址重新映射到内存地址来伪装端口 I/O 。为了提高移植性,内核对驱动隐藏了这些细节。 Linux 内核头文件 ( 体系依赖的头文件 <asm/io.h>) 定义了下列内联函数来存取 I/O 端口 :
/* inb/outb: 读/ 写字节端口(8位 宽)。有些体系将port参数定义为unsigned long;而有些平台则将它定义为unsigned short。inb的返回类型也是依赖体系的 */ unsigned inb (unsigned port); void outb (unsigned char byte, unsigned port); /* inw/outw: 读/ 写字端口(16位 宽) */ unsigned inw (unsigned port); void outw (unsigned short word, unsigned port); /* inl/outl: 读/ 写32位 端口。longword也是依赖体系的,有的体系为unsigned long;而有的为unsigned int */ unsigned inl (unsigned port); void outl (unsigned longword, unsigned port);
从现在开始,当我们使用 unsigned 没有进一步指定类型时,表示是一个依赖体系的定义。
注意,没有 64 位的 I/O 端口操作函数。即便在 64 位体系中,端口地址空间使用一个 32 位 ( 最大 ) 的数据通路。
1.3 、从用户空间访问 I/O 端口
1.2 节介绍的函数主要是提供给驱动使用,但它们也可在用户空间使用,至少在 PC 机上可以。 GNU C 库在 <sys/io.h> 中定义它们。如果在用户空间使用这些函数,必须满足下列条件:
1 )、程序必须使用 -O 选项编译来强制扩展内联函数
2 )、必须使用 ioperm 和 iopl 系统调用 (#include <sys/perm.h>) 来获得进行操作 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为开始的内存空间 */ void insb (unsigned port, void *addr, unsigned long count); /* outsb:将内存地址addr 开始的count个数据(单位字节)写到I/O端口port */ void outsb (unsigned port, void *addr, unsigned long count); /* insw:从I/O 端口port读取count个数据(单位字)到以内存地址addr为开始的内存空间 */ void insw (unsigned port, void *addr, unsigned long count); /* outsw:将内存地址addr 开始的count个数据(单位字)写到I/O端口port */ void outsw (unsigned port, void *addr, unsigned long count); /* insl:从I/O 端口port读取count个数据(单位双字)到以内存地址addr为开始的内存空间 */ void insl (unsigned port, void *addr, unsigned long count); /* outsl:将内存地址addr 开始的count个数据(单位双字)写到I/O端口port */ void outsl(unsigned port, void *addr, unsigned long count);
注意:使用字串函数时,它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。使用 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 宏的定义:
#define outb_p (val,port) outb((val),(port)) #define outw_p (val,port) outw((val),(port)) #define outl_p (val,port) outl((val),(port)) #define inb_p ( port) inb((port)) #define inw_p (port) inw((port)) #define inl_p (port) inl((port)) #define outsb_p (port,from,len) outsb(port,from,len) #define outsw_p (port,from,len) outsw(port,from,len) #define outsl_p (port,from,len) outsl(port,from,len) #define insb_p (port,to,len) insb(port,to,len) #define insw_p (port,to,len) insw(port,to,len) #define insl_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 参数的类型为 unsigned short 。
ARM
端口映射到内存,并且支持本章介绍的所有函数; port 参数的类型为 unsigned int ;字串函数用 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 内存区在使用前必须先分配。分配内存区的函数接口在 <linux/ioport.h> 定义中:
/* request_mem_region分配一个开始于start,len 字节的I/O内存区。分配成功,返回一个非NULL指针;否则返回NULL。系统当前所有I/O内存分配信息都在/proc/iomem文件中列出,你分配失败时,可以看看该文件,看谁先占用了该内存区 */ struct resource *request_mem_region (unsigned long start, unsigned long len, char *name); /* release_mem_region用于释放不再需要的I/O 内存区 */ void release_mem_region (unsigned long start, unsigned long len); /* check_mem_region用于检查I/O 内存区的可用性。同样,该函数不安全,不推荐使用 */ int check_mem_region (unsigned long start, unsigned long len);
在访问 I/O 内存之前,分配 I/O 内存并不是唯一要求的步骤,你还必须保证内核可存取该 I/O 内存。访问 I/O 内存并不只是简单解引用指针, 在许多体系中, I/O 内存无法 以这种方式 直接存取 。因此,还 必须通过 ioremap 函数设置一个映射 。
#include <asm/io.h> /* ioremap 用于将I/O 内存区映射到虚拟地址。参数phys_addr为要映射的I/O内存起始地址,参数size为要映射的I/O内存的大小,返回值为被映射到的虚拟地址 */ void *ioremap (unsigned long phys_addr, unsigned long size);
/* ioremap_nocache为ioremap的无缓存版本。实际上,在大部分体系中,ioremap与ioremap_nocache的实现一样的,因为所有 I/O 内存都是在无缓存的内存地址空间中 */ void *ioremap_nocache (unsigned long phys_addr, unsigned long size); /* iounmap用于释放不再需要的映射 */ void iounmap(void * addr);
经过 ioremap ( 和 iounmap) 之后,设备驱动就可以存取任何 I/O 内存地址。 注意, ioremap 返回的地址不可以直接解引用;相反,应当使用内核提供的访问函数。
2.2 、访问 I/O 内存
访问 I/O 内存的正确方式是通过一系列专门用于实现此目的的函数:
#include <asm/io.h> /* I/O 内存读函数 。参数addr应当是从ioremap获得的地址(可能包含一个整型偏移); 返回值是从给定I/O内存读取到的值 */ unsigned int ioread8 (void *addr); unsigned int ioread16 (void *addr); unsigned int ioread32 (void *addr); /* I/O 内存写函数 。参数addr同I/O内存读函数,参数value为要写的值 */ void iowrite8 (u8 value, void *addr); void iowrite16 (u16 value, void *addr); void iowrite32 (u32 value, void *addr); /* 以下这些函数读和写一系列值到一个给定的 I/O 内存地址,从给定的buf 读或写count个值到给定的addr。参数count表示要读写的数据个数,而不是字节大小 */ void ioread8_rep (void *addr, void *buf, unsigned long count); void ioread16_rep (void *addr, void *buf, unsigned long count); void ioread32_rep (void *addr, void *buf, unsigned long count); void iowrite8_rep (void *addr, const void *buf, unsigned long count); void iowrite16_rep (void *addr, const void *buf, unsigned long count); void iowrite32_rep (void *addr,,onst void *buf,,nsigned long count); /* 需要操作一块I/O 地址时,使用下列函数(这些函数的行为类似于它们的C库类似函数): */ void memset_io (void *addr, u8 value, unsigned int count); void memcpy_fromio (void *dest, void *source, unsigned int count); void memcpy_toio (void *dest, void *source, unsigned int count); /* 旧的I/O内存读写函数,不推荐使用 */ unsigned readb(address); unsigned readw(address); unsigned readl(address); void writeb(unsigned value, address); void writew(unsigned value, address); void writel(unsigned value, 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 (unsigned long port, unsigned int count); /* ioport_unmap用于释放不再需要的映射 */ void ioport_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 <asm-arm/io.h>
/* * Generic IO read/write. These perform native-endian accesses. Note * that some architectures will want to re-define __raw_{read,write}w. */ extern void __raw_writesb(void __iomem *addr, const void *data, int bytelen); extern void __raw_writesw(void __iomem *addr, const void *data, int wordlen); extern void __raw_writesl(void __iomem *addr, const void *data, int longlen); extern void __raw_readsb(const void __iomem *addr, void *data, int bytelen); extern void __raw_readsw(const void __iomem *addr, void *data, int wordlen); extern void __raw_readsl(const void __iomem *addr, void *data, int longlen); #define __raw_writeb(v,a) (__chk_io_ptr(a), *(volatile unsigned char __force *)(a) =(v)) #define __raw_writew(v,a) (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) =(v)) #define __raw_writel(v,a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) =(v)) #define __raw_readb(a) (__chk_io_ptr(a), *(volatile unsigned char __force *)(a)) #define __raw_readw(a) (__chk_io_ptr(a), *(volatile unsigned short __force *)(a)) #define __raw_readl(a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a))
关于 __force 和 __iomem
#include <linux/compiler.h>
/* __force表示所定义的变量类型是可以做强制类型转换的 */ #define __force __attribute__((force)) /* __iomem是用来修饰一个变量的,这个变量必须是非解引用(no dereference)的,即这个变量地址必须是有效的,而且变量所在的地址空间必须是2,即设备地址映射空间。0表示normal space,即普通地址空间,对内核代码来说,当然就是内核空间地址了。1表示用户地址空间,2表示是设备地址映射空间 */ #define __iomem __attribute__((noderef, address_space(2)))
I/O 端口
#include <asm-arm/io.h>
#define outb(v,p) __raw_writeb(v,__io(p)) #define outw(v,p) __raw_writew((__force __u16) \\ cpu_to_le16(v),__io(p)) #define outl(v,p) __raw_writel((__force __u32) \\ cpu_to_le32(v),__io(p)) #define inb(p) ({ __u8 __v = __raw_readb(__io(p)); __v; }) #define inw(p) ({ __u16 __v = le16_to_cpu((__force __le16) \\ __raw_readw(__io(p))); __v; }) #define inl(p) ({ __u32 __v = le32_to_cpu((__force __le32) \\ __raw_readl(__io(p))); __v; }) #define outsb(p,d,l) __raw_writesb(__io(p),d,l) #define outsw(p,d,l) __raw_writesw(__io(p),d,l) #define outsl(p,d,l) __raw_writesl(__io(p),d,l) #define insb(p,d,l) __raw_readsb(__io(p),d,l) #define insw(p,d,l) __raw_readsw(__io(p),d,l) #define insl(p,d,l) __raw_read
以上是关于IO端口和IO内存的区别及分别使用的函数接口的主要内容,如果未能解决你的问题,请参考以下文章
IO 端口和IO 内存(原理篇)
linux 系统对IO端口和IO内存的管理
I/O端口和IO内存
I/O端口和IO内存
IO模型及select,poll,epoll和kqueue的区别
安装文件时输入和输出的io接口错误,啥原因?