|
2在C语言中操作I/O接口
2.1操作I/O接口的常用方法
我们常用/usr/include/io.h或者内核源代码中的linux/linux/asm-i386/io.h中提供的方法来访问I/O接口,要在程序里使用这些方法,你指需要在你的程序的开始部分加上
#include <asm/io.h>
由于gcc的限制,你可能需要使用优化选项,像这样:gcc -01(或更高),来编译你的代码,或者在#include<asm/io.h>前面加上#define extern static,然后加上#undef extern
为了除错方便,如果你用的是新版本的gcc,你可以使用gcc -g -0,但优化后的代码在除错时可能会遇到许多不必要的麻烦,所以你可以将进行I/O操作的那些代码放到另外的文件中使用优化选项进行编译,然后再和其他不直接使用I/O操作的部分连接。
操作权限:
当你访问某些端口时,你必须让你的程序获得足够的权限,你可以通过使用函数ioperm()来实现(包含在unistd.h中,而且在内核中有定义),在你进行I/O操作之前,你需要先使用此函数得到访问某些端口的权限,使用的方法是ioperm(from,num,turn_on),其中from是需要操作的首个端口地址,num表示需要操作的后面几个端口的数目,例如ioperm(0x300,5,1)代表将使用从0x300到0x304共5个端口,最后参数是一个布尔值(1代表true),1表示给程序申请权限,0表示收回访问权限,如果你需要操作多段不连续的端口,你可以多次使用这个函数,详细操作请查看ioperm(2)的man手册。
要使用ioperm()函数的话你需要拥有root权限,所以你需要用root用户运行此程序,或者获得root的权限,当你调用ioperm()取得权限后你就可以放弃root权限了,但你不必在程序的最后使用ioperm(,,0)回收权限,因为程序结束后相应I/O操作权限会被系统自动回收。
当你切换到普通用户时,程序的I/O操作权限不会被回收,但是如果你使用fork()创造一个新进程的话,由于新进程没有获得权限,所以系统会拒绝执行新创建的子进程中的I/O操作。
ioperm()只能获得从0x000到0x3ff的I/O接口的权限,对于其他的端口,需要使用iopl(),这个函数可以获得所有I/O端口的访问权限,同样在使用此函数前你需要root权限,现在你可以使用参数3来获得所有I/O接口的操作,ioperm(3),但是要小心,由于取得了系统中所有端口的操作权限,你的错误的操作可能会造成难以预料的后果。
开始操作端口:
使用inb( <端口号> )来从端口输入一个字节(8个比特),这个函数的返回值就是从这个端口输入的数据。
outb( <数值> , <端口号> )函数用来向端口送出数值。
inw( <起始端口号> )从连续的两个读入一个字的数据(16个比特)。
outw( <一个字长的数值> , <起始端口号> )向连续的两个端口打入一个字节的数据。
如果你不能确定应该使用字节还是字,保守起见,你可以只使用inb()和outb(),大多数设备都可以支持端口的字节操作,每次端口操作都会占用至少一毫秒的时间。
宏inb_p(),out_p(),inw_p()和outw_p()和上面介绍的函数功能相同,但会在端口操作后有大概1微秒的延时,你可以在#include<asm/io.h>前面加上#define REALLY_SLOW_IO来延时4微秒,这些宏用通过向端口0x80写入数据的方法实现延时,延时的时间就是向0x80写入数据的时间(我们再后面会提到这个端口),所以你必须在你的程序中使用ioperm()得到此端口的操作权限,但是如果你使用#define SLOW_IO_BY_JUMPING,就可以使用另外的方法而不用向0x80端口写数据,但此方法可能会导致错误。
关于以上所提到的所有函数和宏定义已经在新版本的man手册中给出了详细资料。
2.2访问端口的另外一种方法:/dev/port
访问I/O接口的另外一种方法是使用open()函数来打开设备文件/dev/port,然后使用lseek()函数将指针移动到此文件的适当位置,例如位置0处就是端口0x00,位置1就是端口0x01,等等,然后你就可以向里面用read()或write()函数写入或读出一个字节或者一个字。
当然,进行上述操作需要你对/dev/port文件的读写权限,而且这种方法的运行效率相对要慢一些,但是此方法不需要编译器的优化选项和ioperm()函数,而且也不需要root权限,只要你有操作/dev/port的权限就可以了。
你可以给所有用户对/dev/port的操作权限,这样就能使他们都能够调试I/O接口。但这会对系统安全造成一定的隐患,因为他们可以使用/dev/port文件来对硬盘,网卡等设备进行I/O操作,从而造成系统数据被窃听或者破坏。
你不可以使用select()或者poll()来读/dev/port文件,如果数据被改变,系统上的硬件并不能通知CPU数据出错。
3关于中断(IRQ)和DMA访问
你不可以在你的用户级操作中使用中断和DMA,这需要事先编写一个驱动程序。
但是你可以在用户级操作中关闭中断,虽然很危险。在你使用iopl()取得权限之后,你可以使用asm("cli")来关闭中断,然后用asm("sti")打开中断。
4高级时间操作
4.1延时
首先,你不能保证你的程序能够完全遵守时间,因为Linux是一个多任务操作系统,在你的延时操作前后系统可能还会执行一些其他进程的操作,造成时间不准。而且,也有可能多个程序会争用一个端口,但你可以将你的程序调至最高优先级(详情参看nice(2)的man手册)或者采用下文中介绍的实时处理。
如果你需要比不同用户程序更精确的时间操作,你便可以考虑使用实时处理,Linux的2.x以上版本的内核已经开始支持这种方式,详细情况请参看sched_setscheduler(2)的man手册,也可以从http://luz.cs.nmt.edu/~rtlinux/得到更多的信息。
延时函数:sleep()和usleep()
现在介绍两个简单的时间操作,如果你要使用几秒的延时,最简单的方法是使用sleep()函数。而使用usleep()函数你可以延时若干个以10毫秒为单位的时间。这两个函数并不使用使CPU空闲的方法实现延时,而是让CPU去执行其他进程。详细情况请参看sleep(3)和usleep(3)的man手册页。
在50毫秒以下的延时也会占用CPU比所指定的值更长的时间,80x86架构下的Linux的调度程序在返回你的程序之前,至少要花费10-30毫秒的时间,因此,在短时间的延时中,usleep()会比所指定的时间多延长时至少10秒。
nanosleep()函数:
从2.0.x版本开始内核就开始支持名叫nanosleep()的系统调用(详细情况请参看nanoslepp(2)的man手册),让你可以使用几微秒的超短延时时间。
如果你将你的程序调到实时状态(使用sched_setscheduler()来切换),这时如要使用2毫秒以下的延时,nanosleep()就会使用完成一次循环的方法延时,或者也许会暂时像usleep()一样使程序休眠。
这个延时循环使用的是系统调用udelay(),详细情况请查看/usr/include/asm/delay.h
使用端口通信进行延时:
使用微小延时另外一种方法是使用端口通信,向0x80写入或者读入一个字节的时间差不多是1微秒(这和你的CPU的处理速度无关),你可以多次使用这种方法来延时几个微秒。这个操作在绝大多数系统中并不会造成任何伤害,所以内核中很多地方都使用这种方法(例如asm/io.h中)。
实际上对0-0x3ff范围内的任意一个端口的操作都会使用差不多1微秒的时间,所以也可以使用向任何一个端口通信的方法来造成延时。
使用汇编指令延时:
如果你预先知道运行你的程序的机器的处理器型号和系统的时钟周期,这时就可以选择运行一些特定运行时间的汇编指令造成延时,但进程管理器很可能会在运行这组汇编指令过程中切换到其他进程,造成延时时间加长。
如下表,处理器的速度会决定运行指令时的时钟周期,例如一个50MHz的处理器(如486DX-50或者486DX2-50)一个时钟周期为1/50000000秒,等于200毫微秒。
指令 386机器的执行周期 486机器的执行周期
xchg %bx,%bx 3 3
nop 3 1
or %ax,%ax 2 1
mov %ax,%ax 2 1
add %ax,0 2 1
奔腾机的时钟周期和486机器大致相同,除了Pebtium Pro/II中,add %ax,0只使用1/2个时钟周期,这是因为它也许会和其他程序并行执行,这是因为在Pentium Pro中可以乱序和并行执行,这就是说在CPU执行程序时,后面的一条指令不一定要在前面指令后面执行。
表中的nop和xchg两条指令除了延时之外不起任何作用,所以这是造成延时的最好方法,其他的指令会修改标志寄存器的内容,不过程序在编译时gcc会提醒你。
如果你决定要使用使用这种方法,你可以在你的程序中使用asm(" <汇编语句> ")来,汇编语句的格式在上面的表中已经列出,如果你需要执行多条语句,你可以在asm中将它们用分号分隔开,例如asm("nop;nop"),执行两条nop语句。
注意:Intel x86架构的机器中不可以使用小于一个时钟周期的延时。
奔腾机中的rdtsc指令:
在奔腾机中,如果你要延时一定数目的时钟周期,可以使用rdtsc指令,下面就是使用这个功能所需的代码:
extern __inline__ unsighed long long int rdtsc()
{
unsighed long long int x;
__asm__ voatile (".byte 0x0f,0x31":"=A"(x))
return x;
}
这样你就可以延时任意个时钟周期。
4.2测量时间
如果需要测量的时间精确到秒,最简单的方法时使用time()。如需要更加精确的话,可以使用gettimeofday(),这个函数可以精确到微秒,但同样这两个函数都可能会在进程调度中会出现偏差。
如果你的进程需要在一定的时间之后得到一些信号,可以使用setitimer()或者alarm()详细使用方法请参看man手册页。
5其他编程语言
以上的例子全部是用C语言写成的,可以直接用到C++和Object C中。而且在汇编语言中你同样可以使用ioperm()或者iopl()函数。
在其他编程语言中,最好是将关于端口通信的部分用C语言写成函数,编译后再将其和你用其他语言编写的程序连接起来。或者你也可以用你喜欢的程序设计语言直接读写/dev/port文件。
6一些常用硬件端口
如果你想使用一些常用的端口,比如打印机或者调制解调器,最好的方法是使用内核中附带的驱动程序,这样会省去很多时间,这节是为了那些希望向PC上连接一些非标准设备(比如一个小LCD显示屏,小电机或者其他一些什么东西)的人准备的。
在http://www.hut.fi/Misc/Electroni ... 的有用资料。
6.1并行端口
并行端口/dev/lp0的基地址(下文称做BASE)是0x3bc,/dev/lp1是0x378,/dev/lp2是0x278,如果你只是想通过并口操纵打印机的话,请参看Printing-HOWTO。
并行端口也可以通过ECP或EPP等扩展模式,实现数据双向传输,有关这方面的信息请参看http://www.fapo.com/和http://ww ... ock/parallel.htm。
需要注意的是,在用户模式下你不可以使用中断和DMA,如果你需要使用ECP或EPP,你必须编写一个驱动程序。
端口BASE+1是只读的,它指示出并行接口的状态,下面便是它各个位表示的意义:
第0,1位被保留
第2位IRQ 中断状态
第3位ERROR 错误显示位 (1有效)
第4位 SLCT (1有效)
第5位 PE (1有效)
第6位ACK (1有效)
第7位-BUSY 忙信号 (0有效)
端口BASE+2是控制端口,你可以向其中写入控制信息,对这个端口的读操作会返回最近一次写入的数据,各个位的意义如下:
第0位-STROBE (0有效)
第1位-AUTO_FD_XT (0有效)
第2位INIT (1有效)
第3位-SLCT_IN (0有效)
第4位 当这位置1时允许中断(ACK的上升沿表示中断)
第5位 控制扩展模式的方向,0表示写,1表示读,而且只能向这位写数据,不可以读数据
第6,7位被保留
接脚定义(i表示输入,o表示输出)
1io -STROBE 2io D0 3io D1 4io D2
5io D3 6io D4 7io D5 8io D6
9io D7 10i ACK 11i -BUSY 12i PE
13i SLCT 14o -AUTO_FD_XT 15i ERROR 16o INIT
17o -SLCT_IN 18-25 接地
注意:机器在打开的状态下向并行接口连接设备可能会烧毁并行口,如果你需要反复实验的话你可以购买一块I/O卡,将它连接道电脑上后将卡上的并行接口的I/O地址调到一个空闲的地址。如果你不需要使用中断的话,IRQ的设置你可以不必关心。
6.2游戏端口
游戏端口的地址为0x200到0x207。如果你需要使用游戏杆的话,最好不要使用内核自带的驱动程序。
游戏端口的接脚:
1,8,9,15:+5V电源
4,5,12 :接地
2,7,10,14:数字信号输入,分别为BA1,BA2,BB1,BB2
3,6,11,13:模拟信号输入,分别为AX,AY,BX,BY
数字信号输入的信号来源是两个游戏杆上的各自两个按钮,你可以直接读取它们的状态,当按钮按下时返回低电平(0V),其他时候是高电平(5V)。
//这里有一部分没有翻译
6.3串行口
类似RS-232的接口我们将其称做串行接口,Linux内核对这种接口支持得很完善,而且通用性也很好,可以很容易支持各种波特率的传输。详细情况请参看termio(3)的手册页和内核源代码中的linux/driver/char/serial.c,http://www.easysw.com/~mike/seri ... 有用的资料。
To Be Continue...... |
|