linux串口編程 select

1、串口的阻塞和非阻塞

阻塞的定義:

       對於read,block指當串口輸入緩衝區沒有數據的時候,read函數將會阻塞在這裏,一直到串口輸入緩衝區中有數據可讀取,read讀到了需要的字節數之後,返回值爲讀到的字節數,然後整個程序才繼續運行下去;(收)

       對於write,block指當串口輸出緩衝區滿,或剩下的空間小於將要寫入的字節數,則write將阻塞,一直到串口輸出緩衝區中剩下的空間大於等於將要寫入的字節數,執行寫入操作,返回寫入的字節數,然後整個程序才繼續運行下去。(發)

非阻塞的定義:

       對於read,no block指當串口輸入緩衝區沒有數據的時候,read函數立即返回,返回值爲0。

       對於write,no block指當串口輸出緩衝區滿,或剩下的空間小於將要寫入的字節數,則write將進行寫操作(不會等待在這裏),寫入當前串口輸出緩衝區剩下空間允許的字節數,然後返回寫入的字節數。


控制方法:

有兩個方法可以控制串口阻塞性(同時控制read和write):一個是在打開串口的時候,open函數是否帶O_NDELAY;第二個是可以在打開串口之後通過fcntl()函數進行控制。

open方式:

阻塞:fd = open(devname, O_RDWR | O_NOCTTY);

非阻塞:fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY);

fcntl函數:

阻塞:fcntl(fd,F_SETFL,0)

非阻塞:fcntl(fd,F_SETFL,FNDELAY) 


  1.        if(fcntl(fd,F_SETFL,FNDELAY) < 0)//非阻塞,覆蓋前面open的屬性  
  2.         {     
  3.             printf("fcntl failed\n");     
  4.         }     
  5.         else{     
  6.         printf("fcntl=%d\n",fcntl(fd,F_SETFL,FNDELAY));     
  7.         }   
 

  1.        if(fcntl(fd,F_SETFL,0) < 0){   //阻塞,即使前面在open串口設備時設置的是非阻塞的,這裏設爲阻塞後,以此爲準  
  2.         printf("fcntl failed\n");     
  3.         }     
  4.         else{     
  5.         printf("fcntl=%d\n",fcntl(fd,F_SETFL,0));     
  6.         }   


2、串口配置

需要包含<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總是立即就返回,即不會被阻塞。

3、select編程

selcet函數:

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

在說明參數之前,先說明2個結構體:

struct fd_set

        可以理解爲一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄,這可以是我們所說的普通意義的文件,當然Unix下任何設備、管道、FIFO等都是文件形式,全部包括在內,所以毫無疑問一個socket就是一個文件,socket句柄就是一個文件描述符。fd_set集合可以通過一些宏由人爲來操作,比如:

?    FD_ZERO(fd_set *set):清除一個文件描述符集;

?        FD_SET(int fd, fd_set *set):將一個文件描述符加入文件描述符集中;

?        FD_CLR(int fd, fd_set *set):將一個文件描述符從文件描述符集中清除;

?        FD_ISSET(int fd, fd_set *set): 檢查集合中指定的文件描述符是否可以讀寫。


struct timeval

struct timeval{

long tv_sec;

lone tv_usec;

設置超時時間,作爲select的最後一個參數。


下面說明select的參數:

int maxfdp

        是一個整數值,是指集合中所有文件描述符的範圍,即所有文件描述符的最大值加1,不能錯!在Windows中這個參數的值無所謂,可以設置不正確。

fd_set *readfds

        是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的讀變化的,即我們關心是否可以從這些文件中讀取數據了,如果這個集合中有一個文件可讀,select就會返回一個大於0的值,表示有文件可讀,如果沒有可讀的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的讀變化。

fd_set *writefds

        是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的寫變化的,即我們關心是否可以向這些文件中寫入數據了,如果這個集合中有一個文件可寫,select就會返回一個大於0的值,表示有文件可寫,如果沒有可寫的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的寫變化。

fd_set *errorfds

        同上面兩個參數的意圖,用來監視文件錯誤異常。

struct timeval* timeout

        是select的超時時間,這個參數至關重要,它可以使select處於三種狀態,第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化爲止;第二,若將時間值設爲00毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;第三,timeout的值大於0,這就是等待的超時時間,即selecttimeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。

返回值ret

        負值:select錯誤

        正值:某些文件可讀寫或出錯;

         0:等待超時,沒有可讀寫或錯誤的文件;


在select編程時,一般來說,首先使用FD_ZERO、FD_SET來初始化文件描述符集,在使用了select函數時,可循環使用FD_ISSET測試描述符集,在執行完對相關的文件描述符後,使用FD_CLR來清除描述符集。

使用FD_ISSET檢測串口是否有讀寫動作時,每次循環都要清空,否則不會檢測到有變化:

FD_ZERO(&rfds);// 清空串口接收端口集

FD_SET(fd,&rfds);// 設置串口接收端口集


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;
}

select方式讀取數據代碼:
int main(void)
{
  int fd;
  int nread,nwrite,i;
  char buff[8];
  fd_set rd;
  fd = 0;
  /*打開串口*/
  if((fd = open_port(fd,1)) < 0)
  {
    perror("open_port error!\n");
    return ;
  }
  /*設置串口*/
  if((i= set_opt(fd,115200,8,'N',1)) < 0)
  {
    perror("set_opt error!\n");
    return (-1);
  }
  /*利用select函數來實現多個串口的讀寫*/
while(1)
{
  FD_ZERO(&rd);
  FD_SET(fd,&rd);
  while(FD_ISSET(fd,&rd))
  {
    if(select(fd+1,&rd,NULL,NULL,NULL) < 0)
      perror("select error!\n");
    else
    {
      while((nread = read(fd,buff,8))>0)
      {
        printf("nread = %d,%s\n",nread,buff);
      }
    }
  }
} 
close(fd);
    return ;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章