Linux下的串口编程对于大多数的系统编程人员来说肯定不陌生,特别是对于嵌入式系统开发人员来说,其会经常使用串口与各种各样的硬件设备进行通信。下面总结一下,Linux串口编程的基本模式和常见问题。
Linux下的串口以设备文件的形式存在,所以,对于串口设备的所有操作都时围绕其设备文件而展开。熟悉Linux文件相关操作的开发人员,应该很熟悉下面的模式:
没错,Linux下操作串口就像读写普通文件一样简单。唯一不同的就是,串口属性需要特殊的配置,即通过tcsetattr系统调用实现对于串口的属性配置。下面详细介绍串口编程的各个步骤;
进行串口操作之前,首先需要打开串口设备文件。对于串口文件的打开操作与一般的文件操作有很大的不同。其不同之处主要体现在open系统调用的flags参数,串口文件打开需要配置的flags包括如下几个:
下面是通过fcntl配置串口I/O模式的方式:
O_NONBLOCK 非阻塞I/O;如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误;
/*打开非阻塞I/O)*/
int flags;
if(flags = fcntl(fd, F_GETFL, 0) < 0)
{
perror("fcntl");
return -1;
}
flags |= O_NONBLOCK;
if(fcntl(fd, F_SETFL, flags) < 0)
{
perror("fcntl");
return -1;
}
/*关闭非阻塞I/O)*/
flags &= ~O_NONBLOCK;
if(fcntl(fd, F_SETFL, flags) < 0)
{
perror("fcntl");
return -1;
}
Linux下对于串口属性的操作包括以下函数族:
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions,
const struct termios *termios_p);
int tcsendbreak(int fd, int duration);
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);
void cfmakeraw(struct termios *termios_p);
speed_t cfgetispeed(const struct termios *termios_p);
speed_t cfgetospeed(const struct termios *termios_p);
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetspeed(struct termios *termios_p, speed_t speed);
上面的函数族,主要完成Linux下终端类设备的属性设置/读取、line control、波特率的设置/读取等操作。对于普通的UART串口来说,我们一般只会使用其进行数据的传输功能,对于其他终端类属性我们一般不会涉及到,所以,本文只介绍UART数据传输相关的属性设置。串口相关的几个最为基本属性包括:波特率、字符大小掩码、校验方式、停止位。下面分别介绍如何设置上述参数。
大部分串口属性操作函数都会包含 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]; /* special characters */
对于只进行数据传输的串口来说,一般将其配置成Raw模式,对于该模式的的termios参数设置如下:
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;
具体到各个属性的含义,可以通过man tcsetattr进行具体查询。
需要的注意的就是c_cc[NCCS]控制字符,在非规范模式下(Raw 模式就是非规范模式),通过c_cc的VMIN和VTIME控制read(2)的行为。
VMIN和VTIME有四种配置方式,其含义如下:
波特率:波特率的设置比较独立,其包括三种配置方式:cfsetispeed、cfsetispeed、cfsetspeed,即设置输入、输出、输入和输出的波特率。
linux支持的波特率:
B0
B50
B75
B110
B134
B150
B200
B300
B600
B1200
B1800
B2400
B4800
B9600
B19200
B38400
B57600
B115200
B230400
数据位:数据位有CS7、CS8,通过termios的c_cflag设置。
校验位:
switch(nParity)
{
case 0: //no parity check
termios_new.c_cflag &= ~PARENB;
break;
case 1: //odd check
termios_new.c_cflag |= PARENB;
termios_new.c_cflag |= PARODD;
termios_new.c_cflag |= (INPCK | ISTRIP);
break;
case 2: //even check
termios_new.c_cflag |= (INPCK | ISTRIP);
termios_new.c_cflag |= PARENB;
termios_new.c_cflag &= ~PARODD;
break;
default: //no parity check
termios_new.c_cflag &= ~PARENB;
break;
}
停止位:
if(nStop == 1)
termios_new.c_cflag &= ~CSTOPB;
else if(nStop == 2)
termios_new.c_cflag |= CSTOPB;
else
termios_new.c_cflag &= ~CSTOPB;
串口数据传输分为读数据和写数据。下面分别介绍串口读、写数据的一般编程模式;
参考“termios设置”一节关于c_cc的配置,读取设备数据,推荐使用select代替直接使用read读取数据,因为如果程序采用轮询模式读取设备数据,会造成CPU资源极大的浪费,select可以极大的减少无效的设备数据读取操作。下面为采用select基本模式:
int32
select_read(int32 fd, int8* buff, int32 data_len, uint32 timeout)
{
int read_len= 0;
fd_set fs_read;
struct timeval tv_timeout;
tv_timeout.tv_sec = timeout;
tv_timeout.tv_usec = 0;
FD_ZERO(&fs_read);
FD_SET(fd, &fs_read);
Select(fd + 1, &fs_read, NULL, NULL, &tv_timeout);
if(FD_ISSET(fd, &fs_read)) {
read_len = Read(fd, buff, data_len);
}
return read_len;
}
对于写操作,由于串口设备存在写缓冲区,如果缓冲区满了的话,再次write就会导致出错(Resource temporarily unavailable),所以,一般往串口设备中写数据时,需要检查write的返回值,否则会出现数据丢失的情况,下面为write的一般编程模式:
void
Write(int fd, void *ptr, size_t count)
{
size_t nbytes = 0;
signed int i = 0;
while(nbytes < count) {
i = write(fd, ptr + nbytes, count - nbytes);
if(i < 0) {
continue;
}
nbytes += i;
}
}