【linux】系統調用版串口分析&源碼實戰


前言

  • 目前不涉及驅動源碼

參考

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;
}