分类: LINUX
2012-02-21 13:17:37
在学习之前,首先了解下ARM和PC总线的结构。
I/O端口和I/O内存
每种外设都是通过读写寄存器来进行控制。
在硬件层,内存区和 I/O 区域没有概念上的区别: 它们都是通过向在地址总线和控制总线发出电平信号来进行访问,再通过数据总线读写数据。
因为外设要与I/O总线匹配,而大部分流行的 I/O 总线是基于个人计算机模型(主要是 x86 家族:它为读和写 I/O 端口提供了独立的线路和特殊的 CPU 指令),所以即便那些没有单独I/O 端口地址空间的处理器,在访问外设时也要模拟成读写I/O端口。这一功能通常由外围芯片组(PC 中的南北桥)或 CPU 中的附加电路实现(嵌入式中的方法) 。
Linux 在所有的计算机平台上实现了 I/O 端口。但不是所有的设备都将寄存器映射到 I/O 端口。虽然ISA设备普遍使用 I/O 端口,但大部分 PCI 设备则把寄存器映射到某个内存地址区,这种 I/O 内存方法通常是首选的。因为它无需使用特殊的处理器指令,CPU 核访问内存更有效率,且编译器在访问内存时在寄存器分配和寻址模式的选择上有更多自由。
I/O寄存器和常规内存
尽管硬件寄存器和内存非常相似,但程序员在访问I/O寄存器的时候必须注意避免由于CPU或编译器不恰当的优化而改变预期的I/O操作(也即对寄存器的地址都声明为volatile)。
I/O寄存器和RAM的最主要区别就是I/O操作具有边际效应(其实边际效应就是对I/O寄存器操作,导致高低电平的变化,从而促使硬件进行相对应的行为)。
因为存储单元的访问速度对 CPU 性能至关重要,编译器会对源代码进行优化,主要是: 使用高速缓存保存数值 和 重新编排读/写指令顺序。但对I/O 寄存器操作来说,这些优化可能造成致命错误。因此,驱动程序必须确保在操作I/O 寄存器时,不使用高速缓存,且不能重新编排读/写指令顺序。
解决的方法:
对于硬件自身缓存引起的问题:只要把底层硬件配置成在访问I/O区域时禁止硬件缓存即可。
对于编译器优化和硬件重新排序引起的问题:对硬件必须以特定顺序执行的操作之间设置内存屏障。linux提供了以下宏来解决可能的排序问题。
#include
void barrier(void) 这个函数通知编译器插入一个内存屏障,但对硬件没影响。编译后的代码会把当前CPU寄存器的所有修改过的数值保存到内存中,需要这些数据时再读出来。对barrier的调用,可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序。其实<linux/kernel.h> 中并没有这个函数,因为它是在kernel.h包含的头文件compiler.h中定义的*/
#include
#define barrier() _memory_barrier()
但在内核中也有如下定义方式:
#define barrier() _asm_volatile("":::"memory")
CPU越过内存屏障后,将刷新自已对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。
#include
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 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用。*/
这里介绍个小资料
1.内核中往往有如下语句:
#define _set_task_state(tsk,state_value) /
do {(tsk)->state = state_value;} while(0)
#define set_task_state(tsk,state_value) /
set_mb((tsk)->state,state_value)
两 者区别在于:set_task_state(tsk,state_value)带有一个memory barrier,而_set_task_state却没有。当task的state为RUNNING时,由于scheduler可能会访问这个 state,因此此时要改变为其他状态(如INTERRUPTIBLE),则应该用set_task_state来保证其原子性。而当state不为 RUNNING时,因为没有人会访问task,所以可以用_set_task_state。但用set_task_state总是安全的,但 _set_task_state会比较快。
2.在include/asm-i386/system.h中,定义了如下一条语句:
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory") 分析如下几点:
1)set_mb(),mb(),barrier()函数追踪到底,就是__asm__ __volatile__("":::"memory"),而这行代码就是内存屏障。
2)__asm__用于指示编译器在此插入汇编语句
3)__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
4)memory
强制gcc编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不
得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。
5)"":::表示这是个空指令。barrier()不用在此插入一条串行化汇编指令。在后文将讨论什么叫串行化指令。
6)__asm__,__volatile__,memory在前面已经解释
7)lock前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu的一个内存屏障。
8)addl
$0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。即这是一条无用的汇编
指令。在此利用这条无价值的汇编指令来配合lock指令,在__asm__,__volatile__,memory的作用下,用作cpu的内存屏障。
9)set_current_state()和__set_current_state()区别就不难看出。
10)至于barrier()就很易懂了。
3.#include
"void rmb(void);"
"void wmb(void);"
"void mb(void);"
这些函数
在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。
wmb 保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是
barrier函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是
普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以
正确的次序完成。其实在指令序列中放一个wmb的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb前面的写指令一定会在
wmb的写指令之前执行。
这里有篇文章,分析的很好:
典型的应用:
writel(dev->registers.addr, io_destination_address); |
内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。
某些体系允许赋值和内存屏障组合,以提高效率。它们定义如下:
#define set_mb(var, value) do {var = value; mb();} while 0 |
使用do..while来构成宏,使得宏展开后可以作为一个完整的语句。
使用I/O端口
I/O端口是驱动程序与许多设备之间进行通信的方式。
I/O端口分配
在尚未取得对这些端口的独占访问之前,不应对这些端口进行操作。内核提供了一个注册用的接口,他允许驱动程序声明自己要操作的端口。接口函数是request_region:
#include
struct resource *request_region(unsigned long first,unsigned long n,const char *name);
这个函数告诉内核,我们要使用起始于first的n个端口,参数name应该是设备的名称。如果分配成功,则返回为NULL值。如果request_region返回NULL,则不能使用这些期望的端口。
所有端口分配信息可从/proc/ioports中得到。
如果不再需要使用某组I/O端口,则应该使用下面的函数将这些端口释放掉。
void release_region(unsigned long start,unsigned long n);
下面的函数允许驱动程序检查给定的端口集是否可用:
int check_region(unsigned long first,unsigned long n);
check_region这个函数并不赞成使用,因为其检查过程不是原子的。但request_region驱动程序可以使用它,这个函数执行了必要的锁定。
操作I/O端口
当驱动程序请求了要使用的I/O端口范围后,必须读取或写入这些I/O端口。为此,大多数硬件都会把8位、16位、32位的端口区分开来。一般他们不能像访问系统内存那样使用。
因此,C语言必须调用不同的函数来访问大小不同的窗口。有些只支持内存映射的I/O寄存器的计算机体系架构通过把I/O端口地址重新映射到内存地址 来伪装端口I/O,并且为了易于移植,内核对驱动程序隐藏了这些细节。linux内核头文件中定义了如下一些访问I/O端口的内联函数。
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。*/
注意这里没有定义64为I/O端口操作,即使在64位的体系结构上,端口地址空间也只适用最大32位的数据通路。
用户空间访问I/O端口
以上函数主要提供给设备驱动使用,但它们也可在用户空间使用,至少在 PC上可以。 GNU C 库在
(1)程序必须使用 -O 选项编译来强制扩展内联函数。
(2)必须用ioperm 和 iopl 系统调用(#include
(3)程序以 root 来调用 ioperm 和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的)
若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。
串操作
除了一次传输一个数据的I/O操作,一些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指 令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:
void insb(unsigned port, void *addr, unsigned long count); |
使用时注意: 它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。 使用 inw 读取端口应在必要时自行转换字节序,以匹配主机字节序。然而串函数不会完成这种交换。
暂停式 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)) |
由此可见,由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。
平台相关性
由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:
解惑-驱动开发中的I/O地址空间:http://blog.chinaunix.net/u3/93255/showart.php?id=2301193
使用I/O内存
除了X86上普遍使用的I/O端口之外,和设备通信的另一种主要机制是通过使用映射到内存的寄存器或设备内存。这两种称为I/O内存。
I/O内存仅仅是类似RAM的一个区域,在那里处理器可以通过总线访问设备。这种内存用途很多,比如存放视频数据或以太网数据包,也可以用来实现类似I/O端口的设备寄存器(也就是说对他们的访问也具有边际效应)。
根据计算机平台和所使用总线的不同,I/O内存可能是也可能不是通过 页表访问的。如果访问是经由页表进行的,内核必须首先安排物理地址使其对设备驱动程序可见(通常意味着在进行任何I/O之前必须先调用ioremap)。 如果无需页表,那么I/O内存区域就非常类似于I/O端口,可以使用适当形式的函数访问他们。
因为“side effect”的影响,不管是否需要 ioremap ,都不鼓励直接使用 I/O 内存的指针。而使用专用的 I/O 内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。
I/O内存分配和映射
在使用I/O内存之前,必须首先分配I/O内存区域,用于分配内存区域的借口(
struct resource *request_mem_region(unsigned long start,unsigned long len,const char *name)
与分匹配内存端口strucr resource *request_region(unsigned long start,unsgined long len,const char *name)差不多。
该函数从start开始分配len字节长的内存区域,如果成功,返回非NULL指针;否则返回NULL,所有I/O内存分配情况均可从/proc/iomem中获得。
不再使用已分配的内存区域,使用下面的接口释放:
int release_mem_region(unsigned long start,unsigned long len);
/*一个旧的检查 I/O 内存区可用性的函数,不推荐使用*/
int check_mem_region(unsigned long start, unsigned long len);
分 配I/O内存并不是访问这些内存之前需要完成的唯一步骤,我们必须确保该I/O内存对内核是可访问的。获取I/O内存并不仅仅意味着可引用对应的指针;在 许多系统上,I/O内存根本不能通过这种方式直接访问。因此,我们必须首先建立映射。映射的建立由ioremap函数完成。该函数专门为I/O内存分配虚 拟地址。
一旦调用了ioremap函数,设备驱动程序即可访问任意的I/O内存地址,而无论I/O内存地址是否直接映射到虚拟地址空间。但是不可以直接访问ioremap返回的地址,而应该使用内核提供的accessor函数。
#include
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内存
在有些平台上,我们可以将ioremap的返回值直接当做指针使用。但这种方式不具有可移植性,我们应通过下面一组函数来访问I/O内存。
/*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内存一样使用端口
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端口。
关于ioport_map函数可以参考:
下面的转自:http://blog.chinaunix.net/u1/34474/showart.php?id=422428
s3c24x0处理器是使用I/O内存的,也就是说:他们的外设接口是通过读写相应的寄存器实现的,这些寄存器和内存是使用单一的地址空间,并使用和读写内存一样的指令。所以推荐使用I/O内存的相关指令。
但这并不表示I/O端口的指令在s3c24x0中不可用。但是只要你注意其源码,你就会发现:其实I/O端口的指令只是一个外壳,内部还是使用和I/O内存一样的代码。以下列出一些:
I/O端口
#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; })
I/O内存
#define ioread8(p) ({ unsigned int
__v = __raw_readb(p); __v; })
#define
ioread16(p) ({ unsigned
int __v = le16_to_cpu(__raw_readw(p)); __v; })
#define ioread32(p) ({ unsigned int
__v = le32_to_cpu(__raw_readl(p)); __v; })
#define
iowrite8(v,p) __raw_writeb(v, p)
#define iowrite16(v,p) __raw_writew(cpu_to_le16(v), p)
#define
iowrite32(v,p) __raw_writel(cpu_to_le32(v), p)
我对I/O端口的指令和I/O内存的指令都写了相应的驱动程序,都通过了测试。在这里值得注意的有4点:
(1)所有的读写指令所赋的地址必须都是虚拟地址,你有两种选择:使用内核已经定义好的地址,如 S3C2440_GPJCON等等,这些都是内核定义好的虚拟地址,有兴趣的可以看源码。还有一种方法就是使用自己用ioremap映射的虚拟地址。绝对不能使用实际的物理地址,否则会因为内核无法处理地址而出现oops。
(2)在使用I/O指令时,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因为request的功能只是告诉内核端口被谁占用了,如再次request,内核会制止。
(3)在使用I/O指令时,所赋的地址数据有时必须通过强制类型转换为 unsigned long ,不然会有警告(具体原因请看) 。虽然你的程序可能也可以使用,但是最好还是不要有警告为妙。
(4)在include/asm-arm/arch-s3c2410/hardware.h中定义了很多io口的操作函数,有需要可以在驱动中直接使用,很方便。