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)
2、串口配置
需要包含這個文件,該文件中定義了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 timevalstruct 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置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化為止;第二,若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;第三,timeout的值大於0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。
返回值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#include 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方式讀取數據代碼:
intmain(void) { intfd; intnread,nwrite,i; charbuff[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; }