什么是串口?
????????串口全稱叫做串行接口,串行接口指的是數據一個一個的按順序傳輸,通信線路簡單。使用兩條線即可實現雙向通信,一條用于發送,一條用于接收。串口通信距離遠,但是速度相對會低,串口是一種很常用的工業接口
????????串口(UART)在嵌入式 Linux 系統中常作為系統的標準輸入、輸出設備, 系統運行過程產生的打印信息通過串口輸出;同理,串口也作為系統的標準輸入設備, 用戶通過串口與 Linux 系統進行交互。所以串口在 Linux 系統就是一個終端, 提到串口, 就不得不引出“終端(Terminal)”這個概念了
什么是終端(Terminal)?
????????終端就是處理主機輸入、輸出的一套設備,它用來顯示主機運算的輸出,并且接受主機要求的輸入。典型的終端包括顯示器鍵盤套件,打印機打字機套件等。
????????只要能提供給計算機輸入和輸出功能,它就是終端,而與其所在的位置無關。
終端的分類
?????????1.本地終端:? PC 機連接了顯示器、鍵盤以及鼠標等設備,這樣的一個顯示器/鍵盤組合就是一個本地終端;同樣對于開發板來說也是如此,開發板也可以連接一個LCD 顯示器、鍵盤和鼠標等,同樣可以構成本地終端。
? ? ? ? 2.用串口連接的遠程終端: 對于嵌入式 Linux 開發來說,這是最常見的終端—串口終端。譬如我們的開發板通過串口線連接到一個帶有顯示器和鍵盤的 PC 機,在 PC 機通過運行一個終端模擬程序,譬如 Windows 超級終端、 putty、 MobaXterm、 SecureCRT 等來獲取并顯示開發板通過串口發出的數據、同樣還可以通過這些終端模擬程序將用戶數據通過串口發送給開發板 Linux 系統,系統接收到數據之后便會進行相應的處理、譬如執行某個命令,這就是一種人機交互!
? ? ? ?3. 基于網絡的遠程終端: 譬如我們可以通過 ssh、 Telnet 這些協議登錄到一個遠程主機。
????????以上列舉的這些都是終端,前兩類稱之為物理終端;最后一個稱之為偽終端。前兩類都是在本地就直接關聯了物理設備的,譬如顯示器、鼠標鍵盤、串口等之類的,這種終端叫做物理終端,而第三類在本地則沒有關聯任何物理設備,注意,不要把物理網卡當成終端關聯的物理設備,它們與終端并不直接相關,所以這類不直接關聯物理設備的終端叫做偽終端
終端對應的設備節點
????????每一個終端在/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 等)設備節點:這類設備節點是偽終端對應的設備節點,也就是說,偽終端對應的設備節點都在/dev/pts 目錄下、以數字編號命令。 譬如我們通過ssh 或 Telnet 這些遠程登錄協議登錄到開發板主機,那么開發板 Linux 系統會在/dev/pts 目錄下生成一個設備節點,這個設備節點便對應偽終端,如下所示
????????串口終端設備節點/dev/ttymxcX:對于 ALPHA/Mini I.MX6U 開發板來說, 有兩個串口,也就是有兩個串口終端,對應兩個設備節點
????????(這里為什么是 0 和 2、而不是 0 和 1? 我們知道, I.MX6U SoC 支持 8 個串口外設,分別是 UART1~UART8;出廠系統只注冊了 2 個串口外設,分別是 UART1 和 UART3,所以對應這個數字就是 0 和 2、而不是 0 和1,這里了解一下就行!)
????????還需要注意的是, mxc 這個名字不是一定的,這個名字的命名與驅動有關系(與硬件平臺有關)
串口應用編程
????????串口的應用編程也很簡單,無非就是通過 ioctl()對串口進行配置,調用 read()讀取串口的數據、調用 write()向串口寫入數據, 是的,就是這么簡單!但是我們不這么做,因為 Linux 為上層用戶做了一層封裝,將這些 ioctl()操作封裝成了一套標準的 API,我們就直接使用這一套標準 API 編寫自己的串口應用程序即可。
????????把這一套接口稱為 termios API, 這些 API 其實是 C 庫函數, 可以使用 man 手冊查看到它們的幫助信息;這里需要注意的是,這一套接口并不是針對串口開發的,而是針對所有的終端設備,串口是一種終端設備,計算機系統本地連接的鼠標、鍵盤也是終端設備,通過 ssh 遠程登錄連接的偽終端也是終端設備。
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
????????輸入模式控制輸入數據(終端驅動程序從串口或鍵盤接收到的字符數據)在被傳遞給應用程序之前的處理方式。通過設置 struct termios 結構體中 c_iflag 成員的標志對它們進行控制。所有的標志都被定義為宏,除 c_iflag 成員外, c_oflag、 c_cflag 以及 c_lflag 成員也都采用這種方式進行配置,可用于 c_iflag 成員的宏如下所示:
????????以上所列舉出的這些宏,我們可以通過 man 手冊查詢到它們的詳細描述信息,執行命令" man 3 termios ",如下圖所示
二、輸出模式: c_oflag
?????????輸出模式控制輸出字符的處理方式,即由應用程序發送出去的字符數據在傳遞到串口或屏幕之前是如何處理的。可用于 c_oflag 成員的宏如下所示
三、控制模式: c_cflag
????????控制模式控制終端設備的硬件特性,譬如對于串口來說,該字段比較重要,可設置串口波特率、數據位、校驗位、停止位等硬件特性。通過設置 struct termios 結構中 c_cflag 成員的標志對控制模式進行配置。可用于 c_cflag 成員的標志如下所示:
????????在 struct termios 結構體中,有一個 c_ispeed 成員變量和 c_ospeed 成員變量,在其它一些系統中,可能會使用這兩個變量來指定串口的波特率;在 Linux 系統下, 則是使用 CBAUD 位掩碼所選擇的幾個 bit 位來指定串口波特率。事實上, termios API 中提供了 cfgetispeed()和 cfsetispeed()函數分別用于獲取和設置串口的波特率
四、本地模式: c_lflag
????????本地模式用于控制終端的本地數據處理和工作模式。 通過設置 struct termios 結構體中 c_lflag 成員的標志對本地模式進行配置。可用于 c_lflag 成員的標志如下所示:
五、特殊控制字符: 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。
成員賦值問題:
????????對于這些變量盡量不要直接對其初始化,而要將其通過“按位與”、“按位或” 等操作添加標志或清除某個標志。如
ter.c_iflag |= (IGNBRK | BRKINT | PARMRK | ISTRIP);
????????同時不同的終端設備,本身硬件上就存在很大的區別,所以會導致這些配置參數并不是對所有終端設備都是有效的。
終端的三種工作模式:
?
????????終端有三種工作模式,分別為規范模式(canonical mode)、非規范模式(non-canonical mode)和原始模式(raw mode),通過在 struct termios 結構體的 c_lflag 成員中設置 ICANNON 標志來定義終端是以規范模式(設置 ICANNON 標志)還是以非規范模式(清除 ICANNON 標志)工作,默認情況為規范模式。
????????在規范模式下,所有的輸入是基于行進行處理的。在用戶輸入一個行結束符(回車符、 EOF 等)之前,系統調用 read()函數是讀不到用戶輸入的任何字符的。除了 EOF 之外的行結束符(回車符等)與普通字符一樣會被 read()函數讀取到緩沖區中。在規范模式中,行編輯是可行的,而且一次 read()調用最多只能讀取一行數據。如果在 read()函數中被請求讀取的數據字節數小于當前行可讀取的字節數,則 read()函數只會讀取被請求的字節數,剩下的字節下次再被讀取。
????????在非規范模式下,所有的輸入是即時有效的,不需要用戶另外輸入行結束符,而且不可進行行編輯。在非規范模式下,對參數 MIN(c_cc[VMIN])和 TIME(c_cc[VTIME])的設置決定 read()函數的調用方式。
????????TIME 和 MIN 的值只能用于非規范模式,兩者結合起來可以控制對輸入數據的讀取方式。 根據 TIME 和 MIN 的取值不同,會有以下 4 種不同情況:
????????1.MIN = 0 和 TIME = 0: 在這種情況下, read()調用總是會立即返回。若有可讀數據,則讀取數據并返回被讀取的字節數; 否則讀取不到任何數據并返回 0。
????????2.MIN > 0 和 TIME = 0:在這種情況下, read()函數會被阻塞, 直到有 MIN 個字符可以讀取時才返回,返回值是讀取的字符數量。到達文件尾時返回 0。
????????3.MIN = 0 和 TIME > 0:在這種情況下, 只要有數據可讀或者經過 TIME 個十分之一秒的時間, read()函數則立即返回,返回值為被讀取的字節數。如果超時并且未讀到數據,則 read()函數返回 0。
????????4.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;
????????串口本就是一種數據串行傳輸接口, 通過串口可以與其他設備或傳感器進行數據傳輸、通信,譬如很多 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()函數
????????tcgetattr()函數可以獲取到串口終端當前的配置參數, tcgetattr 函數原型如下所示(可通過命令"man 3 tcgetattr"查詢):
#include <termios.h>
#include <unistd.h>
int tcgetattr(int fd, struct termios *termios_p);
????????首先在我們的應用程序中需要包含 termios.h 頭文件和 unistd.h 頭文件。第一個參數對應串口終端設備的文件描述符 fd。
????????調用 tcgetattr 函數之前,我們需要定義一個 struct termios 結構體變量,將該變量的指針作為 tcgetattr()函數的第二個參數傳入; 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()和cfsetospeed(),這兩個函數在<termios.h>頭文件中申明, 使用方法很簡單,如下所示:
cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);
????????cfsetispeed()函數設置數據輸入波特率,而 cfsetospeed()函數設置數據輸出波特率。一般來說,用戶需將終端的輸入和輸出波特率設置成一樣的。
? ? ? ? 除此之外,我們還可以直接使用 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 的值
????????如前面所介紹那樣, MIN 和 TIME 的取值會影響非規范模式下 read()調用的行為特征,原始模式是一種特殊的非規范模式,所以 MIN 和 TIME 在原始模式下也是有效的。
????????在對接收字符和等待時間沒有特別要求的情況下,可以將 MIN 和 TIME 設置為 0, 這樣則在任何情況下 read()調用都會立即返回,此時對串口的 read 操作會設置為非阻塞方式,如下所示
?
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0
緩沖區的處理
????????我們在使用串口之前,需要對串口的緩沖區進行處理,因為在我們使用之前,其緩沖區中可能已經存在一些數據等待處理或者當前正在進行數據傳輸、接收,所以使用之前, 所以需要對此情況進行處理。 這時就可以調用<termios.h>中聲明的 tcdrain()、 tcflow()、 tcflush()等函數來處理目前串口緩沖中的數據,它們的函數原型如下所示:
#include <termios.h>
#include <unistd.h>
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);
???????????調用 tcdrain()函數后會使得應用程序阻塞, 直到串口輸出緩沖區中的數據全部發送完畢為止!調用 tcflow()函數會暫停串口上的數據傳輸或接收工作,具體情況取決于參數 action,參數 action 可取值如下:
????????TCOOFF:暫停數據輸出(輸出傳輸);
????????TCOON: 重新啟動暫停的輸出;
?????????TCIOFF: 發送 STOP 字符,停止終端設備向系統發送數據;
?????????TCION: 發送一個 START 字符,啟動終端設備向系統發送數據;
????????再來看看 tcflush()函數,調用該函數會清空輸入/輸出緩沖區中的數據,具體情況取決于參數queue_selector,參數 queue_selector 可取值如下:
?????????TCIFLUSH: 對接收到而未被讀取的數據進行清空處理;
????????TCOFLUSH: 對尚未傳輸成功的輸出數據進行清空處理;
????????TCIOFLUSH: 包括前兩種功能,即對尚未處理的輸入/輸出數據進行清空處理。
????????以上這三個函數,調用成功時返回 0;失敗將返回-1、并且會設置 errno 以指示錯誤類型。
通常我們會選擇 tcdrain()或 tcflush()函數來對串口緩沖區進行處理。譬如直接調用 tcdrain()阻塞
tcdrain(fd);
或者調用 tcflush()清空緩沖區:
?
tcflush(fd, TCIOFLUSH);
寫入配置、使配置生效: tcsetattr()函數
?????
???????????前面已經完成了對 struct termios 結構體各個成員進行配置,但是配置還未生效,我們需要將配置參數寫入到終端設備(串口硬件),使其生效。通過 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()
串口應用編程實戰
?
????????任務:在串口終端的原始模式下,使用串口進行數據傳輸,包括通過串口發送數據、以及讀取串口接收到的數據,并將其打印出來
加了注釋源碼:
#define _GNU_SOURCE // 在源文件開頭定義_GNU_SOURCE宏,以啟用GNU擴展
#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表示串口終端的設備節點* 返回值為0表示成功,-1表示失敗*/
static int uart_init(const char *device)
{// 打開串口終端// O_RDWR表示讀寫打開,O_NOCTTY表示不將此設備分配為進程的控制終端fd = open(device, O_RDWR | O_NOCTTY);if (0 > fd) {fprintf(stderr, "open error: %s: %s\n", device, strerror(errno)); // 打開失敗時輸出錯誤信息return -1;}// 獲取串口當前的配置參數并保存到old_cfgif (0 > tcgetattr(fd, &old_cfg)) {fprintf(stderr, "tcgetattr error: %s\n", strerror(errno)); // 獲取失敗時輸出錯誤信息close(fd); // 關閉文件描述符return -1;}return 0; // 成功返回0
}/*** 串口配置* 參數cfg指向一個uart_cfg_t結構體對象* 返回值為0表示成功,-1表示失敗*/
static int uart_cfg(const uart_cfg_t *cfg)
{struct termios new_cfg = {0}; // 初始化termios結構體并清零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;}// 配置成功返回0return 0;
}/*** 打印幫助信息*/
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/Oflag = fcntl(fd, F_GETFL); // 獲取當前文件狀態標志flag |= O_ASYNC; // 設置異步I/O標志fcntl(fd, F_SETFL, flag); // 設置文件狀態標志// 設置異步I/O的所有者fcntl(fd, F_SETOWN, getpid()); // 將進程ID設為當前進程// 指定實時信號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; // 讀寫標志,-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/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]。
編譯:
scp命令傳至開發板
執行
./uart_test --help
可選選項表示是可選的,如果沒有指定則會使用默認值
先進行讀測試:
?
./uart_test --dev=/dev/ttymxc2 --type=read
程序將使用默認的配置,波特率 115200、數據位個數為 8、停止位個數為 1、無校驗
程序執行之后, 在 Windows 下打開串口調試助手上位機軟件,譬如正點原子的 XCOM 串口調試助手,
點擊發送按鈕向開發板 RS232 串口發送 8 個字節數據[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],此時我們的應用程序便會讀取到串口的數據,這些數據就是 PC 機串口調試助手發送過來的數據, 如下所示
(因為這沒有rs485/232模塊,所以直接用正點現象了)
測試完讀串口后,我們再來測試向串口寫數據,按 Ctrl+C 結束測試程序,再次執行測試程序,本次測試寫串口,如下所示:
?
./uart_test --dev=/dev/ttymxc2 --type=write
執行測試程序后,測試程序會每隔 1 秒中將 8 個字節數據[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]寫入到 RS232 串口,此時 PC 端串口調試助手便會接收到這些數據,如下所示: