基於Linux的tty架構及UART驅動詳解

更多嵌入式Linux原創,請關注公眾號:一口Linux

一、模組硬體學習

1.1. Uart介紹

通用非同步收發傳輸器(Universal Asynchronous Receiver/Transmitter),通常稱為UART,是一種非同步收發傳輸器,是電腦硬體的一部分。它將要傳輸的資料在串列通訊與並行通訊之間加以轉換。作為把並行輸入訊號轉成串列輸出訊號的晶片,UART 通常被集成於其他通訊介面的連上。

UART 是一種通用串列數據匯流排,用於非同步通訊。該匯流排雙向通訊,可以實現全雙工傳輸和接收。在嵌入式設備中,UART 用於主機與輔助設備通訊,如汽車音與外接AP 之間的通訊,與PC 機通訊包括與監控調試器和其它器件,如EEPOM通訊。

1.1.1. 通訊協議

UART作為非同步串口通訊協議的一種,工作原理是將傳輸數據的每個字元一位接一位地傳輸。
其中各位的意義如下:

  • 起始位:先發出一個邏輯」0」的訊號,表示傳輸字元的開始。

  • 數據位:緊接著起始位之後。數據位的個數可以是5、6、7、8等,構成一個字元。通常採用ASCII碼。從最低位開始傳送,靠時鐘定位。

  • 奇偶校驗位:數據位加上這一位後,使得「1」的位數應為偶數(偶校驗)或奇數(奇校驗),以此來校驗數據傳送的正確性
    。

  • 停止位:它是一個字元數據的結束標誌。可以是1位、1.5位、2位的高電平。

由於數據是在傳輸線上定時的,並且每一個設備有其自己的時鐘,很可能在通訊中兩台設備間出現了小小的不同步。
因此停止位不僅僅是表示傳輸的結束,並且提供電腦校正時鐘同步的機會。適用於停止位的位數越多,不同時鐘同步的容忍程度越大,但是數據傳輸率同時也越慢。

  • 空閑位:處於邏輯「1」狀態,表示當前線路上沒有數據傳送。

Uart傳輸數據如圖2-1所示:

1.1.2. 波特率

波特率是衡量資料傳送速率的指標。表示每秒鐘傳送的符號數(symbol)。一個符號代表的資訊量(比特數)與符號的階數有關。例如傳輸使用256階符號,每8bit代表一個符號,數據傳送速率為120字元/秒,則波特率就是120 baud,比特率是120*8=960bit/s。這兩者的概念很容易搞錯。

UART 的接收和發送是按照相同的波特率進行收發的。波特率發生器產生的時鐘頻率不是波特率時鐘頻率,而是波特率時鐘頻率的16倍,目的是為在接收時進行精確的取樣,以提取出非同步的串列數據。根據給定的晶振時鐘和要求的波特率,可以算出波特率分頻計數值。

1.1.3. 工作原理

發送數據過程:空閑狀態,線路處於高電位;當收到發送數據指令後,拉低線路一個數據位的時間T,接著數據位按低位到高位依次發送,數據發送完畢後,接著發送奇偶檢驗位和停止位(停止位為高電位),一幀數據發送結束。

接收數據過程: 空閑狀態,線路處於高電位;當檢測到線路的下降沿(線路電位由高電位變為低電位)時說明線路有數據傳輸,按照約定的波特率從低位到高位接收數據,數據接收完畢後,接著接收並比較奇偶檢驗位是否正確,如果正確則通知則通知後續設備準備接收數據或存入快取。

由於UART是非同步傳輸,沒有傳輸同步時鐘。為了能保證數據傳輸的正確性,UART採用16倍數據波特率的時鐘進行取樣。每個數據有16個時鐘取樣,取中間的取樣值,以保證取樣不會滑碼或誤碼。一般UART一幀的數據位為8,這樣即使每一個數據有一個時鐘的誤差,接收端也能正確地取樣到數據。

UART的接收數據時序為:當檢測到數據下降沿時,表明線路上有數據進行傳輸,這時計數器CNT開始計數,當計數器,當計數器為8時,取樣的值為「0」表示開始位;當計數器為24=161+8時,取樣的值為bit0數據;當計數器的值為40=162+8時,取樣的值為bit1數據;依次類推,進行後面6個數據的取樣。如果需要進行奇偶校驗位,則當計數器的值為152=169+8時,取樣的值為奇偶位;當計數器的值為168=1610+8時,取樣的值為「1」表示停止位,一幀數據收發完成。

1.1.4. RS232與RS485

UART:通常說的UART指的是一種串列通訊協議,規定了數據幀格式,波特率等。
RS232和RS485:是兩種不同的電氣協議,也就是說,是對電氣特性以及物理特性的規定,作用於數據的傳輸通路上,它並不含對數據的處理方式。對應的物理器件有RS232或者RS485驅動晶片,將CPU經過UART傳送過來的電壓訊號驅動成RS232或者RS485電平邏輯。

RS232使用3-15V有效電平,而UART,因為對電氣特性沒有規定,所以直接使用CPU使用的電平,即TTL電平(在0-3.3V之間)。

更具體的,電氣的特性也決定了線路的連接方式,比如RS232,規定用電平表示數據,因此線路就是單線路的,兩根線能達到全雙工的目的;RS485使用差分電平表示數據,因此必須用兩根線才能達到傳輸數據的基本要求,要實現全雙工,必須使用4根線。

