Linux串口調試詳解


測試平台

宿主機平台:Ubuntu 16.04.6

目標機:iMX6ULL

目標機內核:Linux 4.1.15


 

目標機添加串口設備

一般嵌入式主板的默認鏡像可能只配置了調試串口,並用於 console 控制台打印;

接下來對怎么樣通過設備樹來分配引腳用於用戶串口通信 進行描述;

前提:

目標機以及正常燒錄 uboot、內核、文件系統、dtb等;本文僅更新設備樹dtb文件;

設備樹文件修改

在內核源碼中找到相關板子對應的dtb文件;

位置: arch/arm/boot/dts 目錄下

本文使用的板子相關文件有:

imx6ull.dtsi              // 官方通用板層dtsi

mys-imx6ull-14x14-evk.dts       // 基於imx6ull-14x14-evk.dts模板修改

mys-imx6ull-14x14-evk-gpmi-weim.dts  // 用戶層dts

添加 uart3和uart4 的支持,修改 mys-imx6ull-14x14-evk.dts 文件如下

        pinctrl_uart2: uart2grp {
            fsl,pins = <
                MX6UL_PAD_UART2_TX_DATA__UART2_DCE_TX    0x1b0b1
                MX6UL_PAD_UART2_RX_DATA__UART2_DCE_RX    0x1b0b1
            >;
        };

        pinctrl_uart2dte: uart2dtegrp {
            fsl,pins = <
                MX6UL_PAD_UART2_TX_DATA__UART2_DTE_RX    0x1b0b1
                MX6UL_PAD_UART2_RX_DATA__UART2_DTE_TX    0x1b0b1
                MX6UL_PAD_UART3_RX_DATA__UART2_DTE_CTS    0x1b0b1
                MX6UL_PAD_UART3_TX_DATA__UART2_DTE_RTS    0x1b0b1
            >;
        };
        
        /* 增加uart3/4/5的引腳配置 */
        pinctrl_uart3: uart3grp {
            fsl,pins = <
                MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX    0x1b0b1
                MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX    0x1b0b1
            >;
        };
        
        pinctrl_uart4: uart4grp {
            fsl,pins = <
                MX6UL_PAD_UART4_TX_DATA__UART4_DCE_TX    0x1b0b1
                MX6UL_PAD_UART4_RX_DATA__UART4_DCE_RX    0x1b0b1
            >;
        };
        
        pinctrl_uart5: uart5grp {
            fsl,pins = <
                MX6UL_PAD_UART5_TX_DATA__UART5_DCE_TX    0x1b0b1
                MX6UL_PAD_UART5_RX_DATA__UART5_DCE_RX    0x1b0b1
            >;
        };

.....

/* 使能串口 */
&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

&uart2 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart2>;
    /*fsl,uart-has-rtscts;*/
    /* for DTE mode, add below change */
    /* fsl,dte-mode; */
    /* pinctrl-0 = <&pinctrl_uart2dte>; */
    status = "disabled";
};

/* 增加使用串口,其中使能3、關閉4/5 */
&uart3 {        
    pinctrl-names = "default";        
    pinctrl-0 = <&pinctrl_uart3>;        
    status = "okay";
}; 

&uart4 {        
     pinctrl-names = "default";        
     pinctrl-0 = <&pinctrl_uart4>;        
     status = "okay";
}; 
/* 這里必須注意一點,由於UART5和I2C2接口的引腳是復用的,I2C2默認是使能的所以必須禁用I2C2,再使能UART5. */
&uart5 {        
     pinctrl-names = "default";        
     pinctrl-0 = <&pinctrl_uart5>;        
     status = "disabled";
};

然后重新編譯生成 設備樹 dtb 文件

cp  arch/arm/configs/mys_imx6_defconfig .config
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

生成的 dtb 文件:arch/arm/boot/dts/mys-imx6ull-14x14-evk-gpmi-weim.dtb

設備樹文件更新

采用 MFGTool2 進行設備樹更新,怎么單獨僅更新設備樹參見 https://www.cnblogs.com/silencehuan/p/11010148.html

mys-imx6ull-14x14-evk-gpmi-weim.dtb 替換 /Profiles/Wh Linux Update/OS Firmware/files/ 下面的 dtb文件

然后執行 mfgtool2-linux-mys-6ulx-nand-dtb.vbs

