访问IO设备

一、I/O端口和I/O内存

1.1 I/O端口和I/O内存的概念

外设都是通过读写其寄存器来进行访问的,可以通过寄存器来对其进行配置、获取其运行状态。
由于不是处理器自己的寄存器,因而无法直接使用指令访问,外设的寄存器需要通过其地址来进行访问。外设寄存器的地址可能位于内存地址空间也可能位于单独的I/O地址空间。
在为外设提供了单独的I/O地址空间的架构上,处理器提供了称为I/O端口的独立的线路来访问I/O地址空间,并且使用特殊的CPU指令来访问I/O端口。
对于使用统一的地址空间来访问外设的架构来说,有时候为了适应设备所连接的总线的需求,也需要在访问外设时模拟成使用I/O端口来访问。因此linux在所有的架构上都实现了I/O端口的概念。
但是需要注意的是,即便在提供了独立的I/O空间来访问外设的架构上,也不是所有的外设都会使用I/O端口来访问外设,比如大部分PCI设备都将自己的寄存器映射到了某个内存地址段,从而可以直接通过读写内存来访问这些外设。相对来说,使用内存来访问外设的方法更优一些,因为它不需要使用特殊的指令,CPU访问内存也更有效。

1.2 访问硬件

在访问硬件寄存器时,必须避免由于CPU或编译器的优化而导致出现非预期的行为。
常规的内存访问就是对一个内存地址的读写,就是写一个数据到一个地址或者从一个地址读取一个数据。由于内存读写对CPU性能太过重要,因而存在很多机制和方法对内存读写进行优化,比如使用高速缓存,重新排序读/写指令等。
但是对于访问硬件的场景来说就不同了,对硬件寄存器的访问是要控制硬件的行为或者获取硬件的当前状态,我们不期望对这种操作进行缓存,而且硬件的访问有时是有其固定的顺序的。典型的比如网卡的发送行为,你必须在准备好你的发送数据后,才能讲数据提交给硬件,否则你肯定会得到不是你所期望的结果。正是因为这些特殊之处,因而对硬件的访问不能使用高速缓存,其指令顺序不能被编译器优化。
不使用高速缓存可以将底层硬件配置成在访问I/O区域时禁止硬件缓存。而至于禁止编译器优化硬件访问指令顺序则需要通过内存屏障来实现,linux内核提供了如下几个宏来实现内存屏障:
void barrier(void);
该函数告知编译器插入一个内存屏障,但是该内存屏障对硬件没有影响。其作用是:编译后的代码会把当前CPU的所有修改过的数值保存到内存中,需要这些数据时再重新读出来。它可以防止编译器的优化,但是无法防止硬件优化。
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
这些函数用于插入硬件内存屏障,它们是架构相关的。其作用如下:
rmb保证屏障之前的读操作一定会在屏障之后的读操作被执行之前结束。
wmb保证屏障之前的写操作一定会在屏障之后的写操作被执行之前结束。
mb同时具有两者的功能。
read_barrier_depends是读屏障的一个特殊的形式。与rmb的不同之处在于,它保证的是在屏障之前的读操作完成之前,屏障之后的和屏障之前的读相关的读(即屏障之后的依赖于屏障之前的读的读)操作不会被执行。
它们的smp版本如下:
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
使用内存屏障会影响系统性能,因而它们只能被用于确实需要进行屏障的位置,另外大多数内核同步原语都有内存屏障的功效。

二、I/O 端口

I/O端口也是一种系统资源,内核提供了一套API来管理他们。

2.1 I/O 端口分配

如果想要使用I/O端口,首先需要向系统申请I/O端口的使用权,这是通过以下API来完成的。
request_region(start,n,name);
这是一个宏,定义在”include/linux/ioport.h”中,参数含义分别为:
start:起始端口号
n:使用多少个端口
name:申请使用这些I/O端口的使用者名字
它返回一个struct resource数据结构指针,如果申请失败,则返回NULL。
但使用完I/O端口时,要通过如下API来释放I/O端口资源:
release_region(start,n);
其参数含义同申请时的参数。

2.2 使用I/O 端口