RS232和RS485的區別
(1)抗干擾性

  • RS485 介面是採用平衡驅動器和差分接收器的組合,具有抑制共模干擾的能力,抗雜訊干擾性強。
  • RS232介面使用一根訊號線和一根訊號返回線而構成供地的傳輸形式,這種共地傳輸容易產生共模干擾,所以抗雜訊干擾性弱。
    (2)傳輸距離
  • RS485 介面的最大傳輸距離標準值為1200 米(9600bps 時),實際上可達3000米。
  • RS232 傳輸距離有限,最大傳輸距離標準值為50米,實際上也只能用15米左右。
    (3)通訊能力
  • RS485介面在匯流排上最多可以連接128個收發器,即具有多站能力,而這樣的用戶可以利用單一的RS485介面方便的建立起設備網路。
  • RS232隻允許一對一通訊。
    (4)傳輸速率
  • RS232傳輸速率較低,在非同步傳輸時,波特率為20Kbps.
  • RS485的數據最高傳輸速率為10Mbps.
    (5) 訊號線
  • RS485全雙工:uart-tx 1根線,變成 RS485- A/B 2根線;uart-rx 1根線,變成 RS485- x/y 2根線,
  • RS485半雙工: 將全雙工的 A/B; X/Y 合併起來,分時復用。
  • RS232隻允許一對一通訊
    (6)電氣電平值
  • 邏輯「1」以兩線間的電壓差為+(2-6)V表示;邏輯「0」以兩線間的電壓差為-(2-6)V表示。
  • 在RS232中任何一條訊號的電壓均為負邏輯關係。即:邏輯「1」-5-15V;邏輯「0」,+5~+15V,雜訊容限為2V。即要求接收器能識別低至+3V的訊號作為邏輯「0」,高到-3V的訊號的訊號作為邏輯「1」。
  • RS232介面的訊號電平值較高,易損壞介面電路的晶片,又因為與TTL電平不兼容故使用電平轉換電路方能與TTL電路連接。
  • RS485介面訊號電平比RS232降低了,就不易損壞介面電路的晶片,且該電平與TTL電平兼容,方便與TTL電路連接。

1.1.5. 流控

數據在兩個串口傳輸時,常常會出現丟失數據的現象,或者兩台電腦的處理速度不同,如台式機與單片機之間的通訊,接收端數據緩衝區以滿,此時繼續發送的數據就會丟失,流控制能解決這個問題,當接收端數據處理不過來時,就發出「不再接收」的訊號,發送端就停止發送,直到收到「可以繼續發送」的訊號再發送數據。

因此流控制可以控制數據傳輸的進程,防止數據丟失。PC機中常用的兩種流控為:硬體流控(包括RTS/CTS、DTR/CTS等)和軟體流控制XON/XOFF(繼續/停止)。

1.1.5.1. 硬體流控

硬體流控制常用的有RTS/CTS流控制和DTR/DSR流控制兩種。
DTR–數據終端就緒(Data Terminal Ready)
低有效,當為低時,表示本設備自身準備就緒。此訊號輸出對端設備,使用對端設備決定能否與本設備通訊。
DSR-數據裝置就緒(Data Set Ready)
低有效,此訊號由本設備相連接的對端設備提供,當為低時,本設備才能與設備端進行通訊。
RTS - 請求發送(數據)(Request To Send)
低有效,此訊號由本設備在需要發送數據給對端設備時設置。當為低時,表示本設備有數據需要向對端設備發送。對端設備能否接收到本方的發送數據,則通過CTS訊號來應答。
CTS - 接收發送(請求)(Clear To Send)
低有效,對端設備能否接收本方所發送的數據,由CTS決定。若CTS為低,則表示對端的以準備好,可以接收本端發送數據。

以RTS/CTS流控制分析,分析主機發送/接收流程:

物理連接


主機的RTS(輸出訊號),連接到從機的CTS(輸入訊號)。
主機是CTS(輸入訊號),連接到從機的RTS(輸入訊號)。

  • 1.主機的發送過程:
    主機查詢主機的CTS腳訊號,此訊號連接到從機的RTS訊號,受從機控制。如果主機CTS訊號有效(為低),表示從機的接收FIFO未滿,從機可以接收,此時主機可以向從機發送數據,並且在發送過程中要一直查詢CTS訊號是否為有效狀態。主機查詢到CTS無效時,則中止發送。
    主機的CTS訊號什麼時候會無效呢?
    從機在接收到主機發送的數據時,從機的接收模組的FIFO如果滿了,則會使從機RTS無效,也即主機的CTS訊號無效。主機查詢到CTS無效時,主機發送中止。

  • 2.主機接收模式:
    如果主機接收FIFO未滿,那麼使主機RTS訊號有效(為低),即從機的CTS訊號有效。此時如果從機要發送,發送前會查詢從機的CTS訊號,如果有效,則開始發送。並且在發送過程中要一直查詢從機CTS訊號的有效狀態,如果無效則終止發送。是否有效由主機的RTS訊號決定。如果主機FIFO滿了,則使主機的RTS訊號無效,也即從機CTS訊號無效,主機接收中止。

1.1.5.2. 軟體流控

由於電纜的限制,在普通的控制通訊中一般不採用硬體流控制,而是使用軟體流控制。一般通過XON/XOFF來實現軟體流控制。常用方法是:當接收端的輸入緩衝區內數據量超過設定的高位時,就向數據發送端發送XOFF字元後就立即停止發送數據;當接收端的輸入緩衝區內數據量低於設定的低位時,就向數據發送端發送XON字元(十進位的17或Control-Q),發送端收到XON字元後就立即開始發送數據。一般可從設備配套源程式中找到發送端收到XON字元後就立即發送數據。一般可以從設備配套源程式中找到發送的是什麼位元組。