Set wshShell = CreateObject("WScript.shell")
wshShell.run "mfgtool2.exe -c ""Wh Linux Update"" -l ""NAND-dtb"" -s ""lite=l"" -s ""6uluboot=14x14evk"" -s ""nand=nand"" -s ""6uldtb=14x14-evk"" -s ""nanddtb=gpmi-weim"" -s ""part_uboot=0"" -s ""part_kernel=1"" -s ""part_dtb=2"" -s ""part_rootfs=3"" -s ""ddrsize=256""  -s ""rootfs_name=core-image-base"""
Set wshShell = Nothing

更新成功,設備重啟之后,看到添加的串口設備已支持,串口驅動實現框架另外的文章在分析;

 


 

串口應用編程

1.串口相關操作

在Linux下,除了網絡設備,其余的都是文件的形式,串口設備也一樣在/dev下。

打開串口:

示例:fd = open("/dev/ttyUSB0",O_RDWR|O_NOCTTY|O_NDELAY);

在打開串口時,除了需要用到 O_RDWR (可讀寫)選項標志外,

O_NOCTTY:告訴 Linux “本程序不作為串口的‘控制終端’”。如果不使用該選項,會有一些輸入字符影響進程運行(如一些產生中斷信號的鍵盤輸入字符等)。

O_NDELAY標志則是告訴Linux,這個程序並不關心DCD信號線的狀態——也就是不關心端口另一端是否已經連接。 

關閉串口:

close(fd);

讀寫串口: 

與普通文件一樣,使用read,write函數。 
示例:read(fd,buff,8); 
write(fd,buff,8);

 

2.串口屬性設置

很多系統都支持POSIX終端(串口)接口.程序可以利用這個接口來改變終端的參數,比如,波特率,字符大小等等.要使用這個端口的話,你必須將<termios.h>頭文件包含到你的程序中。這個頭文件中定義了終端控制結構體和POSIX控制函數。

termios 結構體:

