Linux下串口编程

参考:	
1. POSIX操作系统串口编程指南
2. UNIX环境高级编程

在Linux下,标准的串口设备节点名为/dev/ttyS*,如果是USB转串口,则为/dev/ttyUSB*,其中'*'代表0、1...这类数字。


一、访问串口
1 打开串口
打开串口使用open系统调用,例如:
#include <stdio.h>	/* Standard input/output definitions */
#include <string.h>	/* String function definitions */
#include <unistd.h>	/* UNIX standard function definitions */
#include <fcntl.h>	/* File control definitions */
#include <errno.h>	/* Error number definitions */
#include <termios.h>	/* POSIX terminal control definitions */

/*
 * 'open_port()' - Open serial port 1.
 *
 * Returns the file descriptor on sueess or -1 on error.
 */
int
open_port(void)
{
	int fd;	/* File descriptor for the port */
	
	fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
	if (fd == -1)
	{
		/*
		 * Could not open the port.
		 */

		perror("open_port: Unable to open /dev/ttyS0 - ");
	}
	else
		fcntl(fd, F_SETFL, 0);

	return (fd);
}
在上面的open系统调用中,除了O_RDWR标志外,还使用了O_NOCTTY和O_NDELAY这两个标志。
O_NOCTTY:
O_NOCTTY这个标志用于告知UNIX该程序不想作为该端口的”控制终端“,如果没有指定该标志那么所有的输入(例如键盘的终止信号(Ctrl + c)等等)将会影响你的程序。而像gettty(1M/8)这类程序会使用这个特性来启动一个登录进程,通常情况下用户不需要这个特性(也就是说要使用O_NOCTTY这个标志)。

O_NDELAY:
O_NDELAY这个标志用于告知UNIX该程序不需要关心DCD信号的状态,也就是说不需要关心端口的另一端是否已经连接。如果不指定这个标志的话,那么程序将会一直休眠直到DCD信号线上检测到有space电压。

2 写数据到端口
写数据使用write系统调用,例如:
n = write(fd, "ATZ\r", 4);
if (n < 0)
	fputs("write() fo 4 bytes failed!\n", stderr);

write系统调用返回已发送的字节数或者发生错误时返回-1。

3 从端口读取数据
当端口在raw data mode操作模式下,那么read系统调用将返回从串口输入缓冲区中实际得到的字节数,如果没有数据可读,那么该系统调用将会被阻塞(block)直到有数据为止,如果超过一定时间仍然没有数据可读,那么将返回一个错误(读错误)。read函数也可以立即返回(即在没有数据可读的情况下也能立即返回,而不是被阻塞),需要做如下工作:
fcntl(fd, F_SETFL, FNDELAY);

FNDELAY选项的意思是read函数在没有数据可读的情况下立即返回0。需要重新回到阻塞模式(blocking)下,那么在调用fcntl()的时候不要加上FNDELAY这个选项:
fcntl(fd, F_SETFL, 0);

4 关闭串口
关闭串口使用close系统调用,例如:
close(fd);

二、串口配置
需要包含<termios.h>这个文件,该文件中定义了struct termios这个结构体类型。
struct termios结构至少包含以下成员:
	tcflag_t c_iflag;	/* input modes */
	tcflag_t c_oflag;	/* output modes */
	tcflag_t c_cflag;	/* control modes */
	tcflag_t c_lflag;	/* local modes */
	cc_t	 c_cc[NCCS];	/* control chars */

1 c_cflag
c_cflag成员用于控制串口波特率、数据位、校验位、停止位以及硬件流控制等等,位成员有:
CBAUD			波特率掩码位
	B0		
	B50
	B75
	B110
	B134
	B150
	B200
	B300
	B600
	B1200
	B2400
	B4800
	B9600
	B19200
	B38400
	B57600
	B76800
	B115200
EXTA			外部时钟
EXTB			外部时钟
CSIZE			数据位掩码位
	CS5
	CS6
	CS7
	CS8
CSTOPB			2位停止位
CREAD			接收使能
PARENB			奇偶校验使能
PARODD			使用奇校验
CLOCAL			忽略终端状态行
CRTSCTS			硬件流控制使能位

通常情况下,CLOCAL和CREAD这两个选项应该应该总是被打开的。