應注意,若傳輸的是二進位的數據,標誌字元也可能在數據流中出現而引起誤操作,這是軟體流控的缺陷,而硬體流控不會出現這樣的問題。

二、Linux serial框架

在Linux系統中,終端是一種字元型設備,它有多種類型,通常使用tty(Teletype)來簡稱各種類型的終端設備。對於嵌入式系統而言,最普遍採用的是Uart(Universal Asynchronous Receiver/Transmitter),串列埠,日常生活中簡稱埠

2.1. TTY驅動程式框架

2.1.1. TTY概念

2.1.1.1. 串口終端(/dev/ttyS*)

串口終端是使用電腦串口連接的終端設備。Linux把每個串列埠都看做是一個字元設備。這些串列埠所對應的設備名稱是/dev/ttySAC*;

2.1.1.2. 控制台終端(/dev/console)

在Linux系統中,電腦的輸出設備通常被稱為控制台終端,這裡特指printk資訊輸出到設備。/dev/console是一個虛擬的設備,它需要映射到真正的tty上,比如通過內核啟動參數「console=ttySCA0」就把console映射到了串口0

2.1.1.3. 虛擬終端(/dev/tty*)

當用戶登錄時,使用的是虛擬終端。使用Ctcl+Alt[F1 - F6]組合鍵時,我們就可以切換到tty1、tty2、tty3等上面去。tty*就稱為虛擬終端,而tty0則是當前所使用虛擬終端的一個別名。

2.1.2. TTY架構分析

整個 tty架構大概的樣子如圖3.1所示,簡單來分的話可以說成兩層,一層是下層我們的串口驅動層,它直接與硬體相接觸,我們需要填充一個 struct uart_ops 的結構體,另一層是上層 tty 層,包括 tty 核心以及線路規程,它們各自都有一個 Ops 結構,用戶空通過間是 tty 註冊的字元設備節點來訪問。

图3.1tty架构图

如圖3.2所示,tty設備發送數據的流程為:tty核心從一個用戶獲取將要發送給一個tty設備的數據,tty核心將數據傳遞給tty線路規程驅動,接著數據被傳到tty驅動,tty驅動將數據轉換為可以發給硬體的格式。

接收數據的流程為:從tty硬體接收到的數據向上交給tty驅動,接著進入tty線路規程驅動,再進入tty核心,在這裡它被一個用戶獲取。

图3.2 tty设备发送、接收数据流程

2.2. 關鍵數據結構

2.2.1. Struct uart_driver

uart_driver 包含了串口設備名,串口驅動名,主次設備號,串口控制台(可選))等資訊,還封裝了tty_driver
(底層串口驅動無需關心tty_driver)

struct uart_driver {
        struct module    *owner; /*擁有該uart_driver的模組,一般為THIS_MODULE*/
        const char        *driver_name; /*驅動串口名,串口設備名以驅動名為基礎*/
        const char        *dev_name; /*串口設備名*/
        int                 major; /*主設備號*/
        int                 minor; /*次設備號*/
        int                 nr; /*該uart_driver支援的串口數*/
        struct console    *cons; /*其對應的console,若該uart_driver支援serial console,
*否則為NULL*/
/*
* these are private; the low level driver should not
* touch these; they should be initialised to NULL
*/
struct uart_state *state; /*下層,窗口驅動層*/
struct tty_driver  *tty_driver; /*tty相關*/

2.2.2. struct console

實現控制台列印功能必須要註冊的結構體

struct console {
      char name[16];
      void(*write)(struct console *,const char *, unsigined);
      int (*read)(struct console *, char *, unsigned);
      struct tty_driver *(struct console *,int*);
      void (*unblank)(void);
      int  (*setup)(struct console *, char *);
      int  (*early_setup)(void);
      short  flags;
      short  index; /*用來指定該console使用哪一個uart port (對應的uart_port中的line),如果為-1,kernel會自動選擇第一個uart port*/
      int   cflag;
      void  *data;
      struct   console *next;
};

2.2.3. struct uart_state

每一個uart埠對應著一個uart_state,該結構體將uart_port與對應的circ_buf聯繫起來。uart_state有兩個成員在底層串口驅動會用到:xmit和port。用戶空間程式通過串口發送數據時,上層驅動將用戶數據保存在xmit;而串口發送中斷處理函數就是通過xmit獲取到用戶數據並將它們發送出去。串口接收中斷處理函數需要通過port將接收到的數據傳遞給線路規程層。

struct uart_state {
       struct  tty_port  port;
       
       enum uart_pm_state   pm_state;
       struct circ_buf     xmit;
       
