如何进行串口编程
时间:2006-01-19 来源:linuxjet
如何进行串口编程
1.介绍
本文旨在介绍Linux下的串口编程。基本上是关于如何在Linux下通过串口线与其他设备或计算机进行通信的。 主要有如下几种不同的技术: 规范的 I/O 操作(所有的线仅用来发送和接收数据), 异步I/O操作, 以及等待来自多方的输入。
这是对如何在Linux下串口编程初始文档的第一次更新。这次更新的主要目的是把文档转换为DocBook格式。 这次在技术内容方面的改动很小。尽管技术方面的改动我不会通宵达旦的去做,我还是会利用一切可以利用的时间去做。
如果你一直以来在关注如何操作串口方面的事情,并且你已经得到了你想要的。请把你收到的反馈发给我一下,无论是什末,都非常感激。
本文中的所有的例子都在i386机子Linux内核2.0.29下测试通过。
1.1 版权信息
这篇文档的版权隶属于(c) 1997 Peter Baumann, (c) 2001 Gary Frerking 并且在Linux文档工程条款下发布, 声明如下:
除非有另外的声明,Linux HOWTO 文档版权属于他们各自的作者。只要这个版权声明的拷贝存在于任何物理的或电子的媒介上, Linux HOWTO 文档可能通过这些媒介被全部或部分的被复制和发布。 商业性的发布是允许和被鼓励的;然后,这些发布应该及时向作者告知。所有的对这些文档的翻译,扩展或者合成聚合工作都必须在这个版权声明下进行。那就是,你不可以从这些HOWTO文档中派生出作品,以及利用它发布时所列出的限制条件。在特定情况下的授权除外,请根据下面给出的地址联系Linux HOWTO 的协调者。
简而言之, 我们希望通过尽可能多的渠道分发这个消息。
无论如何,只要你愿意在重新发布HOWTO文档的时候被告知,我们真的希望可以通过这种方式保护HOWTO文档的版权。
如果你任何问题,请联系下面的信箱 [email protected]
1.2. 免责声明
我们不保证这些文档一定会对你有用。使用概念,例子以及其他的内容的时候需要自己承担风险。由于这是这份文档的一个崭新版本, 可能会有错误和不妥之处, 甚至可能会对你的系统造成损坏。请小心使用,而且,尽管这些存在高度的不确定性, 对其后果作者不承担任何责任。
所有的版权归属于文档各自的作者, 除非有特别的提示。
文章中条款的使用不会影响任何商标或服务标志的有效性。
特别的产品或者品牌的命名不应该在签注中被看到。
强烈推荐你在安装前先备份你的系统并在以后定期的进行备份。
1.3. 新版本
就像前面提到的,文章中目前还没有太多技术内容的改动。
1.4. 鸣谢
感谢这份文档的原创作者Mr.Strudthoff, Michael Carter, Peter Waltenberg, Antonino Ianella, Greg Hankins, Dave Pfaltzgraff, Sean Lincolne, Michael Wiedmann, 和 Adrey Bonar。
1.5. 反馈
我会非常感谢您对这篇文章的反馈。没有您的帮助和奉献,这篇文章就不会存在。请发送的建议、注释以及意见到下面的电邮地址: [email protected]。
2. 进入正题
2.1. 调试
调试代码的最好办法是安装另一个Linux盒子, 并且通过一个空载的调制解调器电缆把这两台机子连接起来。使用miniterm (是LDP 程序员向导的一个例子程序(ftp://sunsite。unc。edu/pub/Linux/docs/LDP/programmers-guide/lpg-0.4.tar。gz in)) 向你的Linux 盒子传输数据。 Miniterm 可以很容易的编译并且可以通过串口传送所有的键盘输入。 使用的时候需要检查下面这个宏:
#define MODEMDEVICE "/dev/ttyS0"。
如果是COM1,则对应ttyS0, COM2 对应ttyS1,等等。有必要在没有任何输出的情况下通过串口线传送所有字符的输入。要测试你的连接是否正确,在两台机子上启动miniterm ,然后敲一下键盘。那末刚才在计算机上输入的字符会在另一台机子上显示出来,反之亦然。这个输入不会回显到触动键盘集资的显示屏上。
要制作一条零讯号的调制解调器电缆,你需要通过TxD (传输) 和RxD (接收) 线。 有关电缆设置的描述可以参看下面的第七部分。
如果在你的机子上有两个空闲的串口,你也可以只通过这一台计算机运行上面的测试程序。那末你可以在两个控制台上同时运行miniterm。如果你想拔掉鼠标释放这个串口, 要查看一下是否存在/dev/mouse,如存在记着对它进行重定向。如果你使用的是一个多串口的控制卡, 要确保配置正确。我曾经犯过这样的配置错误:程序只在我自己的机子上才正常工作。当我连接另一台机子的时候, 串口开始零星的丢失字符。 不过,在一台程序上运行两个程序并不是完全异步的。
2.2. 端口设置
设备文件 /dev/ttyS* 用来与你的Linux机子中的终端关联,这些关联是在启动之后完成配置的。在使用原始设备进行通讯编程时需要特别注意,举例来说,有些设备被配置成在设备上进行数据回显的,当用这些端口进行数据传输时需要对配置进行改变以适应数据传输的要求。
所有的参数都可以通过程序来配置。这些配置存放在一个结构termios中:它的定义在<asm/termbits.h>中:
#define NCCS 19
struct termios {
tcflag_t c_iflag; /* 输入模式标志*/
tcflag_t c_oflag; /*输出模式标志*/
tcflag_t c_cflag; /*控制模式标志*/
tcflag_t c_lflag; /*本地模式标志*/
cc_t c_line; /*引脚设置*/
cc_t c_cc[NCCS]; /*字符控制*/
};
这个文件中也包含了所有的标志定义。c_iflag中的输入模式标志控制所有的输入处理,这意味着从设备上传来的字符在被读出之前可以处理一下。 类似地,c_oflag控制输出处理。c_cflag 包含了一些端口设置参数, 比如波特率, 每个字符的位数, 终止位, 等等。。。 存放在c_lflag中的本地模式标志决定字符是否回显, 信号是否被发送到你的程序, 等等… 最后的数组c_cc定义了一些控制字符,比如文件尾,停止, 等等… 控制字符的缺省值的定义在文件 <asm/termios.h>中。这些标志在termios(3)的手册中都有描述。Termios结构包含c_line(引脚设置) 成员, 它在POSIX兼容机中不被使用。
2.3. 串口设备的输入概念
下面有三种输入方式的介绍。我们可以根据程序选择适当的方式。 值得一提的是无论采用哪种方式,不要通过一个个字符的读取方式来获取一个字符串。因为当我试图这样做的时候,即使没有任何错误,也会丢失一些字符。
2.3.1. 规范的输入处理
这是通常的终端处理模式, 但是这种模式下一个读取操作会返回整条线上的输入,这在跟其它设备以整条线的输入作为通信单元通信的时候也是很有用的。线路输入缺省情况下可以被一个NL(ASCII LF)中断, 它可以看作是文件结尾或者行结尾。 在缺省的设置中一个CR符号(DOS/Windows 下的缺省行终止符)不会中断线路的传输。
规范的输入处理擦除,删除字符,以及重复打印字符,将CR转化成NL, 等等。
2.3.2. 不规范的输入处理
不规范的输入处理可以再一次读取中处理固定量的字符,并且允许使用字符定时器。 如果你的程序总是读取定量的字符,或者被连接的设备常常短时间内发送大量数据的时候,可以使用这种模式。
2.3.3. 异步输入
上面描述的两种模式可以用在同步和异步模式下。缺省使用同步模式,在此模式下,读取操作会一直处于阻塞状态直到允许读取。在异步模式下读取操作会立刻返回并在完成后向调用程序发送一个信号。这个信号可以被信号处理者接受到。
2.3.4. 多方输入的等待处理
如果你需要同时控制多个设备,多方输入的等待处理可能会很有用,其实跟前面的几种方式没有本质的区别的。在我的应用中我同时通过TCP/IP套接字和串口连接来控制另一台机子。 下面给出的例子会等待两个不同的输入资源。如果一个输入资源变为有效, 它会被处理,同时程序会等待新的输入。下面介绍的方法看起来相当复杂, 但是要记着Linux是多任务操作系统。 用来等待输入的系统调用不会消耗CPU时间,只有有输入并进行处理的时候才会降低同一时刻运行的其他程序的处理速度。
3. 程序例子
所有的例子都来自miniterm.c。 前面定义的缓冲区大小局限于255 个字符,这跟规范输入处理的最大字符串长度是一样的(<linux/limits.h>或者 <posix1_lim.h>)。
可以根据代码注释来研究不同的输入模式。我希望代码可以被读懂。其中规范输入的注释是最好的, 其他模式的注释只标出了与规范输入不同的地方以作强调。
描述虽然不完整,但是欢迎你进行试验,从而在你的程序中找到最好的解决方案。
不要忘了为使用的串口设置正确的访问权限 (举例来说.: chmod a+rw /dev/ttyS1)!
3.1. 规范的输入处理
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
/* 波特率的定义在<asm/termbits.h>中,它包含在<termios.h>中 */
#define BAUDRATE B38400
/* 为对应的的端口设置对应的节点名称*/
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 兼容资源*/
#define FALSE 0
#define TRUE 1
volatile int STOP=FALSE;
main()
{
int fd,c, res;
struct termios oldtio,newtio;
char buf[255];
/*
以读写方式和不受tty可能控制的方式打开调制设备,因为我们不希望由于线路上误输入CTRL−C而终止程序。
*/
fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY );
if (fd <0) {perror(MODEMDEVICE); exit(−1); }
tcgetattr(fd,&oldtio); /* 保存当前的串口设置*/
bzero(&newtio, sizeof(newtio)); /*对新的设置清零*/
/*
BAUDRATE: 设置速率。你也可以使用cfsetispeed 和 cfsetospeed进行设置。
CRTSCTS : 输出硬件流程(仅在电缆有所有必需的线的时候使用,参看串口HOWTO的第七部分)
CS8 : 8n1的意思是(位数8,不用奇偶校验,停止位是1)
CLOCAL : 本地连接, 没有解调器控制
CREAD : 允许接收字符*/
newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
/*
IGNPAR : 忽略奇偶校验错误的字节
ICRNL : 把CR 影射为NL (否则在其他机子上的一个CR输入无法中断输入过程) 否则使设备处于自然状态(没有其他输入处理)
*/
newtio.c_iflag = IGNPAR | ICRNL;
/*
原始输出
*/
newtio.c_oflag = 0;
/*
ICANON : 规范输入,使所有的回显功能失效, 并且不发送调用程序信号
*/
newtio.c_lflag = ICANON;
/*
初始化所有的控制字符,缺省值在/usr/include/termios.h中可以找到,主食里面有详细的描述,在这里我们不需要这些*/
newtio.c_cc[VINTR] = 0; /* Ctrl−c */
newtio.c_cc[VQUIT] = 0; /* Ctrl−\ */
newtio.c_cc[VERASE] = 0; /* del */
newtio.c_cc[VKILL] = 0; /* @ */
newtio.c_cc[VEOF] = 4; /* Ctrl−d */
newtio.c_cc[VTIME] = 0; /* 字符间的定时器,此处未使用*/
newtio.c_cc[VMIN] = 1; /* 阻塞读取,直到有一个字符输入时才解除*/
newtio.c_cc[VSWTC] = 0; /* '\0' */
newtio.c_cc[VSTART] = 0; /* Ctrl−q */
newtio.c_cc[VSTOP] = 0; /* Ctrl−s */
newtio.c_cc[VSUSP] = 0; /* Ctrl−z */
newtio.c_cc[VEOL] = 0; /* '\0' */
newtio.c_cc[VREPRINT] = 0; /* Ctrl−r */
newtio.c_cc[VDISCARD] = 0; /* Ctrl−u */
newtio.c_cc[VWERASE] = 0; /* Ctrl−w */
newtio.c_cc[VLNEXT] = 0; /* Ctrl−v */
newtio.c_cc[VEOL2] = 0; /* '\0' */
/*
现在清除调制解调器线路并激活端口设置
*/
tcflush(fd, TCIFLUSH);
tcsetattr(fd,TCSANOW,&newtio);
/*
上面的设置做完了,现在控制输入。比如, 在开始的线路上输入一个'z'就退出程序。
*/
while (STOP==FALSE) { /* 直到满足终止条件时终止循环*/
/* 如果么没有读到线路上输入终止字符,即使有超过255个字符的输入,读取数据块的操作也不会停止。如果一次读不完所有字符, 下一轮将会读取剩下的字符。res 变量会保存实际读到的字符个数。*/
res = read(fd,buf,255);
buf[res]=0; /* 设置字符串结尾以便打印输出*/
printf(":%s:%d\n", buf, res);
if (buf[0]=='z') STOP=TRUE;
}
/* 恢复原来的设置 */
tcsetattr(fd,TCSANOW,&oldtio);
}
3.2. 不规范的输入处理
在不规范输入的处理模式中, 输入不会局限在控制线上,没有输入处理操作(擦除, 杀掉, 删除, 等等)。 有两个参数来控制它的行为:: c_cc[VTIME] 用来设置字符定时器, c_cc[VMIN] 用来在读取动作执行前设置接收的最小字符数。
如果MIN > 0 并且TIME = 0, MIN 用来在读操作执行前设置要接收的字符个数。 由于TIME是零,所以此处不用。
如果MIN = 0 并且TIME > 0, TIME 会作为一个超时值。读操作会在如下的情况下被执行:一个单字符被读到, 或者 TIME 过期 (时间t = TIME *0。1 秒)。 如果TIME 超时, 不会读到任何字符。
如果MIN > 0 并且 TIME > 0, TIME就会作为一个字符间的定时器。读操作会在如下两种情况下执行:一是最少的(MIN)字符被接收到, 或者字符间的延时超过了TIME。 定时器会在每个字符被接收到以后重启动,并且仅当收到首字符以后才被激活。
如果MIN = 0 并且 TIME = 0, 读操作会立刻被执行。 当前有效字符串的个数, 或者被请求的字符个数会被返回。根据安东尼奥的研究 (参看下面的文档), 需要调用fcntl(fd, F_SETFL, FNDELAY)以后才能读到相同的结果。
通过修改newtio.c_cc[VTIME]和newtio.c_cc[VMIN],上面描述的所有模式都可以进行测试的。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX兼容资源*/
#define FALSE 0
#define TRUE 1
volatile int STOP=FALSE;
main()
{
int fd,c, res;
struct termios oldtio,newtio;
char buf[255];
fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY );
if (fd <0) {perror(MODEMDEVICE); exit(−1); }
tcgetattr(fd,&oldtio); /* 保存当前设置*/
bzero(&newtio, sizeof(newtio));
newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio.c_iflag = IGNPAR;
newtio.c_oflag = 0;
/* set input mode (non−canonical, no echo,。。。) */
newtio.c_lflag = 0;
newtio.c_cc[VTIME] = 0; /* 字符间定时器,此处未用*/
newtio.c_cc[VMIN] = 5; /* 收到五个字符时阻塞读取操作*/
tcflush(fd, TCIFLUSH);
tcsetattr(fd,TCSANOW,&newtio);
while (STOP==FALSE) { /* 输入循环*/
res = read(fd,buf,255); /* 收到5个字符后返回*/
buf[res]=0; /* 这一步方便打印。。。 */
printf(":%s:%d\n", buf, res);
if (buf[0]=='z') STOP=TRUE;
}
tcsetattr(fd,TCSANOW,&oldtio);
}
3.3. 异步输入
#include <termios.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/signal.h>
#include <sys/types.h>
#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 兼容资源*/
#define FALSE 0
#define TRUE 1
volatile int STOP=FALSE;
void signal_handler_IO (int status); /* 定义信号控制*/
int wait_flag=TRUE; /* 没有信号输入时为TRUE*/
main()
{
int fd,c, res;
struct termios oldtio,newtio;
struct sigaction saio; /* 信号动作定义*/
char buf[255];
/* 以非阻塞方式打开设备节点(读取操作会立刻返回) */
fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd <0) {perror(MODEMDEVICE); exit(−1); }
/*在设备进行异步操作前安装信号控制*/
saio.sa_handler = signal_handler_IO;
saio.sa_mask = 0;
saio.sa_flags = 0;
saio.sa_restorer = NULL;
sigaction(SIGIO,&saio,NULL);
/* 当收到SIGIO信号时同意处理 */
fcntl(fd, F_SETOWN, getpid());
/*使文件描述符异步(手册上说只能使用O_APPEND 和 O_NONBLOCK方式, 通过设置F_SETFL标记工作。。。) */
fcntl(fd, F_SETFL, FASYNC);
tcgetattr(fd,&oldtio); /* 保存当前的端口设置*/
/* 为标准输入处理设置新的端口设置*/
newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio.c_iflag = IGNPAR | ICRNL;
newtio.c_oflag = 0;
newtio.c_lflag = ICANON;
newtio.c_cc[VMIN]=1;
newtio.c_cc[VTIME]=0;
tcflush(fd, TCIFLUSH);
tcsetattr(fd,TCSANOW,&newtio);
/* 当读取到输入时进行循环。通常我们在这里会做些有用的事情 */
while (STOP==FALSE) {
printf("。\n");usleep(100000);
/* 收到SIGIO信号以后,wait_flag被置为FALSE, 输入操作有效并开始读取*/
if (wait_flag==FALSE) {
res = read(fd,buf,255);
buf[res]=0;
printf(":%s:%d\n", buf, res);
if (res==1) STOP=TRUE; /* 如果只有一个字符输入终止操作*/
wait_flag = TRUE; /* 等待新的输入*/
}
}
/* 恢复原来的设置*/
tcsetattr(fd,TCSANOW,&oldtio);
}
/***************************************************************************
* 信号控制。设置 wait_flag 为 FALSE以表明已经收到字符的输入。 *
***************************************************************************/
void signal_handler_IO (int status)
{
printf("received SIGIO signal。\n");
wait_flag = FALSE;
}
3.4. 多方输入的等待处理
This section is kept to a minimum。 It is just intended to be a hint, and therefore the example code is kept short。 这种模式不仅可以适用多个串口而且对其他的设备描述字集也适用。
选择调用和伴随的宏设置通过fd_set来进行。它是一个位数组,它对每个有效的文件描述符都有个位入口。Select 操作会接受一个a fd_set对一个文件描述符进行设置并返回一个fd_set, 在里面文件描述符的一些操作位会被设置,诸如输入,输出或者例外发生。所有的fd_set处理都是通过提供的宏定义来设置的。也请参看手册select(2)。
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
main()
{
int fd1, fd2; /* 输入来源有两个: 1 和 2 */
fd_set readfs; /* 文件描述符设置*/
int maxfd; /* 可用的最大文件描述符*/
int loop=1; /* 为TRUE时循环操作 */
/* open_input_source 函数打开一个设备, 正确的设置端口并返回一个文件描述符*/
fd1 = open_input_source("/dev/ttyS1"); /* COM2 */
if (fd1<0) exit(0);
fd2 = open_input_source("/dev/ttyS2"); /* COM3 */
if (fd2<0) exit(0);
maxfd = MAX (fd1, fd2)+1; /* 要测试的最大位入口*/
/* 循环输入*/
while (loop) {
FD_SET(fd1, &readfs); /* 对来源(文件描述符)1进行设置*/
FD_SET(fd2, &readfs); /*对来源(文件描述符)2进行设置*/
/* 在有有效输入之前进行阻塞*/
select(maxfd, &readfs, NULL, NULL, NULL);
if (FD_ISSET(fd1)) /* 如果来源1的输入有效*/
handle_input_from_source1();
if (FD_ISSET(fd2)) /*如果来源2的输入有效*/
handle_input_from_source2();
}
}
给定的例子会不定期的阻塞,收到有效输入时阻塞解除。如果你想设置输入超时, 只需要用下面的代码替换掉select调用:
int res;
struct timeval Timeout;
/* 在输入循环中设置超时值*/
Timeout.tv_usec = 0; /* 毫秒 */
Timeout.tv_sec = 1; /*秒 */
res = select(maxfd, &readfs, NULL, NULL, &Timeout);
if (res==0) /* 有输入的文件描述符个数为0时,会出现超时。*/
例子会在一秒后超时。 如果超时发生,select会返回0。需要注意的是超时是为了等待接受输入的,如果超时值为0,select 会立刻返回。
4. 其他资源
1〉Linux 串口HOWTO文档:描述了如何设置串口并包含了相关的硬件信息。
2〉POSIX兼容操作系统的串口编程向导, Michael Sweet。
3〉termios(3)的手册中介绍了所有termios结构的标志。