struct termios {
  tcflag_t c_cflag    /* 控制標志
  tcflag_t c_iflag;   /* 輸入標志
  tcflag_t c_oflag;   /* 輸出標志
  tcflag_t c_lflag;   /* 本地標志
  tcflag_t c_cc[NCCS]; /* 控制字符
};

其中我們更關注的是c_cflag控制選項。其中包含了波特率、數據位、校驗位、停止位的設置。

c_cflag 控制標志常量:

  CBAUD   (不屬於 POSIX) 波特率掩碼 (4+1 位)。
  CBAUDEX  (不屬於 POSIX) 擴展的波特率掩碼 (1 位),包含在 CBAUD 中。
  (POSIX 規定波特率存儲在 termios 結構中,並未精確指定它的位置,而是提供了函數 cfgetispeed() 和 cfsetispeed() 來存取它。一些系統使用 c_cflag 中 CBAUD 選擇的位,其他系統使用單獨的變量,例如 sg_ispeed 和 sg_ospeed 。)
  CSIZE   字符長度掩碼。取值為 CS5, CS6, CS7, 或 CS8。
  CSTOPB 設置兩個停止位,而不是一個。
  CREAD    打開接受者。
  PARENB  允許輸出產生奇偶信息以及輸入的奇偶校驗。
  PARODD  輸入和輸出是奇校驗。
  HUPCL    在最后一個進程關閉設備后,降低 modem 控制線 (掛斷)。(?)
  CLOCAL 忽略 modem 控制線。
  LOBLK   (不屬於 POSIX) 從非當前 shell 層阻塞輸出(用於 shl )。(?)
  CIBAUD (不屬於 POSIX) 輸入速度的掩碼。CIBAUD 各位的值與 CBAUD 各位相同,左移了 IBSHIFT 位。
  CRTSCTS  (不屬於 POSIX) 啟用 RTS/CTS (硬件) 流控制。

 

c_iflag 輸入標志常量:

  IGNBRK   忽略輸入中的 BREAK 狀態。
  BRKINT   如果設置了 IGNBRK,將忽略 BREAK。如果沒有設置,但是設置了 BRKINT,那么 BREAK 將使得輸入和輸出隊列被刷新,如果終端是一個前台進程組的控制終端,這個進程組中所有進程將收到 SIGINT 信號。如果既未設置 IGNBRK 也未設置 BRKINT,BREAK 將視為與 NUL 字符同義,除非設置了 PARMRK,這種情況下它被視為序列 \377 \0 \0。
  IGNPAR    忽略楨錯誤和奇偶校驗錯。
  PARMRK  如果沒有設置 IGNPAR,在有奇偶校驗錯或楨錯誤的字符前插入 \377 \0。如果既沒有設置 IGNPAR 也沒有設置 PARMRK,將有奇偶校驗錯或楨錯誤的字符視為 \0。
  INPCK   啟用輸入奇偶檢測。
  ISTRIP  去掉第八位。
  INLCR   將輸入中的 NL 翻譯為 CR。
  IGNCR   忽略輸入中的回車。
  ICRNL   將輸入中的回車翻譯為新行 (除非設置了 IGNCR)。
  IUCLC   (不屬於 POSIX) 將輸入中的大寫字母映射為小寫字母。
  IXON    啟用輸出的 XON/XOFF 流控制。
  IXANY  (不屬於 POSIX.1;XSI) 允許任何字符來重新開始輸出。(?)
  IXOFF  啟用輸入的 XON/XOFF 流控制。
  IMAXBEL  (不屬於 POSIX) 當輸入隊列滿時響零。Linux 沒有實現這一位,總是將它視為已設置。

 

c_oflag 輸出標志常量:

  OPOST   啟用具體實現自行定義的輸出處理。
  其余 c_oflag 標志常量定義在 POSIX 1003.1-2001 中,除非另外說明。
  OLCUC   (不屬於 POSIX) 將輸出中的小寫字母映射為大寫字母。
  ONLCR   (XSI) 將輸出中的新行符映射為回車-換行。
  OCRNL   將輸出中的回車映射為新行符
  ONOCR    不在第 0 列輸出回車。
  ONLRET   不輸出回車。
  OFILL       發送填充字符作為延時,而不是使用定時來延時。
  OFDEL   (不屬於 POSIX) 填充字符是 ASCII DEL (0177)。如果不設置,填充字符則是 ASCII NUL。
  NLDLY   新行延時掩碼。取值為 NL0 和 NL1。
  CRDLY   回車延時掩碼。取值為 CR0, CR1, CR2, 或 CR3。
  TABDLY  水平跳格延時掩碼。取值為 TAB0, TAB1, TAB2, TAB3 (或 XTABS)。取值為 TAB3,即 XTABS,將擴展跳格為空格 (每個跳格符填充 8 個空格)。(?)
  BSDLY   回退延時掩碼。取值為 BS0 或 BS1。(從來沒有被實現過)
  VTDLY   豎直跳格延時掩碼。取值為 VT0 或 VT1。
  FFDLY   進表延時掩碼。取值為 FF0 或 FF1。

 

c_lflag 本地標志常量:

  ISIG       當接受到字符 INTR, QUIT, SUSP, 或 DSUSP 時,產生相應的信號。
  ICANON   啟用標准模式 (canonical mode)。允許使用特殊字符 EOF, EOL, EOL2, ERASE, KILL, LNEXT, REPRINT, STATUS, 和 WERASE,以及按行的緩沖。
  XCASE     (不屬於 POSIX; Linux 下不被支持) 如果同時設置了 ICANON,終端只有大寫。輸入被轉換為小寫,除了以 \ 前綴的字符。輸出時,大寫字符被前綴 \,小寫字符被轉換成大寫。
  ECHO    回顯輸入字符。
  ECHOE  如果同時設置了 ICANON,字符 ERASE 擦除前一個輸入字符,WERASE 擦除前一個詞。
  ECHOK  如果同時設置了 ICANON,字符 KILL 刪除當前行。
  ECHONL  如果同時設置了 ICANON,回顯字符 NL,即使沒有設置 ECHO。
  ECHOCTL (不屬於 POSIX) 如果同時設置了 ECHO,除了 TAB, NL, START, 和 STOP 之外的 ASCII 控制信號被回顯為 ^X, 這里 X 是比控制信號大 0x40 的 ASCII 碼。例如,字符 0x08 (BS) 被回顯為 ^H。
  ECHOPRT (不屬於 POSIX) 如果同時設置了 ICANON 和 IECHO,字符在刪除的同時被打印。
  ECHOKE   (不屬於 POSIX) 如果同時設置了 ICANON,回顯 KILL 時將刪除一行中的每個字符,如同指定了 ECHOE 和 ECHOPRT 一樣。
  DEFECHO (不屬於 POSIX) 只在一個進程讀的時候回顯。
  FLUSHO    (不屬於 POSIX; Linux 下不被支持) 輸出被刷新。這個標志可以通過鍵入字符 DISCARD 來開關。
  NOFLSH   禁止在產生 SIGINT, SIGQUIT 和 SIGSUSP 信號時刷新輸入和輸出隊列。
  TOSTOP   向試圖寫控制終端的后台進程組發送 SIGTTOU 信號。
  IEXTEN    啟用實現自定義的輸入處理。這個標志必須與 ICANON 同時使用,才能解釋特殊字符 EOL2,LNEXT,REPRINT 和 WERASE,IUCLC 標志才有效。

 

c_cc 特殊的控制字符

  VINTR   (003, ETX, Ctrl-C, or also 0177, DEL, rubout) 中斷字符。發出 SIGINT 信號。當設置 ISIG 時可被識別,不再作為輸入傳遞。
  VQUIT   (034, FS, Ctrl-\) 退出字符。發出 SIGQUIT 信號。當設置 ISIG 時可被識別,不再作為輸入傳遞。
  VERASE  (0177, DEL, rubout, or 010, BS, Ctrl-H, or also #) 刪除字符。刪除上一個還沒有刪掉的字符,但不刪除上一個 EOF 或行首。當設置 ICANON 時可被識別,不再作為輸入傳遞。
  VKILL       (025, NAK, Ctrl-U, or Ctrl-X, or also @) 終止字符。刪除自上一個 EOF 或行首以來的輸入。當設置 ICANON 時可被識別,不再作為輸入傳遞。
  VEOF       (004, EOT, Ctrl-D) 文件尾字符。更精確地說,這個字符使得 tty 緩沖中的內容被送到等待輸入的用戶程序中,而不必等到 EOL。如果它是一行的第一個字符,那么用戶程序的 read() 將返回 0,指示讀到了 EOF。當設置 ICANON 時可被識別,不再作為輸入傳遞。
  VMIN       非 canonical 模式讀的最小字符數。
  VEOL       (0, NUL) 附加的行尾字符。當設置 ICANON 時可被識別。
  VTIME  非 canonical 模式讀時的延時,以十分之一秒為單位。
  VEOL2  (not in POSIX; 0, NUL) 另一個行尾字符。當設置 ICANON 時可被識別。
  VSWTCH   (not in POSIX; not supported under Linux; 0, NUL) 開關字符。(只為 shl 所用。)
  VSTART  (021, DC1, Ctrl-Q) 開始字符。重新開始被 Stop 字符中止的輸出。當設置 IXON 時可被識別,不再作為輸入傳遞。
  VSTOP    (023, DC3, Ctrl-S) 停止字符。停止輸出,直到鍵入 Start 字符。當設置 IXON 時可被識別,不再作為輸入傳遞。
  VSUSP    (032, SUB, Ctrl-Z) 掛起字符。發送 SIGTSTP 信號。當設置 ISIG 時可被識別,不再作為輸入傳遞。
  VDSUSP    (not in POSIX; not supported under Linux; 031, EM, Ctrl-Y) 延時掛起信號。當用戶程序讀到這個字符時,發送 SIGTSTP 信號。當設置 IEXTEN 和 ISIG,並且系統支持作業管理時可被識別,不再作為輸入傳遞。
  VLNEXT  (not in POSIX; 026, SYN, Ctrl-V) 字面上的下一個。引用下一個輸入字符,取消它的任何特殊含義。當設置 IEXTEN 時可被識別,不再作為輸入傳遞。
  VWERASE (not in POSIX; 027, ETB, Ctrl-W) 刪除詞。當設置 ICANON 和 IEXTEN 時可被識別,不再作為輸入傳遞。
  VREPRINT (not in POSIX; 022, DC2, Ctrl-R) 重新輸出未讀的字符。當設置 ICANON 和 IEXTEN 時可被識別,不再作為輸入傳遞。
  VDISCARD  (not in POSIX; not supported under Linux; 017, SI, Ctrl-O) 開關:開始/結束丟棄未完成的輸出。當設置 IEXTEN 時可被識別,不再作為輸入傳遞。

調用read 函數讀取串口數據時, 返回讀取數據的數量需要考慮兩個變量: MIN 和 TIME 。

MIN 和 TIME 在 termios 結構的 c_cc 成員的數組 下標名為 VMIN 和 VTIME 。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 。

 

終端api函數接口:

tcgetattr() 得到與 fd 指向的對象相關的參數,將它們保存於 termios_p 引用的 termios 結構中。函數可以從后台進程中調用;但是,終端屬性可能被后來的前台進程所改變。

 

tcsetattr() 設置與終端相關的參數 (除非需要底層支持卻無法滿足),使用 termios_p 引用的 termios 結構。optional_actions 指定了什么時候改變會起作用:

  TCSANOW   改變立即發生
  TCSADRAIN   改變在所有寫入 fd 的輸出都被傳輸后生效。這個函數應當用於修改影響輸出的參數時使用。
  TCSAFLUSH  改變在所有寫入 fd 引用的對象的輸出都被傳輸后生效,所有已接受但未讀入的輸入都在改變發生前丟棄。

 

tcsendbreak() 傳送連續的 0 值比特流,持續一段時間,如果終端使用異步串行數據傳輸的話。如果 duration 是 0,它至少傳輸 0.25 秒,不會超過 0.5 秒。如果 duration 非零,它發送的時間長度由實現定義。如果終端並非使用異步串行數據傳輸,tcsendbreak() 什么都不做。


tcdrain() 等待直到所有寫入 fd 引用的對象的輸出都被傳輸。

 

tcflush() 丟棄要寫入 引用的對象,但是尚未傳輸的數據,或者收到但是尚未讀取的數據,取決於 queue_selector 的值:

  TCIFLUSH     刷新收到的數據但是不讀
  TCOFLUSH   刷新寫入的數據但是不傳送
  TCIOFLUSH  同時刷新收到的數據但是不讀,並且刷新寫入的數據但是不傳送

 

tcflow() 掛起 fd 引用的對象上的數據傳輸或接收,取決於 action 的值:

  TCOOFF     掛起輸出
  TCOON    重新開始被掛起的輸出
  TCIOFF    發送一個 STOP 字符,停止終端設備向系統傳送數據
  TCION      發送一個 START 字符,使終端設備向系統傳輸數據
  打開一個終端設備時的默認設置是輸入和輸出都沒有掛起。

 

2.1 串口屬性設置示例

設置串口屬性主要是配置termios結構體中的各個變量,大致流程如下:

1.使用函數tcgetattr保存原串口屬性
struct termios newtio,oldtio;
tcgetattr(fd,&oldtio);

2.通過位掩碼的方式激活本地連接和接受使能選項:CLOCAL和CREAD
newtio.c_cflag | = CLOCAL | CREAD;

3.使用函數cfsetispeed和cfsetospeed設置數據傳輸率
cfsetispeed(&newtio,B115200);
cfsetospeed(&newtio,B115200);

4.通過位掩碼設置字符大小。
newtio.c_cflag &= ~CSIZE;
newtio.c_cflag |= CS8;

5.設置奇偶效驗位需要用到兩個termios中的成員:c_cflag和c_iflag。首先要激活c_cflag中的校驗位使能標志PARENB和是否進行奇偶效驗,同時還要激活c_iflag中的奇偶效驗使能。
設置奇校驗:
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
設置偶校驗:
newtio.c_iflag |= (INPCK|ISTRIP);
newtio.c_cflag |= PARENB;
newtio.c_cflag |= ~PARODD;

6.激活c_cflag中的CSTOPB設置停止位。若停止位為1,則清除CSTOPB;若停止位為0,則激活CSTOPB。
newtio.c_cflag &= ~CSTOPB;

7.設置最少字符和等待時間。在對接收字符和等待時間沒有特別要求的情況下,可以將其設置為0。
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 0;

8.調用函數”tcflush(fd,queue_selector)”來處理要寫入引用的對象,queue_selector可能的取值有以下幾種。
TCIFLUSH:刷新收到的數據但是不讀
TCOFLUSH:刷新寫入的數據但是不傳送
TCIOFLUSH:同時刷新收到的數據但是不讀,並且刷新寫入的數據但是不傳送。
9.激活配置。在完成配置后,需要激活配置使其生效。使用tcsetattr()函數。
int tcsetattr(int filedes,int opt,const struct termios *termptr);

3.串口編程實例

備注:

1.串口設備操作要進行設備檢查,設置之前最好進行termios參數清零;

2.串口讀取要善於使用select函數;

/**@file      main.c
 * @brief       串口應用編程測試
 * @details
 * @author      wanghuan  any question please send mail to 371463817@qq.com
 * @date        2019-06-17
 * @version     V1.0
 * @copyright    Copyright (c) 2019-2022 
 **********************************************************************************
 * @attention
 * 硬件平台:iMX6ULL \n
 * 內核版本:L4.1.15
 * @par 修改日志:
 * <table>
 * <tr><th>Date        <th>Version  <th>Author    <th>Description
 * <tr><td>2019/06/17  <td>1.0      <td>       <td>創建初始版本
 * </table>
 **********************************************************************************
 */

/* 包含的頭文件 */
#include <stdio.h>        //標准輸入輸出,如printf、scanf以及文件操作
#include <stdlib.h>        //標准庫頭文件,定義了五種類型、一些宏和通用工具函數
#include <unistd.h>        //定義 read write close lseek 等Unix標准函數
#include <sys/types.h>    //定義數據類型,如 ssiz e_t off_t 等
#include <sys/stat.h>    //文件狀態
#include <fcntl.h>        //文件控制定義
#include <termios.h>    //終端I/O
#include <errno.h>        //與全局變量 errno 相關的定義
#include <getopt.h>        //處理命令行參數
#include <string.h>        //字符串操作
#include <time.h>        //時間
#include <sys/select.h>    //select函數

#define DEV_NAME    "/dev/ttymxc3"    ///< 串口設備


/**@brief   設置串口參數:波特率,數據位,停止位和效驗位
 * @param[in]  fd         類型  int      打開的串口文件句柄
 * @param[in]  nSpeed     類型  int     波特率
 * @param[in]  nBits     類型  int     數據位   取值 為 7 或者8
 * @param[in]  nParity     類型  int     停止位   取值為 1 或者2
 * @param[in]  nStop      類型  int      效驗類型 取值為N,E,O,,S
 * @return     返回設置結果
 * - 0         設置成功
 * - -1     設置失敗
 */
int setOpt(int fd, int nSpeed, int nBits, int nParity, int nStop)
{
    struct termios newtio, oldtio;

    // 保存測試現有串口參數設置,在這里如果串口號等出錯,會有相關的出錯信息
    if (tcgetattr(fd, &oldtio) != 0)
    {
        perror("SetupSerial 1");
        return -1;
    }

    bzero(&newtio, sizeof(newtio));        //新termios參數清零
    newtio.c_cflag |= CLOCAL | CREAD;    //CLOCAL--忽略 modem 控制線,本地連線, 不具數據機控制功能, CREAD--使能接收標志
    // 設置數據位數
    newtio.c_cflag &= ~CSIZE;    //清數據位標志
    switch (nBits)
    {
        case 7:
            newtio.c_cflag |= CS7;
        break;
        case 8:
            newtio.c_cflag |= CS8;
        break;
        default:
            fprintf(stderr, "Unsupported data size\n");
            return -1;
    }
    // 設置校驗位
    switch (nParity)
    {
        case 'o':
        case 'O':                     //奇校驗
            newtio.c_cflag |= PARENB;
            newtio.c_cflag |= PARODD;
            newtio.c_iflag |= (INPCK | ISTRIP);
            break;
        case 'e':
        case 'E':                     //偶校驗
            newtio.c_iflag |= (INPCK | ISTRIP);
            newtio.c_cflag |= PARENB;
            newtio.c_cflag &= ~PARODD;
            break;
        case 'n':
        case 'N':                    //無校驗
            newtio.c_cflag &= ~PARENB;
            break;
        default:
            fprintf(stderr, "Unsupported parity\n");
            return -1;
    }
    // 設置停止位
    switch (nStop)
    {
        case 1:
            newtio.c_cflag &= ~CSTOPB;
        break;
        case 2:
            newtio.c_cflag |= CSTOPB;
        break;
        default:
            fprintf(stderr,"Unsupported stop bits\n");
            return -1;
    }
    // 設置波特率 2400/4800/9600/19200/38400/57600/115200/230400
    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 19200:
            cfsetispeed(&newtio, B19200);
            cfsetospeed(&newtio, B19200);
            break;
        case 38400:
            cfsetispeed(&newtio, B38400);
            cfsetospeed(&newtio, B38400);
            break;
        case 57600:
            cfsetispeed(&newtio, B57600);
            cfsetospeed(&newtio, B57600);
            break;
        case 115200:
            cfsetispeed(&newtio, B115200);
            cfsetospeed(&newtio, B115200);
            break;
        case 230400:
            cfsetispeed(&newtio, B230400);
            cfsetospeed(&newtio, B230400);
            break;
        default:
            printf("\tSorry, Unsupported baud rate, set default 9600!\n\n");
            cfsetispeed(&newtio, B9600);
            cfsetospeed(&newtio, B9600);
            break;
    }
    // 設置read讀取最小字節數和超時時間
    newtio.c_cc[VTIME] = 1;     // 讀取一個字符等待1*(1/10)s
    newtio.c_cc[VMIN] = 1;        // 讀取字符的最少個數為1