       struct uart_port     *uart_port; /*對應於一個串口設備*/
};

2.2.4. struct uart_port

uart_port用於描述串口埠的I/O埠或I/O記憶體地址、FIFO大小、埠類型、串口時鐘等資訊。實際上,一個uart_port實現對應一個串口設備。

struct uart_port {
        spinlock_t              lock;                   /* port lock */
        unsigned long           iobase;                 /* in/out[bwl] */
        unsigned char __iomem   *membase;               /* read/write[bwl] */
        unsigned int            (*serial_in)(struct uart_port *, int);
        void                    (*serial_out)(struct uart_port *, int, int);
        void                    (*set_termios)(struct uart_port *,
                                               struct ktermios *new,
                                               struct ktermios *old);
        int                     (*handle_irq)(struct uart_port *);
        void                    (*pm)(struct uart_port *, unsigned int state,
                                      unsigned int old);
        void                    (*handle_break)(struct uart_port *);
        unsigned int            irq;                    /* irq number */
        unsigned long           irqflags;               /* irq flags  */
        unsigned int            uartclk;                /* base uart clock */
        unsigned int            fifosize;               /* tx fifo size */
        unsigned char           x_char;                 /* xon/xoff char */
        unsigned char           regshift;               /* reg offset shift */
        unsigned char           iotype;                 /* io access style */
        unsigned char           unused1;

#define UPIO_PORT               (0)
#define UPIO_HUB6               (1)
#define UPIO_MEM                (2)
#define UPIO_MEM32              (3)
#define UPIO_AU                 (4)                     /* Au1x00 and RT288x type IO */
#define UPIO_TSI                (5)                     /* Tsi108/109 type IO */

        unsigned int            read_status_mask;       /* driver specific */
        unsigned int            ignore_status_mask;     /* driver specific */
        struct uart_state       *state;                 /* pointer to parent state */
        struct uart_icount      icount;                 /* statistics */

        struct console          *cons;                  /* struct console, if any */
#if defined(CONFIG_SERIAL_CORE_CONSOLE) || defined(SUPPORT_SYSRQ)
        unsigned long           sysrq;                  /* sysrq timeout */
#endif

        upf_t                   flags;

#define UPF_FOURPORT            ((__force upf_t) (1 << 1))
#define UPF_SAK                 ((__force upf_t) (1 << 2))
#define UPF_SPD_MASK            ((__force upf_t) (0x1030))
#define UPF_SPD_HI              ((__force upf_t) (0x0010))
#define UPF_SPD_VHI             ((__force upf_t) (0x0020))
#define UPF_SPD_CUST            ((__force upf_t) (0x0030))
#define UPF_SPD_SHI             ((__force upf_t) (0x1000))
#define UPF_SPD_WARP            ((__force upf_t) (0x1010))
#define UPF_SKIP_TEST           ((__force upf_t) (1 << 6))
#define UPF_AUTO_IRQ            ((__force upf_t) (1 << 7))
#define UPF_HARDPPS_CD          ((__force upf_t) (1 << 11))
#define UPF_LOW_LATENCY         ((__force upf_t) (1 << 13))
#define UPF_BUGGY_UART          ((__force upf_t) (1 << 14))
#define UPF_NO_TXEN_TEST        ((__force upf_t) (1 << 15))
#define UPF_MAGIC_MULTIPLIER    ((__force upf_t) (1 << 16))
/* Port has hardware-assisted h/w flow control (iow, auto-RTS *not* auto-CTS) */
#define UPF_HARD_FLOW           ((__force upf_t) (1 << 21))
/* Port has hardware-assisted s/w flow control */
#define UPF_SOFT_FLOW           ((__force upf_t) (1 << 22))
#define UPF_CONS_FLOW           ((__force upf_t) (1 << 23))
#define UPF_SHARE_IRQ           ((__force upf_t) (1 << 24))
#define UPF_EXAR_EFR            ((__force upf_t) (1 << 25))
#define UPF_BUG_THRE            ((__force upf_t) (1 << 26))
/* The exact UART type is known and should not be probed.  */
#define UPF_FIXED_TYPE          ((__force upf_t) (1 << 27))
#define UPF_BOOT_AUTOCONF       ((__force upf_t) (1 << 28))
#define UPF_FIXED_PORT          ((__force upf_t) (1 << 29))
#define UPF_DEAD                ((__force upf_t) (1 << 30))
#define UPF_IOREMAP             ((__force upf_t) (1 << 31))

#define UPF_CHANGE_MASK         ((__force upf_t) (0x17fff))
#define UPF_USR_MASK            ((__force upf_t) (UPF_SPD_MASK|UPF_LOW_LATENCY))

        unsigned int            mctrl;                  /* current modem ctrl settings */
        unsigned int            timeout;                /* character-based timeout */
        unsigned int            type;                   /* port type */
        const struct uart_ops   *ops;
        unsigned int            custom_divisor;
        unsigned int            line;                   /* port index */
        resource_size_t         mapbase;                /* for ioremap */
        struct device           *dev;                   /* parent device */
        unsigned char           hub6;                   /* this should be in the 8250 driver */
        unsigned char           suspended;
        unsigned char           irq_wake;
        unsigned char           unused[2];
        void                    *private_data;          /* generic platform data pointer */
};

2.2.5. struct uart_ops

struct uart_ops涵蓋了驅動可對串口的所有操作

 struct uart_ops {
        unsigned int    (*tx_empty)(struct uart_port *);
        void            (*set_mctrl)(struct uart_port *, unsigned int mctrl);
        unsigned int    (*get_mctrl)(struct uart_port *);
        void            (*stop_tx)(struct uart_port *);
        void            (*start_tx)(struct uart_port *);
        void            (*throttle)(struct uart_port *);
        void            (*unthrottle)(struct uart_port *);
        void            (*send_xchar)(struct uart_port *, char ch);
        void            (*stop_rx)(struct uart_port *);
        void            (*enable_ms)(struct uart_port *);
        void            (*break_ctl)(struct uart_port *, int ctl);
        int             (*startup)(struct uart_port *);
        void            (*shutdown)(struct uart_port *);
        void            (*flush_buffer)(struct uart_port *);
        void            (*set_termios)(struct uart_port *, struct ktermios *new,
                                       struct ktermios *old);
        void            (*set_ldisc)(struct uart_port *, int new);
        void            (*pm)(struct uart_port *, unsigned int state,
                              unsigned int oldstate);
        int             (*set_wake)(struct uart_port *, unsigned int state);