1.1 设置波特率
波特率的存储位置依赖于操作系统,在比较老接口上波特率存储在c_cflag成员中,在后来的接口中提供了c_ispeed和c_ospeed这两个成员来存储实际的波特率值,所以在设置波特率时应该使用cfsetospeed和cfsetispeed这两个函数(而不是直接赋值的方式)。例如:
struct termios options;

/*
 * Get the current options 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和tcsetattr这两个函数用于获取和设置串口的属性。
tcgetattr函数原型如下:
int tcgetattr(int fd, struct termios *termios_p);
tcgetattr用于获取当前的串口设置到它的参数termios_p中,而要修改串口设置则使用tcsetattr函数,原型如下:
int tcsetattr(int fd, int optional_actions,
	      const struct termios *termios_p);
其中options_actions有几个选项值:
TCSANOW		立即修改设置
TCSADRAIN	等待所有数据传输完成后才修改设置
TCSAFLUSH	同样需要等待,但是它是立即刷新输入、输出缓冲区,然后才修改设置。

而cfsetispeed和cfsetospeed函数是专门用于设置串口波特率的,函数原型如下:
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);

1.2 设置数据位
options.c_cflag &= ~CSIZE;	/* Mask the character size bits */
options.c_cflag |= CS8;		/* Select 8 data bits */

1.3 设置奇偶校验(连同数据位、停止位一起设置)
无校验(8N1):
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~SIZE;
options.c_cflag |= CS8;

1.4 设置硬件流控制
禁用硬件流控制:
options.c_cflag &= ~CRTSCTS;

2 c_lflag
ISIG		使能SIGINTR、SIGSUSP、SIGDSUSP和SIGQUIT信号
ICANON		使能规范输入模式
ECHO		使能输入字符回显功能

2.1 选择标准输入模式
options.c_lflag |= (ICANON | ECHO | ECHOE);

2.2 选择原始输入模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

那么什么是标准输入模式(Canonical Input),什么又是原始输入模式(Raw Input)呢?
所谓标准输入模式是指输入是以行为单位的,可以这样理解,输入的数据最开始存储在一个缓冲区里面(但并未真正发送出去),可以使用Backspace或者Delete键来删除输入的字符,从而达到修改字符的目的,当按下回车键时,输入才真正的发送出去,这样终端程序才能接收到。
通常情况下我们都是使用的是原始输入模式,也就是说输入的数据并不组成行。在标准输入模式下,系统每次返回的是一行数据,在原始输入模式下,系统又是怎样返回数据的呢?如果读一次就返回一个字节,那么系统开销就会很大,但在读数据的时候,我们也并不知道一次要读多少字节的数据,解决办法是使用c_cc数组中的VMIN和VTIME,如果已经读到了VMIN个字节的数据或者已经超过VTIME时间,系统立即返回。关于VMIN和VTIME这两个选项后面还会详细说明。

3 c_iflag
INPCK		使能输入校验
IGNPAR		忽略校验错误
PARMRK		标记校验错误
IXON		使能输出软件流控制
IXOFF		使能输入软件流控制

3.1 使能软件流控制
例如:
options.c_iflag |= (IXON | IXOFF | IXANY);

3.2 禁用软件流控制
例如:
options.c_iflag &= ~(IXON | IXOFF | IXANY);

4 c_oflag
OPOST		启用输出处理

可以启用和禁止输出处理,例如:
options.c_oflag |= OPOST;	/* Choosing Processed Output */

options.c_oflag &= ~OPOST;	/* Choosing Raw Output */

5 c_cc
那么可能需要关注的是VMIN和VTIME这两个选项。
VMIN		最少读取字符数
VTIME		超时时间

这两个参数只有当设置为阻塞模式时才有效,有以下几种可能值:
5.1 MIN > 0 && TIME > 0
MIN为最少读取的字符数,当读取到一个字符后,会启动一个定时器,在定时器超时事前,如果已经读取到了MIN个字符,则read返回MIN个字符。如果在接收到MIN个字符之前,定时器已经超时,则read返回已读取到的字符,注意这个定时器会在每次读取到一个字符后重新启用,即重新开始计时,而且是读取到第一个字节后才启用,也就是说超时的情况下,至少读取到一个字节数据。

5.2 MIN > 0 && TIME == 0
在只有读取到MIN个字符时,read才返回,可能造成read被永久阻塞。

5.3 MIN == 0 && TIME > 0
和第一种情况稍有不同,在接收到一个字节时或者定时器超时时,read返回。如果是超时这种情况,read返回值是0。