      tcflush(fd,TCIFLUSH);         //清空緩沖區
      if (tcsetattr(fd, TCSANOW, &newtio) != 0)    //激活新設置
      {
        perror("SetupSerial 3");
          return -1;
     }
      printf("Serial set done!\n");
    return 0;
}

/**@brief 串口讀取函數
 * @param[in]  fd         打開的串口文件句柄
 * @param[in]  *rcv_buf 接收緩存指針
 * @param[in]  data_len    要讀取數據長度
 * @param[in]  timeout     接收等待超時時間,單位ms
 * @return     返回設置結果
 * - >0      設置成功
 * - 其他      讀取超時或錯誤
 */
int UART_Recv(int fd, char *rcv_buf, int data_len, int timeout)
{
    int len, fs_sel;
    fd_set fs_read;
    struct timeval time;

    time.tv_sec = timeout / 1000;              //set the rcv wait time
    time.tv_usec = timeout % 1000 * 1000;    //100000us = 0.1s

    FD_ZERO(&fs_read);        //每次循環都要清空集合,否則不能檢測描述符變化
    FD_SET(fd, &fs_read);    //添加描述符

    // 超時等待讀變化,>0:就緒描述字的正數目, -1:出錯, 0 :超時
    fs_sel = select(fd + 1, &fs_read, NULL, NULL, &time);
//    printf("fs_sel = %d\n", fs_sel);
    if(fs_sel)
    {
        len = read(fd, rcv_buf, data_len);
        return len;
    }
    else
    {
//        printf("Sorry,I am wrong!");
        return -1;
    }
}

