您的位置:首页 > 其它

IO端口和IO内存

2015-09-12 16:26 260 查看
一、IO端口与IO内存的概念

⒈ 概念

每个外设都是通过读写它的寄存器来控制。大部分时间一个设备有几个寄存器,并且在连续地址存取它们,或者在内存地址空间或者在I/O地址空间。在硬件级别上,内存区和I/O区域没有概念上的区别:它们都是通过在地址总线和控制总线上发出电信号来存取,并且读或写到数据总线上。

但有人认为外设不同于内存,因此应该有一个分开的地址空间,这样一些CPU制造商在他们的芯片上实现了一个单个地址空间,这些处理器(如X86系列)有分开的读和写信号给I/O端口和特殊的CPU指令来存取端口。这就是所谓的I/O总线(IO BUS)和内存总线(Mem BUS), 这样I/O地址空间与内存地址空间就是分开的,独立的。(注,不同的处理器有不一样,有些处理器上没有IO总线;即便外设总线有一个单独的地址空间给 I/O 端口, 也不是所有的设备映射它们的寄存器到 I/O 端口。)。

有了IO总线与内存总线的区分后,就有了IO端口与IO内存的区别了。


I/O端口:当一个外设寄存器位于I/O空间(外设通过IO总线与CPU通信,如上图的“外设2”)时,称其为I/O端口;

I/O内存:当一个外设寄存器位于内存空间(外设通过内存总线与CPU通信,如上图的“外设1”)时,称其为I/O内存。

⒉ I/O寄存器与常规内存

尽管硬件寄存器与内存之间有很强的相似性,存取I/O寄存器的程序员必须小心避免被CPU(或编译器)优化所戏弄,它可能修改希望的I/O行为。

I/O寄存器和RAM的主要不同是I/O操作有side effects(注:原文为side effects,有人把它译为“边际效应”,也有人把译为“副作用”。),而内存操作没有:一个内存写操作的唯一效果就是存储一个值到一个位置,并且一个内存读操作返回最近写到那里的值。

编译器能够缓存数据值到CPU寄存器而不写到内存,并且即便存储它们,读和写操作都能够在缓冲内存中进行而不接触物理RAM。重编排也可能在编译器级别和在硬件级别都发生:一个指令序列能够执行得更快,如果它以不同于在程序文本中出现的顺序来执行。

当这种优化应用于传统内存时(至少在单处理器系统中),是透明的、有益的、没有问题的。但是当应用于I/O寄存器时,结果可能是致命的。因为I/O寄存器,有side effects(我的理解是:对I/O寄存器的读写操作,就是对相应的外设进行命令交互,这些操作有特定的时序要求,而且即便是读一个I/O寄存器,也有可能改变这个寄存器的内容。)所以,编译器或CPU对I/O寄存器的访问所做的优化,可能会对带来致命的后果。

硬件缓冲的问题,容易解决,底层的硬件已经配置(或者自动地或者通过linux初始化代码)成禁止任何硬件缓冲,当存取I/O区时(不管它们是I/O内存还是I/O端口区域)。

对编译器优化和重编排的解决方法是使用“内存屏障”。Linux提供了以下几个宏:

#include <linux/kernel.h>
void barrier(void);


这个函数通知编译器插入一个内存屏障,但对硬件没有影响。编译后的代码会把当前CPU寄存器中的所有修改过的数值保存到内存中,需要这些数据的时候再重新读出来。对barrier的调用可避免在屏障前后编译器优化,但硬件能完成自己的重新排序。


#include <asm/system.h>
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);


这些函数在已编译的指令流中插入硬件内存屏障。具体的实现方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作会在后来的读操作之前执行。wmb(写内存屏障)保证了屏障之前的写操作会在后来的写操作之前执行。mb两者(读写)都保证。这些函数都是barrier的超集。
read_barrier_depends是一种特殊的、弱一些的读屏障形式。read_barrier_depends仅仅阻止某些望到操作的重新排序,这些读取依赖于其他读取操作返回的数据,它和rmb的区别很微妙,而且不是所有的平台都有这个函数。 除非你能够正确理解它们之间的差别,并且有理由相信完整的读取屏障会导致额外的性能开销,否则就应该始终使用rmb。


void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);


SMP上面的版本。


二、I/O端口的使用

⒈ I/O端口的申请与释放

在尚未对这些端口进行申请之前我们是不应该对这些端口进行操作的。内核为我们提供了一个注册用的接口:

#include <linux/ioport.h>
struct resource *request_region(unsigned long first,
unsigned long n,
const char *name);


这个函数告诉内核,你要使用从first开始的n个端口。name参数应该是你设备的名字。如果分配成功返回非NULL, 如果返回NULL,我们就不能使用这些期望的端口。
所有的端口分配显示在/proc/inports中。如果我们无法分配到需要的端口集合,则可以通过个文件得知哪个驱动程序已经分配了这些端口。

当你用完一组I/O端口时(如,模块卸载时),应当返回给系统,使用:


void release_region(unsigned long start, unsigned long n);

