Linux串口应用编程
目錄
- Demo
- 串口應用編程介紹
- 終端Terminal
- 串口應用編程(配置、讀取、寫入)
- struct termios 結構體配置
- 輸入模式: c_iflag
- 輸出模式: c_oflag
- 控制模式: c_cflag(波特率、數據位、校驗位、停止位)
- 本地模式: c_lflag
- 特殊控制字符: c_cc
- 注意事項
- 三種工作模式(原始模式read是否阻塞)
- 什么時候會使用原始模式
- 打開串口設備
- 獲取終端當前的配置參數:tcgetattr()函數
- 串口終端配置(原始模式為例)
- 1) 配置原始模式
- 2) 接收使能
- 3) 設置串口的波特率
- 4) 設置數據位大小
- 5) 設置奇偶校驗位
- 6) 設置停止位
- 7) 設置MIN 和TIME 的值(read是否阻塞)
- 緩沖區的處理(清空、阻塞、暫停)
- 使配置生效:tcsetattr()函數
- 讀寫數據:read()、write()
- 串口應用編程實戰
- 讀取串口數據
- 寫操作
- 在開發板上進行測試
- 讀測試
- 寫測試
Demo
參考:【嵌入式LINUX系統編程】嵌入式LINUX串口編程
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <termios.h> #include <errno.h>int set_opt(int,int,int,char,int);void main(){int fd,nByte;char *uart3 = "/dev/ttyS2";char bufferR[2];char bufferW[2]; memset(bufferR, '\0', 2);memset(bufferW, '\0', 2);if((fd=open(uart3,O_RDWR,0777))<0){printf("failed\n");}else{printf("success\n");set_opt(fd, 115200, 8, 'N', 1); }while(1){ nByte = 0;memset(bufferR, '\0', 2);memset(bufferW, '\0', 2);// printf("hello\n");if((nByte = read(fd, bufferR, 1)) == 1){ //MCU串口發送接收都是單字節(單字符)函數printf("receive:%c\n",bufferR[0]);bufferW[0] = 'A';write(fd,bufferW,1); //串口發送單字節(單字符) buffer[0] = datamemset(bufferR, '\0', 2);memset(bufferW, '\0', 2);nByte = 0;}}close(fd);}//串口通用初始化函數 int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop) {struct termios newtio,oldtio;//定義結構體newtio和oldtio//將原串口的數據取到oldtioif ( tcgetattr( fd,&oldtio) != 0) { perror("SetupSerial 1");return -1;}//將newio清零和設置c_cflagbzero( &newtio, sizeof( newtio ) );newtio.c_cflag |= CLOCAL | CREAD;//使能接收和忽略控制線newtio.c_cflag &= ~CSIZE;//設置數據位switch( nBits ){case 7:newtio.c_cflag |= CS7;break;case 8:newtio.c_cflag |= CS8;break;}//設置校驗位switch( nEvent ){//偶校驗case 'O':newtio.c_cflag |= PARENB;//使能奇偶校驗newtio.c_cflag |= PARODD;//偶校驗newtio.c_iflag |= (INPCK | ISTRIP);//輸入校驗并忽略第八位break;case 'E': newtio.c_iflag |= (INPCK | ISTRIP);newtio.c_cflag |= PARENB;newtio.c_cflag &= ~PARODD;//取消偶校驗(置零偶校驗位),開啟奇校驗break;case 'N': newtio.c_cflag &= ~PARENB;//不進行奇偶校驗break;}//設置波特率switch( nSpeed ){case 2400:cfsetispeed(&newtio, B2400);cfsetospeed(&newtio, B2400);break;case 4800:cfsetispeed(&newtio, B4800);cfsetospeed(&newtio, B4800);break;case 9600:cfsetispeed(&newtio, B9600);cfsetospeed(&newtio, B9600);break;case 115200:cfsetispeed(&newtio, B115200);cfsetospeed(&newtio, B115200);break;case 460800:cfsetispeed(&newtio, B460800);cfsetospeed(&newtio, B460800);break;default:cfsetispeed(&newtio, B9600);cfsetospeed(&newtio, B9600);break;}//設置停止位if( nStop == 1 )newtio.c_cflag &= ~CSTOPB;//一位停止位else if ( nStop == 2 )newtio.c_cflag |= CSTOPB;//兩位停止位newtio.c_cc[VTIME] = 0;//不設置讀取超時newtio.c_cc[VMIN] = 0;//讀取最小字符數為0tcflush(fd,TCIFLUSH);//清空緩沖區//使配置生效if((tcsetattr(fd,TCSANOW,&newtio))!=0){perror("com set error");return -1;}// printf("set done!\n\r");return 0; }串口應用編程介紹
本小節我們來學習Linux 下串口應用編程,串口(UART)是一種非常常見的外設,串口在嵌入式開發領域當中一般作為一種調試手段,通過串口輸出調試打印信息,或者發送指令給主機端進行處理;還可以通過串口與其他設備或傳感器進行通信,譬如有些sensor 就使用了串口通信的方式與主機端進行數據交互。
串口全稱叫做串行接口,使用兩條線即可實現雙向通信,一條用于發送,一條用于接收。串口通信距離遠,但是速度相對會低。
串口(UART)在嵌入式Linux 系統中常作為系統的標準輸入、輸出設備,系統運行過程產生的打印信息通過串口輸出;同理,串口也作為系統的標準輸入設備,用戶通過串口與Linux 系統進行交互。
所以串口在Linux 系統就是一個終端,提到串口,就不得不引出“終端(Terminal)”這個概念了。
終端Terminal
終端就是處理主機輸入、輸出的一套設備,它用來顯示主機運算的輸出,并且接受主機要求的輸入。典型的終端包括顯示器鍵盤套件、打印機打字機套件等。只要能提供給計算機輸入和輸出功能,它就是終端。
終端的分類
- 本地終端:PC 機連接了顯示器、鍵盤以及鼠標等設備,這樣的一個顯示器/鍵盤組合就是一個本地終端;同樣對于開發板來說一個LCD 顯示器、鍵盤和鼠標等構成本地終端。
- 用串口連接的遠程終端:開發板通過串口線連接到一個帶有顯示器和鍵盤的PC 機,在PC 機通過運行一個終端模擬程序,譬如SecureCRT 等來獲取并顯示開發板通過串口發出的數據、同樣還可以通過這些終端模擬程序將用戶數據通過串口發送給開發板Linux 系統。
- 基于網絡的遠程終端:譬如我們可以通過ssh、Telnet 這些協議登錄到一個遠程主機。
前面兩個屬于物理終端,遠程終端又叫做偽終端。
終端對應的設備節點
在Linux 當中,一切皆是文件。當然,終端也不例外,每一個終端在/dev 目錄下都有一個對應的設備節點。
? /dev/ttyX(X 是一個數字編號,譬如0、1、2、3 等)設備節點:在Linux 中,/dev/ttyX 代表的都是上述提到的本地終端,包括/dev/tty1~/dev/tty63 共63 個,這是Linux 內核在初始化時所生成的63 個本地終端。如下所示:
? /dev/pts/X(X 是一個數字編號,譬如0、1、2、3 等)設備節點:這類設備節點是偽終端對應的設備節點。譬如我們通過ssh 或Telnet 這些遠程登錄協議登錄到開發板主機,那么開發板Linux 系統會在/dev/pts 目錄下生成一個設備節點,如下所示:
? 串口終端設備節點/dev/ttymxcX:對于ALPHA/Mini I.MX6U 開發板來說,有兩個串口,也就是有兩個串口終端,對應兩個設備節點,如下所示:
這里為什么是0 和2、而不是0 和1?I.MX6U SoC 支持8 個串口外設,出廠系統只注冊了2 個串口外設,分別是UART1 和UART3,所以對應這個數字就是0 和2。
mxc 這個名字不是一定的,命名與驅動有關系(與硬件平臺有關),但是名字前綴都是以“tty”開頭,以表明它是一個終端。
在Linux 系統下,我們可以使用who 命令來查看計算機系統當前連接了哪些終端(一個終端就表示有一個用戶使用該計算機),如下所示:
可以看到,開發板系統當前有兩個終端連接到它,一個是串口終端,即開發板的USB 調試串口(對應/dev/ttymxc0);另一個是偽終端,是筆者通過ssh 連接的。
串口應用編程(配置、讀取、寫入)
我們知道了串口在Linux 系統中是一種終端設備,并且在我們的開發板上,其設備節點為/dev/ttymxc0(UART1)和/dev/ttymxc2(UART3)。
串口的應用編程很簡單,通過ioctl()對串口進行配置,調用read()讀取串口的數據、調用write()向串口寫入數據,但是我們不這么做,因為Linux 為上層用戶做了一層封裝,將這些ioctl()操作封裝成了一套標準的API,我們直接使用這一套標準API 即可。
這些API 是C 庫函數,可以使用man 手冊查看到它們的幫助信息,這一套接口并不是僅針對串口開發的,而是針對所有的終端設備,通過ssh 遠程登錄連接的偽終端也是終端設備。
要使用這個API,需要包含termios.h 頭文件。
struct termios 結構體配置
struct termios 結構體描述了終端的配置信息,struct termios 結構體定義如下:
struct termios {tcflag_t c_iflag; /* input mode flags */ 輸入模式tcflag_t c_oflag; /* output mode flags */輸出模式tcflag_t c_cflag; /* control mode flags */控制模式tcflag_t c_lflag; /* local mode flags */本地模式cc_t c_line; /* line discipline */ 線路規程cc_t c_cc[NCCS]; /* control characters */特殊控制字符speed_t c_ispeed; /* input speed */輸入速率speed_t c_ospeed; /* output speed */輸出速率 };輸入模式: c_iflag
輸入模式控制輸入數據(終端驅動程序從串口或鍵盤接收到的字符數據)在被傳遞給應用程序之前的處理方式。所有的標志都被定義為宏,c_oflag、c_cflag 以及c_lflag 成員也都采用這種方式進行配置。
c_iflag 成員的宏如下所示:
| IGNBRK | 忽略輸入終止條件 |
| BRKINT | 當檢測到輸入終止條件時發送SIGINT 信號 |
| IGNPAR | 忽略幀錯誤和奇偶校驗錯誤 |
| PARMRK | 對奇偶校驗錯誤做出標記 |
| INPCK | 對接收到的數據執行奇偶校驗 |
| ISTRIP | 將所有接收到的數據裁剪為7 比特位、也就是去除第八位 |
| INLCR | 將接收到的NL(換行符)轉換為CR(回車符) |
| IGNCR | 忽略接收到的CR(回車符) |
| ICRNL | 將接收到的CR(回車符)轉換為NL(換行符) |
| IUCLC | 將接收到的大寫字符映射為小寫字符 |
| IXON | 啟動輸出軟件流控 |
| IXOFF | 啟動輸入軟件流控 |
以上所列舉出的這些宏,我們可以通過man 手冊查詢到它們的詳細描述信息,執行命令" man 3 termios",如下圖所示:
輸出模式: c_oflag
輸出模式控制輸出字符的處理方式,即由應用程序發送出去的字符數據在傳遞到串口或屏幕之前是如何處理的。可用于c_oflag 成員的宏如下所示:
| OPOST | 啟用輸出處理功能,如果不設置該標志則其他標志都被忽略 |
| OLCUC | 將輸出字符中的大寫字符轉換成小寫字符 |
| ONLCR | 將輸出中的換行符(NL ‘\n’)轉換成回車符(CR ‘\r’) |
| OCRNL | 將輸出中的回車符(CR ‘\r’)轉換成換行符(NL ‘\n’) |
| ONOCR | 在第0 列不輸出回車符(CR) |
| ONLRET | 不輸出回車符 |
| OFILL | 發送填充字符以提供延時 |
| OFDEL | 如果設置該標志,則表示填充字符為DEL 字符,否則為NULL字符 |
控制模式: c_cflag(波特率、數據位、校驗位、停止位)
控制模式控制終端設備的硬件特性,譬如對于串口來說,該字段比較重要,可設置串口波特率、數據位、校驗位、停止位等硬件特性。可用于c_cflag 成員的標志如下所示:
| B0 | 波特率為0 |
| …… | …… |
| B1200 | 1200 波特率 |
| B1800 | 1800 波特率 |
| B2400 | 2400 波特率 |
| B4800 | 4800 波特率 |
| B9600 | 9600 波特率 |
| B19200 | 19200 波特率 |
| B38400 | 38400 波特率 |
| B57600 | 57600 波特率 |
| B115200 | 115200 波特率 |
| B230400 | 230400 波特率 |
| B460800 | 460800 波特率 |
| B500000 | 500000 波特率 |
| B576000 | 576000 波特率 |
| B921600 | 921600 波特率 |
| B1000000 | 1000000 波特率 |
| B1152000 | 1152000 波特率 |
| B1500000 | 1500000 波特率 |
| B2000000 | 2000000 波特率 |
| B2500000 | 2500000 波特率 |
| B3000000 | 3000000 波特率 |
| …… | …… |
| CS5 | 5 個數據位 |
| CS6 | 6 個數據位 |
| CS7 | 7 個數據位 |
| CS8 | 8 個數據位 |
| CSTOPB | 2 個停止位,如果不設置該標志則默認是一個停止位 |
| CREAD | 接收使能 |
| PARENB | 使能奇偶校驗 |
| PARODD | 使用奇校驗、而不是偶校驗 |
| HUPCL | 關閉時掛斷調制解調器 |
| CLOCAL | 忽略調制解調器控制線 |
| CRTSCTS | 使能硬件流控 |
在其它一些系統中,可能會使用c_ispeed 成員變量和c_ospeed 成員變量來指定串口的波特率;
在Linux 系統下,則是使用CBAUD 位掩碼所選擇的幾個bit 位來指定串口波特率。
事實上,termios API 中提供了cfgetispeed()和cfsetispeed()函數分別用于獲取和設置串口的波特率。
本地模式: c_lflag
本地模式用于控制終端的本地數據處理和工作模式。可用于c_lflag 成員的標志如下所示:
| ISIG | 若收到信號字符(INTR、QUIT 等),則會產生相應的信號 |
| ICANON | 啟用規范模式 |
| ECHO | 啟用輸入字符的本地回顯功能。當我們在終端輸入字符的時候,字符會顯示出來,這就是回顯功能 |
| ECHOE | 若設置ICANON,則允許退格操作 |
| ECHOK | 若設置ICANON,則KILL 字符會刪除當前行 |
| ECHONL | 若設置ICANON,則允許回顯換行符 |
| ECHOCTL | 若設置ECHO,則控制字符(制表符、換行符等)會顯示成“^X”,其中X 的ASCII 碼等于給相應控制字符的ASCII 碼加上0x40。例如,退格字符(0x08)會顯示為“^H”('H’的ASCII 碼為0x48) |
| ECHOPRT | 若設置ICANON 和IECHO,則刪除字符(退格符等)和被刪除的字符都會被顯示 |
| ECHOKE | 若設置ICANON,則允許回顯在ECHOE 和ECHOPRT 中設定的KILL字符 |
| NOFLSH | 在通常情況下,當接收到INTR、QUIT 和SUSP 控制字符時,會清空輸入和輸出隊列。如果設置該標志,則所有的隊列不會被清空 |
| TOSTOP | 若一個后臺進程試圖向它的控制終端進行寫操作,則系統向該后臺進程的進程組發送SIGTTOU 信號。該信號通常終止進程的執行 |
| IEXTEN | 啟用輸入處理功能 |
特殊控制字符: c_cc
特殊控制字符是一些字符組合,如Ctrl+C、Ctrl+Z 等,當用戶鍵入這樣的組合鍵,終端會采取特殊處理方式。struct termios 結構體中c_cc 數組將各種特殊字符映射到對應的支持函數。每個字符位置(數組下標)由對應的宏定義的,如下所示
? VEOF:文件結尾符EOF,對應鍵為Ctrl+D;該字符使終端驅動程序將輸入行中的全部字符傳遞給正在讀取輸入的應用程序。如果文件結尾符是該行的第一個字符,則用戶程序中的read 返回0,表示文件結束。
? VEOL:附加行結尾符EOL,對應鍵為Carriage return(CR);作用類似于行結束符。
? VEOL2:第二行結尾符EOL2,對應鍵為Line feed(LF);
? VERASE:刪除操作符ERASE,對應鍵為Backspace(BS);該字符使終端驅動程序刪除輸入行中的最后一個字符;
? VINTR:中斷控制字符INTR,對應鍵為Ctrl+C;該字符使終端驅動程序向與終端相連的進程發送SIGINT 信號;
? VKILL:刪除行符KILL,對應鍵為Ctrl+U,該字符使終端驅動程序刪除整個輸入行;
? VMIN:在非規范模式下,指定最少讀取的字符數MIN;
? VQUIT:退出操作符QUIT,對應鍵為Ctrl+Z;該字符使終端驅動程序向與終端相連的進程發送SIGQUIT 信號。
? VSTART:開始字符START,對應鍵為Ctrl+Q;重新啟動被STOP 暫停的輸出。
? VSTOP:停止字符STOP,對應鍵為Ctrl+S;字符作用“截流”,即阻止向終端的進一步輸出。用于支持XON/XOFF 流控。
? VSUSP:掛起字符SUSP,對應鍵為Ctrl+Z;該字符使終端驅動程序向與終端相連的進程發送SIGSUSP 信號,用于掛起當前應用程序。
? VTIME:非規范模式下,指定讀取的每個字符之間的超時時間(以分秒為單位)TIME。
在以上所列舉的這些宏定義中,TIME 和MIN 值只能用于非規范模式,可用于控制非規范模式下read()調用的一些行為特性,后面再向大家介紹。
注意事項
對于這些變量盡量不要直接對其初始化,而要將其通過“按位與”、“按位或”等操作添加標志或清除某個標志。譬如,通常不會這樣對變量進行初始化:
struct termios ter;ter.c_iflag = IGNBRK | BRKINT | PARMRK;而是要像下面這樣:
ter.c_iflag |= (IGNBRK | BRKINT | PARMRK | ISTRIP);并非所有標志對于實際的終端設備來說都是有效的,例如串口可以配置波特率、數據位、停止位等參數,但其它終端是不一定支持這些配置的,譬如本地終端鍵盤、顯示器,只不過這些終端設備都使用了這一套API 來編程,
三種工作模式(原始模式read是否阻塞)
當ICANON 標志被設置時表示啟用終端的規范模式,默認情況為規范模式。
規范模式下,所有的輸入是基于行進行處理的。在用戶輸入一個行結束符(回車符、EOF 等)之前,系統調用read()函數是讀不到用戶輸入的任何字符的。除了EOF 之外的行結束符(回車符等)與普通字符一樣會被read()函數讀取到緩沖區中。在規范模式中,行編輯是可行的,而且一次read()調用最多只能讀取一行數據。如果在read()函數中被請求讀取的數據字節數小于當前行可讀取的字節數,則read()函數只會讀取被請求的字節數,剩下的字節下次再被讀取。
非規范模式下,所有的輸入是即時有效的,不需要用戶另外輸入行結束符,而且不可進行行編輯。在非規范模式下,對參數MIN(c_cc[VMIN])和TIME(c_cc[VTIME])的設置決定read()函數的調用方式。
上一小節給大家提到過,TIME 和MIN 的值只能用于非規范模式,兩者結合起來可以控制對輸入數據的讀取方式。根據TIME 和MIN 的取值不同,會有以下4 種不同情況:
? MIN = 0 和TIME = 0:在這種情況下,read()調用總是會立即返回。若有可讀數據,則讀取數據并返回被讀取的字節數;否則讀取不到任何數據并返回0。
? MIN > 0 和TIME = 0:在這種情況下,read()函數會被阻塞,直到有MIN 個字符可以讀取時才返回,返回值是讀取的字符數量。到達文件尾時返回0。
? MIN = 0 和TIME > 0:在這種情況下,只要有數據可讀或者經過TIME 個十分之一秒的時間,read()函數則立即返回,返回值為被讀取的字節數。如果超時并且未讀到數據,則read()函數返回0。
? MIN > 0 和TIME > 0:在這種情況下,當有MIN 個字節可讀或者兩個輸入字符之間的時間間隔超過TIME 個十分之一秒時,read()函數才返回。因為在輸入第一個字符后系統才會啟動定時器,所以,在這種情況下,read()函數至少讀取一個字節后才返回。
原始模式(Raw mode)
按照嚴格意義來講,原始模式是一種特殊的非規范模式。在原始模式下,所有的輸入數據以字節為單位被處理。在這個模式下,終端是不可回顯的,并且禁用終端輸入和輸出字符的所有特殊處理。在我們的應用程序中,可以通過調用cfmakeraw()函數將終端設置為原始模式。
cfmakeraw()函數內部其實就是對struct 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;什么時候會使用原始模式
串口在Linux 系統下是作為一種終端設備存在,終端通常會對用戶的輸入、輸出數據進行相應的處理,如前所述。
但是串口并不僅僅只扮演著人機交互的角色(數據以字符的形式傳輸、也就數說傳輸的數據其實字符對應的ASCII 編碼值);串口本就是一種數據串行傳輸接口,通過串口可以與其他設備或傳感器進行數據傳輸、通信,譬如很多sensor 就使用了串口方式與主機端進行數據交互。那么在這種情況下,我們就得使用原始模式,意味著通過串口傳輸的數據不應進行任何特殊處理、不應將其解析成ASCII 字符。
打開串口設備
接下來編寫串口應用程序。第一步便是打開串口設備,使用open()函數打開串口的設備節點文件,得到文件描述符:
int fd; fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY); if (0 > fd) {perror("open error");return -1; }調用open()函數時,使用了O_NOCTTY 標志,該標志用于告知系統/dev/ttymxc2 它不會成為進程的控制終端。
獲取終端當前的配置參數:tcgetattr()函數
通常在配置終端之前,我們會先獲取到終端當前的配置參數,將其保存到一個struct termios 結構體對象中,以后可很方便地將終端恢復到原來的狀態,這也是為了安全起見以及后續的調試方便。
#include <termios.h> #include <unistd.h>int tcgetattr(int fd, struct termios *termios_p);第一個參數對應串口終端設備的文件描述符fd。
調用tcgetattr 函數之前,我們需要定義一個struct termios 結構體變量,tcgetattr()調用成功后,會將終端當前的配置參數保存到termios_p 指針所指的對象中。
函數調用成功返回0;失敗將返回-1,并且會設置errno以告知錯誤原因。
使用示例如下:
struct termios old_cfg;if (0 > tcgetattr(fd, &old_cfg)) {/* 出錯處理*/do_something(); }串口終端配置(原始模式為例)
假設我們需要采用原始模式進行串口數據通信。
1) 配置原始模式
調用<termios.h>頭文件中申明的cfmakeraw()函數可以將終端配置為原始模式:
struct termios new_cfg;memset(&new_cfg, 0x0, sizeof(struct termios));//配置為原始模式 cfmakeraw(&new_cfg);這個函數沒有返回值。
2) 接收使能
使能接收功能只需在struct termios 結構體的c_cflag 成員中添加CREAD 標志即可,如下所示:
new_cfg.c_cflag |= CREAD; //接收使能3) 設置串口的波特率
不能直接通過位掩碼來操作,使用函數:
cfsetispeed(&new_cfg, B115200);//輸入波特率 cfsetospeed(&new_cfg, B115200);//輸出波特率B115200 是一個宏,表示波特率為115200。
一般輸入和輸出波特率設置成一樣的。
我們還可以直接使用cfsetspeed()函數一次性設置輸入和輸出波特率,該函數也是在<termios.h>頭文件中申明,使用方式如下:
cfsetspeed(&new_cfg, B115200);這幾個函數在成功時返回0,失敗時返回-1。
4) 設置數據位大小
通過位掩碼來設置數據位大小,先將c_cflag 成員中CSIZE 位掩碼所選擇的幾個bit 位清零,然后再設置數據位大小,如下所示:
new_cfg.c_cflag &= ~CSIZE; new_cfg.c_cflag |= CS8; //設置為8 位數據位5) 設置奇偶校驗位
奇偶校驗位配置涉及到struct termios 結構體中的兩個成員變量:c_cflag 和c_iflag。對于c_cflag 成員添加PARENB 標志使能串口的奇偶校驗功能;對于c_iflag需要添加INPCK 標志,這樣才能對接收到的數據執行奇偶校驗,代碼如下所示:
//奇校驗使能 new_cfg.c_cflag |= (PARODD | PARENB); new_cfg.c_iflag |= INPCK;//偶校驗使能 new_cfg.c_cflag |= PARENB; new_cfg.c_cflag &= ~PARODD; /* 清除PARODD 標志,配置為偶校驗*/ new_cfg.c_iflag |= INPCK;//無校驗 new_cfg.c_cflag &= ~PARENB; new_cfg.c_iflag &= ~INPCK;6) 設置停止位
停止位則是通過設置c_cflag 成員的CSTOPB 標志而實現的。若停止位為一個比特,則清除CSTOPB 標志;若停止位為兩個,則添加CSTOPB 標志即可。以下分別是停止位為一個和兩個比特時的代碼:
// 將停止位設置為一個比特 new_cfg.c_cflag &= ~CSTOPB;// 將停止位設置為2 個比特 new_cfg.c_cflag |= CSTOPB;7) 設置MIN 和TIME 的值(read是否阻塞)
前面介紹,MIN 和TIME 的取值會影響非規范模式下read()調用的行為特征,原始模式是一種特殊的非規范模式,所以MIN 和TIME 在原始模式下也是有效的。
在對接收字符和等待時間沒有特別要求的情況下,可以將MIN 和TIME 設置為0,這樣則在任何情況下read()調用都會立即返回,此時對串口的read 操作會設置為非阻塞方式,如下所示:
new_cfg.c_cc[VTIME] = 0; new_cfg.c_cc[VMIN] = 0;緩沖區的處理(清空、阻塞、暫停)
串口使用之前,緩沖區中可能已經存在一些數據等待處理或者當前正在進行數據傳輸、接收。調用<termios.h>中聲明的函數來處理目前串口緩沖中的數據,函數原型:
#include <termios.h> #include <unistd.h>//這三個函數,調用成功時返回0;失敗將返回-1、并且會設置errno 以指示錯誤類型。 int tcdrain(int fd); int tcflush(int fd, int queue_selector); int tcflow(int fd, int action);1、調用tcdrain()函數后會使得應用程序阻塞,直到串口輸出緩沖區中的數據全部發送完畢為止
2、調用tcflow()函數會暫停串口上的數據傳輸或接收工作,具體情況取決于參數action取值如下:
? TCOOFF:暫停數據輸出(輸出傳輸);
? TCOON:重新啟動暫停的輸出;
? TCIOFF:發送STOP 字符,停止終端設備向系統發送數據;
? TCION:發送一個START 字符,啟動終端設備向系統發送數據;
3、調用tcflush()函數會清空輸入/輸出緩沖區中的數據,具體情況取決于參數queue_selector如下:
? TCIFLUSH:對接收到而未被讀取的數據進行清空處理;
? TCOFLUSH:對尚未傳輸成功的輸出數據進行清空處理;
? TCIOFLUSH:包括前兩種功能,即對尚未處理的輸入/輸出數據進行清空處理。
通常我們會選擇tcdrain()或tcflush()函數來對串口緩沖區進行處理。
譬如調用tcdrain()阻塞:
tcdrain(fd);調用tcflush()清空緩沖區:
tcflush(fd, TCIOFLUSH);使配置生效:tcsetattr()函數
前面結構體成員配置還未生效,通過tcsetattr()函數將配置參數寫到硬件設備,其函數原型如下所示:
#include <termios.h> #include <unistd.h>int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);調用該函數會將參數termios_p 所指struct termios 對象中的配置參數寫入到終端設備中,使配置生效!
而參數optional_actions 可以指定更改何時生效,其取值如下:
? TCSANOW:配置立即生效。
? TCSADRAIN:配置在所有寫入fd 的輸出都傳輸完畢之后生效。
? TCSAFLUSH:所有已接收但未讀取的輸入都將在配置生效之前被丟棄。
該函數調用成功時返回0;失敗將返回-1,、并設置errno 以指示錯誤類型。
譬如,調用tcsetattr()將配置參數寫入設備,使其立即生效:
tcsetattr(fd, TCSANOW, &new_cfg);讀寫數據:read()、write()
調用read()、write()函數即可。
串口應用編程實戰
編程實戰,在串口終端的原始模式下,使用串口進行數據傳輸,包括通過串口發送數據、以及讀取串口接收到的數據,并將其打印出來。
本例程源碼對應的路徑為:開發板光盤->11、Linux C 應用編程例程源碼->26_uart->uart_test.c。
/***************************************************************Copyright ? ALIENTEK Co., Ltd. 1998-2021. All rights reserved.文件名 : uart_test.c作者 : 鄧濤版本 : V1.0描述 : 串口在原始模式下進行數據傳輸--應用程序示例代碼其他 : 無論壇 : www.openedv.com日志 : 初版 V1.0 2021/7/20 鄧濤創建***************************************************************/#define _GNU_SOURCE //在源文件開頭定義_GNU_SOURCE宏 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <errno.h> #include <string.h> #include <signal.h> #include <termios.h>typedef struct uart_hardware_cfg {unsigned int baudrate; /* 波特率 */unsigned char dbit; /* 數據位 */char parity; /* 奇偶校驗 */unsigned char sbit; /* 停止位 */ } uart_cfg_t;static struct termios old_cfg; //用于保存終端的配置參數 static int fd; //串口終端對應的文件描述符/**** 串口初始化操作** 參數device表示串口終端的設備節點**/ static int uart_init(const char *device) {/* 打開串口終端 */fd = open(device, O_RDWR | O_NOCTTY);if (0 > fd) {fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));return -1;}/* 獲取串口當前的配置參數 */if (0 > tcgetattr(fd, &old_cfg)) {fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));close(fd);return -1;}return 0; }/**** 串口配置** 參數cfg指向一個uart_cfg_t結構體對象**/ static int uart_cfg(const uart_cfg_t *cfg) {struct termios new_cfg = {0}; //將new_cfg對象清零speed_t speed;/* 設置為原始模式 */cfmakeraw(&new_cfg);/* 使能接收 */new_cfg.c_cflag |= CREAD;/* 設置波特率 */switch (cfg->baudrate) {case 1200: speed = B1200;break;case 1800: speed = B1800;break;case 2400: speed = B2400;break;case 4800: speed = B4800;break;case 9600: speed = B9600;break;case 19200: speed = B19200;break;case 38400: speed = B38400;break;case 57600: speed = B57600;break;case 115200: speed = B115200;break;case 230400: speed = B230400;break;case 460800: speed = B460800;break;case 500000: speed = B500000;break;default: //默認配置為115200speed = B115200;printf("default baud rate: 115200\n");break;}if (0 > cfsetspeed(&new_cfg, speed)) {fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno));return -1;}/* 設置數據位大小 */new_cfg.c_cflag &= ~CSIZE; //將數據位相關的比特位清零switch (cfg->dbit) {case 5:new_cfg.c_cflag |= CS5;break;case 6:new_cfg.c_cflag |= CS6;break;case 7:new_cfg.c_cflag |= CS7;break;case 8:new_cfg.c_cflag |= CS8;break;default: //默認數據位大小為8new_cfg.c_cflag |= CS8;printf("default data bit size: 8\n");break;}/* 設置奇偶校驗 */switch (cfg->parity) {case 'N': //無校驗new_cfg.c_cflag &= ~PARENB;new_cfg.c_iflag &= ~INPCK;break;case 'O': //奇校驗new_cfg.c_cflag |= (PARODD | PARENB);new_cfg.c_iflag |= INPCK;break;case 'E': //偶校驗new_cfg.c_cflag |= PARENB;new_cfg.c_cflag &= ~PARODD; /* 清除PARODD標志,配置為偶校驗 */new_cfg.c_iflag |= INPCK;break;default: //默認配置為無校驗new_cfg.c_cflag &= ~PARENB;new_cfg.c_iflag &= ~INPCK;printf("default parity: N\n");break;}/* 設置停止位 */switch (cfg->sbit) {case 1: //1個停止位new_cfg.c_cflag &= ~CSTOPB;break;case 2: //2個停止位new_cfg.c_cflag |= CSTOPB;break;default: //默認配置為1個停止位new_cfg.c_cflag &= ~CSTOPB;printf("default stop bit size: 1\n");break;}/* 將MIN和TIME設置為0 */new_cfg.c_cc[VTIME] = 0;new_cfg.c_cc[VMIN] = 0;/* 清空緩沖區 */if (0 > tcflush(fd, TCIOFLUSH)) {fprintf(stderr, "tcflush error: %s\n", strerror(errno));return -1;}/* 寫入配置、使配置生效 */if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) {fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));return -1;}/* 配置OK 退出 */return 0; }/*** --dev=/dev/ttymxc2 --brate=115200 --dbit=8 --parity=N --sbit=1 --type=read ***/ /**** 打印幫助信息**/ static void show_help(const char *app) {printf("Usage: %s [選項]\n""\n必選選項:\n"" --dev=DEVICE 指定串口終端設備名稱, 譬如--dev=/dev/ttymxc2\n"" --type=TYPE 指定操作類型, 讀串口還是寫串口, 譬如--type=read(read表示讀、write表示寫、其它值無效)\n""\n可選選項:\n"" --brate=SPEED 指定串口波特率, 譬如--brate=115200\n"" --dbit=SIZE 指定串口數據位個數, 譬如--dbit=8(可取值為: 5/6/7/8)\n"" --parity=PARITY 指定串口奇偶校驗方式, 譬如--parity=N(N表示無校驗、O表示奇校驗、E表示偶校驗)\n"" --sbit=SIZE 指定串口停止位個數, 譬如--sbit=1(可取值為: 1/2)\n"" --help 查看本程序使用幫助信息\n\n", app); }/**** 信號處理函數,當串口有數據可讀時,會跳轉到該函數執行**/ static void io_handler(int sig, siginfo_t *info, void *context) {unsigned char buf[10] = {0};int ret;int n;if(SIGRTMIN != sig)return;/* 判斷串口是否有數據可讀 */if (POLL_IN == info->si_code) {ret = read(fd, buf, 8); //一次最多讀8個字節數據printf("[ ");for (n = 0; n < ret; n++)printf("0x%hhx ", buf[n]);printf("]\n");} }/**** 異步I/O初始化函數**/ static void async_io_init(void) {struct sigaction sigatn;int flag;/* 使能異步I/O */flag = fcntl(fd, F_GETFL); //使能串口的異步I/O功能flag |= O_ASYNC;fcntl(fd, F_SETFL, flag);/* 設置異步I/O的所有者 */fcntl(fd, F_SETOWN, getpid());/* 指定實時信號SIGRTMIN作為異步I/O通知信號 */fcntl(fd, F_SETSIG, SIGRTMIN);/* 為實時信號SIGRTMIN注冊信號處理函數 */sigatn.sa_sigaction = io_handler; //當串口有數據可讀時,會跳轉到io_handler函數sigatn.sa_flags = SA_SIGINFO;sigemptyset(&sigatn.sa_mask);sigaction(SIGRTMIN, &sigatn, NULL); }int main(int argc, char *argv[]) {uart_cfg_t cfg = {0};char *device = NULL;int rw_flag = -1;unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,0x55, 0x66, 0x77, 0x88}; //通過串口發送出去的數據int n;/* 解析出參數 */for (n = 1; n < argc; n++) {if (!strncmp("--dev=", argv[n], 6))device = &argv[n][6];else if (!strncmp("--brate=", argv[n], 8))cfg.baudrate = atoi(&argv[n][8]);else if (!strncmp("--dbit=", argv[n], 7))cfg.dbit = atoi(&argv[n][7]);else if (!strncmp("--parity=", argv[n], 9))cfg.parity = argv[n][9];else if (!strncmp("--sbit=", argv[n], 7))cfg.sbit = atoi(&argv[n][7]);else if (!strncmp("--type=", argv[n], 7)) {if (!strcmp("read", &argv[n][7]))rw_flag = 0; //讀else if (!strcmp("write", &argv[n][7]))rw_flag = 1; //寫}else if (!strcmp("--help", argv[n])) {show_help(argv[0]); //打印幫助信息exit(EXIT_SUCCESS);}}if (NULL == device || -1 == rw_flag) {fprintf(stderr, "Error: the device and read|write type must be set!\n");show_help(argv[0]);exit(EXIT_FAILURE);}/* 串口初始化 */if (uart_init(device))exit(EXIT_FAILURE);/* 串口配置 */if (uart_cfg(&cfg)) {tcsetattr(fd, TCSANOW, &old_cfg); //恢復到之前的配置close(fd);exit(EXIT_FAILURE);}/* 讀|寫串口 */switch (rw_flag) {case 0: //讀串口數據async_io_init(); //我們使用異步I/O方式讀取串口的數據,調用該函數去初始化串口的異步I/Ofor ( ; ; )sleep(1); //進入休眠、等待有數據可讀,有數據可讀之后就會跳轉到io_handler()函數break;case 1: //向串口寫入數據for ( ; ; ) { //循環向串口寫入數據write(fd, w_buf, 8); //一次向串口寫入8個字節sleep(1); //間隔1秒鐘}break;}/* 退出 */tcsetattr(fd, TCSANOW, &old_cfg); //恢復到之前的配置close(fd);exit(EXIT_SUCCESS); }代碼有點長,不過與串口相關的代碼并不是很多。
首先來看下main()函數,進入到main()函數之后有一個for()循環,這是對用戶傳參進行解析,譬如用戶可以指定串口終端的設備節點、串口波特率、數據位個數、停止位個數、奇偶校驗等,具體的使用方法參照show_help()函數。
接著調用uart_init()函數:打開串口終端設備、獲取串口終端當前的配置參數,將其保存到old_cfg 變量中。
接著調用uart_cfg()函數:將串口配置為原始模式、使能串口接收、設置串口波特率、數據位個數、停止位個數、奇偶校驗,以及MIN 和TIME 值的設置,最后清空緩沖區,將配置參數寫入串口設備使其生效。
最后根據用戶傳參中,–type 選項所指定類型進行讀串口或寫串口操作,
–type=read 表示讀操作,–type=write 表示串口寫入操作。
讀取串口數據
程序使用了異步I/O 的方式讀取數據,首先調用async_io_init()函數對異步I/O 進行初始化,注冊信號處理函數。當檢測到有數據可讀時,會跳轉到信號處理函數io_handler()執行,在這個函數中讀取串口的數據并將其打印出來,這里需要注意的是,本例程一次最多讀取8 個字節數據,如果可讀數據大于8 個字節,多余的數據會在下一次read()調用時被讀取出來。
寫操作
我們直接調用write()函數,每隔一秒鐘向串口寫入8 個字節數據[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]。
編譯示例代碼:
在開發板上進行測試
將上小節編譯得到的可執行文件拷貝到開發板Linux 系統/home/root 目錄下,如下所示:
ALPHA I.MX6U 開發板上一共預留出了兩個串口,一個USB 串口(對應I.MX6U 的UART1)、一個RS232/RS485 串口(對應I.MX6U 的UART3),如圖26.3.2 和圖26.3.3 所示。
注意,板子上的485 和232 接口是共用了I.MX6U 的UART3,這兩個接口無法同時使用,可通過配置底板上的JP1 端子來使能RS232 或RS485 接口,使用跳線帽將每一列上面的兩個針腳連接起來,此時RS232接口被使能、而RS485 接口不能使用;如果使用跳線帽將下面兩個針腳連接起來,如圖26.3.2 中所示,則此時RS485 接口被使能、RS232 接口不能使用。
本次測試筆者使用RS232 串口,注意不能使用USB 串口進行測試,它是系統的控制臺終端。由于Mini開發板只有一個USB 串口,沒有RS232 或RS485 接口,所以不太好測試,當然并不是說沒有辦法進行測試;雖然Mini 板上沒有232 或485 接口,但是串口用到的I/O 都已經通過擴展口引出了,你使用一個USB轉TTL 模塊也是可以測試的。
將板上的RS232 接口通過<USB 轉RS232>串口線連接到PC 機。
接下來進行測試,首先執行如下命令查看測試程序的幫助信息:
./testApp --help
可選選項表示是可選的,如果沒有指定則會使用默認值!
讀測試
./testApp --dev=/dev/ttymxc2 --type=read
執行測試程序時,筆者沒有指定波特率、數據位個數、停止位個數以及奇偶校驗等,程序將使用默認的配置,波特率115200、數據位個數為8、停止位個數為1、無校驗!
程序執行之后,在Windows 下打開串口調試助手上位機軟件,譬如正點原子的XCOM 串口調試助手:
打開XCOM 之后,對其進行配置、并打開串口,如下所示:
點擊發送按鈕向開發板RS232 串口發送8 個字節數據[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],此時我們的應用程序便會讀取到串口的數據,這些數據就是PC 機串口調試助手發送過來的數據,如下所示:
寫測試
測試完讀串口后,我們再來測試向串口寫數據,按Ctrl+C 結束測試程序,再次執行測試程序,本次測試寫串口,如下所示:
./testApp --dev=/dev/ttymxc2 --type=write
執行測試程序后,測試程序會每隔1 秒中將8 個字節數據[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]寫入到RS232 串口,此時PC 端串口調試助手便會接收到這些數據,如下所示:
總結
以上是生活随笔為你收集整理的Linux串口应用编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: erlang套接字
- 下一篇: Linux开发板网线直连电脑配置方法