串口的设置主要是设置struct termios结构体的各成员值,如下所示:
#include<termios.h>
struct termios
{
unsigned short c_iflag; /* 输入模式标志 */
unsigned short c_oflag; /* 输出模式标志 */
unsigned short c_cflag; /* 控制模式标志 */
unsigned short c_lflag; /* 本地模式标志 */
unsigned char c_line; /* 线路规程 */
unsigned char c_cc[NCC]; /* 控制特性 */
speed_t c_ispeed; /* 输入速度 */
speed_t c_ospeed; /* 输出速度 */
};
termios是在Posix规范中定义的标准接口,表示终端设备(包括虚拟终端、串口等)。因为串口是一种终端设备,所以通过终端编程接口对其进行配置和控制。因此在具体讨论串口相关编程之前,需要先了解一下终端的相关知识。
终端是指用户与计算机进行对话的接口,如键盘、显示器和串口设备等物理设备,X Window上的虚拟终端。类UNIX操作系统都有文本式虚拟终端,使用【Ctrl+Alt】+F1~F6键可以进入文本式虚拟终端,在X Window上可以打开几十个以上的图形式虚拟终端。类UNIX操作系统的虚拟终端有xterm、rxvt、zterm、eterm等,而Windows上有crt、putty等虚拟终端。
终端有三种工作模式,分别为规范模式(canonical mode)、非规范模式(non-canonical mode)和原始模式(raw mode)。
通过在termios结构的c_lflag中设置ICANNON标志来定义终端是以规范模式(设置ICANNON标志)还是以非规范模式(清除ICANNON标志)工作,默认情况为规范模式。
在规范模式下,所有的输入是基于行进行处理的。在用户输入一个行结束符(回车符、EOF等)之前,系统调用read()函数是读不到用户输入的任何字符的。除了EOF之外的行结束符(回车符等)与普通字符一样会被read()函数读取到缓冲区中。在规范模式中,行编辑是可行的,而且一次调用read()函数最多只能读取一行数据。如果在read()函数中被请求读取的数据字节数小于当前行可读取的字节数,则read()函数只会读取被请求的字节数,剩下的字节下次再被读取。
在非规范模式下,所有的输入是即时有效的,不需要用户另外输入行结束符,而且不可进行行编辑。在非规范模式下,对参数MIN(c_cc[VMIN])和TIME(c_cc[VTIME])的设置决定read()函数的调用方式。设置可以有4种不同的情况。
● MIN = 0和TIME = 0:read()函数立即返回。若有可读数据,则读取数据并返回被读取的字节数,否则读取失败并返回0。
● MIN > 0和TIME = 0:read()函数会被阻塞,直到MIN个字节数据可被读取。
● MIN = 0和TIME > 0:只要有数据可读或者经过TIME个十分之一秒的时间,read()函数则立即返回,返回值为被读取的字节数。如果超时并且未读到数据,则read()函数返回0。
● MIN > 0和TIME > 0:当有MIN个字节可读或者两个输入字符之间的时间间隔超过TIME个十分之一秒时,read()函数才返回。因为在输入第一个字符后系统才会启动定时器,所以,在这种情况下,read()函数至少读取一个字节后才返回。
按照严格意义来讲,原始模式是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的,而且所有特定的终端输入/输出控制处理不可用。通过调用cfmakeraw()函数可以将终端设置为原始模式,而且该函数通过以下代码可以得到实现:
termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
| INLCR | IGNCR | ICRNL | IXON);
termios_p->c_oflag &= ~OPOST;
termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
termios_p->c_cflag &= ~(CSIZE | PARENB);
termios_p->c_cflag |= CS8;
现在讲解设置串口的基本方法。如上所述,串口设置最基本的操作包括波特率设置,校验位和停止位设置。在这个结构中最为重要的是c_cflag,通过对它的赋值,用户可以设置波特率、字符大小、数据位、停止位、奇偶校验位和硬软流控等。另外,c_iflag和c_cc也是比较常用的标志。在此主要对这3个成员进行详细说明。c_cflag支持的常量名称如表2.11所示。其中设置波特率宏名为相应的波特率数值前加上B,由于数值较多,本表没有全部列出。
表2.11 c_cflag支持的常量名称
CBAUD
波特率的位掩码
B0
0波特率(放弃DTR)
…
…
续表
CBAUD
波特率的位掩码
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
本地连接(不改变端口所有者)
CRTSCTS
硬件流控
在这里,对于c_cflag成员不能直接对其初始化,而要将其通过“与”、“或”操作使用其中的某些选项。
输入模式标志c_iflag用于控制端口接收端的字符输入处理。c_iflag支持的常量名称,如表2.12所示。
表2.12 c_iflag支持的常量名称
INPCK
奇偶校验使能
IGNPAR
忽略奇偶校验错误
PARMRK
奇偶校验错误掩码
ISTRIP
裁减掉第8位比特
IXON
启动输出软件流控
IXOFF
启动输入软件流控
续表
INPCK
奇偶校验使能
IXANY
允许输入任意字符可以重新启动输出(默认为输入起始字符才重启输出)
IGNBRK
忽略输入终止条件
BRKINT
当检测到输入终止条件时发送SIGINT信号
INLCR
将接收到的NL(换行符)转换为CR(回车符)
IGNCR
忽略接收到的CR(回车符)
ICRNL
将接收到的CR(回车符)转换为NL(换行符)
IUCLC
将接收到的大写字符映射为小写字符
IMAXBEL
当输入队列满时响铃
c_oflag用于控制终端端口发送出去的字符处理,c_oflag支持的常量名称如表2.13所示。因为现在终端的速度比以前快得多,所以大部分延时掩码几乎没什么用途。
表2.13 c_oflag支持的常量名称
OPOST
启用输出处理功能,如果不设置该标志则其他标志都被忽略
OLCUC
将输出中的大写字符转换成小写字符
ONLCR
将输出中的换行符('\n')转换成回车符('\r')
ONOCR
如果当前列号为0,则不输出回车符
OCRNL
将输出中的回车符('\r')转换成换行符('\n')
ONLRET
不输出回车符
OFILL
发送填充字符以提供延时
OFDEL
如果设置该标志,则表示填充字符为DEL字符,否则为NUL字符
NLDLY
换行符延时掩码
CRDLY
回车符延时掩码
TABDLY
制表符延时掩码
BSDLY
水平退格符延时掩码
VTDLY
垂直退格符延时掩码
FFLDY
换页符延时掩码
c_lflag用于控制终端的本地数据处理和工作模式,c_lflag所支持的常量名称如表2.14所示。
表2.14 c_lflag支持的常量名称
ISIG
若收到信号字符(INTR、QUIT等),则会产生相应的信号
ICANON
启用规范模式
ECHO
启用本地回显功能
ECHOE
若设置ICANON,则允许退格操作
续表
ECHOK
若设置ICANON,则KILL字符会删除当前行
ECHONL
若设置ICANON,则允许回显换行符
ECHOCTL
若设置ECHO,则控制字符(制表符、换行符等)会显示成“^X”,其中X的ASCII码等于给相应控制字符的ASCII码加上0x40。例如,退格字符(0x08)会显示为“^H”('H'的ASCII码为0x48)
ECHOPRT
若设置ICANON和IECHO,则删除字符(退格符等)和被删除的字符都会被显示
ECHOKE
若设置ICANON,则允许回显在ECHOE和ECHOPRT中设定的KILL字符
NOFLSH
在通常情况下,当接收到INTR、QUIT和SUSP控制字符时,会清空输入和输出队列。如果设置该标志,则所有的队列不会被清空
TOSTOP
若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进程的进程组发送SIGTTOU信号。该信号通常终止进程的执行
IEXTEN
启用输入处理功能
c_cc定义特殊控制特性,c_cc所支持的常量名称如表2.15所示。
表2.15 c_cc支持的常量名称
VINTR
中断控制字符,对应键为Ctrl+C
VQUIT
退出操作符,对应键为Ctrl+Z
VERASE
删除操作符,对应键为Backspace(BS)
VKILL
删除行符,对应键为Ctrl+U
VEOF
文件结尾符,对应键为Ctrl+D
VEOL
附加行结尾符,对应键为Carriage return(CR)
VEOL2
第二行结尾符,对应键为Line feed(LF)
VMIN
指定最少读取的字符数
VTIME
指定读取的每个字符之间的超时时间
下面就详细讲解设置串口属性的基本流程。
1.保存原先串口配置
首先,为了安全起见和以后调试程序方便,可以先保存原先串口的配置,在这里可以使用函数tcgetattr(fd, &old_cfg)。该函数得到由fd指向的终端的配置参数,并将它们保存于termios结构变量old_cfg中。该函数还可以测试配置是否正确、该串口是否可用等。若调用成功,函数返回值为0,若调用失败,函数返回值为-1,其使用如下所示:
if (tcgetattr(fd, &old_cfg) != 0)
{
perror("tcgetattr");
return -1;
}
2.激活选项
CLOCAL和CREAD分别用于本地连接和接收使能,因此,首先要通过位掩码的方式激活这两个选项。
newtio.c_cflag |= CLOCAL | CREAD;
调用cfmakeraw()函数可以将终端设置为原始模式,在后面的实例中,采用原始模式进行串口数据通信。
cfmakeraw(&new_cfg);
3.设置波特率
设置波特率有专门的函数,用户不能直接通过位掩码来操作。设置波特率的主要函数有cfsetispeed()和cfsetospeed()。这两个函数的使用很简单,如下所示:
cfsetispeed(&\&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);
cfsetispeed()函数在termios结构中设置数据输入波特率,而cfsetospeed()函数在termios结构中设置数据输入波特率。一般来说,用户需将终端的输入和输出波特率设置成一样的。这几个函数在成功时返回0,失败时返回-1。
4.设置字符大小
与设置波特率不同,设置字符大小并没有现成可用的函数,需要用位掩码。一般首先去除数据位中的位掩码,再重新按要求设置,如下所示:
new_cfg.c_cflag &= ~CSIZE; /* 用数据位掩码清空数据位设置 */
new_cfg.c_cflag |= CS8;
5.设置奇偶校验位
设置奇偶校验位需要用到termios中的两个成员:c_cflag和c_iflag。首先要激活c_cflag中的校验位使能标志PARENB和确认是否要进行校验,这样会对输出数据产生校验位,而对输入数据进行校验检查。同时还要激活c_iflag中的对于输入数据的奇偶校验使能(INPCK)。如使能奇校验时,代码如下所示:
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
而使能偶校验时,代码如下所示:
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除偶奇校验标志,则配置为偶校验 */
new_cfg.c_iflag |= INPCK;
6.设置停止位
设置停止位是通过激活c_cflag中的CSTOPB而实现的。若停止位为一个比特,则清除CSTOPB;若停止位为两个,则激活CSTOPB。以下分别是停止位为一个和两个比特时的代码:
new_cfg.c_cflag &= ~CSTOPB; /* 将停止位设置为一个比特 */
new_cfg.c_cflag |= CSTOPB; /* 将停止位设置为两个比特 */
7.设置最少字符和等待时间
在对接收字符和等待时间没有特别要求的情况下,可以将其设置为0,则在任何情况下read()函数立即返回,此时串口操作会设置为非阻塞方式,如下所示:
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;
8.清除串口缓冲
由于串口在重新设置后,需要对当前的串口设备进行适当的处理,这时就可调用在<termios.h>中声明的tcdrain()、tcflow()、tcflush()等函数来处理目前串口缓冲中的数据,它们的格式如下所示:
int tcdrain(int fd); /* 使程序阻塞,直到输出缓冲区的数据全部发送完毕 */
int tcflow(int fd, int action); /* 用于暂停或重新开始输出 */
int tcflush(int fd, int queue_selector); /* 用于清空输入/输出缓冲区 */
在本实例中使用tcflush()函数,对于在缓冲区中尚未传输的数据,或者收到的但是尚未读取的数据,其处理方法取决于queue_selector的值,它可能的取值有以下几种。
● TCIFLUSH:对接收到而未被读取的数据进行清空处理。
● TCOFLUSH:对尚未传送成功的输出数据进行清空处理。
● TCIOFLUSH:包括前两种功能,即对尚未处理的输入/输出数据进行清空处理。
如在本例中所采用的是第一种方法,当然可以使用TCIOFLUSH参数:
tcflush(fd, TCIFLUSH);
9.激活配置
在完成全部串口配置后,要激活刚才的配置并使配置生效。这里用到的函数是tcsetattr(),它的函数原型是:
tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
其中,参数termios_p是termios类型的新配置变量。
参数optional_actions可能的取值有以下3种。
● TCSANOW:配置的修改立即生效。
● TCSADRAIN:配置的修改在所有写入fd的输出都传输完毕之后生效。
● TCSAFLUSH:所有已接收但未读入的输入都将在修改生效之前被丢弃。
该函数若调用成功则返回0,若失败则返回-1,代码如下所示:
if ((tcsetattr(fd, TCSANOW, &new_cfg)) != 0)
{
perror("tcsetattr");
return -1;
}
下面给出了串口配置的完整函数。为了函数的通用性,通常将常用的选项都在函数中列出,这样可以大大方便以后用户的调试使用。该设置函数如下所示:
int set_com_config(int fd,int baud_rate,
int data_bits, char parity, int stop_bits)
{
struct termios new_cfg,old_cfg;
int speed;
/* 保存并测试现有串口参数设置,在这里如果串口号等出错,会有相关的出错信息 */
if (tcgetattr(fd, &old_cfg) != 0)
{
perror("tcgetattr");
return -1;
}
new_cfg = old_cfg;
cfmakeraw(&new_cfg); /* 配置为原始模式 */
new_cfg.c_cflag &= ~CSIZE;
/* 设置波特率 */
switch (baud_rate)
{
case 2400:
{
speed = B2400;
}
break;
case 4800:
{
speed = B4800;
}
break;
case 9600:
{
speed = B9600;
}
break;
case 19200:
{
speed = B19200;
}
break;
case 38400:
{
speed = B38400;
}
break;
default:
case 115200:
{
speed = B115200;
}
break;
}
cfsetispeed(&new_cfg, speed);
cfsetospeed(&new_cfg, speed);
switch (data_bits) /* 设置数据位 */
{
case 7:
{
new_cfg.c_cflag |= CS7;
}
break;
default:
case 8:
{
new_cfg.c_cflag |= CS8;
}
break;
}
switch (parity) /* 设置奇偶校验位 */
{
default:
case 'n':
case 'N':
{
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
}
break;
case 'o':
case 'O':
{
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
}
break;
case 'e':
case 'E':
{
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD;
new_cfg.c_iflag |= INPCK;
}
break;
case 's': /* as no parity */
case 'S':
{
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_cflag &= ~CSTOPB;
}
break;
}
switch (stop_bits) /* 设置停止位 */
{
default:
case 1:
{
new_cfg.c_cflag &= ~CSTOPB;
}
break;
case 2:
{
new_cfg.c_cflag |= CSTOPB;
}
}
/* 设置等待时间和最小接收字符 */
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 1;
tcflush(fd, TCIFLUSH); /* 处理未接收字符 */
if ((tcsetattr(fd, TCSANOW, &new_cfg)) != 0) /* 激活新配置 */
{
perror("tcsetattr");
return -1;
}
return 0;
}