/**@brief 串口發送函數
 * @param[in]  fd            打開的串口文件句柄
 * @param[in]  *send_buf     發送數據指針
 * @param[in]  data_len     發送數據長度
 * @return     返回結果
 * - data_len    成功
 * - -1            失敗
 */
int UART_Send(int fd, char *send_buf, int data_len)
{
    ssize_t ret = 0;

    ret = write(fd, send_buf, data_len);
    if (ret == data_len)
    {
        printf("send data is %s\n", send_buf);
        return ret;
    }
    else
    {
        printf("write device error\n");
        tcflush(fd,TCOFLUSH);
        return -1;
    }
}


/**@fn main
 * @brief main入口函數
 */
int main (int argc, char *argv[])
{
    int fdSerial;

    // 打開串口設備
    fdSerial = open(DEV_NAME, O_RDWR | O_NOCTTY | O_NDELAY);
    if(fdSerial < 0)
    {
        perror(DEV_NAME);
        return -1;
    }
    // 設置串口阻塞, 0:阻塞, FNDELAY:非阻塞
    if (fcntl(fdSerial, F_SETFL, 0) < 0)    //阻塞,即使前面在open串口設備時設置的是非阻塞的
    {
        printf("fcntl failed!\n");
    }
    else
    {
        printf("fcntl=%d\n", fcntl(fdSerial, F_SETFL, 0));
    }
    if (isatty(fdSerial) == 0)
    {
        printf("standard input is not a terminal device\n");
        close(fdSerial);
        return -1;
    }
    else
    {
        printf("is a tty success!\n");
    }
    printf("fd-open=%d\n", fdSerial);


    // 設置串口參數
    if (setOpt(fdSerial, 115200, 8, 'N', 1)== -1)    //設置8位數據位、1位停止位、無校驗
    {
        fprintf(stderr, "Set opt Error\n");
        close(fdSerial);
        exit(1);
    }

    tcflush(fdSerial, TCIOFLUSH);    //清掉串口緩存
    fcntl(fdSerial, F_SETFL, 0);    //串口阻塞


    char rcv_buf[100];
    int len;

    while(1)    //循環讀取數據
    {
        len = UART_Recv(fdSerial, rcv_buf, 99, 10000);
        if(len > 0)
        {
            rcv_buf[len] = '\0';
            printf("receive data is %s\n", rcv_buf);
            printf("len = %d\n", len);
            UART_Send(fdSerial, rcv_buf, len);
        }
        else
        {
//            printf("cannot receive data\n");
        }
        usleep(100000);    //休眠100ms
    }
}

 

