Chinaunix首页 | 论坛 | 博客
  • 博客访问: 6478855
  • 博文数量: 579
  • 博客积分: 1548
  • 博客等级: 上尉
  • 技术积分: 16635
  • 用 户 组: 普通用户
  • 注册时间: 2012-12-12 15:29
个人简介

http://www.csdn.net/ http://www.arm.com/zh/ https://www.kernel.org/ http://www.linuxpk.com/ http://www.51develop.net/ http://linux.chinaitlab.com/ http://www.embeddedlinux.org.cn http://bbs.pediy.com/

文章分类

全部博文(579)

文章存档

2018年(18)

2015年(91)

2014年(159)

2013年(231)

2012年(80)

分类:

2012-12-14 18:12:39

1I/O 端口和 I/O 内存

每个外设都是通过读写其寄存器来控制的。通常一个设备有几个寄存器,它们位于内存地址空间或者I/O地址空间,并且地址是连续的。

在硬件层上,内存区和I/O区域没有概念上的区别它们都是通过在地址总线和控制总线上发出电信号来存取(即,读写信号),并且通过数据总线来读写数据。

在一些CPU制造商在其芯片上实现了一个单地址空间(统一编址)的同时,其它的CPU制造商认为外设不同于内存,应该有一个独立的地址空间给外设(单独编址),其生产处理器(特别是x86家族)I/O端口有自己的读写信号线和特殊的CPU指令来存取端口。因为外设要与外设总线相匹配,并且大部分流行的I/O总线都是以个人计算机(主要是x86家族)作为模型,所以即便那些没有单独地址空间给I/O端口的处理器,也必须在访问外设时模拟成读写端口。这通常通过外部芯片组(PC中的南北桥)或者在CPU核中附加额外电路来实现(基于嵌入式应用的处理器)。关于编址方式、I/O 端口和 I/O 内存更详细内容见http://blog.chinaunix.net/u3/96613/showart_1926286.html

由于同样的理由,Linux在所有计算机平台上都实现了I/O端口,甚至在那些单地址空间的CPU平台上(模拟I/O端口)。并不是所有的设备都会将其寄存器映射到I/O端口。虽然ISA设备普遍使用I/O端口,但大部分PCI设备将寄存器映射到某个内存地址区。这种I/O内存方法通常是首选的,因为它无需使用特殊的处理器指令,CPU存取内存也更有效率,并且编译器在存取内存时在寄存器分配和寻址模式的选择上有更多自由。

1.1I/O 寄存器和常规内存

I/O寄存器和RAM的主要不同是I/O操作有边际效应side  effect而内存操作没有:访问内存只是在内存某一位置存储数值。因为内存存取速度严重影响CPU的性能,编译器可能会对源码进行优化,主要是:使用高速缓存和重排读/写指令的顺序。对于传统内存(至少在单处理器系统)这些优化是透明有益的,但是对于I/O 寄存器,这可能是致命错误,因为它们干扰了那些"边际效应"(驱动程序存取I/O 寄存器就是为了获取边际效应)。因此,驱动程序必须确保在存取寄存器时,不能使用高速缓存并且不能重新编排读写指令的顺序。

side  effect  是指:访问I/O寄存器时,不仅仅会像访问普通内存一样影响存储单元的值,更重要的是它可能改变CPU的I/O端口电平、输出时序或CPU对I/O端口电 平的反应等等,从而实现CPU的控制功能。CPU在电路中的意义就是实现其side effect  。举个例子,有些设备的中断状态寄存器只要一读取,便自动清零。

硬件缓冲的问题是最易解决的:只要将底层硬件配置(或者自动地或者通过Linux 初始化代码)为当存取I/O区时,禁止任何硬件缓冲(不管是I/O 内存还是I/O 端口)。