还有一个函数允许你的驱动来检查一个给定的I/O端口组是否可用:

int check_region(unsigned long first, unsigned long n);

这里, 如果给定的端口不可用, 返回值是一个负错误码. 这个函数是不推荐的, 因为它的返回值并不能确保分配是否能够成功。这是因为,检查和后来的分配不是一个原子的操作。 我们列在这里,因为仍有一些驱动使用它, 但是我们应该始终使用request_region,因为这个函数执行了必要的锁定,以确保分配过程以安全、原子的方式完成。

⒉ 操作I/O端口

#include <asm/io.h>
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned doubleword, unsigned port);


这些函数用来读写IO端口。后缀b表示8位读写,w表示16 bits, l表示32 bits。


unsigned inb_p(unsigned port);
void outb_p(unsigned char byte, unsigned port);
unsigned inw_p(unsigned port);
void outw_p(unsigned short word, unsigned port);
unsigned inl_p(unsigned port);
void outl_p(unsigned doubleword, unsigned port);


 如果I/O操作之后需要一小段的延时,可以用上面介绍的函数的6个暂停式的变体。这些暂停式的函数都以_p结尾。(这六个函数依赖平台, 有些平台下可能没有实现这六个变体)

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);
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);


这些“串操作函数”为输入端口与内存区之间的数据传输做了优化。这类传输是通过对同一端口连续读/写count次实现的。在使用串I/O时,需要铭记的是:它们直接将字节流从端口中读取或写入,当端口和主机系统具有不同的字节序时,将导致不可预期的结果。

三、 I/O内存的使用

⒈ I/O内存的申请与映射

I/O内存区在使用前必须分配,通过下面这个函数(定义在

struct resource *request_mem_region(unsigned long start, unsigned long len,        char *name);


这个函数分配一个以start为起始地址,长为len 字节的内存区。如果成功返回一个非NULL指针。所有分配的I/O内存区在/proc/iomem下面可以看到。

当申请的I/O内存区不用时,可以通过下面的这个函数,释放:

void release_mem_region(unsigned long start,
unsigned long len);


对I/O内存也有一个老的用于检查I/O内存区域是否可用的函数:

int check_mem_region(unsigned long start,
unsigned long len);


这个函数,和check_region一样,是不安全的,应该尽量避免使用这个函数。

分配I/O内存不是这块内存可以访问的唯一步骤。你得保证内核可以访问这一块I/O内存区域, 分配到一个I/O内存区域,不等于说,就直接可以用指针对它进行引用了。很多系统上,I/O内存是不能这样直接访问的。 还需要有一个映射的过程。Linux内核提供了下面这些函数:

#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);


ioremap用来把一个I/O内存区域映射到虚拟地址空间。

Remember, though, that the addresses returned from ioremap should not be dereferenced directly; instead, accessor functions provided by the kernel should be used.

⒉ 访问I/O内存

在一些平台下面,你可以把ioremap返回的值当作一个普通内存指针使用。但这样做是不可移植,内核开发者应该避免这样的用法。 我们应该用linux内核提供的函数集合(定义在

unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);


Here, addr should be an address obtained from ioremap (perhaps with an integer offset); the return value is what was read from the given I/O memory

There is a similar set of functions for writing to I/O memory:

void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);


If you must read or write a series of values to a given I/O memory address, you can use the repeating versions of the functions:

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


上述函数从给定的buf向给定的addr读取或写入count个值。注意,count是以被写入或被读取的数据大小为单位表示的,比如。ioread32_rep从addr中读取count个32位的值到buf中。

The functions described above perform all I/O to the given addr. If, instead, you need to operate on a block of I/O memory, you can use one of the following:

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


上述函数和C库函数库里对应函数的功能一致。

一些老的I/O内存访问函数:

unsigned readb(address);
unsigned readw(address);
unsigned readl(address);


These macros are used to retrieve 8-bit, 16-bit, and 32-bit data values from I/O memory.

void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);


Like the previous functions, these functions (macros) are used to write 8-bit, 16-bit, and 32-bit data items.

这些函数仍能工作,但是不鼓励在新的代码中使用这些函数。主要原因是因为这些函数不执行类型检查,因些其安全性较差。

⒊ 像I/O内存一样使用I/O端口

Some hardware has an interesting feature: some versions use I/O ports, while others use I/O memory. The registers exported to the processor are the same in either case, but the access method is different. As a way of making life easier for drivers dealing with this kind of hardware, and as a way of minimizing the apparent differences between I/O port and memory accesses, the 2.6 kernel provides a function called ioport_map:

void *ioport_map(unsigned long port,
unsigned int count);


This function remaps count I/O ports and makes them appear to be I/O memory. From that point thereafter, the driver may use ioread8 and friends on the returned addresses and forget that it is using I/O ports at all.

This mapping should be undone when it is no longer needed:

void ioport_unmap(void *addr);


These functions make I/O ports look like memory. Do note, however, that the I/O ports must still be allocated with request_region before they can be remapped in this way.

            

zhoulonglonger.zhou@gmail.com
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: