【linux】系統呼叫版串列埠分析&原始碼實戰

李柱明發表於2020-11-27


前言

  • 目前不涉及驅動原始碼

參考

1. 實戰分析

1.1 開發步驟

  1. 獲取串列埠裝置路徑
  2. 開啟裝置檔案
  3. 配置串列埠
  4. 對該裝置檔案進行讀寫,相當於對該串列埠裝置進行讀寫,即通訊
  5. 關閉裝置檔案

以下程式碼段預設從 附件-最終串列埠測試原始碼 中摘取

1.1.1 獲取串列埠裝置路徑

  • 使用陣列或者巨集定義在相關檔案前面定義預設串列埠路徑,方便修改,原始碼段如下:
/* 不同的裝置,不同的路徑 */
const char def_uart_path[] = "/dev/ttymxc2"  // 預設串列埠路徑(備用)
  • 串列埠路徑優先從傳入引數中獲取,如果引數中沒有傳入,便使用 def_uart_path 預設路徑
/* 終端裝置選擇 */
if(argc > 1)
    path = argv[1];
else
    path = (char *)def_uart_path;

1.1.2 開啟裝置檔案

  • 獲取裝置控制程式碼,如果獲取失敗,便結束
/* 開啟終端 */
fd = open(path, O_RDWR);
if(fd < 0){
    printf("[%s] open err!", path);
    return 0;
}

1.1.3 配置串列埠

  • 定義一個結構體 termios 用於獲取、設定終端裝置的引數
    1. 包括波特率、資料位數、校驗位等
termios 結構體
  • 成員值作用,推薦先看官方手冊,看不懂再看本筆記中文表格
  • termios 結構體定義在編譯連結工具的標頭檔案預設路徑中的bits資料夾中
    • 如如下原始碼來自 /usr/arm-linux-gnueabihf/include/bits/termios.h
成員 說明
c_iflag 輸入模式標誌
c_oflag 輸出模式標誌
c_cflag 控制模式標誌
c_lflag 本地模式標誌
c_line 行控制
c_cc[NCCS] 控制字元
c_ispeed 輸入波特率
c_ospeed 輸出波特率
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 */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
  };
1. c_iflag 輸入模式標誌
  • 用於控制如何對串列埠輸入的字元進行處理
選項值 說明
IGNBRK 忽略輸入中的 BREAK 狀態。 (忽略命令列中的中斷)
BRKINT (命令列出 現中斷時,可產生一插斷)如果設定了IGNBRK,中斷條件被忽略。如果沒有設定IGNBRK而設定了BRKINT,中斷條件清空輸入輸出佇列中所有的資料並且向tty的前 臺程式組中所有程式傳送一個SIGINT訊號。如果這兩個都沒有設定,中斷條件會被看作一個0字元。這時,如果設定了PARMRK,當檢測到一個幀誤差時 將會嚮應用程式傳送三個位元組'/377''/0''/0',而不是隻傳送一個'/0'
IGNPAR 忽略楨錯誤和奇偶校驗錯
PARMRK 如果設定了IGNPAR,則忽略接收到的資料的奇偶檢驗錯誤或幀錯誤(除了前面提到的中斷條件)。如果沒有設定IGNPAR而設定了PARMRK, 當接收到的位元組存在奇偶檢驗錯誤或幀錯誤的時候。將嚮應用程式傳送一個三位元組的'/377''/0''/n'錯誤報告。其中n表示所接收到的位元組。如果兩 者都沒有設定,除了接收到的位元組存在奇偶檢驗錯誤或幀誤差之外的中止條件都會嚮應用程式傳送一個單位元組('/0')的報告
INPCK 如果設定,則進行奇偶校驗。如果不進行奇偶檢驗,PARMRK和IGNPAR將對存在的奇偶校驗錯誤不產生任何的影響
ISTRIP 如果設定,所接收到的所有位元組的高位將會被去除,保證它們是一個7位的字元
INLCR 如果設定,所接收到的換行字元('/n')將會被轉換成回車符('/r')
IGNCR 如果設定,則會忽略所有接收的回車符('/r')
ICRNL 如果設定,但IGNCR沒有設定,接收到的回車符嚮應用程式傳送時會變換成換行符
IUCLC 如果IUCLC和IEXTEN都設定,接收到的所有大寫字母傳送給應程式時都被轉換成小寫字母。POSIX中沒有定義該標記
IXON 如果設定,接收到S後會停止向這個tty裝置輸出,接收到Q後會恢復輸出
IXANY 如果設定,則接到任何字元都會重新開始輸出,而不僅僅是^Q字元
IXOFF 如果設定,為避免tty裝置的輸入緩衝區溢位,tty裝置可以向終端傳送停止符S和開始符Q,要求終端停止或重新開始向計算機傳送資料。通過停 止符和開始符來控制資料流的方式叫軟體流控制,軟體流控制方式較少用,我們主要還是用硬體流控制方式。硬體流控制在c_cflag標誌中設定
IMAXBEL 如果設定,當輸入緩衝區空間滿時,再接收到的任何字元就會發出警報符'/a'。POSIX中沒有定義該標記
IUTF8 (不屬於 POSIX)輸入 IUTF8 ,這是允許 character-erase 在 cooked 模式下被正確執行
2. c_oflag 輸出模式標誌
  • 用於控制串列埠的輸出模式
