LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 2682|回复: 7

I/O-Programming-HOWTO完整版

[复制链接]
发表于 2004-2-20 16:22:18 | 显示全部楼层 |阅读模式
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/Electronics/上你会找到许多关于在计算机上使用非标准设备和自制设备的有用资料。

6.1并行端口
并行端口/dev/lp0的基地址(下文称做BASE)是0x3bc,/dev/lp1是0x378,/dev/lp2是0x278,如果你只是想通过并口操纵打印机的话,请参看Printing-HOWTO。
并行端口也可以通过ECP或EPP等扩展模式,实现数据双向传输,有关这方面的信息请参看http://www.fapo.com/http://www.senet.com.au/~cpeacock/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 ... 有用的资料。

7注意事项
如果你需要进行模拟量变换你可以直接将数/模转换或者模/数转换芯片直接连接到并行接口上,但如果你使用的不是低功率元件的话,你最好不要使用并行端口提供的电源,游戏接口或者磁盘电源都可以利用,最好为你的芯片制作一个独立的电源。你当然也可以购买一块AD/DA卡,但如果你的精度要求不太高的话,可以用声卡代替AD/DA卡。

如果需要在Linux上设计印刷电路板的话,一个叫做PCB的图形界面程序大致可以满足要求,在如下地址中你可以找到它的各个版本:
ftp://sunsite.unc.edu/pub/Linux/apps/circuits/

8常见问题
问题1:我的程序在访问端口时被告知segment fault
回答:也许你的程序没有root权限,或者有其他一些原因,请查看一下ioperm()的返回值。如果你使用了inb_p(),outb_p()等需要延时的函数,查看一下你是否在使用它们前用ioperm()得到了0x80端口的访问权限。

问题2:编译时gcc告知没有in*()和out*()函数定义
回答:你没有使用优化选项(-0),所以gcc无法对asm/io.h中的宏进行解析,也有可能你没在代码中加入#include <asm/io.h>

问题3ut*()没有正常工作
回答:out*的格式应该是这样outb(<数值>,<端口>)并不是像MS-DOS中的outportb(<端口号>,<数值>)

问题4:我想控制一个标准的RS-232接口设备
回答:你最好不要使用现有的内核驱动程序,Linux内核驱动程序的设计是为了兼容各种不同的硬件设备,如果你使用标准设备的话请查看相关文档。

9程序示例
/*
* 请使用gcc -02 -o example来编译,
* 并且在root权限下打入./example来运行程序
*/

#include <stdio.h>
#include <unistd.h>
#include <asm/io.h>

#define BASEPORT 0x378 /* lp1 */

int main()
{
/* 取得端口访问权限 */
if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);}

/* 将端口的8个数据位(D0-7)清零 */
outb(0, BASEPORT);

/* 等待100毫秒 */
usleep(100000);

/* 读取状态指示端口(BASE+1)并打印出结果 */
printf("status: %d\n", inb(BASEPORT + 1));

/* 放弃端口访问权限 */
if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);}

exit(0);
}

/* 程序结束 */
 楼主| 发表于 2004-2-20 16:53:10 | 显示全部楼层
中间有一段没有翻译,看过这篇HOWTO的请帮我翻译一下,多谢多谢了
最近又有事了,可能暂时不能翻译了
发表于 2004-3-25 01:08:05 | 显示全部楼层
好文

问:那里没有翻译?原文在那里?

建议

在联接地址那些地方两边加上空格(地址和文章全都连在一起了)

在整个文章前后加上[CO D E ]标志,(段落看得不清晰)
 楼主| 发表于 2004-3-25 10:24:44 | 显示全部楼层
过些时间我会把这文章转成latex或者PDF
原文在这里
http://howtos.linux.com/howtos/IO-Port-Programming.shtml
发表于 2004-12-28 16:38:46 | 显示全部楼层
太好了,勇哥!

太感谢了。

纠正勇哥一个小毛病,第六行,gcc的参数应该是 字母'O'而不是数字0。第八行也是。

还有4.1   6.1   7 中[/url]标签加的不对,请修改一下。
发表于 2005-5-13 18:41:43 | 显示全部楼层
请问版主:1。在用户态程序中,如果打开并口设备,如何检测并口的状态? 2。默认的并口驱动在往并口送数据前是否检测busy信号?
回复 支持 反对

使用道具 举报

发表于 2005-6-6 04:11:26 | 显示全部楼层

辛苦楼主把原文也一起考上来....

小弟身在学校,不能上外网........T_Y
回复 支持 反对

使用道具 举报

 楼主| 发表于 2005-8-17 12:35:42 | 显示全部楼层
原文  

2. Using I/O ports in C programs
2.1 The normal method

Routines for accessing I/O ports are in /usr/include/asm/io.h (or linux/include/asm-i386/io.h in the kernel source distribution). The routines there are inline macros, so it is enough to #include <asm/io.h>; you do not need any additional libraries.

Because of a limitation in gcc (present in all versions I know of, including egcs), you have to compile any source code that uses these routines with optimisation turned on (gcc -O1 or higher), or alternatively use #define extern static before you #include <asm/io.h> (remember to #undef externafterwards).

For debugging, you can use gcc -g -O (at least with modern versions of gcc), though optimisation can sometimes make the debugger behave a bit strangely. If this bothers you, put the routines that use I/O port access in a separate source file and compile only that with optimisation turned on.

Permissions

Before you access any ports, you must give your program permission to do so. This is done by calling the ioperm() function (declared in unistd.h, and defined in the kernel) somewhere near the start of your program (before any I/O port accesses). The syntax is ioperm(from, num, turn_on), where from is the first port number to give access to, and num the number of consecutive ports to give access to. For example, ioperm(0x300, 5, 1) would give access to ports 0x300 through 0x304 (a total of 5 ports). The last argument is a Boolean value specifying whether to give access to the program to the ports (true (1)) or to remove access (false (0)). You can call ioperm() multiple times to enable multiple non-consecutive ports. See the ioperm(2) manual page for details on the syntax.

The ioperm() call requires your program to have root privileges; thus you need to either run it as the root user, or make it setuid root. You can drop the root privileges after you have called ioperm() to enable the ports you want to use. You are not required to explicitly drop your port access privileges with ioperm(..., 0) at the end of your program; this is done automatically as the process exits.

A setuid() to a non-root user does not disable the port access granted by ioperm(), but a fork() does (the child process does not get access, but the parent retains it).

ioperm() can only give access to ports 0x000 through 0x3ff; for higher ports, you need to use iopl() (which gives you access to all ports at once). Use the level argument 3 (i.e., iopl(3)) to give your program access to all I/O ports (so be careful --- accessing the wrong ports can do all sorts of nasty things to your computer). Again, you need root privileges to call iopl(). See the iopl(2) manual page for details.

Accessing the ports

To input a byte (8 bits) from a port, call inb(port), it returns the byte it got. To output a byte, call outb(value, port) (please note the order of the parameters). To input a word (16 bits) from ports x and x+1 (one byte from each to form the word, using the assembler instruction inw), call inw(x). To output a word to the two ports, use outw(value, x). If you're unsure of which port instructions (byte or word) to use, you probably want inb() and outb() --- most devices are designed for bytewise port access. Note that all port access instructions take at least about a microsecond to execute.

The inb_p(), outb_p(), inw_p(), and outw_p() macros work otherwise identically to the ones above, but they do an additional short (about one microsecond) delay after the port access; you can make the delay about four microseconds with #define REALLY_SLOW_IO before you #include <asm/io.h>. These macros normally (unless you #define SLOW_IO_BY_JUMPING, which is probably less accurate) use a port output to port 0x80 for their delay, so you need to give access to port 0x80 with ioperm() first (outputs to port 0x80 should not affect any part of the system). For more versatile methods of delaying, read on.

There are manual pages for ioperm(2), iopl(2), and the above macros in reasonably recent releases of the Linux manual page collection.

2.2 An alternate method: /dev/port

Another way to access I/O ports is to open() /dev/port (a character device, major number 1, minor 4) for reading and/or writing (the stdio f*() functions have internal buffering, so avoid them). Then lseek() to the appropriate byte in the file (file position 0 = port 0x00, file position 1 = port 0x01, and so on), and read() or write() a byte or word from or to it.

Naturally, for this to work your program needs read/write access to /dev/port. This method is probably slower than the normal method above, but does not need compiler optimisation nor ioperm(). It doesn't need root access either, if you give a non-root user or group access to /dev/port --- but this is a very bad thing to do in terms of system security, since it is possible to hurt the system, perhaps even gain root access, by using /dev/port to access hard disks, network cards, etc. directly.

You cannot use select(2) or poll(2) to read /dev/port, because the hardware does not have a facility for notifying the CPU when a value in an input port changes.