编译器优化和硬件重编排读写指令顺序的解决方法是:在硬件或处理器必须以一个特定顺序执行的操作之间安放一个内存屏障(memory  barrierLinux 提供以下几个宏来实现这个功能:

#include <linux/kernel.h>
/* barrier(软件内存屏障)告知编译器插入一个内存屏障但是对硬件没有影响。编译后的代码将当前CPU寄存器所有修改过的值保存到内存,并且在需要时重新读取它们。barrier可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序 */
void barrier(void)

#include <asm/system.h>
/* rmb(read memory barrier)保证任何出现于屏障前的读在执行任何后续读之前完成 */
void rmb(void);
/*  read_barrier_depends是一种特殊的、弱些的读屏障形式。rmb  阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小,  并且不是所有体系都支持。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/
void read_barrier_depends(void);
/* wmb(write memory barrier)保证任何出现于屏障前的写在执行任何后续的写之前完成 */
void wmb(void);
/* rmb(memory barrier)保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成 */
void mb(void);
/* 以上这些宏都是是barrier的超集,它们插入硬件内存屏障在编译的指令流中,并且它们的实际实现是平台相关的 */


/* 以下这些屏障宏仅当SMP系统时插入硬件屏障;否则它们只是简单被替换成barrier宏 */
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);


典型例子:
 

writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);

wmb( ); /* 在开始执行操作(最后一个写操作)之前,先将各相关控制寄存器设置好(前三个写操作)。其实wmb就是一条分界线,它保证其之前的三个写操作执行完了之后,才执行其后的写操作,但是前面三个写操作顺序则无法保证  */

writel(dev->registers.control, DEV_GO);

 

内存屏障影响系统性能,所以只能在确实需要它们的地方使用。不同类型的屏障也有不同的性能特性,因此,应当尽可能使用最合适的屏障类型

值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t操作,也具有内存屏障的功能。还有就是有些外设总线(PCI 总线)有它们自己的缓冲问题我们在后面章节遇到时讨论这些问题

某些体系允许将赋值和内存屏障组合在一起,以提高效率。们如下定义:

#define set_mb(varvalue) do {var = value; mb();} while 0
#define set_wmb(var,,value) do {var = value; wmb();} while 0
#define set_rmb(var,,value) do {var = value; rmb();} while 0

在合适的地方,为了更快的完成任务,<asm/system.h> 使用体系特定的(architecture-specific)指令来定义这些宏。注意,很少体系支持set_rmb使用do...while 结构来定义宏是标准C的惯用方法(在内核源码中非常常见),它使被扩展的宏可在所有上下文环境中作为一个正常的C语句被执行

 

2、使用I/O 端口

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

2.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);


2.2、操作I/O端口

在驱动成功请求到I/O 端口后,就可以读写这些端口了。部分硬件会将81632位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。

如同前面所讲的,仅支持单地址空间的计算机体系通过将I/O端口地址重新映射到内存地址来伪装端口I/O 。为了提高移植性,内核对驱动隐藏了这些细节。Linux 内核头文件(系依赖的头文件定义了下列内联函数来存取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(最大)的数据通路。

2.3、从用户空间访问I/O端口

2.2节介绍的函数主要是提供给驱动使用,但它们也可在用户空间使用,至少在PC机上可以GNU C 库在  中定义它们。如果在用户空间使用这些函数,必须满足下列条件:

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

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

3)、程序必须以root来调用ioperm或者iopl,或者其父进程(祖先)必须以root获得的端口操作权限

如果平台不支持iopermiopl系统调用,通过使用/dev/prot设备文件,用户空间仍然可以存取I/O 端口。但是要注意的是,这个文件的定义也是依赖平台的

2.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 读取端口应在必要时自行转换字节序,以匹配主机字节序。

2.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 同样的代码。

2.6、平台依赖性

于自身的特性,I/O指令高度依赖于处理器,非常难以隐藏各体系间的不同。因此,大部分的关于端口  I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:

IA-32 (x86)

x86_64

这个体系支持本章介绍的所有函数;port参数的类型为unsigned short。

ARM

端口映射到内存,并且支持本章介绍的所有函数;port参数的类型为unsigned int;字串函数用C语言实现。

 

3、使用 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 内存的情况进行了优化。

3.1I/O 内存分配和映射

I/O 内存区在使用前必须先分配。分配内存区的函数接口定义中:

/*   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 函数(第8章第4节介绍过)设置一个映射。

#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返回的地址不可以直接解引用相反,应当使用内核提供的访问函数。

3.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 *bufunsigned 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管理员在2009年8月13日编辑了该文章文章。

-->
阅读(1643) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~