5.4 MIN == 0 && TIME == 0
这种情况下read总是立即就返回,即不会被阻塞。


三、关于串口读写阻塞与非阻塞
1 设置阻塞与非阻塞,可以在打开串口时指定,也可以在打开串口之后通过fcntl函数进行设置。例如:
fd = open(devname, O_RDWR | O_NOCTTY);
上面打开串口时是以阻塞方式打开的,如果加上O_NDELAY标志那么就是以非阻塞方式打开。
fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY);

2 通过fcntl()函数设置
fcntl(fd, F_SETFL, 0); /* 设置为阻塞方式 */
fcntl(fd, F_SETFL, FNDELAY); /* 设置为非阻塞方式 */

3 阻塞与非阻塞
那么阻塞与非阻塞是什么含义呢?对于读来说,阻塞(blocking IO)是指当前串口输入缓冲区中没有数据的时候,read函数将会阻塞在这里,直到串口输入缓冲区有数据可读取,read函数在读到了数据之后,才返回,然后整个程序才继续运行下去。对于写来说,阻塞是指当前输出缓冲区已满,或者剩下的空间小于将要写入的字节数,则write函数将会阻塞在这里,直到串口输出缓冲区剩下的空间大于或等于将要写入的字节数,执行写入操作,返回,程序才继续运行下去。

对于读来说,非阻塞(non-blocking IO)指当前输入缓冲区没有数据的时候,read函数立即返回,返回值为0。对于写来说,非阻塞指当前串口输出缓冲区已满,或者剩下的空间小于将要写入的字节数,wirte执行写操作(并不会等待在这里),写入当前串口输出缓冲区剩下空间允许的字节数,然后返回写入的字节数。

4 read阻塞配置
除了在open函数或者fcntl函数中配置阻塞方式外,read操作还有额外的配置:
options.c_cc[VMIN] = xxx;
options.c_cc[VTIME] = xxx;
这两个配置只有当设置为阻塞方式(blocking IO)时才有效,否则是无效的,这两个参数的默认值为0。
其中VMIN表示read操作时最小读取的字节数。
VTIME表示read操作时没有读到数据时等待的时间,单位为10毫秒。例如:
options.c_cc[VMIN] = 8;	/* 表示最少读取8个字节 */
options.c_cc[VTIM] = 5;	/* 表示超时时间为50毫秒 */

5 ioctl
那么对于读来说,还可以使用ioctl函数在read之前获取可读的字节数,这样也就不用关心read是阻塞与非阻塞了,例如:
#include <unistd.h>
#include <termios.h>

int fd;
int bytes;

ioctl(fd, FIONREAD, &bytes);


附录:串口打开和初始化部分代码

#define DEVNAME "/dev/ttyUSB0"

int serial_init(void)
{
	struct termios options;

	/* 以非阻塞方式打开串口 */
	fd = open(DEVNAME, O_RDWR | O_NOCTTY | O_NDELAY);
	if (fd < 0) {
		printf("Open the serial port error!\n");
		return -1;
	}

	fcntl(fd, F_SETFL, 0);

	tcgetattr(fd, &options);

	/*
	 * Set the baud rates to 9600
	 */
	cfsetispeed(&options, B9600);
	cfsetospeed(&options, B9600);

	/*
	 * Enable the receiver and set local mode
	 */
	options.c_cflag |= (CLOCAL | CREAD);

	/*
	 * Select 8 data bits, 1 stop bit and no parity bit
	 */
	options.c_cflag &= ~PARENB;
	options.c_cflag &= ~CSTOPB;
	options.c_cflag &= ~CSIZE;
	options.c_cflag |= CS8;

	/*
	 * Disable hardware flow control
	 */
	options.c_cflag &= ~CRTSCTS;

	/*
	 * Choosing raw input
	 */
	options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

	/*
	 * Disable software flow control
	 */
	options.c_iflag &= ~(IXON | IXOFF | IXANY);

	/*
	 * Choosing raw output
	 */
	options.c_oflag &= ~OPOST;


	/*
	 * Set read timeouts
	 */
	options.c_cc[VMIN] = 8;
	options.c_cc[VTIME] = 10;
	//options.c_cc[VMIN] = 0;
	//options.c_cc[VTIME] = 0;

	tcsetattr(fd, TCSANOW, &options);

	return 0;
}


你可能感兴趣的:(Linux下串口编程)