在申请到了I/O端口之后,就可以使用它们来访问硬件了,由于硬件会区分8位,16位,32位的端口,因此不能向使用普通内存一样使用它们。
内核提供了如下API来提供8位,16位,32位的I/O访问:
u8 inb(unsigned long addr)
u16 inw(unsigned long addr)
u32 inl(unsigned long addr)
以上三个分别用于读取8位,16位,32位的I/O端口.
void outb(u8 b, unsigned long addr)
void outw(u16 b, unsigned long addr)
void outl(u32 b, unsigned long addr)
以上三个分别用于写8位,16位,32位的I/O端口.
内核还提供了一套API来实现读写连续的序列,如下:
void insb(unsigned long addr, void *buffer, int count)
void insw(unsigned long addr, void *buffer, int count)
void insl(unsigned long addr, void *buffer, int count)
以上三个函数分别用于从addr开始连续读取count个8位,16位,32位的值到buffer中。实际上它们就是对应的一次读取一个值的函数的包装器。
void outsb (unsigned long addr, const void *buffer, int count)
void outsw (unsigned long addr, const void *buffer, int count)
void outsl (unsigned long addr, const void *buffer, int count)
以上三个函数分别用于将buffer中的count个8位,16位,32位的值写到addr中。

三、I/O 内存

I/O内存可以是映射到内存空间的寄存器或者设备内存。它是类似于RAM的区域,可以通过对其进行读写来访问设备。
由于架构和总线的不同,I/O内存可能通过页表也可能不通过页表来访问。如果通过页表访问,则内核必须首先使得这些物理地址对驱动程序是可见的(这通常需要在进行任何I/O之前首先调用ioremap来实现)。如果无需页表,则I/O内存就和I/O端口一样,需要使用适当的函数来访问。
无论是否需要使用ioremap,都不要直接使用指向I/O内存的指针,最好是通过系统提供的API来进行。

3.1 I/O内存分配和映射

类似于I/O端口,I/O内存也必须在使用前进行分配。其原型如下定义在“include/linux/ioport.h”:
request_mem_region(start,n,name);
这是一个宏,它的参数的含义如下:
start:起始地址
n:内存长度
name:申请这片内存区域的使用权的部件的名字
它在分配成功时返回一个指向struct resource数据结构指针,如果申请失败,则返回NULL
系统所分配的所有I/O内存可以通过/proc/iomem查看。
在使用完I/O内存后需要通过如下API来释放它:
release_mem_region(start,n)
这也是一个宏,用于释放申请到的I/O内存。
内核还提供了另外一个API:
check_mem_region(start,n)
该宏用于检查从start开始的长度为n的I/O内存是否正在被使用(即是否忙)。
除了分配I/O内存之外,我们还必须确保所指定的I/O内存对内核来说是可以用访问的。大多数架构下,I/O内存无法直接读写,使用者需要首先建立映射,建立映射使用ioremap来完成。
在调用完ioremap后,I/O内存即可使用了。但是在使用I/O内存时,建议使用内核提供的访问函数,而不是直接使用。

3.2 存取 I/O 内存

内核提供了一套API用于读写I/O内存,这些API如下:
ioread8(addr)
ioread16(addr);
ioread32(addr);
以上三个API分别用于从指定的I/O内存地址读取8位,16位,32位的值。Addr取值应该在ioremap返回的地址以及ioremap返回的地址+该片I/O内存大小之间。
iowrite8(v, addr);
iowrite16(v, addr);
iowrite32(v, addr);
以上三个API分别用于将8位,16位,32位的v写到指定的I/O内存地址。Addr的含义同读。
类似于I/O端口的访问,I/O内存的访问也有一个包装器版本用于一次读写一个序列。详见文件“include/asm-generic/io.h”。
如果要在一块I/O内存上执行操作,可以使用如下的API:
memset_io(a, b, c)
该API用于将从a开始的大小为c的I/O内存的内容设置为b
memcpy_fromio(a, b, c)
该API用于将从b开始的大小为c的I/O内存的内容拷贝到从a开始的内存。
memcpy_toio(a, b, c)
该API用于将从b开始的大小为c的内存的内容拷贝到从a开始的I/O内存。
如果硬件同时支持使用I/O端口和I/O内存来访问,则可以通过使用如下API来将I/O端口转变成I/O内存:
void __iomem *ioport_map(unsigned long port, unsigned int len);
它将从port开始的len个端口映射成I/O内存。在映射完后即可使用得到的I/O内存。
在使用玩这样的I/O内存后,需要使用
void ioport_unmap(void __iomem *addr)来解除映射。

猜你喜欢

转载自blog.csdn.net/goodluckwhh/article/details/16986871
今日推荐