        /*
         * Return a string describing the type of the port
         */
        const char      *(*type)(struct uart_port *);

        /*
         * Release IO and memory resources used by the port.
         * This includes iounmap if necessary.
         */
        void            (*release_port)(struct uart_port *);

        /*
         * Request IO and memory resources used by the port.
         * This includes iomapping the port if necessary.
         */
        int             (*request_port)(struct uart_port *);
        void            (*config_port)(struct uart_port *, int);
        int             (*verify_port)(struct uart_port *, struct serial_struct *);
        int             (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
        int             (*poll_init)(struct uart_port *);
        void            (*poll_put_char)(struct uart_port *, unsigned char);
        int             (*poll_get_char)(struct uart_port *);
#endif
};

2.3. 關鍵流程

2.3.1. 註冊流程

2.3.1.1. 註冊uart_driver

此介面在uart driver中調用,用來註冊uart_driver到kernel中,調用階段在uart driver的初始階段,例如:module_init(), uart_driver的註冊流程圖

图3.3uart driver注册流程

註冊過程主要做了以下操作:

  • 1、根據driver支援的最大設備數,申請n個uart_state空間,每一個uart_state都有一個uart_port。
  • 2、分配一個tty_driver,並將uart_driver->tty_driver指向它。
  • 3、對tty_driver進行設置,其中包括默認波特率、檢驗方式等,還有一個重要的ops,結構體tty_operation的註冊,它是tty核心與串口驅動通訊的介面。
  • 4、初始化每一個uart_state的tty_port;
  • 5、註冊tty_driver。
    註冊uart_driver實際上是註冊tty_driver,與用戶空間打交道的工作完全交給tty_driver,這一部分是內核實現好的不需要修改

2.3.1.2. 添加uart_port

此介面用於註冊一個uart port 到uart driver上,通過註冊,uart driver就可以訪問對應的uart port,進行數據收發。該介面在uart driver中的probe函數調用,必須保證晚於uart_register_drver的註冊過程。

uart driver在調用介面前,要手動設置uart_port的操作uart_ops,使得通過調用uart_add_one_port介面後驅動完成硬體的操作介面註冊。uart添加port流程如圖3-4所示:

图3-4 uart添加port流程图

2.4. 數據收發流程

2.4.1. 打開設備(open操作)

open設備的大體流程如圖3-5所示:

图3-5 open设备流程

2.4.2. 數據發送流程(write操作)

發送數據大體流程如圖3-6所示:

图3-6 发送数据流程

2.4.3. 數據接收流程(read操作)

接收數據的大體流程如圖3-7所示:

图3-7数据接收流程

2.4.4. 關閉設備(close操作)

close設備的大體流程如圖3-8所示:

图3-8 close设备流程

2.4.5. 註銷流程

2.4.5.1. 移除uart_port

此介面用於從uart driver上註銷一個uart port,該介面在uart driver中的remove函數中調用。uart移除port的流程如圖3-9所示:

图3.9 uart移除port流程图

2.4.5.2. 註銷uart_driver

此介面在uart driver中調用,用來從kernel中註銷uart_driver,調用階段在uart driver的退出階段,例如:module_exit(),uart driver的註銷流程如圖3.10所示

2.5. 使用rs485通訊

2.5.1. rs485和rs232的區別

uart(TTL-3.3V)/rs232(工業級 +-12V)是電壓驅動,rs485是電流驅動(能傳輸更遠的距離)
rS232用電平表示數據,使用2根線可實現全雙工,rs485用差分電平表示數據,因此必須用4根線實現全雙工rs485;

全雙工:uart-tx 1根線變成rs485-A/B 2根線;uart-rx 1根線變成rs485- X/Y兩根線;

rs485半雙工: 將全雙工的A/B和X/Y合併起來分時復用;
rs485-de/re是給轉換器的一個控制訊號,對我們晶片來說,都是輸出;

2.5.2. rs485調試方法:

首先保證uart模組和相關gpio,電壓轉換晶片工作正常:

  • a,保證uart tx/rx功能正常。
  • b,用gpio-output來控制 de/re 相關的2個gpio,觀察 de/re的gpio輸出low/high是否正常
  • c,在b的基礎上,單獨調試 rs485-tx/rs485-rx,單端調試是否pass.

模式1
2-gpio-normal-uart-rs485-halfduplex
(2個gpio獨立控制de/re, enable就是將相關gpio設置到active電平;不用uart控制器的rs485模式;uart控制器處於normal模式)

  • a, 默認re-en, de-dis,默認rs485-rx
  • b, 當要發送的時候,re-dis, de-enable, 然後uart-tx.
  • c, tx完成之後,de-dis; re-en,進入默認的rs485-rx模式。

模式2
1-gpio-normal-uart-rs485-halfduplex
這個模式的前提條件,外設器件的 de/re必須是相反極性的,比如de是高電平有效,re是低電平有效,則可以用一個gpio,來控制 de/re,此時de/re一定是互斥的。
(1個gpio控制de/re, enable就是將相關gpio設置到active電平;不用uart控制器的rs485模式;uart控制器處於normal模式)