選項值 說明
OPOST 開啟該標記,後面的輸出標記才會生效。否則,不會對輸出資料進行處理
OLCUC 如果設定,大寫字母被轉換成小寫字母輸出
ONLCR 如果設定,在傳送換行符('/n')前先傳送回車符('/r')
OCRNL 如果設定,回車符會被轉換成換行符。另外,如果設定了ONLRET,則current column會被設為0
ONOCR 如果設定,當current column為0時,回車符不會被髮送也不會被處理
ONLRET 如果設定,當一個換行符或回車符被髮送的時候,current column會被設定為0
OFILL 傳送填充字元作為延時,而不是使用定時來延時
OFDEL (不屬於 POSIX) 填充字元是 ASCII DEL (0177)。如果不設定,填充字元則是 ASCII NUL
VTDLY 豎直跳格延時掩碼。取值為 VT0 或 VT1
3. c_cflag 控制模式標誌
  • 用於控制串列埠的基本引數,如資料位、停止位等,常用配置見下表,特別地,c_cflag結構體成員還包含了波特率的引數
選項值 說明
CLOCAL 如果設定,modem的控制線將會被忽略。如果沒有設定,則open()函式會阻塞直到載波檢測線宣告modem處於摘機狀態為止
CREAD 只有設定了才能接收字元,該標記是一定要設定的
CSIZE 設定傳輸字元的位數。CS5表示每個字元5位,CS6表示每個字元6位,CS7表示每個字元7位,CS8表示每個字元8位
CSTOPB 設定停止位的位數,如果設定,則會在每幀後產生兩個停止位,如果沒有設定,則產生一個停止位。一般都是使用一位停止位。需要兩位停止位的裝置已過時 了
HUPCL 如果設定,當裝置最後開啟的檔案描述符關閉時,串列埠上的DTR和RTS線會減弱訊號,通知Modem結束通話。也就是說,當一個使用者通過Modem拔號 登入系統,然後登出,這時Modem會自動結束通話
PARENB 允許輸出產生奇偶資訊以及輸入的奇偶校驗(啟用同位產生與偵測)
PARODD 輸入和輸出是奇校驗(使用奇同位而非偶同位)
CRTSCTS 使用硬體流控制。在高速(19200bps或更高)傳輸時,使用軟體流控制會使效率降低,這個時候必須使用硬體流控制
4. c_lflag 本地模式標誌
  • 主要用於控制驅動程式與使用者的互動,在串列埠通訊中,實際上用不到該成員變數
選項值 說明
ISIG 當接受到字元 INTR, QUIT, SUSP, 或 DSUSP 時,產生相應的訊號
ICANON 啟用標準模式 (canonical mode)。允許使用特殊字元EOF, EOL, EOL2, ERASE, KILL, LNEXT, REPRINT, STATUS, 和WERASE,以及按行的緩衝
ECHO 它可以讓你阻止鍵入字元的回應
ECHOE 如果同時設定了 ICANON,字元 ERASE 擦除前一個輸入字元,WERASE 擦除前一個詞
ECHOK 如果同時設定了 ICANON,字元 KILL 刪除當前行
ECHONL 如果同時設定了 ICANON,回顯字元 NL,即使沒有設定 ECHO
NOFLSH 禁止在產生 SIGINT, SIGQUIT 和 SIGSUSP 訊號時重新整理輸入和輸出佇列,即關閉queue中的flush
TOSTOP 向試圖寫控制終端的後臺程式組傳送 SIGTTOU 訊號(傳送欲寫入的資訊到後臺 處理)
IEXTEN 啟用實現自定義的輸入處理。這個標誌必須與 ICANON同時使用,才能解釋特殊字元 EOL2,LNEXT,REPRINT 和WERASE,IUCLC 標誌才有效
5. c_cc[NCCS] 控制字元
  • 該陣列包含了終端的所有特殊字元,可以修改特殊字元對應的鍵值(Ctrl+C產生的^C,ASCII碼為0x03)
  • 僅列出常用的
選項值 說明
VINTR 中斷字元。發出 SIGINT 訊號。當設定了c_lflag的ISIG標誌位時,該字母不再作為輸入傳遞
VERASE 刪除字元。刪除上一個還沒有刪掉的字元,但不刪除上一個EOF 或行首。當設定 ICANON 時可被識別,不再作為輸入傳遞
VIM 設定非標準模式讀取的最小位元組數
VTIM 設定非標準模式讀取時的延時值,單位為十分之一秒
6. c_ispeed和c_ospeed 波特率
  • 注意以 0 開頭的數字在是 C語言 的 8進位制 數字形式