3. Interrupts (IRQs) and DMA access

You cannot use IRQs or DMA directly from a user-mode process. You need to write a kernel driver; see The Linux Kernel Hacker's Guide for details and the kernel source code for examples.

You can disable interrupts from within a user-mode program, though it can be dangerous (even kernel drivers do it for as short a time as possible). After calling iopl(3), you can disable interrupts simply by calling asm("cli");, and re-enable them with asm("sti");.

4. High-resolution timing
4.1 Delays

First of all, I should say that you cannot guarantee user-mode processes to have exact control of timing because of the multi-tasking nature of Linux. Your process might be scheduled out at any time for anything from about 10 milliseconds to a few seconds (on a system with very high load). However, for most applications using I/O ports, this does not really matter. To minimise this, you may want to nice your process to a high-priority value (see the nice(2) manual page) or use real-time scheduling (see below).

If you want more precise timing than normal user-mode processes give you, there are some provisions for user-mode `real time' support. Linux 2.x kernels have soft real time support; see the manual page for sched_setscheduler(2) for details. There is a special kernel that supports hard real time; see http://luz.cs.nmt.edu/~rtlinux/ for more information on this.

Sleeping: sleep() and usleep()

Now, let me start with the easier timing calls. For delays of multiple seconds, your best bet is probably to use sleep(). For delays of at least tens of milliseconds (about 10 ms seems to be the minimum delay), usleep() should work. These functions give the CPU to other processes (``sleep''), so CPU time isn't wasted. See the manual pages sleep(3) and usleep(3) for details.

For delays of under about 50 milliseconds (depending on the speed of your processor and machine, and the system load), giving up the CPU takes too much time, because the Linux scheduler (for the x86 architecture) usually takes at least about 10-30 milliseconds before it returns control to your process. Due to this, in small delays, usleep(3) usually delays somewhat more than the amount that you specify in the parameters, and at least about 10 ms.

nanosleep()

In the 2.0.x series of Linux kernels, there is a new system call, nanosleep() (see the nanosleep(2) manual page), that allows you to sleep or delay for short times (a few microseconds or more).

For delays <= 2 ms, if (and only if) your process is set to soft real time scheduling (using sched_setscheduler()), nanosleep() uses a busy loop; otherwise it sleeps, just like usleep().

The busy loop uses udelay() (an internal kernel function used by many kernel drivers), and the length of the loop is calculated using the BogoMips value (the speed of this kind of busy loop is one of the things that BogoMips measures accurately). See /usr/include/asm/delay.h) for details on how it works.

Delaying with port I/O

Another way of delaying small numbers of microseconds is port I/O. Inputting or outputting any byte from/to port 0x80 (see above for how to do it) should wait for almost exactly 1 microsecond independent of your processor type and speed. You can do this multiple times to wait a few microseconds. The port output should have no harmful side effects on any standard machine (and some kernel drivers use it). This is how {in|out}[bw]_p() normally do the delay (see asm/io.h).

Actually, a port I/O instruction on most ports in the 0-0x3ff range takes almost exactly 1 microsecond, so if you're, for example, using the parallel port directly, just do additional inb()s from that port to delay.

Delaying with assembler instructions

If you know the processor type and clock speed of the machine the program will be running on, you can hard-code shorter delays by running certain assembler instructions (but remember, your process might be scheduled out at any time, so the delays might well be longer every now and then). For the table below, the internal processor speed determines the number of clock cycles taken; e.g., for a 50 MHz processor (e.g. 486DX-50 or 486DX2-50), one clock cycle takes 1/50000000 seconds (=200 nanoseconds).

Instruction   i386 clock cycles   i486 clock cycles
xchg %bx,%bx          3                   3
nop                   3                   1
or %ax,%ax            2                   1
mov %ax,%ax           2                   1
add %ax,0             2                   1

Clock cycles for Pentiums should be the same as for i486, except that on Pentium Pro/II, add %ax, 0 may take only 1/2 clock cycles. It can sometimes be paired with another instruction (because of out-of-order execution, this need not even be the very next instruction in the instruction stream).

The instructions nop and xchg in the table should have no side effects. The rest may modify the flags register, but this shouldn't matter since gcc should detect it. xchg %bx, %bx is a safe choice for a delay instruction.

To use these, call asm("instruction") in your program. The syntax of the instructions is as in the table above; if you want multiple instructions in a single asm() statement, separate them with semicolons. For example, asm("nop ; nop ; nop ; nop") executes four nop instructions, delaying for four clock cycles on i486 or Pentium processors (or 12 clock cycles on an i386).

asm() is translated into inline assembler code by gcc, so there is no function call overhead.

Shorter delays than one clock cycle are impossible in the Intel x86 architecture.

rdtsc for Pentiums

For Pentiums, you can get the number of clock cycles elapsed since the last reboot with the following C code (which executes the CPU instrution named RDTSC):

   extern __inline__ unsigned long long int rdtsc()
   {
     unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
   }

You can poll this value in a busy loop to delay for as many clock cycles as you want.

4.2 Measuring time

For times accurate to one second, it is probably easiest to use time(). For more accurate times, gettimeofday() is accurate to about a microsecond (but see above about scheduling). For Pentiums, the rdtsc code fragment above is accurate to one clock cycle.

If you want your process to get a signal after some amount of time, use setitimer() or alarm(). See the manual pages of the functions for details.

5. Other programming languages

The description above concentrates on the C programming language. It should apply directly to C++ and Objective C. In assembler, you have to call ioperm() or iopl() as in C, but after that you can use the I/O port read/write instructions directly.

In other languages, unless you can insert inline assembler or C code into the program or use the system calls mentioned above, it is probably easiest to write a simple C source file with functions for the I/O port accesses or delays that you need, and compile and link it in with the rest of your program. Or use /dev/port as described above.

6. Some useful ports

Here is some programming information for common ports that can be directly used for general-purpose TTL (or CMOS) logic I/O.

If you want to use these or other common ports for their intended purpose (e.g., to control a normal printer or modem), you should most likely use existing drivers (which are usually included in the kernel) instead of programming the ports directly as this HOWTO describes. This section is intended for those people who want to connect LCD displays, stepper motors, or other custom electronics to a PC's standard ports.

If you want to control a mass-market device like a scanner (that has been on the market for a while), look for an existing Linux driver for it. The Hardware-HOWTO is a good place to start.

http://www.hut.fi/Misc/Electronics/ is a good source for more information on connecting devices to computers (and on electronics in general).

6.1 The parallel port

The parallel port's base address (called ``BASE'' below) is 0x3bc for /dev/lp0, 0x378 for /dev/lp1, and 0x278 for /dev/lp2. If you only want to control something that acts like a normal printer, see the Printing-HOWTO.

In addition to the standard output-only mode described below, there is an `extended' bidirectional mode in most parallel ports. For information on this and the newer ECP/EPP modes (and the IEEE 1284 standard in general), see http://www.fapo.com/ and http://www.senet.com.au/~cpeacock/parallel.htm. Remember that since you cannot use IRQs or DMA in a user-mode program, you will probably have to write a kernel driver to use ECP/EPP; I think someone is writing such a driver, but I don't know the details.