編譯之后,測試結果如下: 

 

 

4. RS485編程實例

首先RS485和RS232在應用層面的編程是沒有區別的。

 

RS232用兩根線實現全雙工,兩根線各做各的,互不影響,可以同時進行;RS485雖然可以用四根線實現全雙工,但是實際應用中比較少見,更常見的是只用兩根線實現半雙工,這樣一來,就涉及到“收狀態”和“發狀態”的切換,這一切換又涉及兩種情況:

1、驅動程序中已經含有對半雙工情況下的接受切換,驅動程序會根據你讀或寫的動作,自動進行切換。這種情況下,RS485的編程就與RS232完全沒有區別。

2、驅動程序不帶自動切換,此時,為了完成切換,必須使用額外的GPIO連接RS485收發模塊的接受使能端,在接受、發送數據之前,首先對使能端置位,使之處於正確的“接收”或“發送”狀態。

其中第二種操作較為簡單,可以基於上面例程增加一個IO控制實現,在此不做贅述(對於不了解接觸底層的可以使用這種方式)。

 

實現方式:

設備樹添加 RS485設備

&uart3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    fsl,rs485-gpio-txen = <&gpio1 18 GPIO_ACTIVE_HIGH>;
    linux,rs485-enabled-at-boot-time;
    status = "okay";
};