#define  B0		0000000		/* hang up */
#define  B50		0000001
#define  B75		0000002
#define  B110		0000003
#define  B134		0000004
#define  B150		0000005
#define  B200		0000006
#define  B300		0000007
#define  B600		0000010
#define  B1200		0000011
#define  B1800		0000012
#define  B2400		0000013
#define  B4800		0000014
#define  B9600		0000015
#define  B19200	0000016
#define  B38400	000001

#define  B57600  	 0010001
#define  B115200 	 0010002
#define  B230400 	 0010003
#define  B460800  	0010004
#define  B500000  	0010005
#define  B576000  	0010006
#define  B921600  	0010007
#define  B1000000 	0010010
#define  B1152000 	0010011
#define  B1500000 	0010012
#define  B2000000 	0010013
#define  B2500000 	0010014
#define  B3000000 	0010015
#define  B3500000 	0010016
#define  B4000000 	0010017
分析
  • 以上只是介紹了 termios 結構體,在編寫程式碼時,我們使用相關 api 去配置該結構體從而配置串列埠
  • api 介面推薦先看本文推薦連結,不懂再看本文
// api 如下
#include <termios.h>
#include <unistd.h>

int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

int tcsendbreak(int fd, int duration);

int tcdrain(int fd);

int tcflush(int fd, int queue_selector);

int tcflow(int fd, int action);

void cfmakeraw(struct termios *termios_p);

speed_t cfgetispeed(const struct termios *termios_p);

speed_t cfgetospeed(const struct termios *termios_p);

int cfsetispeed(struct termios *termios_p, speed_t speed);

int cfsetospeed(struct termios *termios_p, speed_t speed);

int cfsetspeed(struct termios *termios_p, speed_t speed);
  • 清空接收緩衝區,獲取串列埠引數,配置,更新配置
/* 定義串列埠結構體 */
struct termios opt;
/* 清空串列埠接收緩衝區 */
tcflush(fd, TCIOFLUSH);
/* 獲取串列埠引數 */
tcgetattr(fd, &opt);
/* 設定輸入、輸入波特率 */
cfsetospeed(&opt, B9600);
cfsetispeed(&opt, B9600);
/* 設定資料位數 */
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
/* 校驗位 */
opt.c_cflag &= ~PARENB;
opt.c_iflag &= ~INPCK;
/* 設定停止位 */
opt.c_cflag &= ~CSTOPB;
/* 更新配置 */
tcsetattr(fd, TCSANOW, &opt);

1.1.4 串列埠收發測試

  • 串列埠收發測試就是對該串列埠進行讀寫
 /* 傳送測試 */
write(fd, bufW, strlen(bufW));
/* 接收測試 */
res = read(fd, bufR, 512);
if(res > 0){
    printf("receive data is %s", bufR);
}

1.1.5 關閉裝置檔案

  • 程式正常結束,不要忘記關閉裝置檔案
/* 關閉檔案 */
close(fd);

附件

最終串列埠測試原始碼

/** @file main.c
* @brief 串列埠測試檔案-系統呼叫版
* @details 詳細說明
* @author lzm
* @date 2020-11-23 19:18:20
* @version v1.0
* @copyright Copyright By lizhuming, All Rights Reserved
*
**********************************************************
* @LOG 修改日誌:
**********************************************************
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <string.h>
#include <sys/ioctl.h>
/* 不同的裝置,不同的路徑 */
const char def_uart_path[] = "/dev/ttymxc2"; // 預設串列埠路徑(備用)
/**
* @brief 主函式
* @param 無
* @retval 無
*/
int main(int argc, char *argv[])
{
    int fd;
    int res;
    char *path;
    char bufW[512] = "This is sys uart test!\n";
    char bufR[512];
    /* 終端裝置選擇 */
    if(argc > 1)
        path = argv[1];
    else
        path = (char *)def_uart_path;
    /* 開啟終端 */
    fd = open(path, O_RDWR);
    if(fd < 0){
        printf("[%s] open err!", path);
        return 0;
    }
    /* 定義串列埠結構體 */
    struct termios opt;
    /* 清空串列埠接收緩衝區 */
    tcflush(fd, TCIOFLUSH);
    /* 獲取串列埠引數 */
    tcgetattr(fd, &opt);
    /* 設定輸入、輸入波特率 */
    cfsetospeed(&opt, B9600);
    cfsetispeed(&opt, B9600);
    /* 設定資料位數 */
    opt.c_cflag &= ~CSIZE;
    opt.c_cflag |= CS8;
    /* 校驗位 */
    opt.c_cflag &= ~PARENB;
    opt.c_iflag &= ~INPCK;
    /* 設定停止位 */
    opt.c_cflag &= ~CSTOPB;
    /* 更新配置 */
    tcsetattr(fd, TCSANOW, &opt);
    do{
        /* 傳送測試 */
        write(fd, bufW, strlen(bufW));
        /* 接收測試 */
        res = read(fd, bufR, 512);
        if(res > 0){
            printf("receive data is %s", bufR);
        }
    }while(res >= 0);
    /* 讀取錯誤 */
    printf("read error, res = %d", res);
    /* 關閉檔案 */
    close(fd);
    return 0;
}

相關文章