Linux 串口编程(基于RAW模式)

Linux下的串口编程对于大多数的系统编程人员来说肯定不陌生,特别是对于嵌入式系统开发人员来说,其会经常使用串口与各种各样的硬件设备进行通信。下面总结一下,Linux串口编程的基本模式和常见问题。

编程模式

Linux下的串口以设备文件的形式存在,所以,对于串口设备的所有操作都时围绕其设备文件而展开。熟悉Linux文件相关操作的开发人员,应该很熟悉下面的模式:

  1. 打开文件(open);
  2. 配置串口文件属性(fcntl);
  3. 使用isatty进一步测试打开的设备文件描述符是否为终端设备;
  4. 设置串口属性(tcsetattr,该操作为串口设备独有);
  5. 读取数据(read)|写入数据(write);
  6. 关闭串口设备文件(close);

没错,Linux下操作串口就像读写普通文件一样简单。唯一不同的就是,串口属性需要特殊的配置,即通过tcsetattr系统调用实现对于串口的属性配置。下面详细介绍串口编程的各个步骤;

打开串口

进行串口操作之前,首先需要打开串口设备文件。对于串口文件的打开操作与一般的文件操作有很大的不同。其不同之处主要体现在open系统调用的flags参数,串口文件打开需要配置的flags包括如下几个:

  1. O_RDWR:串口设备为全双工的,所以其读写模式应该为可读、可写;
  2. O_NOCTTY:该属性告知UNIX系统,该程序不想成为对于该串口的“控制终端”。如果不配置该属性,任何输入事件(键盘事件)都会影响到你的进程。
  3. O_NDELAY:该属性表示串口设备文件以非阻塞的方式打开(通知linux系统不关心DCD信号线所处的状态(端口的另一端是否激活或者停止));也可以通过fcntl设置ON_ONBLOCK来达到同样的效果。

下面是通过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数据传输相关的属性设置。串口相关的几个最为基本属性包括:波特率、字符大小掩码、校验方式、停止位。下面分别介绍如何设置上述参数。

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];   /* 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)的行为。

  1. VMIN:最小可读的字符数量,表read(2)返回时,串口缓存中最小的字符个数。
  2. VTIME:等待数据可读的超时时间,单位是1/10秒。

VMIN和VTIME有四种配置方式,其含义如下:

Linux 串口编程(基于RAW模式)_第1张图片
Linux 串口编程(基于RAW模式)_第2张图片

其他参数设置

波特率:波特率的设置比较独立,其包括三种配置方式: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;
    }
}

你可能感兴趣的:(Linux,C/C++,HardWare,ARM-linux)