POSIX操作系统的串行编程指南
第五版
Michael R.Sweet
Copyright 1994-1999, Allrights Reserved
目录
序言
第一章 基本的串口通讯
什么是串口通讯
什么是RS-232
信号定义
异步通讯
什么是双工和单工
流量控制
什么是断开/break
同步通讯
访问串行端口
串行端口文件
打开串行端口
写入端口
读出端口
关闭端口
第二章 配置串行端口
POSIX终端接口
控制选项
本地选项
输入选项
输出选项
控制字符
第三章 Modem通讯
什么是Modem
与Modem通讯
标准Modem命令
常见的Modem通讯问题
第四章 高级串行端口编程
串行端口IOCTLs
获得控制信号
设置控制信号
获得有效字节数
从一个串行端口中选择输入
select系统调用
使用select系统调用
使用X Intrinsics库的Select接口
附录A.引出线
RS-232 引出线
RS-422 引出线
RS-574 (IBM PC/AT)引出线
SGI 引出线
附录B.控制符的ASCII编码
控制编码
序言
这篇POSIX操作系统的串口编程指南将告诉你如何正确,有效,可移植地对PC上的Unix工作站的串行端口进行编程.每一章提供了
使用POSIX终端控制函数的程序范例,只需要进行少量的更动就可以在IRIX,HP-UX,SunOS,Solaris,DigitalUnix,Linux
和其它的Unix上运行.并且你将会发现,各个操作系统之间的最大区别仅仅是用于标志串口设备和锁文件的文件名不同.
该指南被按如下的章节和附录给予说明.
第一章 基本的串口通讯
第二章 配置串行端口
第三章 Modem通讯
第四章 高级串行端口编程
附录A.引出线
附录B.控制符的ASCII编码
第一章 基本的串口通讯
这一章介绍了串行通讯,RS-232和其它用于大多数计算机上的标准,同时给出了一个用于访问串口的C程序.
1.1 什么是串口通讯 ?
计算机在某一时间内会传送一个或多个位,而串口是一次一位地传送.串口通讯包括大多数网络设备,键盘,鼠标,
调制解调器和终端设备.
当使用串口通讯时,你发送或收到的每一个字,(比如一个字节或者一个字符),实际上都是一次一位地传送的.每一
位或者为1或者为0.之后在你将听到的术语中,mark表示on(1),space表示off(0).
串行数据的速度经常用每秒位数(bps)或者波特率(baud)来表示.用来表示一秒中有多少1或者0被传送.在计算机
发明的初期,300baud被认为是很快的,而今天计算机的RS232可以达到430,800baud的速度!当波特率超过1,000时,
你通常会发现速率用千波特率,或kps(例如9,6k,19,2k)来表示.对于超过1,000,000的速率用兆波特率,或Mbps
(例如1.5Mbps).
当提到串口设备或者端口时,它们或者被标记为数据通信设备("DCE")或者数据终端设备("DTE").两者的不同非常简单:
每一个信号对,比如发送和接收,刚好反过来了.当要将两个DTE或者两个DCE接口连在一起时,一个串行的可以交换信号
对的NULL-Modem电缆或适配器可以完成这一任务.
1.2 什么是RS-232
RS-232是一种标准的用于EIA定义的串口通讯的电气接口.RS-232实际上由三种不同特点组成.每一种定义了不同
电压范围的on和off.常见的是RS-232C,定义mark(1)(on)指-3V到-12V之间的电压,space(0)指+3V到+12V之间的电压.
RS-232C规范中指出这些信号可以发送到25英尺(约8米)远.如果波特率足够低的话,这些信号可以发送的更远些.
除了用于传输的线路,还要提供定时,状态和握手:
表1 RS-232 引脚定义
引出口号 说明 引出口号 说明 引出口号 说明 引出口号 说明 引出口号 说明
---------+-----------+----------+---------------+---------+--------+---------+---------------+---------+------------
1 |接地 |6 |DSR数据序列就绪|11 |未定义 |16 |后备RXD |21 |信号质量检测
---------+-----------+----------+---------------+---------+--------+---------+---------------+---------+------------
2 |TXD输出 |7 |GND逻辑地 |12 |后备DCD |17 |接收者时钟 |22 |闹钟检测
---------+-----------+----------+---------------+---------+--------+---------+---------------+---------+------------
3 |RXD输入 |8 |DCD数据负载检测|13 |后备CTS |18 |未定义 |23 |数据速率选择
---------+-----------+----------+---------------+---------+--------+---------+---------------+---------+------------
4 |RTS请求发送|9 |保留 |14 |后备TXD |19 |后备RTS |24 |传输时钟
---------+-----------+----------+---------------+---------+--------+---------+---------------+---------+------------
5 |CTS请求接收|10 |保留 |15 |传输时钟|20 |DTR数据终端就绪|25 |未定义
---------+-----------+----------+---------------+---------+--------+---------+---------------+---------+------------
另外还有用于串口的两个标准,RS-422和RS-574.RS-422使用更低的电压和微分信号使电缆通讯距离达到将近1000英尺
(约300米).RS-574则定义了9针的PC串口连接器和电压.
1.3 信号定义
RS-232标准定义了18个不同的用于串口通讯的信号.其中,仅有6个在Unix环境中是普遍适用的.
GND-逻辑地
严格地来说,逻辑地并不是信号,但是没有它,其它的信号无法工作.逻辑地作为一个参照电压以告诉电器
哪个电压是正哪些是负.
TXD-数据被发送
TXD信号承载从你的工作站发送到其它的机器或设备上的数据.mark电压表示1,space电压表示0.
RXD-被接收的数据
RXD信号承载从其它机器或设备上向你的工作站发送数据.类似TXD,mark和space电压分别表示1和0.
DCD-数据载波探测
DCD信号从串口电缆的另一端的机器或设备发送出来.串行线上的space(0)电压表示机器或设备处于连线状态.
DCD并不总是被使用.
DTR-数据终端就绪
DTR信号是从你的工作站发送出来,告诉另一端的机器或设备你已经就绪或者尚未就绪,分别用space(0)电压和
mark(1)电压表示.自从你的工作站打开串行接口之后,DTR一直处于自动激活状态.
CTS-设备接收就绪,等待新的数据被发送 ( Clear to Send )
CTS信号来自串行电缆的另一端.space(0)电压表示你可以从你的工作站上发送更多数据了.(以前接收的已
经处理完毕)
CTS常用来调节工作站与串行线另一端的数据传送流量.
RTS-请求发送
RTS信号被你的工作站置成space(0)电压用来表示有更多的数据等待发送.
与CTS类似,RTS也是用来调节串行电缆两个终端之间的数据传送流量.大多数工作站把这个信号一直置为
space(0)电压.
1.4 异步通讯
对于支持串行通讯的机器而言,它需要知道如何判别数据起始和终止.这篇指南将介绍异步串行数据.
在异步模式下,串行数据直到有字符被发送前一直保持mark(1)状态.在每个字符之前有一开始位,紧接着是结束位,一
个可选的奇偶位和若干个停止位.起始位总是space(0),告诉机器串行数据已经准备好了.数据可以在任何时候被发送
或接收,因此称之为异步的.
图1 异步数据传输
space +--+ +-----+ +--+ +--+ +12伏
|开|0 1 |2 3 |4 |5 |6 |7 |
|始|位 位|位 位|位|位|位|位|
| | | | | | | |
mark ----+ +-----+ +--+ +--+ +------ -12伏
可选的奇偶位简单地统计数据中包含奇数个1还是偶数个1.如果有偶数个1,则这位为0,称偶校验位.如果是奇数个1,则
该位为0,称奇校验位.通常还有spaceparity,mark parity和no parity的说法.SpaceParity就是校验位总是0,markParity
就是校验位总是1,NoParity表示不使用或不传输校验位.
接着的那位是终止位.在字符之间可以有1,1.5或2个终止位,它们的值总是1.以前终止位是为了让机器有更多的时间去
处理前一个字符,但现在仅仅是用于在机器接收字符时的同步.
异步数据格式通常用诸如"8N1","7E1"等等表示.前者表示8个数据位,没有校验位,1个终止位.后者表示7个数据位,偶校
验位,1个终止位.
1.4.1 什么是全双工和单双工
全双工表示机器可以同时既发送数据又可以接收数据,有两个独立的数据通道(一进一出).
单双工表示机器不能在发送数据的同时又接收数据.通常这意味这仅仅有一个数据通道在使用.但这并不意味不能使用
RS-232信号,而是通讯联接仅能使用不支持全双工操作的其它的标准,而不是RS-232.
1.4.2 流量控制
当在两个串行接口之间传输数据时通常有需要调节数据流量.这可能需要对之间的串行通讯联接,某个串行接口或者某个
存储介质实行一些限制.对于异步数据通常有两种方法:
一是软件流量控制.使用特殊的字符去标志数据流的开始(XON,DC1,八进制的021或结束(XOFF,DC3,八进制的023).这些
字符可以在ASCII中找到定义.然而当传输文本信息时,这些字符本身拥有一定的含义,不能被使用.
另一种是硬件流量控制,用RS232的CTS和RTS信号代替特殊字符用于控制.当接收方准备接收更多的数据时,设置CTS为
space(0),反之设成mark(1).对应的,当发送方准备发送更多的数据时,设置RTS为space(0).由于硬件的流量控制使用
独立的一套信号,比软件的实现更快,因为作同样的工作,后者需要发送或接收多位信息.另外需要注意的是,并不是所
有的硬件和操作系统都支持CTS/RTS流量控制.
1.4.3 什么是中断/Break
一般而言,一个接收或者传输数据的信号直到一个新的字符被传送之前一直保持mark(1).信号过了很长时间后才被降
低到space(1),通常时1/4到1/2秒,那么我们称这种状态为break.
Break有时被用来重置通讯线路或者改变诸如Modem的通讯设备的工作模式.第三章的"如何操作Modem"将更深入地谈论
这个问题.
1.5 同步通讯
与异步数据不同的是,同步数据是一种恒定的位流.为了从通讯线上读出数据,机器必须发送或者接收一个普通的位作为
时钟以达到发送端和接收端的同步.
对于同步,机器必须标志数据的起始位置.实现它的最普通的方法时使用类似串行数据链路控制SDLC或者高速数据链路控
制HDLC之类的数据包协议.
每一个协议定义了固定的位顺序以表示数据包的开始和结束,同时也定义了被用来表示没有数据时使用的位顺序.机器
根据这些位顺序可以知道数据包的开始位.
因为同步协议不使用每个字符的同步位,所以它们一般能比异步通讯提高至少25%的性能,适合远程网络和两个以上的串行
接口的配置.
尽管同步协议拥有速度优势,但大多数RS232硬件仍然由于额外的硬件和必要的软件而没有支持它.
1.6 访问串行端口
类似于所有设备,UNIX系统通过设备文件提供了对串行端口的访问.为了访问它,仅仅需要打开相应的设备文件.
1.6.1 串行端口文件
在UNIX系统的每一个串行端口都有一个或者更多的设备文件与它们相对应(在/dev/目录下).
表2 -----串行端口的设备文件名
----------------------------------------+------------------+-------------- --+
操作系统 | 端口1 | 端口2 |
---------------------------------------+------------------+-----------------+
IRIX(R) | /dev/ttyf1 | /dev/ttyf2 |
---------------------------------------+------------------+-----------------+
HP-UX | /dev/tty1p0 | /dev/tty2p0 |
---------------------------------------+------------------+-----------------+
Solairis(R)/SunOS(R) | /dev/ttya | ./dev/ttyb |
---------------------------------------+------------------+-----------------+
Linux(R) | /dev/ttyS0 | /dev/ttyS1 |
---------------------------------------+------------------+-----------------+
Digital UNIX(R) | /dev/tty01 | /dev/tty02 |
---------------------------------------+------------------+-----------------+
1.6.2 打开串行端口
既然串行端口是一个文件,open(2)函数可以被用来访问它.UNIX系统的一个特点是一般用户无法访问设备文件.所以需要
更改你所关心的设备文件的访问权限,以超级用户来运行程序或者在你的程序中使用setuid,从而拥有设备文件的访问权.
这里,我们假设该文件可以被所有用户访问.在运行IRIX的SGI工作站上打开串行端口的代码如下:
listing 1 open a serial port
#include <stdio.h> /*标准的输入输出*/
#include <string.h> /*字符串相关函数*/
#include <unistd.h> /*UNIX标准的函数*/
#include <fcntl.h> /*文件控制*/
#iclude <errno.h> /*出错信息*/
#include <termios.h> /*POSIX终端控制定义*/
/* open_port() ----打开串行端口*/
打开成功则返回文件描述符,否则返回-1
*/
int open_port(void)
{
int fd;
fd = open("/dev/ttyf1",O_RDWR | O_NOCTTY | O_NDELAY );
if( fd == -1 ){
perror("open_port:Unable to open /dev/ttyf1 -");
}
else
fcntl(fd,F_SETFL,0);
return fd;
}
其他的系统只需要更换设备文件名,其它代码是一样的.
1.6.3 端口的打开选项
你会发现当我们打开设备文件后,我们除了用读写模式外还用了另外两个标志:
fd = open("/dev/ttyf1",O_RDWR | O_NOCTTY | O_NDELAY );
O_NOCTTY标志告诉UNIX系统,这个程序不会成为对应这个端口的控制终端,如果没有指定这个标志,那么任何一个
输入,诸如键盘中止信号等等,都将会影响你的进程.类似于getty的程序当启动login进程时都使用了这个特性,
但是一般的用户程序不需要这这样作.
O_NDELAY标志告诉UNIX系统这个程序不关心DCD信号线所处的状态,它用来表示端口的另一端是否激活或者停止.
如果你想指定这个标志,你的进程将会一直处在睡眠态,直到DCD信号线是space(0).
1.6.4 写数据到端口
写数据到端口很简单,就是用write(2)系统调用把数据发送出去.
n = write(fd,"ATZ/r",4);
if( n < 0 )
fputs("write() of 4 bytes failed!/n",stderr);
write返回有多少个字节被发送出去,如果出错,则返回为-1.通常你可能遇到的唯一的错误是EIO,当MODEM或者
数据链路撤销数据载波探测(DCD linke)时会出现这种情况,这种情况将一直存在,直到你关闭端口.
1.6.5 从端口中读取数据
从端口中读出数据有些技巧.当你在RAW数据模式下操作时,每一个read(2)系统调用将返回当前串行输入缓存区中
存在的字符数.如果没有字符,这个系统调用将一直阻塞到有字符到达或者间隔时钟过期,或者错误发生.当read函数
如下设置后将会立即返回.
fcntl(fd,F_SETFL,FNDELAY);
FNDELAY选项将导致当端口上没有字符可读时,read函数返回0.为恢复到一般状态,可以在调用fcntl时不带FNDELAY选项.
fcntl(fd,F_SETFL,0);
在用O_NDELAY打开端口之后也可以这样来恢复.
1.6.6 关闭串行端口
为了关闭串行端口,只需要使用close系统调用
close(fd);
关闭串行端口经常会设置DTR信号为低,导致大多数的MODEM挂起.
第二章 配置串行端口
这一章将讨论如何用C语言的POSIX终端接口去配置串行端口.
2. 1 POSIX终端接口
大多数系统都支持POSIX终端接口,用于改变诸如波特率,字符大小等等参数.你所需要作的第一件事情是将termios.h
include到你的程序中,这个文件中其中定义了POSIX控制函数和终端控制结构.
两个重要的POSIX函数是tcgetattr(3)和tcsetattr(3),分别用于获得和设置终端属性;你需要提供一个包含所有可用串
行选项的终端结构:
表3 - Termios结构的成员
+---------------------------------------------------------------+
| 成员 | 说明 |
+---------------------------------------------------------------+
| c_cflag | 控制选项 |
+---------------------------------------------------------------+
| c_cflag | 线路选项 |
+---------------------------------------------------------------+
| c_iflag | 输入选项 |
+---------------------------------------------------------------+
| c_oflag | 输出选项 |
+---------------------------------------------------------------+
| c_cc | 控制特性 |
+---------------------------------------------------------------+
| c_ispeed | 输入波特律(新接口) |
+---------------------------------------------------------------+
| c_ospeed | 输出波特律(新接口) |
+---------------------------------------------------------------+
2.2 控制选项
c_cflag成员用于控制波特率,数据位的数目,奇偶校验位,终止位和硬件流控制.以下是所支持的配置常数.
表4 - c_cfalg成员的可设置常量
+---------------------+-----------------------------------------+
| 常量 | 说明 |
+---------------------+-----------------------------------------+
| CBAND | 波特律的位掩码 |
+---------------------+-----------------------------------------+
| B0 | 0 波特 (放弃DTR) |
+---------------------+-----------------------------------------+
| B50 | 50 波特 |
+---------------------+-----------------------------------------+
| B75 | 75 波特 |
+---------------------+-----------------------------------------+
| B110 | 110 波特 |
+---------------------+-----------------------------------------+
| B134 | 134 波特 |
+---------------------+-----------------------------------------+
| B150 | 150 波特 |
+---------------------+-----------------------------------------+
| B200 | 200 波特 |
+---------------------+-----------------------------------------+
| B300 | 300 波特 |
+---------------------+-----------------------------------------+
| B600 | 600 波特 |
+---------------------+-----------------------------------------+
| B1200 | 1200 波特 |
+---------------------+-----------------------------------------+
| B1800 | 1800 波特 |
+---------------------+-----------------------------------------+
| B2400 | 2400 波特 |
+---------------------+-----------------------------------------+
| B4800 | 4800 波特 |
+---------------------+-----------------------------------------+
| B9600 | 9600 波特 |
+---------------------+-----------------------------------------+
| B19200 | 19200 波特 |
+---------------------+-----------------------------------------+
| B38400 | 38400 波特 |
+---------------------+-----------------------------------------+
| B57600 | 57600 波特 |
+---------------------+-----------------------------------------+
| B115200 | 115200 波特 |
+---------------------+-----------------------------------------+
| EXTA | 外部时钟律 |
+---------------------+-----------------------------------------+
| EXTB | 外部时钟律 |
+---------------------+-----------------------------------------+
| CSIZE | 数据位的位掩码 |
+---------------------+-----------------------------------------+
| CS5 | 5个数据位 |
+---------------------+-----------------------------------------+
| CS6 | 6个数据位 |
+---------------------+-----------------------------------------+
| CS7 | 7个数据位 |
+---------------------+-----------------------------------------+
| CS8 | 8个数据位 |
+---------------------+-----------------------------------------+
| CSTOPB | 2个停止位(不设则是1个停止位) |
+---------------------+-----------------------------------------+
| CREAD | 接收使能 |
+---------------------+-----------------------------------------+
| PARENB | 校验位使能 |
+---------------------+-----------------------------------------+
| PARODD | 使用奇校验而不使用偶校验 |
+---------------------+-----------------------------------------+
| HUPCL | 最后关闭时挂线(放弃DTR) |
+---------------------+-----------------------------------------+
| CLOCAL | 内部线路 - 不改变端口所有者 |
+---------------------+-----------------------------------------+
| LOBLK | 块作业控制输出 |
+---------------------+-----------------------------------------+
| CNET_CTSRTS | 硬件流控制使能 |
| CRTSCTS | (并非所有硬件都支持) |
+---------------------+-----------------------------------------+
c_cflag成员包含了两个应该一直激活的选项,CLOCAL和 CREAD.These will ensure that your program does not
become the 'owner' of the port to sporatic job control and hangup signals,and also that the serail
interface driver will read incomfing data bytes.
波特率常数(CBAUD,B9600,等)被用于没有c_ispeed 和 c_ospeed成员的早期接口.下一小节中将使用POSIX函数设置波特率.
不要直接去初始化c_cflag的(或其它标志的)成员;你应该总是去使用逐位的AND,OR和NOT操作符去设置或清除成员中的位.
不同版本的操作系统(甚至补丁)都可以不同方式地使用这些位,所以用逐位操作符将会避免你使用将在更新的串口驱动
中使用的位标志.
2.2.1 设置波特率
不同的系统的波特率储存在不同的位置.早期的接口用表4中的某一个波特率常数储存在c_cflag中,然而更新的接口则
是使用c_ispeed和c_ospeed去储存实际的波特率值.
cfsetospeed和cfsetispeed函数用来设置termios结构中的波特率,不管是在哪一个系统接口下.一般你可以用以下的代
码设置波特率:
listing-2 Setting the baud rate.
stuct termios options;
/* Get the current optinos for the port ...*/
tcgetattr(fd,&options);
/* Set the baud rates to 19200 ... */
cfsetispeed(&options,B19200);
cfsetospeed(&options,B19200);
/* Enable the receiver and set local mode ... */
options.c_cflag |= (CLOCAL | CREAD );
/* Set the new options for the port ... */
tcsetattr(fd,TCSANOW,&options);
tcgetattr(3) 函数将当前串行端口的配置填充到options结构中.在我们设置波特率,激活本地模式和串行数据接收后,用
tcsetattr(3)去设置新的配置.TCSANOW常数指定所有的更改立即生效,而不会去等待输出的数据发送完毕和输入的数据接收
完毕,也可以用其它的常数去等待输入和输出的完成或者刷新输入和输出缓存.
大多数系统不支持输入与输出的速率不等,所以为了可移植性必须将它们设成相等的值.
表5 - tcsetattr可设置常量
+---------------------+-----------------------------------------+
| 常量 | 说明 |
+---------------------+-----------------------------------------+
| TCSANOW | 不等数据传输完全就改变|
+---------------------+-----------------------------------------+
| TCSADRAIN | 等待所有数据传输结束 |
+---------------------+-----------------------------------------+
| TCSAFLUSH | 清空输入输出缓冲区并设置|
+---------------------+-----------------------------------------+
2.2.2 设置字符的大小
不象波特率,设置字符大小没有可用的函数.你不得不用位掩码去设置它们.字符大小用位指定:
options.c_cflag &= ~CSIZE; /* mask the character size bits */
options.c_cflag |= CS8; /* select 8 data bits */
2.2.3 设置奇偶校验位
类似字符大小,你必须手工激活奇偶校验和设置校验类型.UNIX串行驱动支持偶,奇和无校验位.space(0)将用更好的方式去
表示.
.NO parity(8N1):
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= ~CS8;
.Even parity(7E1):
options.c_cflag |= PARENB;
options.c_cflag &= ~PARODD;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS7;
.Odd Parity(7O1):
options.c_cflag |= PARENB;
options.c_cflag |= PARODD;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS7;
.Space parity is setup the same as no parity(7S1):
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
2.2.4 设置硬件流控制
一些版本的UNIX支持使用CTS(Clear to Send)和RTS(Requeut to send)信号线的硬件流量控制.如果你的系统中定义了
CNEW_RTSCTS或者CRTSCTS常数,那么就可能支持硬件流量控制.以下可以激活硬件流量控制:
options.c_cflag |= CNEW_RTSCTS; /* also called CRTSCTS */
类似地可以这样关闭硬件流量控制:
options.c_cflag &= ~CNEW_RTSCTS;
2.3 本地选项
本地模式c_lflags控制驱动程序如何管理输入字符.一般而言,你可以配置c_lflag为规范或者原始输入.
表6 - c_lflag成员可设置常量
+---------------------+-----------------------------------------+
| 常量 | 说明 |
+---------------------+-----------------------------------------+
| ISIG | 将SIGINTR, SIGSUSP, SIGDSUSP
|和 SIGQUIT信号使能 |
+---------------------+-----------------------------------------+
| ICANON | 标准(canonical)输入模式使能
(否则是原始[raw]方式) |
+---------------------+-----------------------------------------+
| XCASE | 映射大写字母为 /小写字母(此常量已过时) |
+---------------------+-----------------------------------------+
| ECHO | 输入字符回显使能 |
+---------------------+-----------------------------------------+
| ECHOE | 删除字符回显为BS-SP-BS使能 |
+---------------------+-----------------------------------------+
| ECHOK | kill字符后回显NL(新行)字符 |
+---------------------+-----------------------------------------+
| ECHONL | 回显NL字符 |
+---------------------+-----------------------------------------+
| NOFLUSH | 取消中断或退出字符后清空输入缓冲区|
+---------------------+-----------------------------------------+
| IEXTEN | 扩展功能使能 |
+---------------------+-----------------------------------------+
| ECHOCTL | 回显控制字符为"^字符", 删除字符为"~?"|
+---------------------+-----------------------------------------+
| ECHOPRT | 回显被删除的字符为字符删除|
+---------------------+-----------------------------------------+
| ECHOKE | 在行kill字符时回显全行BS-SP-BS|
+---------------------+-----------------------------------------+
| FLUSHO | 清空输出 |
+---------------------+-----------------------------------------+
| PENDIN | 在下一个读入或输入字符
时重打入等待输入 数据 |
+---------------------+-----------------------------------------+
| TOSTOP | 在后台输出时发送SIGTTOU信号|
+---------------------+-----------------------------------------+
2.3.1 选择规范输入
规范输入是面向行的输入方式.输入字符被放进用于和用户交互可以编辑的缓存区中,直到读入回车键或者换行符才结束.
当选择这种模式时,你一般可以使用ICANON,ECHO和ECHOE几个选项:
options.c_lflag |= ( ICANON | ECHO | ECHOE );
2.3.2 选择原始输入
原始输入是未经处理的.当接收时,输入的字符在它们被收到后立即被传送.使用原始输入时,一般你可以取消选择
ICANOON,ECHO,ECHOE和ISIG选项:
options.c_lflag &= ~ ( ICANON | ECHO | ECHOE );
2.3.3 输入回显的注意事项
输入模式c_iflag控制端口接收端的字符输入处理.类似于c_cflag,储存在c_iflag中的值是所需选项的逐位或.
2.4 输入选项
Table 7
2.4.1 设置输入奇偶选项
当你激活c_cflag中的奇偶检验后,你应该激活输入的奇偶检验.与之相关的常数有INPCK,IGNPAR,PARMRK和ISTRTP.一般你
将选择INPCK和ISTRTP去激活检验和移除奇偶位:
options.c_iflag |= (INPCK | ISTRIP );
IGNPAR有些危险,它告诉串行驱动忽略奇欧错误,继续传送字符就好像没有错误发生一样.这可能对检验通讯链路的质量
有帮助,当一般情况下由于实际原因而不使用.
PARMRK导致奇偶错误在输入流中用特殊字符被标记.如果IGNPAR被激活,在每一个奇偶错误发生的字符之前,NULL字符
(000八进制)将被发送给你的程序.否则,DEL(177八进制)和 NUL字符将和错误的字符一起被发送.
2.4.2 设置软件流控制
软件流控制可以用IXON,IXOFF和IXANY常数来激活:
options.c_iflag |= (IXON | IXOFF | OXANY );
取消它仅需将那些位掩住:
options.c_iflag &= ~(IXON | IXOFF | OXANY );
IXON(起始数据)和XOFF(终止数据)在以下介绍的c_cc数组中被定义
2.5 输出选项
c_oflag包含了输出过滤选项.类似于输入模式,你可以选择已预处理或者原始的数据输出.
Table 8
2.5.1 选择预处理的输出
可以通过设置OPOST选项来选择预处理的输出:
options.c_oflag |= OPOST;
在所有各种选项中,你也许仅仅可以使用ONLCR选项,它将新行映射成CR和LF两个符号.
其它的输出选项主要是由于过去的行式打印机和终端的速率不能跟上串行数据流.
2.5.2 选择原始输出
原始输出可以用c_oflag的OPOST选项
options.c_oflag &= ~OPOST;
当OPOST选项取消后,所有c_oflag的其它选项位都忽略.
2.6 控制字符
c_cc字符数组包含了过时参数和控制字符的定义.数组中的每一个元素都用常数定义.
Table 9
2.6.1 设置软件流控制字符
c_cc数组中的VSTART 和 VSTOP是勇于软件流控制的字符.一般而言,它们应该被设置成
DC1(021八进制)和DC3(023八进制),分别表示ASCII码中的XON和XOFF字符.
2.6.2 设置读超时
UNIX串行接口驱动提供了对字符和数据包的读超时控制.c_cc数组中的VMIN和VTIME就是
这样的两个控制字符.在规范的输入模式下或者当NDELAY选项在对文件操作的open或fcntl
被指定时,这儿的超时控制就被忽略而使用无效.
VMIN指定了最少读取的字符数.如果被设置为零,那么VTIME值就指定了读取每个字符的等待
时间.注意的是,这并不意味着读取N字节的read系统调用将会等待到读取N个字符为止.相反,
这个超时控制仅仅用于第一个字符,read系统调用将立即返回有效的字符数(倚赖于read请求
的字符数).
/*------这地方没明白,好像是如果你请求了n个字节,这样设置的话,当读到一个字
节的话,read立即返回,并返回为n,也许这种情况下,多用于1个字节的超时读取,n=1.所以这种i
情况下读取n个字节的话,应该for(int i = 0;i< n; i++) read(fd,c,1);
如果是这样的话,原文这样写就好懂多了.
Rather,the timeout will apply the first charater and the read call will return one
or zero,at most one byte.So if you want to read n bytes,you should loop again,which
is up to the number you want to request.
*/
如果VMIN非零,VTIME指定了读取第一个字符的等待时间,如果在给定时间内读取了一个字符,那么
read系统调用将阻塞直到所有VMIN指定的字符数被读取.也就是说一旦读到第一个字符,串行接口
驱动将期望收到完整的VMIN长度的数据包.如果在给定的时间内没有没有读到任何字符.read调
用将会返回为零.这种设置要求串行驱动准确地读取N字节的字符,read调用要么返回为零或者N.
但是,超时仅仅指定在读取第一个字符,所以如果由于某种原因,驱动丢失了N字节数据包中的一个
字符,那么read调用将会永久性地阻塞在这儿,等待更多的输入字符.
VTIME是以1/10秒为单位指定接收字符的超时时间.如果VTIME设置为零(缺省情况下),而该端口又没
有用open或fcntl设置NDELAY,read将会阻塞不确定的时间.
第三章 Modem通讯
这一章描述了拨号电话调制解调器通讯的基本知识.并给出了一个使用标准AT指令集的MODEM范例.
3.1 什么是Modem
MODEMs是一个将串行数据调制可以在模拟数据链路上传输的一定频率的设备,比如电话线或者有线电
视线 . 一个标准的电话调制解调将串行数据转换成可以可以在电话线上传输的音调;由于这种转换
的速度和复杂,这些音调听起来更象大声的尖叫.
现在的电话MODEMs可以以每秒53,000位,也就是53kbps的速度在电话线上传输数据,另外大多数的MODEMs
使用数据压缩技术提高传输速率,有些类型的数据可以超过100kbps.
3.2 与MODEM通讯
第一步是打开并配置原始输入的端口
Listing 3 配置原始输入端口
int fd;
struct termios options;
/*打开端口*/
fd = open("/dev/ttyf1",O_RDWR|O_NOCTTY|O_NDELAY);
fcntl(fd,F_SETFL,0);
/*获取当前选项*/
tcgetattr(fd,&options);
/*设置原始输入,1秒的超时*/
options.c_cflag |= (CLOCAL|CREAD);
options.c_lflag &= ~(ICANON | ECHO | ECHOE |ISIG);
optiions.c_oflag &= ~OPOST;
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 10;
/*设定选项*/
tcsetattr(fd,TCSANOW,&options);
下一步是建立预MODEM的通讯.最好的方法是发送AT命令给MODEM.这也可以让MODEM去探测你
将使用的波特率.当MODEM打开并成功地连接上后,它将回应"OK".
Listing 4 初始化MODEM
int /* 返回值 0 = 成功, -1 = 失败 */
init_modem(int fd) /* 串行端口文件描述符 */
{
char buffer[255]; /* 输入缓冲区 */
char *bufptr; /* 缓存区中的当前字符 */
int nbytes; /* 读取的字节数 */
int tries; /* 目前重试的次数 */
for(tries = 0; tries < 3 ;tries ++)
{
/*发送AT命令,后跟一个CR */
if(write(fd,"AT/r",3) < 3)
continue;
/*将字符读入字符串缓冲中,直到一个新行 */
bufptr = buffer;
while( ( nbytes = read(fd,bufptr,buffer+sizeof(buffer) - bufptr -1 )) > 0)
{
bufptr += nbytes;
if( bufptr[-1] == '/n' || bufptr[-1] == '/r' )
break;
}
/*设置 NULL中止字符串,看我们是否得到了一个成功的回应 */
*bufptr = '/0';
if( strncmp(buffer,"OK",2) == 0)
return (0);
}
return (-1);
}
3.3 标准的MODEM命令
大多数的MODEM支持AT命令集,所以每个命令前冠以"AT".每个命令按照以下根式被发送:AT+指定的命令+回车字符
(CR,015八进制).在处理命令之后,MODEM将会根据命令回应相应的文本消息.
3.3.1 ATD ----拨单个数字
ATD命令拨指定的数字.除了数字和长划符号外,你可以指定音调("T"),脉冲("P"),每秒钟的暂停(",")和等待拨号音("W"):
ATDT 555-1212
ATDT 18009009009W1234,1,1234
ATD T555-1212WP1234
MODEM的回应消息有以下几种:
NO DIALTONE
BUSY
NO CARRIER
CONNECT
CONNECT baud
3.3.2 ATH---挂起
ATH命令导致MODEM挂起.因为MODEM必须是在"命令"模式中,你也许在普通的电话呼叫中不使用它.
如果DTR被dropped,大多数MODEM也会挂起.你可以通过在至少一秒的时间中设置波特率为0实现这一点.DroppingDTR也会让MODEM
回到命令模式中.
在一次成功的挂起之后,MODEM将会回应"NO CARRIER".如果MODEM仍然是被连接的,"CONNECT"或者"CONNECT baud"消息将会被发送.
3.3.3 ATZ---重置MODEM命令
ATZ命令将重置MODEM.MODEM将回应"OK".
3.4 一般MODEM通讯的问题
首先最重要的是,不要忘记取消输入回显.输入回显将会造成MODEM和机器之间的一次反馈.
其次,当发送MODEM命令时,你必须以回车CR来结束它,而不是一个新行NL.C语言中CR的字符是'/r'.
最后,当使用MODEM时,取保你使用的是MODEM所支持的波特率.尽管许多MODEM可以自动波特率探测,但一些仍然有19.2kbps的限制,
不得不注意.
2005年11月15日
Linux 操作系统从一开始就对串行口提供了很好的支持,本文就 Linux 下的串行口通讯编程进行简单的介绍。
串口简介
串行口是计算机一种常用的接口,具有连接线少,通讯简单,得到广泛的使用。常用的串口是 RS-232-C 接口(又称 EIA RS-232-C)它是在 1970 年由美国电子工业协会(EIA)联合贝尔系统、 调制解调器厂家及计算机终端生产厂家共同制定的用于串行通讯的标准。它的全名是"数据终端设备(DTE)和数据通讯设备(DCE)之间串行二进制数据交换接口技术标准"该标准规定采用一个 25 个脚的 DB25 连接器,对连接器的每个引脚的信号内容加以规定,还对各种信号的电平加以规定。传输距离在码元畸变小于 4% 的情况下,传输电缆长度应为 50 英尺。
Linux 操作系统从一开始就对串行口提供了很好的支持,本文就 Linux 下的串行口通讯编程进行简单的介绍,如果要非常深入了解,建议看看本文所参考的 《Serial Programming Guide for POSIX Operating Systems》
计算机串口的引脚说明
序号 | 信号名称 | 符号 | 流向 | 功能 |
2 | 发送数据 | TXD | DTE→DCE | DTE发送串行数据 |
3 | 接收数据 | RXD | DTE←DCE | DTE 接收串行数据 |
4 | 请求发送 | RTS | DTE→DCE | DTE 请求 DCE 将线路切换到发送方式 |
5 | 允许发送 | CTS | DTE←DCE | DCE 告诉 DTE 线路已接通可以发送数据 |
6 | 数据设备准备好 | DSR | DTE←DCE | DCE 准备好 |
7 | 信号地 | | | 信号公共地 |
8 | 载波检测 | DCD | DTE←DCE | 表示 DCE 接收到远程载波 |
20 | 数据终端准备好 | DTR | DTE→DCE | DTE 准备好 |
22 | 振铃指示 | RI | DTE←DCE | 表示 DCE 与线路接通,出现振铃 |
串口操作
串口操作需要的头文件
#include <stdio.h> /*标准输入输出定义*/
#include <stdlib.h> /*标准函数库定义*/
#include <unistd.h> /*Unix 标准函数定义*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> /*文件控制定义*/
#include <termios.h> /*PPSIX 终端控制定义*/
#include <errno.h> /*错误号定义*/
|
打开串口
在 Linux 下串口文件是位于 /dev 下的
串口一 为 /dev/ttyS0
串口二 为 /dev/ttyS1
打开串口是通过使用标准的文件打开函数操作:
int fd;
/*以读写方式打开串口*/
fd = open( "/dev/ttyS0", O_RDWR);
if (-1 == fd){
/* 不能打开串口一*/
perror(" 提示错误!");
}
|
设置串口
最基本的设置串口包括波特率设置,效验位和停止位设置。
串口的设置主要是设置 struct termios 结构体的各成员值。
struct termio
{ unsigned short c_iflag; /* 输入模式标志 */
unsigned short c_oflag; /* 输出模式标志 */
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /* local mode flags */
unsigned char c_line; /* line discipline */
unsigned char c_cc[NCC]; /* control characters */
};
|
设置这个结构体很复杂,我这里就只说说常见的一些设置:
波特率设置
下面是修改波特率的代码:
struct termios Opt;
tcgetattr(fd, &Opt);
cfsetispeed(&Opt,B19200); /*设置为19200Bps*/
cfsetospeed(&Opt,B19200);
tcsetattr(fd,TCANOW,&Opt);
|
设置波特率的例子函数:
/**
*@brief 设置串口通信速率
*@param fd 类型 int 打开串口的文件句柄
*@param speed 类型 int 串口速度
*@return void
*/
int speed_arr[] = { B38400, B19200, B9600, B4800, B2400, B1200, B300,
B38400, B19200, B9600, B4800, B2400, B1200, B300, };
int name_arr[] = {38400, 19200, 9600, 4800, 2400, 1200, 300, 38400,
19200, 9600, 4800, 2400, 1200, 300, };
void set_speed(int fd, int speed){
int i;
int status;
struct termios Opt;
tcgetattr(fd, &Opt);
for ( i= 0; i < sizeof(speed_arr) / sizeof(int); i++) {
if (speed == name_arr[i]) {
tcflush(fd, TCIOFLUSH);
cfsetispeed(&Opt, speed_arr[i]);
cfsetospeed(&Opt, speed_arr[i]);
status = tcsetattr(fd1, TCSANOW, &Opt);
if (status != 0) {
perror("tcsetattr fd1");
return;
}
tcflush(fd,TCIOFLUSH);
}
}
}
|
效验位和停止位的设置:
无效验 | 8位 | Option.c_cflag &= ~PARENB; Option.c_cflag &= ~CSTOPB; Option.c_cflag &= ~CSIZE; Option.c_cflag |= ~CS8; |
奇效验(Odd) | 7位 | Option.c_cflag |= ~PARENB; Option.c_cflag &= ~PARODD; Option.c_cflag &= ~CSTOPB; Option.c_cflag &= ~CSIZE; Option.c_cflag |= ~CS7; |
偶效验(Even) | 7位 | Option.c_cflag &= ~PARENB; Option.c_cflag |= ~PARODD; Option.c_cflag &= ~CSTOPB; Option.c_cflag &= ~CSIZE; Option.c_cflag |= ~CS7; |
Space效验 | 7位 | Option.c_cflag &= ~PARENB; Option.c_cflag &= ~CSTOPB; Option.c_cflag &= &~CSIZE; Option.c_cflag |= CS8; |
设置效验的函数:
/**
*@brief 设置串口数据位,停止位和效验位
*@param fd 类型 int 打开的串口文件句柄
*@param databits 类型 int 数据位 取值 为 7 或者8
*@param stopbits 类型 int 停止位 取值为 1 或者2
*@param parity 类型 int 效验类型 取值为N,E,O,,S
*/
int set_Parity(int fd,int databits,int stopbits,int parity)
{
struct termios options;
if ( tcgetattr( fd,&options) != 0) {
perror("SetupSerial 1");
return(FALSE);
}
options.c_cflag &= ~CSIZE;
switch (databits) /*设置数据位数*/
{
case 7:
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
fprintf(stderr,"Unsupported data size/n"); return (FALSE);
}
switch (parity)
{
case 'n':
case 'N':
options.c_cflag &= ~PARENB; /* Clear parity enable */
options.c_iflag &= ~INPCK; /* Enable parity checking */
break;
case 'o':
case 'O':
options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'e':
case 'E':
options.c_cflag |= PARENB; /* Enable parity */
options.c_cflag &= ~PARODD; /* 转换为偶效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'S':
case 's': /*as no parity*/
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;break;
default:
fprintf(stderr,"Unsupported parity/n");
return (FALSE);
}
/* 设置停止位*/
switch (stopbits)
{
case 1:
options.c_cflag &= ~CSTOPB;
break;
case 2:
options.c_cflag |= CSTOPB;
break;
default:
fprintf(stderr,"Unsupported stop bits/n");
return (FALSE);
}
/* Set input parity option */
if (parity != 'n')
options.c_iflag |= INPCK;
tcflush(fd,TCIFLUSH);
options.c_cc[VTIME] = 150; /* 设置超时15 seconds*/
options.c_cc[VMIN] = 0; /* Update the options and do it NOW */
if (tcsetattr(fd,TCSANOW,&options) != 0)
{
perror("SetupSerial 3");
return (FALSE);
}
return (TRUE);
}
|
需要注意的是:
如果不是开发终端之类的,只是串口传输数据,而不需要串口来处理,那么使用原始模式(Raw Mode)方式来通讯,设置方式如下:
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
options.c_oflag &= ~OPOST; /*Output*/
|
读写串口
设置好串口之后,读写串口就很容易了,把串口当作文件读写就是。
关闭串口
关闭串口就是关闭文件。
例子
下面是一个简单的读取串口数据的例子,使用了上面定义的一些函数和头文件
/**********************************************************************代码说明:使用串口二测试的,发送的数据是字符,
但是没有发送字符串结束符号,所以接收到后,后面加上了结束符号。我测试使用的是单片机发送数据到第二个串口,测试通过。
**********************************************************************/
#define FALSE -1
#define TRUE 0
/*********************************************************************/
int OpenDev(char *Dev)
{
int fd = open( Dev, O_RDWR ); //| O_NOCTTY | O_NDELAY
if (-1 == fd)
{
perror("Can't Open Serial Port");
return -1;
}
else
return fd;
}
int main(int argc, char **argv){
int fd;
int nread;
char buff[512];
char *dev = "/dev/ttyS1"; //串口二
fd = OpenDev(dev);
set_speed(fd,19200);
if (set_Parity(fd,8,1,'N') == FALSE) {
printf("Set Parity Error/n");
exit (0);
}
while (1) //循环读取数据
{
while((nread = read(fd, buff, 512))>0)
{
printf("/nLen %d/n",nread);
buff[nread+1] = '/0';
printf( "/n%s", buff);
}
}
//close(fd);
// exit (0);
}
|