linux串列埠程式設計說明
1.參考文章1
2.linux手冊參考
3.詳解linux下的串列埠通訊開發
在linux下所有的裝置都是檔案,串列埠也不例外,所以對串列埠的操作也是open,close,write,read這幾個操作,只不過串列埠通訊要想正常溝通,還需要設定正確的屬性。
一 必備知識
1.1 標頭檔案
#include <termios.h>
1.2 主要結構struct termios
struct termios {
tcflag_t c_cflag /* 控制標誌 */
tcflag_t c_iflag; /* 輸入標誌 */
tcflag_t c_oflag; /* 輸出標誌 */
tcflag_t c_lflag; /* 本地標誌 */
tcflag_t c_cc[NCCS]; /* 控制字元 */
};
1.3 function
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, struct termios *termios_p);
int tcflush(int fd, int queue_selector);
speed_t cfgetispeed(struct termios *termios_p);
speed_t cfgetospeed(struct termios *termios_p);
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
二 串列埠的基本操作
2.1 開啟串列埠
int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY |O_NDELAY);
if (fd < 0) {
perror("open uart device error\n");
}
O_NOCTTY
O_NDELAY:告訴系統此程式不關心DCD訊號線狀態,即其他埠是否執行,不說明這個標誌的話,該程式就會在DCD訊號線為低電平時一直睡眠。
O_NONBLOCK: 該標誌與O_NDELAY(早期使用)標誌作用差不多,允許多次開啟時必須設成非阻塞模式.
2.2 讀寫關閉串列埠
unsigned char buf[11];
int len = read(fd, buf, sizeof(buf));
if (len < 0){
printf("reading data faile \n");
}
len = write(fd, buf, sizeof(buf));
if (len < 0) {
printf("write data to serial failed! \n");
}
close(fd);
如果操作埠設定成原資料模式(raw data mode),每次read呼叫都會返回從串列埠輸入緩衝區中實際得到的字元的個數。在讀取不到資料的情況下,read呼叫就會一直等著,直到埠上有新的字元可以讀取或者發生超時和錯誤。如果需要read函式迅速返回的話,可以使用下面這個方式改變裝置屬性:
fcntl(fd, F_SETFL, FNDELAY);
標誌FNDELAY可以保證read函式在埠上讀不到字元的時候返回0。需要回到正常(阻塞)模式的時候,需要再次呼叫fcntl函式設定成不帶FNDELAY標誌的情況:
fcntl(fd, F_SETFL, 0);
當然,如果你最初就是以O_NDELAY標誌開啟串列埠的,你也可在之後使用這個方法改變讀取的行為方式。
三 串列埠屬性設定
屬性的設定示例
主要就是修改termios成員,它的各引數具體看下第4小節newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD; newtio.c_iflag = IGNPAR | IGNCR; newtio.c_oflag = 0; newtio.c_lflag &= ~(ICANON|ECHO|ECHOE|ISIG); newtio.c_cc[VMIN]=1; newtio.c_cc[VTIME]=0; tcflush(fd, TCIFLUSH);/*丟棄所有驅動以接收但還沒讀取的資料 ,可選項還有TCOFLUSH,TCIOFLUSH*/ tcsetattr(fd,TCSANOW,&newtio);
tcgetattr和tcsetattr說明
int tcgetattr(int fd, struct termios *termptr);/* 獲取終端屬性*/ int tcsetattr(int fd, int opt, const struct termios *termptr);/* 設定終端屬性*/
在串列埠驅動程式裡有輸入緩衝區和輸出緩衝區。在改變串列埠屬性時, 緩衝區可能有資料存在,如何處理緩衝區中的資料,可通過 opt 引數實現:
- TCSANOW: 更改立即發生;
- TCSADRAIN: 傳送了所有輸出後更改才發生,若更改輸出引數則應用此選項;
- TCSAFLUSH: 傳送了所有輸出後更改才發生, 在更改發生時未讀的所有輸入資料被刪除(Flush) 。
上述兩函式執行時,若成功則返回 0,若出錯則返回-1。
- tcflush()的queue_selector 值
- TCIFLUSH
重新整理串列埠輸入緩衝中收到但還未被讀取的資料 - TCOFLUSH
重新整理串列埠輸出緩衝中寫入但還沒傳送出去的資料 - TCIOFLUSH
重新整理串列埠中所有快取的資料
- TCIFLUSH
四 struct termios各成員詳解
4.1 c_cflag 控制選項
作用:可設定串列埠的波特率、資料位、奇偶校驗、停止位以及
硬體流控制波特率標誌常量
標 志 說 明 標 志 說 明 B0 0 位/秒(掛起) B9600 9600 位/秒 B110 100 位/秒 B19200 19200 位/秒 B134 134 位/秒 B57600 57600 位/秒 B1200 1200 位/秒 B115200 115200 位/秒 B2400 2400 位/秒 B460800 460800 位/秒 B4800 4800 位/秒 波特率系統提供了單獨的函式來設定,一般來說,輸入、輸出的波特率應該是一致的。
speed_t cfgetispeed(struct termios *termios_p); speed_t cfgetospeed(struct termios *termios_p); int cfsetispeed(struct termios *termios_p, speed_t speed); int cfsetospeed(struct termios *termios_p, speed_t speed);
資料位,奇偶校驗、停止位,硬體流控制開關標誌常量
標 志 說 明 標 志 說 明 CSIZE 資料位遮蔽 CS7 7 位資料位 CS5 5 位資料位 CS8 8 位資料位 CS6 6 位資料位 CLOCAL 忽略 modem 控制線 PARENB 進行奇偶校驗,否則不校驗 PARODD 奇校驗,否則為偶校驗,需先開啟前者 CSTOPB 設2位停止位,否則1位 CREAD 開啟接受者 CRTSCTS 硬體流控制
4.2 c_iflag 輸入設定
負責控制串列埠輸入資料的處理
標 志 說 明 標 志 說 明 IGNPAR 忽略楨錯誤和奇偶校驗錯 IGNBRK 忽略 BREAK 條件 INPCK 開啟輸入奇偶校驗 PARMRK 標記奇偶錯,只有設定了INPCK並且沒有設定 IGNPAR 才有效 ISTRIP 去掉字元第8位 IGNCR 忽略輸入中的回車CR ICRNL 將輸入的CR轉換為 NL,除非設了IGNCR INLCR 將輸入的NL(換行)轉換為CR IXON 啟用輸出的 XON/XOFF 流控制 IXOFF 啟用輸入的 XON/XOFF 流控制 IXANY 嘗試任何字元可做重啟輸出訊號,預設只能START字元恢復輸出 IUCLC (not in POSIX)將輸入中的大寫字母對映為小寫字母 - 使用軟體流控制是啟用 IXON、IXOFF 和 IXANY 選項:
options.c_iflag |= (IXON | IXOFF | IXANY); 相反,要禁用軟體流控制是禁止上面的選項:
options.c_iflag &= ~(IXON | IXOFF | IXANY);什麼是流控制 ?
兩個序列介面之間的傳輸資料流通常需要協調一致才行。這可能是由於用以通訊的某個序列介面或者某些儲存介質的中間序列通訊鏈路的限制造成的。對於非同步資料這裡有兩個方法做到這一點。
第一種方法通常被叫做“軟體”流控制。這種方法採用特殊字元來開始(XON,DC1,八進位制數021)或者結束(XOFF,DC3或者八進位制數023)資料流。而這些字元都在ASCII中定義好了。雖然這些編碼對於傳輸文字資訊非常有用,但是它們卻不能被用於在特殊程式中的其他型別的資訊。
第二種方法叫做“硬體”流控制。這種方法使用RS-232標準的CTS和RTS訊號來取代之前提到的特殊字元。當準備就緒時,接受一方會將CTS訊號設定成為space電壓,而尚未準備就緒時它會被設定成為mark電壓。相應得,傳送方會在準備就緒的情況下將RTS設定成space電壓。正因為硬體流控制使用了於資料分隔的訊號,所以與需要傳輸特殊字元的軟體流控制相比它的速度很快。但是,並不是所有的硬體和作業系統都支援CTS/RTS流控制。
- 使用軟體流控制是啟用 IXON、IXOFF 和 IXANY 選項:
4.3 c_oflag 輸出設定
負責控制串列埠輸出資料的處理
標 志 說 明 標 志 說 明 OPOST 執行輸出處理 OFILL 對於延遲使用填充符 ONLCR 將NL轉換為CR-NL OCRNL 將輸出的CR轉換為NL ONLRET 不輸出CR回車 ONOCR 在0列(行首)不輸出CR FFDLY 換頁延遲遮蔽 CRDLY CR 延遲遮蔽 OFDEL 填充符為DEL,否則為 NULL OXTABS 將製表符擴充為空格 BSDLY 退格延遲遮蔽 OLCUC 將輸出的小寫字元轉換為大寫字元 CMSPAR 標誌或空奇偶性 啟用輸出處理
啟用輸出處理需要在 c_oflag 成員中啟用 OPOST 選項,其操作方法如下:options.c_oflag |= OPOST;
使用原始輸出
就是禁用輸出處理,使資料能不經過處理、過濾地完整地輸出到串列埠。
當 OPOST 被禁止,c_oflag 其它選項也被忽略,其操作方法如下:options.c_oflag &= ~OPOST;
4.4 c_lflag 本地設定
控制串列埠驅動怎樣控制輸入字元
標 志 說 明 標 志 說 明 ISIG 當接受到字元 INTR, QUIT, SUSP, 或 DSUSP 時,產生相應的訊號 NOFLSH 在中斷或退出鍵後禁用刷清 ICANON 啟用規範輸入,預設開啟 IEXTEN 啟用擴充的輸入字元處理 XCASE 如果同時設定了 ICANON,終端只有大寫 ECHOCTL 如果設定了 ECHO,除了 TAB, NL, START, 和 STOP 之外的 ASCII 控制訊號被回顯為^X字元形式, ECHOPRT 硬拷貝的可見擦除方式 ECHO 回送輸入字元 ECHOE 如果設定了 ICANON,字元 ERASE 擦除前一個輸入字元,WERASE 擦除前一個詞 ECHONL 如果設定了 ICANON,回送NL ECHOK 如果設定了 ICANON,字元KILL刪除當前行。 ECHOKE 如果設定了 ICANON,回顯 KILL 時將刪除一行中的每個字元,如同指定了 ECHOE 和 ECHOPRT 一樣 PENDIN 在讀入下一個字元時,輸入佇列中所有字元被重新輸出 TOSTOP 對於後臺輸出傳送SIGTTOU訊號 經典輸入
經典輸入是以面向行設計的。輸入字元會被放入一個緩衝之中,這樣可以與使用者以互動的方式編輯緩衝的內容,直到收到CR(carriage return)或者LF(line feed)字元。options.c_lflag |= (ICANON | ECHO | ECHOE);
原始輸入
輸入字元只是被原封不動的接收。一般情況中,如果要使用原始輸入模式,程式中需要去掉ICANON,ECHO,ECHOE和ISIG選項:options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
4.5 c_cc[NCCS] 控制字元
控制字元
標 志 說 明 標 志 說 明 VINTR 中斷 VEOL 行結束 VQUIT 退出 VEOF 行結束 VMIN 需讀取的最小位元組數 VERASE 擦除 VTIME 與“VMIN”配合使用,是指限定的傳輸或等待的最長時間 MIN是指一次read呼叫期望返回的最小位元組數。 VTIME說明等待資料到達的分秒數 (秒的 1/10 為分秒) 。TIME 與 MIN 組合使用的具體含義分為以下四種情形:
- 當 MIN > 0,TIME > 0 時
計時器在收到第一個位元組後啟動, 在計時器超時之前 (TIME 的時間到) , 若已收到 MIN個位元組,則 read 返回 MIN 個位元組,否則,在計時器超時後返回實際接收到的位元組。
注意:因為只有在接收到第一個位元組時才開始計時,所以至少可以返回 1 個位元組。這種情形中,在接到第一個位元組之前,呼叫者阻塞。如果在呼叫 read 時資料已經可用,則如同在 read 後資料立即被接到一樣。 - 當 MIN > 0,TIME = 0 時
MIN 個位元組完整接收後,read 才返回,這可能會造成 read 無限期地阻塞。 - 當 MIN = 0, TIME > 0 時
TIME 為允許等待的最大時間,計時器在呼叫 read 時立即啟動,在串列埠接到 1 位元組資料或者計時器超時後即返回,如果是計時器超時,則返回 0。 - 當 MIN = 0,TIME = 0 時
如果有資料可用,則 read 最多返回所要求的位元組數,如果無資料可用,則 read 立即返回 0。
- 當 MIN > 0,TIME > 0 時
五 程式清單
5.1 標準輸入程式
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include<errno.h>
/* 波特率設定被定義在此 */
#define BAUDRATE B38400
/* 定義正確的串列埠裝置 */
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 系統相容 */
#define FALSE 0
#define TRUE 1
volatile int STOP=FALSE;
main()
{
int fd,c, res;
struct termios oldtio,newtio;
char buf[255];
/*
開啟資料機裝置以讀取並寫入而不以控制終端tty的模式
因為我們不想程式在送出 CTRL-C 後就被殺掉.
*/
fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY );
if (fd <0) {perror(MODEMDEVICE); exit(-1); }
tcgetattr(fd,&oldtio); /* 儲存目前的序列埠設定 */
bzero(&newtio, sizeof(newtio)); /* 清除結構體以放入新的序列埠設定值 */
/*
BAUDRATE: 設定 bps 的速度. 你也可以用 cfsetispeed 及 cfsetospeed 來設定.
CRTSCTS : 輸出資料的硬體流量控制 (只能在具完整線路的纜線下工作,沒有就不要設,不然無法用)
CS8 : 8n1 (8 位元, 不做同位元檢查,1 個終止位元)
CLOCAL : 本地連線, 不具資料機控制功能
CREAD : 致能接收字元
*/
newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
/*
IGNPAR : 忽略經同位元檢查後, 錯誤的位元組
ICRNL : 比 CR 對應成 NL (否則當輸入訊號有 CR 時不會終止輸入)
在不然把裝置設定成 raw 模式(沒有其它的輸入處理)
*/
newtio.c_iflag = IGNPAR | ICRNL;
/*
Raw 模式輸出.
*/
newtio.c_oflag = 0;
/*
ICANON : 致能標準輸入, 使所有迴應機能停用, 並不送出訊號以叫用程式
*/
newtio.c_lflag = ICANON;
/*
初始化所有的控制特性
預設值可以在 /usr/include/termios.h 找到, 在註解中也有,但我們在這不需要看它們
*/
newtio.c_cc[VINTR] = 0; /* Ctrl-c */
newtio.c_cc[VQUIT] = 0; /* Ctrl-/ */
newtio.c_cc[VERASE] = 0; /* del */
newtio.c_cc[VKILL] = 0; /* @ */
newtio.c_cc[VEOF] = 4; /* Ctrl-d */
newtio.c_cc[VTIME] = 0; /* 不使用分割字元組的計時器 */
newtio.c_cc[VMIN] = 1; /* 在讀取到 1 個字元前先停止 */
newtio.c_cc[VSWTC] = 0; /* '/0' */
newtio.c_cc[VSTART] = 0; /* Ctrl-q */
newtio.c_cc[VSTOP] = 0; /* Ctrl-s */
newtio.c_cc[VSUSP] = 0; /* Ctrl-z */
newtio.c_cc[VEOL] = 0; /* '/0' */
newtio.c_cc[VREPRINT] = 0; /* Ctrl-r */
newtio.c_cc[VDISCARD] = 0; /* Ctrl-u */
newtio.c_cc[VWERASE] = 0; /* Ctrl-w */
newtio.c_cc[VLNEXT] = 0; /* Ctrl-v */
newtio.c_cc[VEOL2] = 0; /* '/0' */
/*
現在清除資料機線並啟動序列埠的設定
*/
tcflush(fd, TCIFLUSH);
tcsetattr(fd,TCSANOW,&newtio);
/*
終端機設定完成, 現在處理輸入訊號
在這個範例, 在一行的開始處輸入 'z' 會退出此程式.
*/
while (STOP==FALSE) { /* 迴圈會在我們發出終止的訊號後跳出 */
/* 即使輸入超過 255 個字元, 讀取的程式段還是會一直等到行終結符出現才停止.
如果讀到的字元組低於正確存在的字元組, 則所剩的字元會在下一次讀取時取得.
res 用來存放真正讀到的字元組個數 */
res = read(fd,buf,255);
buf[res]=0; /* 設定字串終止字元, 所以我們能用 printf */
printf(":%s:%d/n", buf, res);
if (buf[0]=='z') STOP=TRUE;
}
/* 回存舊的序列埠設定值 */
tcsetattr(fd,TCSANOW,&oldtio);
}
5.2 非標準輸入程式
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include<errno.h>
#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 系統相容 */
#define FALSE 0
#define TRUE 1
volatile int STOP=FALSE;
main()
{
int fd,c, res;
struct termios oldtio,newtio;
char buf[255];
fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY );
if (fd <0) {perror(MODEMDEVICE); exit(-1); }
tcgetattr(fd,&oldtio); /* 儲存目前的序列埠設定 */
bzero(&newtio, sizeof(newtio));
newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio.c_iflag = IGNPAR;
newtio.c_oflag = 0;
/* 設定輸入模式 (非標準型, 不迴應,...) */
newtio.c_lflag = 0;
newtio.c_cc[VTIME] = 0; /* 不使用分割字元組計時器 */
newtio.c_cc[VMIN] = 5; /* 在讀取到 5 個字元前先停止 */
tcflush(fd, TCIFLUSH);
tcsetattr(fd,TCSANOW,&newtio);
while (STOP==FALSE) { /* 輸入迴圈 */
res = read(fd,buf,255); /* 在輸入 5 個字元后即返回 */
buf[res]=0; /* 所以我們能用 printf... */
printf(":%s:%d/n", buf, res);
if (buf[0]=='z') STOP=TRUE;
}
tcsetattr(fd,TCSANOW,&oldtio);
}
5.3 非同步式輸入
非同步串列埠通訊,使用軟中斷模式接收串列埠資料,主要用到了linux的訊號相關知識
訊號(signal),又稱為軟中斷訊號,用來通知程序發生了非同步事件。程序之間可以互相傳送軟中斷訊號。 核心也可以因為內部事件而給程序傳送訊號, 通知程序發生了某個事件。
注意,訊號只是用來通知程序發生了什麼事件,並不給該程序傳遞任何資料。
此處通過sigaction(SIGIO,&saio,NULL);給SIGIO訊號安裝了一箇中斷處理函式signal_handler_IO 。當串列埠有收發資料時就會發出一個SIGIO訊號,系統就會呼叫訊號中斷處理函數了。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1 /* POSIX 系統相容 */
#define FALSE 0
#define TRUE 1
volatile int STOP=FALSE;
void signal_handler_IO (int status); /* 定義訊號處理程式 */
int wait_flag=TRUE; /* 沒收到訊號的話就會是 TRUE */
main()
{
int fd,c, res;
struct termios oldtio,newtio;
struct sigaction saio; /* definition of signal action */
char buf[255];
/* 開啟裝置為 non-blocking (讀取功能會馬上結束返回) */
fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd <0) {perror(MODEMDEVICE); exit(-1); }
/* 在使裝置非同步化前, 安裝訊號處理程式 */
saio.sa_handler = signal_handler_IO;
saio.sa_mask = 0;
saio.sa_flags = 0;
saio.sa_restorer = NULL;
sigaction(SIGIO,&saio,NULL);
/* 允許行程去接收 SIGIO 訊號*/
fcntl(fd, F_SETOWN, getpid());
/* 使文件ake the file descriptor 非同步 (使用手冊上說只有 O_APPEND 及 O_NONBLOCK, 而 F_SETFL 也可以用...) */
fcntl(fd, F_SETFL, FASYNC);
tcgetattr(fd,&oldtio); /* 儲存目前的序列埠設定值 */
/* 設定新的序列埠為標準輸入程式 */
newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio.c_iflag = IGNPAR | ICRNL;
newtio.c_oflag = 0;
newtio.c_lflag = ICANON;
newtio.c_cc[VMIN]=1;
newtio.c_cc[VTIME]=0;
tcflush(fd, TCIFLUSH);
tcsetattr(fd,TCSANOW,&newtio);
/* 等待輸入訊號的迴圈. 很多有用的事我們將在這做 */
while (STOP==FALSE) {
printf("./n");usleep(100000);
/* 在收到 SIGIO 後, wait_flag = FALSE, 輸入訊號存在則可以被讀取 */
if (wait_flag==FALSE) {
res = read(fd,buf,255);
buf[res]=0;
printf(":%s:%d/n", buf, res);
if (res==1) STOP=TRUE; /* 如果只輸入 CR 則停止迴圈 */
wait_flag = TRUE; /* 等待新的輸入訊號 */
}
}
/* 回存舊的序列埠設定值 */
tcsetattr(fd,TCSANOW,&oldtio);
}
/***************************************************************************
* 訊號處理程式. 設定 wait_flag 為 FALSE, 以使上述的迴圈能接收字元 *
***************************************************************************/
void signal_handler_IO (int status)
{
printf("received SIGIO signal./n");
wait_flag = FALSE;
}
上面的中斷訊號不能區分是寫入還是發出資料時的中斷,這個可以具體看看Linux的訊號章節。或我寫的訊號函式sigaction說明
5.4 等待來自多個訊號來源的輸入
Linux下直接用read讀串列埠可能會造成堵塞,或資料讀出錯誤。然而用select先查詢串列埠,再用read去讀就可以避免,並且當串列埠延時時,程式可以退出,這樣就不至於由於串列埠堵塞,程式就死了。利用select函式還可以實現多個串列埠的讀寫,
main()
{
int fd1, fd2; /* 輸入源 1 及 2 */
fd_set readfs; /* 文件敘述結構設定 */
int maxfd; /* 最大可用的文件敘述結構 */
int loop=1; /* 迴圈在 TRUE 時成立 */
int res;
struct timeval Timeout;
/* 設定輸入迴圈的逾時值 */
Timeout.tv_usec = 0; /* 毫秒 */
Timeout.tv_sec = 1; /* 秒 */
/* open_input_source 開啟一個裝置, 正確的設定好序列埠,
並回傳回此文件敘述結構體 */
fd1 = open_input_source("/dev/ttyS1"); /* COM2 */
if (fd1<0) exit(0);
fd2 = open_input_source("/dev/ttyS2"); /* COM3 */
if (fd2<0) exit(0);
maxfd = MAX (fd1, fd2)+1; /* 測試最大位元輸入 (fd) */
/* 輸入迴圈 */
while (loop) {
FD_ZERO(&readfs)//清除一個檔案描述符集;
FD_SET(fd1, &readfs); /* 測試輸入源 1 */
FD_SET(fd2, &readfs); /* 測試輸入源 2 */
/* block until input becomes available */
res = select(maxfd, &readfs, NULL, NULL, &Timeout);
if (res==0)
continue;/*超時*/
else if(res<0){
if (errno == EINTR)
continue;
ERR_EXIT("select error");
}
else{
if (FD_ISSET(fd1)) /* 如果輸入源 1 有訊號 */
handle_input_from_source1();
if (FD_ISSET(fd2)) /* 如果輸入源 2 有訊號 */
handle_input_from_source2();
}
}
}