應用代碼如下:

/* 包含的頭文件 */
#include <stdio.h>        //標准輸入輸出,如printf、scanf以及文件操作
#include <stdlib.h>        //標准庫頭文件,定義了五種類型、一些宏和通用工具函數
#include <unistd.h>        //定義 read write close lseek 等Unix標准函數
#include <sys/types.h>    //定義數據類型,如 ssiz e_t off_t 等
#include <sys/stat.h>    //文件狀態
#include <fcntl.h>        //文件控制定義
#include <termios.h>    //終端I/O
#include <linux/ioctl.h>
#include <asm-generic/ioctls.h>    //tty ioctl numbers,TIOCGRS485 + TIOCSRS485 ioctl 定義
#include <linux/serial.h>    //serial驅動相關
#include <errno.h>        //與全局變量 errno 相關的定義
#include <getopt.h>        //處理命令行參數
#include <string.h>        //字符串操作
#include <time.h>        //時間
#include <sys/select.h>    //select函數

.....

int rs485_enable(const int fd, const RS485_ENABLE_t enable)
{
        struct serial_rs485 rs485conf;
        int res;

        // 獲取設備的485配置
        res = ioctl(fd, TIOCGRS485, &rs485conf);
        if (res < 0)
        {
            perror("Ioctl error on getting 485 configure:");
            close(fd);
            return res;
        }

        // 設置485模式的使能/禁止
        if (enable)
        {
            // 使能485模式
            rs485conf.flags |= SER_RS485_ENABLED;
        }
        else
        {
            // 關閉485模式
            rs485conf.flags &= ~(SER_RS485_ENABLED);
        }

        rs485conf.delay_rts_before_send = 0x00000004;

        // 將485配置設置到設備中
        res = ioctl(fd, TIOCSRS485, &rs485conf);
        if (res < 0) {
                perror("Ioctl error on setting 485 configure:");
                close(fd);
        }
        return res;
}


int main (int argc, char *argv[])
{
         // 打開串口設備
         .....
    
         // 使能485功能
         rs485_enable(fd,ENABLE);
         ......

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM