I/O端口 VS I/O内存
知识点:
1.每种外设都是通过读写寄存器进行控制。
2.在硬件层,内存区域和I/O区域没有概念上的区别:他们都通过像地址总线和控制总线发送电平信号进行访问,在通过数据总线读写数据。
3.

4.

I/O寄存器 VS 常规内存
1.I/O寄存器与RAM的最主要区别就是I/O操作具有边际效应(个人理解就是副作用):读取某个地址时可能导致这个地址的内容发生变化,比如很多中断寄存器的值一经读取,便自动清零。
2.编译器可以将数据缓存在CPU寄存器而不用写入内存,即使存储数据,读写操作也都能在高速缓存中进行而不用访问物理内存。
3.常规内存可以优化,但是对寄存器进行优化可能导致致命错误。驱动程序一定要保证不适用那个高速缓存,并且,在访问寄存器时不发生读或写指令的重新排序。
4.由硬件自身缓存引起的问题很好解决:只要在把底层硬件配置成(自动配置或是linux初始化代码完成)在访问I/O区域(不管是内存还是端口)时禁止硬件缓存即可。
5.由编译器优化和硬件重新排序引起的问题的解决办法:对硬件或其他处理器必须以特定顺序执行的操作之间设置内存屏障。linux有四个宏解决所有可能的排序问题。
#include<linux/kernel.h> void barrier(void); 对barrier的调用可防止在屏障前后的编译器优化,但硬件能完成自己的重新排序。 |
#include<asm/system.h> void rmb(void); /*保证任何出现于屏障前的读在执行任何后续的读之前完成*/ void wmb(void); /*保证任何出现于屏障前的写在执行任何后续的写之前完成*/ void mb(void); /*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/ void read_barrier_depends(void); /*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/ /*以上指令是barrier的超集*/ void smp_rmb(void); void smp_read_barrier_depends(void); void smp_wmb(void); void smp_mb(void); /*仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用。*/ |
典型的应用:
writel(dev->registers.addr, io_destination_address); writel(dev->registers.size, io_size); writel(dev->registers.operation, DEV_READ); wmb();/*类似一条分界线,上面的写操作必然会在下面的写操作前完成,但是上面的三个写操作的排序无法保证*/ writel(dev->registers.control, DEV_GO); |
内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。
某些体系允许赋值和内存屏障组合,以提高效率。它们定义如下:
#define set_mb(var, value)do {var = value; mb();}while 0 /*以下宏定义在ARM体系中不存在*/ #define set_wmb(var, value)do {var = value; wmb();}while 0 #define set_rmb(var, value)do {var = value; rmb();}while 0 |
使用do...while 结构来构造宏是标准 C 的惯用方法,它保证了扩展后的宏可在所有上下文环境中被作为一个正常的 C 语句执行。
使用I/O端口
对I/O端口的使用:申请,访问,释放
1.申请:
#include<linux/ioport.h> struct resource *request_region(unsigned long first,unsigned long n,const char *name); 该函数通知内核我们要使用起始于first的n个端口。name应该是设备的名称。若申请成功,返回非NULL,失败,返回NULL |
所有的端口分配可从/proc/ioports中得到,如果我们无法分配到需要的端口集合,则可以从这个文件中得知哪个驱动程序已经分配了这个端口。
2.访问:
unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port); /*读/写字节端口( 8 位宽 )。port 参数某些平台定义为 unsigned long ,有些为 unsigned short 。 inb 的返回类型也体系而不同。*/
|
unsigned inw(unsigned port); void outw(unsigned short word, unsigned port); /*访问 16位 端口( 一个字宽 )*/ |
unsigned inl(unsigned port); void outl(unsigned longword, unsigned port); /*访问 32位 端口。 longword 声明有的平台为 unsigned long ,有的为 unsigned int。*/ |
3.释放:
void release_region(unsigned long start,unsigned long n); 当不再使用I/O端口,或者卸载模块时应使用该函数将这些端口返回给系统。 |
int check_region(unsigned long first,unsigned long n); 该函数用来检测给定的I/O端口是否可用。 |
在用户空间访问I/O端口
以上函数主要用在驱动程序的使用上的,但他们也可以在用户空间使用,至少在PC类计算机上可以使用。GNU的C库在<sys/io.h>中定义了这些函数。如果要在用户空间代码中使用inb及相关函数,则必须满足下面这些条件:
程序必须使用 -O 选项编译来强制扩展内联函数 |
必须用ioperm 和 iopl 系统调用(#include <sys/perm.h>) 来获得对端口 I/O 操作的权限。ioperm 为获取单独端口操作权限,而 iopl 为整个 I/O 空间的操作权限。 (x86 特有的) |
程序以 root 来调用 ioperm 和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的) |
若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。
串操作
除了一次传递一个数据的I/O操作,linux还提供了一次传递一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:
void insb(unsigned port, void *addr, unsigned long count); void outsb(unsigned port, void *addr, unsigned long count); 字节端口读写 |
void insw(unsigned port, void *addr, unsigned long count); void outsw(unsigned port, void *addr, unsigned long count); 16位端口读写 |
void insl(unsigned port, void *addr, unsigned long count); void outsl(unsigned port, void *addr, unsigned long count); 32位端口读写,即使在64位的体系结构上,端口地址也只使用最大32位的数据通路 |
暂停式I/O
为了匹配低速外设的速度,有时若 I/O 指令后面还紧跟着另一个类似的I/O指令,就必须在 I/O 指令后面插入一个小延时。在这种情况下,可以使用暂停式的I/O函数代替通常的I/O函数,它们的名字以 _p 结尾,如 inb_p、outb_p等等。 这些函数定义被大部分体系支持,尽管它们常常被扩展为与非暂停式I/O 同样的代码。因为如果体系使用一个合理的现代外设总线,就没有必要额外暂停。细节可参考平台的 asm 子目录的 io.h 文件。以下是include\asm-arm\io.h中的宏定义:
#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 同样的代码。
平台相关性
由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:
IA-32 (x86)
x86_64
这个体系支持所有的以上描述的函数,端口号是 unsigned short 类型。
ARM
端口映射到内存,支持所有函数。串操作 用C语言实现。端口是 unsigned int 类型。
使用 I/O 内存
除了 x86上普遍使用的I/O 端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器或设备内存,统称为 I/O 内存。因为寄存器和内存之间的区别对软件是透明的。I/O 内存仅仅是类似 RAM 的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
根据平台和总线的不同,I/O 内存可以就是否通过页表访问分类。若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何 I/O 之前必须调用 ioremap。若不通过页表,I/O 内存区域就类似I/O 端口,可以使用适当形式的函数访问它们。因为“side effect”的影响,
不管是否需要 ioremap ,都不鼓励直接使用 I/O 内存的指针。而使用专用的 I/O 内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。
I/O 内存分配和映射
I/O 内存区域使用前必须先分配,函数接口在 <linux/ioport.h> 定义:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);/* 从 start 开始,分配一个 len 字节的内存区域。成功返回一个非NULL指针,否则返回NULL。所有的 I/O 内存分配情况都 /proc/iomem 中列出。*/ /*I/O内存区域在不再需要时应当释放*/ void release_mem_region(unsigned long start, unsigned long len); /*一个旧的检查 I/O 内存区可用性的函数,不推荐使用*/ int check_mem_region(unsigned long start, unsigned long len); |
然后必须设置一个映射,由 ioremap 函数实现,此函数专门用来为I/O 内存区域分配虚拟地址。经过ioremap 之后,设备驱动即可访问任意的 I/O 内存地址。注意:ioremap 返回的地址不应当直接引用;应使用内核提供的 accessor 函数。以下为函数定义:
#include <asm/io.h> void *ioremap(unsigned long phys_addr, unsigned long size); void *ioremap_nocache(unsigned long phys_addr, unsigned long size);/*如果控制寄存器也在该区域,应使用的非缓存版本,以实现side effect。*/ void iounmap(void * addr); |
访问I/O 内存
访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在 <asm/io.h> 中定义的):
/*I/O 内存读函数*/ unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); /*addr 是从 ioremap 获得的地址(可能包含一个整型偏移量), 返回值是从给定 I/O 内存读取的值*/ /*对应的I/O 内存写函数*/ void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr); /*读和写一系列值到一个给定的 I/O 内存地址,从给定的 buf 读或写 count 个值到给定的 addr */ 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, const void *buf, unsigned long count); /*需要操作一块 I/O 地址,使用一下函数*/ 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); /*旧函数接口,仍可工作, 但不推荐。*/ unsigned readb(address); unsigned readw(address); unsigned readl(address); void writeb(unsigned value, address); void writew(unsigned value, address); void writel(unsigned value, address); |
像 I/O 内存一样使用端口
一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:
void *ioport_map(unsigned long port, unsigned int count);/*重映射 count 个I/O 端口,使其看起来像 I/O 内存。,此后,驱动程序可以在返回的地址上使用 ioread8 和同类函数。其在编程时消除了I/O 端口和I/O 内存的区别。 /*这个映射应当在它不再被使用时撤销:*/ void ioport_unmap(void *addr); /*注意:I/O 端口仍然必须在重映射前使用 request_region 分配I/O 端口。ARM9不支持这两个函数!*/ |