The port BASE+0 (Data port) controls the data signals of the port (D0 to D7 for bits 0 to 7, respectively; states: 0 = low (0 V), 1 = high (5 V)). A write to this port latches the data on the pins. A read returns the data last written in standard or extended write mode, or the data in the pins from another device in extended read mode.

The port BASE+1 (Status port) is read-only, and returns the state of the following input signals:

    * Bits 0 and 1 are reserved.
    * Bit 2 IRQ status (not a pin, I don't know how this works)
    * Bit 3 ERROR (1=high)
    * Bit 4 SLCT (1=high)
    * Bit 5 PE (1=high)
    * Bit 6 ACK (1=high)
    * Bit 7 -BUSY (0=high)

The port BASE+2 (Control port) is write-only (a read returns the data last written), and controls the following status signals:

    * Bit 0 -STROBE (0=high)
    * Bit 1 -AUTO_FD_XT (0=high)
    * Bit 2 INIT (1=high)
    * Bit 3 -SLCT_IN (0=high)
    * Bit 4 enables the parallel port IRQ (which occurs on the low-to-high transition of ACK) when set to 1.
    * Bit 5 controls the extended mode direction (0 = write, 1 = read), and is completely write-only (a read returns nothing useful for this bit).
    * Bits 6 and 7 are reserved.

Pinout (a 25-pin female D-shell connector on the port) (i=input, o=output):

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 Ground

The IBM specifications say that pins 1, 14, 16, and 17 (the control outputs) have open collector drivers pulled to 5 V through 4.7 kiloohm resistors (sink 20 mA, source 0.55 mA, high-level output 5.0 V minus pullup). The rest of the pins sink 24 mA, source 15 mA, and their high-level output is min. 2.4 V. The low state for both is max. 0.5 V. Non-IBM parallel ports probably deviate from this standard. For more information on this, see http://www.hut.fi/Misc/Electronics/circuits/lptpower.html.

Finally, a warning: Be careful with grounding. I've broken several parallel ports by connecting to them while the computer is turned on. It might be a good thing to use a parallel port not integrated on the motherboard for things like this. (You can usually get a second parallel port for your machine with a cheap standard `multi-I/O' card; just disable the ports that you don't need, and set the parallel port I/O address on the card to a free address. You don't need to care about the parallel port IRQ if you don't use it.)

6.2 The game (joystick) port

The game port is located at port addresses 0x200-0x207. If you want to control normal joysticks, you're probably better off using the drivers distributed with the Linux kernel.

Pinout (a 15-pin female D-shell connector on the port):

    * 1,8,9,15: +5 V (power)
    * 4,5,12: Ground
    * 2,7,10,14: Digital inputs BA1, BA2, BB1, and BB2, respectively
    * 3,6,11,13: ``Analog'' inputs AX, AY, BX, and BY, respectively

The +5 V pins seem to often be connected directly to the power lines in the motherboard, so they may be able to source quite a lot of power, depending on the motherboard, power supply and game port.

The digital inputs are used for the buttons of the two joysticks (joystick A and joystick B, with two buttons each) that you can connect to the port. They should be normal TTL-level inputs, and you can read their status directly from the status port (see below). A real joystick returns a low (0 V) status when the button is pressed and a high (the 5 V from the power pins through an 1 Kohm resistor) status otherwise.

The so-called analog inputs actually measure resistance. The game port has a quad one-shot multivibrator (a 558 chip) connected to the four inputs. In each input, there is a 2.2 Kohm resistor between the input pin and the multivibrator output, and a 0.01 uF timing capacitor between the multivibrator output and the ground. A real joystick has a potentiometer for each axis (X and Y), wired between +5 V and the appropriate input pin (AX or AY for joystick A, or BX or BY for joystick B).

The multivibrator, when activated, sets its output lines high (5 V) and waits for each timing capacitor to reach 3.3 V before lowering the respective output line. Thus the high period duration of the multivibrator is proportional to the resistance of the potentiometer in the joystick (i.e., the position of the joystick in the appropriate axis), as follows:

    R = (t - 24.2) / 0.011,

where R is the resistance (ohms) of the potentiometer and t the high period duration (microseconds).

Thus, to read the analog inputs, you first activate the multivibrator (with a port write; see below), then poll the state of the four axes (with repeated port reads) until they drop from high to low state, measuring their high period duration. This polling uses quite a lot of CPU time, and on a non-realtime multitasking system like (normal user-mode) Linux, the result is not very accurate because you cannot poll the port constantly (unless you use a kernel-level driver and disable interrupts while polling, but this wastes even more CPU time). If you know that the signal is going to take a long time (tens of ms) to go down, you can call usleep() before polling to give CPU time to other processes.

The only I/O port you need to access is port 0x201 (the other ports either behave identically or do nothing). Any write to this port (it doesn't matter what you write) activates the multivibrator. A read from this port returns the state of the input signals:

    * Bit 0: AX (status (1=high) of the multivibrator output)
    * Bit 1: AY (status (1=high) of the multivibrator output)
    * Bit 2: BX (status (1=high) of the multivibrator output)
    * Bit 3: BY (status (1=high) of the multivibrator output)
    * Bit 4: BA1 (digital input, 1=high)
    * Bit 5: BA2 (digital input, 1=high)
    * Bit 6: BB1 (digital input, 1=high)
    * Bit 7: BB2 (digital input, 1=high)

6.3 The serial port

If the device you're talking to supports something resembling RS-232, you should be able to use the serial port to talk to it. The Linux serial driver should be enough for almost all applications (you shouldn't have to program the serial port directly, and you'd probably have to write a kernel driver to do it); it is quite versatile, so using non-standard bps rates and so on shouldn't be a problem.

See the termios(3) manual page, the serial driver source code (linux/drivers/char/serial.c), and http://www.easysw.com/~mike/serial/ for more information on programming serial ports on Unix systems.
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表