  • a, re-en,進入rs485-rx模式 (re 通常是低電平有效,這一步就是 設置 re對應的gpio為低電平)
  • b, 當要發送的時候,設置gpio:re-disable, de-enable, 然後uart-tx.(re 通常是低電平有效,這一步就是 設置 re對應的gpio為高電平)
  • c, tx完成之後,de-disable; re-enable,進入默認的rs485-rx模式。(re 通常是低電平有效,這一步就是 設置 re對應的gpio為低電平)

模式3
rs485-software-halfduplex(de/re 獨立輸出)
(使能uart控制器的rs485模式; 通過uart模組內部reg來控制 de/re 訊號)

  • a,使能uart控制器的 rs485模式,並按照電壓轉換晶片的特性,設置de/re polarity
  • b, 設置rs485的模式為 sw-half-duplex, 設置 de-timing暫存器; 設置 de/re turnaround 暫存器。
  • c, 默認為rs485-rx模式,設置 de-dis/re-en
  • d, 當要tx的時候,設置 de-en/re-dis
  • e, 發送完成,設置 de-dis/re-en

模式4
rs485-hardware-halfduplex(de/re 獨立輸出)
基本配置同模式3,但是設置 rs485模式為 hardware-halfduplex模式

  • a, 只要設置 de-en/rx-en 都為1,然後就不用管了,硬體實現半雙工切換。

模式5:
使用純硬體的辦法實現RS485半雙工功能,電路如圖所示:


接收:
默認沒有數據時,UART_TX為高電平,三極體導通,485晶片RE低電平使能,RO接收數據使能,此時從485AB口收到什麼數據就會通過RO通道傳到MCU,完成數據接收過程。
發送:
當發送數據時,UART_TX會有一個下拉的電平,表示開始發送數據,此時三極體截止,DE為高電平發送使能。當發送數據『0』時,由於DI口連接地,此時數據『0』就會傳輸到AB口 A-B<0,傳輸『0』,完成了低電平的傳輸。當發送『1』時,此時三極體導通,按理說RO使能,此時由於還處在發送數據中,這種狀態下485處於高阻態,此時的狀態通過A上拉B下拉電阻決定,此時A-B>0傳輸『1』,完成高電平的傳輸。

3. 模組詳細設計

3.1. 關鍵函數介面

3.1.1. uart_register_driver

/*功能:  uart_register_driver用於串口驅動uart_driver註冊到內核(串口核心層)中,通常在模組初始化函數調用該函數。
 *參數:drv:要註冊的uart_driver
 *返回值:成功,返回0;否則返回錯誤碼
 */
int uart_register_driver(struct uart_driver *drv)

3.1.2. uart_unregister_driver

/*功能:uart_unregister 用於註銷我們已註冊的uart_driver,通常在模組卸載函數調用該函數,
 *參數 : drv:要註銷的uart_driver
 *返回值:成功返回0,否則返回錯誤碼
 */
void uart_unregister_driver(struct uart_driver *drv)

3.1.3. uart_add_one_port

/*功能:uart_add_one_port用於為串口驅動添加一個串口埠,通常在探測到設備後(驅動的設備probe方法)調用該函數
 *參數:
 *     drv:串口驅動
 *     port:要添加的串口埠
 *返回值:成功,返回0;否則返回錯誤碼
 */
int uart_add_one_port(struct uart_driver *drv,struct uart_port *port)

3.1.4. uart_remove_one_port

/*功能:uart_remove_one_port用於刪除一個已經添加到串口驅動中的串口埠,通常在驅動卸載時調用該函數
 *參數:
 *     drv:串口驅動
 *     port:要刪除的串口埠
 *返回值:成功,返回0;否則返回錯誤碼
 */
int uart_remove_one_port(struct uart_driver *drv,struct uart_port *port)

3.1.5. uart_write_wakeup

/*功能:uart_write_wakeup喚醒上層因串口埠寫數據而堵塞的進程,通常在串口發送中斷處理函數中調用該函數
 *參數:
 *     port: 需要喚醒寫堵塞進程的串口埠
 */
void uart_write_wakeup(struct uart_port *port)

3.1.6. uart_suspend_port

/*功能:uart_suspend_port用於掛起特定的串口埠
 *參數:
 *     drv:要掛起的串口埠鎖所屬的串口驅動
 *     port:要掛起的串口埠
 *返回值:成功返回0;否則返回錯誤碼
 */
int uart_suspend_port(struct uart_driver *drv, struct uart_port *port)

3.1.7. uart_resume_port

/*功能:uart_resume_port用於恢復某一已掛起的串口
 *參數:
 *     drv:要恢復的串口埠所屬的串口驅動
 *     port:要恢復的串口埠
 *返回值:成功返回0;否則返回錯誤碼
 */
int uart_resume_port(struct uart_driver *drv, struct uart_port *port)

3.1.8. uart_get_baud_rate

/*功能:uart_get_baud_rate通過解碼termios結構體來獲取指定串口的波特率
 *參數:
 *     port:要獲取波特率的串口埠
 *     termios:當前期望的termios配置(包括串口波特率)
 *     old:以前的termios配置,可以為NULL
 *     min:可以接受的最小波特率
 *     max:可以接受的最大波特率
 *     返回值:串口波特率
 */
unsigned int uart_get_baund_rate(struct uart_port *port, struct ktermios *termios, struct ktermios *old,unsigned int min, unsigned int max)

3.1.9. uart_get_divisor

/*功能:uart_get_divisor 用於計算某一波特率的串口時鐘分頻數(串口波特率除數)
 *參數:
 *     port:要計算分頻數的串口埠
 *     baud:期望的波特率
 *返回值:串口時鐘分頻數
 */
unsigned int uart_get_divisor(struct uart_port *port, unsigned int baund)

3.1.10. uart_update_timeout

/*功能:uart_update_timeout用於更新(設置)串口FIFO超出時間
 *參數:
 *     port:要更新超時間的串口埠
 *     cfalg:termios結構體的cflag值
 *     baud:串口的波特率
 */
void uart_update_timeout(struct uart_port *port,unsigned int cflag, unsigned int baud)

3.1.11. uart_insert_char

/*功能:uart_insert_char用於向uart層插入一個字元
 *參數:
 *     port:要寫資訊的串口埠
 *     status:RX buffer狀態
 *     overrun:在status中的overrun bit掩碼
 *     ch:需要插入的字元
 *     flag:插入字元的flag:TTY_BREAK,TTY_PSRIYY, TTY_FRAME
 */
void uart_insert_char(struct uart_port *port, unsigned int status, unsigned int overrun,unsigned int ch, unsigned int flag)

3.1.12. uart_console_write

/*功能:uart_console_write用於向串口埠寫一控制台資訊
 *參數:
 *     port:要寫資訊的串口埠
 *     s:要寫的資訊
 *     count:資訊的大小
 *     putchar:用於向串口埠寫字元的函數,該函數有兩個參數:串口埠和要寫的字元
 */
Void uart_console_write(struct uart_port *port,const char *s, unsigned int count,viod(*putchar)(struct uart_port*, int))

4. 模組使用說明

4.1. 串口編程

4.1.1. 串口控制函數

屬性 說明
tcgetatrr 取屬性(termios結構)
tcsetarr 設置屬性(termios結構)
cfgetispeed 得到輸入速度
cfsetispeed 得到輸出速度
cfstospeed 設置輸出速度
tcdrain 等待所有輸出都被傳輸
tcflow 掛起傳輸或接收
tcflush 刷請未決輸出和/或輸入
tcsendbreak 送BREAK字元
tcgetpgrp 得到前台進程組ID
Tcsetpgrp 設置前台進程組ID

4.1.2. 串口配置流程

