The Linux Serial Programming HOWTO by Peter H. Baumann, [1]Peter.Baumann@dlr.de 译者: 曾元佑 [2]yytseng@ms16.hinet.net v1.0, 22 一月 1998 _________________________________________________________________ 本文件将叙述如何在 Linux 环境下撰写序列埠的通讯程式. _________________________________________________________________ 1. 简介 * 1.1 版权 * 1.2 本文最新的版本 * 1.3 回馈 2. 开始 * 2.1 侦错 * 2.2 连接埠设定 * 2.3 序列装置的输入观念 3. 程式□例 * 3.1 标准输入程序 * 3.2 非标准输入程序 * 3.3 非同步式输入 * 3.4 等待来自多个讯号来源的输入 4. 其它资源 5. 贡献 _________________________________________________________________ 1. 简介 本文是 Linux 序列埠程式撰写的 HOWTO. 全篇都在讨论如何在 Linux 环境下, 以序列埠与其他 装置/电脑 通讯的程式写法. 所解释的技术包含: 标准的 I/O (只具备 传送/接收 线的), 非同步 I/O, 及 等待来自多信号源的输入讯号 的写 法. 本文不会叙述如何设定序列埠, 因为这在 Greg Hankins 的 Serial-HOWTO 已经 有说明了. 我必需强调我并非此领域中的专家, 而是在专案中曾遇到过这类的通讯问题. 在 这所提到的□例程式是衍生自 miniterm 的程式码. 可在 LDP 程式设计师指南取 得 (ftp://sunsite.unc.edu/pub/Linux/docs/LDP/programmers-guide/lpg-0.4.ta r.gz 及其他映射站) 在□例那个目录下. 我开始写这份文件是在 1997 年 六月, 现在我已经移转到 WinNT 以满足客户的 需求, 以致於我没能学得更深入的知识. 如果任何人有什麽意见, 我很乐意把它 摆进这份文件中 (参考 回馈 那一节). 如果有人能接手这份工作并加以改进, 请 e-mail 给我. 所有的□例都在 i386 Linux Kernel 2.0.29 下测试过. 1.1 版权 Linux Serial-Programming-HOWTO 的版权(C) 1997 是 Peter Baumann 所有. Linux HOWTO 文件可以完整或部份以实际或电子型式重制或散布, 只要版权宣告 能保留在所有散布的副本中. 商业性的重制散布是许可并被鼓励的; 不过, 如果 以此型式的散布 应该 告知作者. 所有有关的翻译, 衍生的工作, 或整合合并任何 Linux HOWTO 文件皆必须在此版 权宣告规□之下. 也就是, 你不可以自 HOWTO 所衍生的工作中, 散布的文件上附 加额外的限制条款. 除了这些规则之外皆可在某种条件的授与; 请联络 Linux HOWTO 协调员: 如以下所给的位址. 简而言之, 我们希望尽可能得透过各种管道促进这份资讯的流通, 不过, 我强烈 的希望将版权宣告置於 HOWTO 的文件上, 任何 想 重新散布 HOWTO 的人, 均希 望您能知会我们一下. 如果你有问题, 请经由 email 与 Tim Bynum, Linux HOWTO 协调员连络, [3]linux-howto@sunsite.unc.edu. 1.2 本文最新的版本 Serial-Programming-HOWTO 最新的版本将放在 [4]ftp://sunsite.unc.edu:/pub/Linux/docs/HOWTO/Serial-Programming-HOWT O 及其他映设站台. 有许多的格式, 如 PostScript 及 DVI 的版本放在 other-formats 目录下. Serial-Programming-HOWTO 也放在 [5]http://sunsite.unc.edu/LDP/HOWTO/Serial-Programming-HOWTO.html 并会 每个月摆一份到 [6]comp.os.linux.answers. 1.3 回馈 请把任何修正, 问题, 意见, 建议, 或其它附加的题材传送给我. 以让我改进这 份 HOWTO! 并详细告诉我哪个部份是您不能了解, 或不够清楚的. 你可以用 email 连络我 [7]Peter.Baumann@dlr.de. 请把 Serial-Programming-HOWTO 的 版本号码附上, 本文版本号码是 0.3. 2. 开始 2.1 侦错 最好的侦错你程式码的方法是建构另一台 Linux box, 并把两台电脑用 null-modem 缆线连接. 用 miniterm (可在 LDP 程式设计师指南取得 (ftp://sunsite.unc.edu/pub/Linux/docs/LDP/programmers-guide/lpg-0.4.ta r.gz 在□例那个目录下) 以传送字元到你的 Linux box. Miniterm 很容易编译 而它会把所有输入到键盘的字元透过序列埠传送. 只有这个宣告定义会被检查 #define MODEMDEVICE "/dev/ttyS0". 如果是 COM1 设定为 ttyS0, 如果是 COM2 设定为 ttyS1 等等.. 先前的测试是必要的, 所有的 字元都将以 raw 方式 (不经任何处理) 直接传送. 测试是否连接正确, 在两台电脑上都启动 miniterm 然後随便在键盘上乱按. 在其中一台上输入的字元应该会显示在另一台电脑上反 之亦同. 但输入的字元不会回应到与之相连的萤幕上. 要自制 null-modem 的电缆, 你必需要把 TxD (传送) 及 RxD (接收) 两线对 调. 详细的说明在 Serial-HOWTO 的第 7 段. 当然也可以只用一台电脑来作相同的测试, 只要电脑上有两个未使用的序列埠. 当然你也就要执行两个 miniterm 来当虚拟控制台. 如果你是藉由拔去滑鼠来取 得另一个序列埠, 记得要把 /dev/mouse 装置重新导向, 如果它存在的话. 如果 你使用多埠的序列埠控制卡, 请确定它已设定正确. 当我在我的电脑上测试时也 曾经因为设定错误而出过槌. 当我连到另一台电脑, 通讯埠开始传送字元. 就因 为刚好这不是完整的非同步式传输, 所以可在同一台电脑上执行两个程式. 2.2 连接埠设定 /dev/ttyS* 装置会被当成连接到你的 Linux box 的终端机, 并且在启动後就设 定好了. 这个观念在你写 raw 装置的通讯程式时必需记住. 也就是说这个连接埠 被设定为回应所有自这个装置送出的字元, 而用在资料传输时通常这种要改变这 种工作模式. 所有的参数可以由一个小程式简单的完成. 设定参数被放在一个结构体内 struct termios, 他的定义档在 : #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; /* 行控制 (line discipline) */ cc_t c_cc[NCCS]; /* 控制特性 */ }; 这个档案也包含所有的旗标定义. 输入模式旗标在 c_iflag 掌管所有的输入处 理, 这就意谓著由装置上传来的字元在还没用 read 功能读取前可以先处理过. 同理 c_oflag 掌管所有的输出处理. c_cflag 包含连接埠的设定, 如 鲍率, 每 字元多少位元, 停止位元, 等等.. 区域模式旗标放在 c_lflag 用来侦测字元是 否回应, 而讯号会送到你的程式, 等等.. 最後 c_cc 阵列定义了档案终了的控制 字元, 停止, 等等.. 预设的控制字元值放在 . 有关旗标的细节 摆在使用手册 termios(3). termios 结构体内的 c_line 行控制 (line discipline) 元素, 不能在 POSIX 相容的系统下使用 译者注:这里所说的 line discipline 虽然我翻成 行控制 但还是很难说出那是舍. 如果想知道请看看 kernel :( . 2.3 序列装置的输入观念 有三个输入的观念要说明. 按照所要写的应用程式选用适合的观念. 尽量避免使 用回圈来读取单一的字元再组成字串. 我曾这样做过, 会掉字元, 且对 read 而 言不会显示任何错误. 标准输入程序 这是终端机的标准处理程序, 但用来与其他 dl 型式的以行为单位的输入通讯也 很有用, 也就是 read 会传回一整行完整的输入资料. 行预设的终止字元是 NL (ASCII LF), 档案结束符, 或行终止字元. 预设环境下, CR (是 DOS/Windows 预 设的行终止符) 不会终止一行的叙述. 标准的输入处理程序还可以处理 清除, 删除字, 重印字元, 及转换 CR 为 NL 等 等功能.. 非标准输入程序 非标准输入程序可以用在需要每次读取固定数量字元的情况, 并允许使用字元输 入时间的计时器. 这种模式可以用在读取固定字元数量的应用程式, 或者所连接 的装置会突然送出大量字元的状况. 非同步式输入 以上所叙述的两种模式都可以用在非同步与同步的传输模式. 预设是在同步的模 式下工作, 也就是在尚未读取完之前, read 的状态会被阻断. 而非同步模式下 read 的状态会直接返回并送出讯号到所叫用的程式直到完成工作. 这个讯号可以 由讯号的处理程式 handler...来接收. 等待来自多个讯号来源的输入 这并不是一个不一样的输入模式. 如果你要透过序列埠连接并处理多个装置的 话, 它是满有用的. 在我的应用程式中我必需在几乎同一时间内, 透过 TCP/IP socket 及序列埠处理来自其他电脑的输入讯号. 下面这个□例程式将等待来自两 个不同输入源的讯号. 如果其中一个信号源出现, 他就会被处理, 而程式会继续 等待新的输入讯号. 以下这个方法看起来相当覆杂, 但请记住 Linux 是一个多工的作业系统. select 这个系统呼叫并不会在等待输入讯号时把 CPU 负载加重, 而如果你用回 圈方式来等待输入讯号将使得其它同时执行的行程被拖慢. 3. 程式□例 所有的□例来源自 miniterm.c. The type ahead 暂存器被限制在 255 个字元, 就跟标准输入程序的最大字串长度相同 (). 参考程式码中的注解它会解释不同输入模式的使用. 我希望这些程式码都能被了 解. 标准输入程序的程式□例的注解写得最好, 其它的□例都只在不同於其它□ 例的地方做注解. 叙述不是很完整, 但可以激励你对这□例做实验, 以延生出合於你所需应用程式 的最佳解. 别忘记要把序列埠的权限设定正确 (也就是: chmod a+rw /dev/ttyS1)! 3.1 标准输入程序 #include #include #include #include #include /* 鲍率设定被定义在 , 这在 被引入 */ #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: 设定 bps 的速度. 你也可以用 cfsetispeed 及 cfsetospeed 来设定. CRTSCTS : 输出资料的硬体流量控制 (只能在具完整线路的缆线下工作 参考 Serial-HOWTO 第七节) CS8 : 8n1 (8 位元, 不做同位元检查,1 个终止位元) CLOCAL : 本地连线, 不具数据机控制功能 CREAD : 致能接收字元 */ newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD; /* IGNPAR : 忽略经同位元检查後, 错误的位元组 ICRNL : 比 CR 对应成 NL (否则当输入讯号有 CR 时不会终止输入) 在不然把装置设定成 raw 模式(没有其它的输入处理) */ newtio.c_iflag = IGNPAR | ICRNL; /* Raw 模式输出. */ 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; /* 在读取到 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 */ 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 s). 如果超过 TIME 所定义的时间, 则不会传回任何字元. 如果 MIN > 0 且 TIME > 0, TIME 将被当做一个分割字元组的计时器. 满足读取 功能的条件为 接收到 MIN 个数的字元, 或两个字元的间隔时间超过 TIME 所定 义的值. 计时器会在每读到一个字元後重新计时, 且只会在第一个字元收到後才 会启动. 如果 MIN = 0 且 TIME = 0, 读取功能就马上被满足. 目前所存在的字元组个 数, 或者 将回传的字元组个数. 根据 Antonino (参考 贡献) 所说, 你可以用 fcntl(fd, F_SETFL, FNDELAY); 在读取前得到相同的结果. 藉由修改 newtio.c_cc[VTIME] 及 newtio.c_cc[VMIN] 上述的模式就可以测试 了. #include #include #include #include #include #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; /* 设定输入模式 (非标准型, 不回应,...) */ newtio.c_lflag = 0; newtio.c_cc[VTIME] = 0; /* 不使用分割字元组计时器 */ newtio.c_cc[VMIN] = 5; /* 在读取到 5 个字元前先停止 */ tcflush(fd, TCIFLUSH); tcsetattr(fd,TCSANOW,&newtio); while (STOP==FALSE) { /* 输入回圈 */ res = read(fd,buf,255); /* 在输入 5 个字元後即返回 */ buf[res]=0; /* 所以我们能用 printf... */ printf(":%s:%d\n", buf, res); if (buf[0]=='z') STOP=TRUE; } tcsetattr(fd,TCSANOW,&oldtio); } 3.3 非同步式输入 #include #include #include #include #include #include #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; /* definition of signal action */ char buf[255]; /* 开启装置为 non-blocking (读取功能会马上结束返回) */ 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()); /* 使档案ake the file descriptor 非同步 (使用手册上说只有 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; /* 如果只输入 CR 则停止回圈 */ 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 等待来自多个讯号来源的输入 这一段很短. 它只能被拿来当成写程式时的提示, 故□例程式也很简短. 但这个 □例不只能用在序列埠上, 还可以用在被当成档案来使用的装置上. select 呼叫及伴随它所引发的巨集共用 fd_set. fd_set 则是一个 位元阵列, 而其中每一个位元代表一个有效的档案叙述结构. select 呼叫接受一个有效的档 案叙述结构并传回 fd_set 位元阵列, 而该位元阵列中若有某一个位元为 1, 就 表示相对映的档案叙述结构的档案发生了输入, 输出或有例外事件. 而这些巨集 提供了所有处理 fd_set 的功能. 亦可参考手册 select(2). #include #include #include 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; /* 测试最大位元输入 (fd) */ /* 输入回圈 */ while (loop) { FD_SET(fd1, &readfs); /* 测试输入源 1 */ FD_SET(fd2, &readfs); /* 测试输入源 2 */ /* block until input becomes available */ 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) /* 档案叙述结构数在 input = 0 时, 会发生输入逾时. */ 这个程式会在 1 秒钟後逾时. 如果超过时间, select 会传回 0, 但是应该留意 Timeout 的时间递减是由 select 所等待输入讯号的时间为基准. 如果逾时的值 是 0, select 会马上结束返回. 4. 其它资源 * Linux Serial-HOWTO 叙述如何设定序列埠及所有相关的硬体资讯. * 由 Michael Sweet 所写的 [8]Serial Programming Guide for POSIX Compliant Operating Systems. 这个连结已经荒废了但我找不到它的新位 址. 有人知道能在哪找到它吗? 它是很棒的文件! * termios(3) 的使用手册. 叙述所有有关 termios 结构体的旗标. 5. 贡献 就跟简介所说的一样, 我并非在这领域有所专精, 但我自己遇到问题, 并透过他 人的帮助找到答案. 感谢来自 European Transonic Windtunnel 的 Strudthoff 先生, Cologne, Michael Carter (mcarter@rocke.electro.swri.edu), 及 Peter Waltenberg (p.waltenberg@karaka.chch.cri.nz) 与我同时准备这份文件的 Antonino Ianella (antonino@usa.net 所篆写的 Serial-Port-Programming Mini HOWTO. Greg Hankins 要求我把 Antonino's Mini-HOWTO 一并放入这份文件. 这份文件的结构及 SGML 的格式是源自 Greg Hankins 的 Serial-HOWTO. 感谢 Dave Pfaltzgraff (Dave_Pfaltzgraff@patapsco.com), Sean Lincolne (slincol@tpgi.com.au), Michael Wiedmann (mw@miwie.in-berlin.de), 及 Adrey Bonar (andy@tipas.lt) 各方面的协助. References 1. mailto:Peter.Baumann@dlr.de 2. mailto:yytseng@ms16.hinet.net 3. mailto:linux-howto@sunsite.unc.edu 4. ftp://sunsite.unc.edu/pub/Linux/docs/HOWTO/Serial-Programming-HOWTO 5. http://sunsite.unc.edu/LDP/HOWTO/Serial-Programming-HOWTO.html 6. news:comp.os.linux.answers 7. mailto:Peter.Baumann@dlr.de 8. http://www.easysw.com/~mike/serial