  • (1) 保持原先串口配置,使用tegetatrr(fd, &oldtio);
struct termious newtio, oldtio;
tegetattr(fd, &oldtio);
  • (2) 激活選項有CLOCAL和CREAD,用於本地連接和接收使用
newtio.cflag |= CLOCAL|CREAD;
  • (3) 設置波特率
newtio.c_cflag = B115200;
  • (4) 設置數據位,需使用掩碼設置
newtio.c_cflag &= ~CSIZE;
Newtio.c_cflag |= CS8;
  • (5) 設置停止位,通過激活c_cflag中的CSTOP實現。若停止位為1,則清除CSTOPB,若停止位為2,則激活CSTOP
newtio.c_cflag &= ~CSTOPB; /*停止位設置為1*/
Newtio.c_cflag |= CSTOPB; /*停止位設置為2 */
  • (6) 設置流控
newtio.c_cfag |= CRTSCTS /*開啟硬體流控 */
newtio.c_cfag |= (IXON | IXOFF | IXANY); /*開啟軟體流控*/
  • (7) 奇偶檢驗位設置,使用c_cflag和c_ifag.
    設置奇校驗
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;
  • (8) 設置最少字元和等待時間,對於接收字元和等待時間沒有什麼特別的要求,可設置為0:
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN]  = 0;
  • (9) 處理要寫入的引用對象
    tcflush函數刷清(拋棄)輸入緩衝(終端程式已經接收到,但用戶程式尚未讀)或輸出緩衝(用戶程式已經寫,但未發送)。
int tcflash(int filedes, int quene)
quene數應當是下列三個常數之一:
  *TCIFLUSH 刷清輸入隊列
  *TCOFLUSH 刷清輸出隊列
  *TCIOFLUSH 刷清輸入、輸出隊列
例如:
tcflush(fd, TCIFLUSH);
  • (10) 激活配置,在完成配置後,需要激活配置使其生效。使用tcsetattr()函數:
int tcsetarr(int filedes, const struct termios *termptr);
opt 指定在什麼時候新的終端屬性才起作用,
   *TCSANOW:更改立即發生
   *TCSADRAIN:發送了所有輸出後更改才發生。若更改輸出參數則應使用此選項
   *TCSAFLUSH:發送了所有輸出後更改才發生。更進一步,在更改發生時未讀的
                 所有輸入數據都被刪除(刷清)
例如:tcsetatrr(fd, TCSANOW, &newtio);

4.1.3. 使用流程

  • (1)打開串口,例如"/dev/ttySLB0"
fd = open("/dev/ttySLB0",O_RDWR | O_NOCTTY | O_NDELAY);
O_NOCTTY:是為了告訴Linux這個程式不會成為這個埠上的「控制終端」。如果不這樣做的話,所有的輸入,比如鍵盤上過來的Ctrl+C中止訊號等等,會影響到你的進程。
O_NDELAY:這個標誌則是告訴Linux這個程式並不關心DCD訊號線的狀態,也就是不管串口是否有數據到來,都是非阻塞的,程式繼續執行。
  • (2)恢復串口狀態為阻塞狀態,用於等待串口數據的讀入,用fcntl函數:
fcntl(fd,F_SETFL,0);  //F_SETFL:設置文件flag為0,即默認,即阻塞狀態
  • (3)接著測試打開的文件描述符是否應用一個終端設備,以進一步確認串口是否正確打開。
isatty(STDIN_FILENO);
  • (4)讀寫串口
串口的讀寫與普通文件一樣,使用read,write函數
read(fd, buf ,8);
write(fd,buff,8);

4.1.4. Demo

以下給出一個測溫模組收取數據的例子

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <log/log.h>
#include <stdlib.h>

#define UART_DEVICE     "/dev/ttySLB1"

struct temp {
	float temp_max1;
	float temp_max2;
	float temp_max3;
	float temp_min;
	float temp_mean;
	float temp_enviromem;
	char temp_col[1536];
};

int main(void)
{
	int count, i, fd;
	struct termios oldtio, newtio;
	struct temp *temp;
	temp = (struct temp *)malloc(sizeof(struct temp));
	if (!temp) {
		printf("malloc failed\n");
		return -1;
	}

	char cmd_buf1[] = { 0xAA, 0x01, 0x04, 0x00, 0x06, 0x10, 0x05, 0x00, 0xBB};
	char cmd_buf2[] = { 0xAA, 0x01, 0x04, 0x00, 0x00, 0xA0, 0x00, 0x03, 0xBB};
	char cmd_buf3[] = { 0xAA, 0x01, 0x04, 0x00, 0x03, 0x10, 0x01, 0x00, 0xBB};
	char read_buf[2000];

	//-----------打開uart設備文件------------------
	fd = open(UART_DEVICE, O_RDWR | O_NOCTTY);
	if (fd < 0) {
		printf("Open %s failed\n", UART_DEVICE);
		return -1;
	} else {
		printf("Open %s successfully\n", UART_DEVICE);
	}

	//-----------設置操作參數-----------------------
	tcgetattr(fd, &oldtio);//獲取當前操作模式參數
	memset(&newtio, 0, sizeof(newtio));

	//波特率=230400 數據位=8 使能數據接收
	newtio.c_cflag = B230400 | CS8 | CLOCAL | CREAD | CSTOPB;
	newtio.c_iflag = IGNPAR;

	tcflush(fd, TCIFLUSH);//清空輸入緩衝區和輸出緩衝區
	tcsetattr(fd, TCSANOW, &newtio);//設置新的操作參數

	//printf("input: %s, len = %d\n", cmd_buf, strlen(cmd_buf));
	//------------向urat發送數據-------------------

	for (i = 0; i < 9; i++)
		printf("%#X ", cmd_buf1[i]);

	count = write(fd, cmd_buf1, 9);
	if (count != 9) {
		printf("send failed\n");
		return -1;
	}

	usleep(500000);

	memset(read_buf, 0, sizeof(read_buf));
	count = read(fd, read_buf, sizeof(read_buf));
	if (count > 0) {
		for (i = 0; i < count; i++);
		temp->temp_max1 = read_buf[7] << 8 | read_buf[6];
		temp->temp_max2 = read_buf[9] << 8 | read_buf[8];
		temp->temp_max3 = read_buf[11] << 8 | read_buf[10];
		temp->temp_min  = read_buf[13] << 8 | read_buf[12];
		temp->temp_mean = read_buf[15] << 8 | read_buf[14];

		printf("temp->temp_max1 = %f\n", temp->temp_max1 * 0.01);
		printf("temp->temp_max2 = %f\n", temp->temp_max2 * 0.01);
		printf("temp->temp_max3 = %f\n", temp->temp_max3 * 0.01);
		printf("temp->temp_min  = %f\n", temp->temp_min  * 0.01);
		printf("temp->temp_mean = %f\n", temp->temp_mean * 0.01);
		

	} else {
		printf("read temp failed\n");
		return -1;
	}

	count = write(fd, cmd_buf3, 9);
	if (count != 9) {
		printf("send failed\n");
		return -1;
	}

	usleep(365);
	memset(read_buf, 0, sizeof(read_buf));
	count = read(fd, read_buf, sizeof(read_buf));
	if (count > 0) {
		for (i = 0; i < count; i++);
		temp->temp_enviromem = read_buf[7] << 8 | read_buf[6];

		printf("temp->temp_enviromem = %f\n", temp->temp_enviromem * 0.01);	
	} else {
		printf("read enviromem failed\n");
		return -1;
	}
		

	count = write(fd, cmd_buf2, 9);
	if (count != 9) {
		printf("send failed\n");
		return -1;
	}

	usleep(70000);
	memset(read_buf, 0, sizeof(read_buf));
	memset(temp->temp_col, 0, sizeof(temp->temp_col));
	count = read(fd, read_buf, sizeof(read_buf));
	printf("count = %d\n", count);
	if (count > 0) {
		for (i = 0; i < count - 7; i++)
		temp->temp_col[i] = read_buf[i+6];
		for (i = 0; i < 1536; i++)
		{
			if (!(i%10))
				printf("\n");
			printf("%#X ", temp->temp_col[i]);
		}
	} else {
		printf("read temp colour failed\n");
		return -1;
	}
	free(temp); 

	close(fd);

	tcsetattr(fd, TCSANOW, &oldtio); //恢復原先的設置

	return 0;
}
Tags: