1. 程式人生 > >Linux串列埠程式設計詳解

Linux串列埠程式設計詳解

串列埠本身,標準和硬體 

串列埠是計算機上的序列通訊的物理介面。計算機歷史上,串列埠曾經被廣泛用於連線計算機和終端裝置和各種外部裝置。雖然乙太網介面和USB介面也是以一個序列流進行資料傳送的,但是串列埠連線通常特指那些與RS-232標準相容的硬體或者調變解調器的介面。雖然現在在很多個人計算機上,原來用以連線外部裝置的串列埠已經廣泛的被USB和Firewire替代;而原來用以連線網路的串列埠則被乙太網替代,還有用以連線終端的串列埠裝置則已經被MDA或者VGA取而代之。但是,一方面因為串列埠本身造價便宜技術成熟,另一方面因為串列埠的控制檯功能RS-232標準高度標準化並且非常普及,所以直到現在它仍然被廣泛應用到各種裝置上。 某些計算機使用一個叫做UART的積體電路來作為串列埠裝置。這個積體電路可以進行字元和非同步序列通訊序列之間的轉換,並且可以自動地處理資料的時序。而某些低端裝置則會讓CPU直接通過輸出針來傳送資料,這種技術叫做bit-banging。 因為“串列埠”,RS-232和UARTs基本上總是在同一個語境中出現,所以這些名詞通常會被搞混。下面逐一解釋以下一些重要的名詞和術語。

什麼是序列通訊 

計算機可以每次傳送一個或者多個位(bit)的資料。“序列”指的式每次只傳輸一位(1bit)資料。 當需要通過序列通訊傳輸一個字(word)的資料時,只能以每次一位的方式接收或者傳送。每個位可能是on(1)或者off(0)。很多技術術語中經常用mark表示on,而space表示off。

序列資料的速度通常用每秒傳輸的位元組數bits-per-second(bps)或者波特率(baud)表示。這個值表示的是每秒鐘被送出的0和1的個數。很久很久以前,300bps就是很快的速度了,而現在的電腦可以處理高達430,800的RS-232速率。表示波特率的單位還有kpbs和Mbps,1kps=1000bps而1Mbps=1000kbps。 一般有人提到序列裝置的時候,它通常說可能是某種資料通訊裝置-DCE(Data Communications Equipment)或者資料終端裝置-DTE(Data Terminal Equipment)。它們之間的區別非常簡單,每個訊號對,比如傳送和接收,它們倆正好是相反的。如果需要將兩個DTE或者DCE裝置連線起來的話,需要介面卡或者交叉線纜將訊號對交換。

什麼是RS-232 

RS-232是EIA(Electronic Industries Association)定義的序列通訊的電器介面。RS-232事實上有三種(A,B和C),它們分別採用不同的電壓來表示on和off。最被廣泛使用的是RS-232C,它將mark(on)位元的電壓定義為-3V到-12V之間,而將space(off)的電壓定義到+3V到+12V之間。雖然RS-232C標準說訊號最遠被傳輸8m,但事實上你可以使用它傳輸更長的距離,直到訊號波特率已經小到不行了為止。 RS-232的連結線中除去用來傳入傳出資料的電線,還有一些用來提供時序,狀態和握手的電線:

RS-232 針腳定義

DB-25

針腳 描述 針腳 描述 針腳 描述 針腳 描述 針腳 描述
1 Earth Ground 6 DSR - Data Set Ready 11 Unassigned 16 Secondary RXD 21 Signal Quality Detect
2 TXD - Transmitted Data 7 GND - Logic Ground 12 Secondary DCD 17 Receiver Clock 22 Ring Detect
3 RXD - Received Data 8 DCD - Data Carrier Detecter 13 Secondary CTS 18 Unassigned 23 Data Rate Select
4 RTS - Request To Send 9 Reserved 14 Secondary TXD 19 Secondary RTS 24 Transmit Clock
5 CTS - Clear To Send 10 Reserved 15 Transmit Clock 20 DTR - Data Terminal Ready 25 Unassigned

DB-9

針腳 名稱 全名 方向(主機 外設)
3 TD Transmit Data ->
2 RD Receive Data <-
7 RTS Request To Send ->
8 CTS Clear To Send <-
6 DSR Data Set Ready <-
4 DTR Data Terminal Ready ->
1 CD Data Carrier Detect <-
9 RI Ring Indicator <-
5 - Signal Ground  

另外兩個比較常見的序列介面的標準式RS-422和RS-574。RS-422使用更低的電壓和差分訊號,這樣可以將傳輸距離擴張到300m。而RS-574定義了通常可以見到的用在電腦上的9針聯結器和電壓。

訊號定義 

RS-232標準定義了18個不同的序列通訊的訊號。而這些之中,僅僅有如下6個可以在UNIX環境中使用。

  • GND - Logic Ground

    從技術角度講,GND不能算是訊號。但是沒有它其他訊號都不能用了。基本上,logic ground有點像一個參考電壓,通過它來判斷哪個電壓表示正哪個電壓表示負。

  • TXD - Transmitted Data

    TXD訊號負載著從你的電腦或者裝置到另一端(比如調變解調器)的資料。Mark範圍的電壓被解析成1,而space範圍電壓被解析成0。

  • RXD - Received Data

    RXD於TXD正好相反。它負載著從另一端的電腦或者裝置上傳到你的工作站的資料。Mark和space的解析方法於TXD一致。

  • DCD - Data Carrier Detect

    DCD訊號通常來自串列埠連結線的另一端。這條訊號線上的space電壓表示另一端的電腦或者裝置現在已經連線。但是,DCD訊號線卻不是總可以得到的,有些裝置上有這條訊號線,而有的則沒有。

  • DTR - Data Terminal Ready

    DTR訊號是你的工作站產生的,用以告訴另一端的電腦或者裝置你已經是否已經準備好了。Space電壓表示準備好了,而mark電壓表示沒有準備好。當你在工作站上開啟序列介面時,DTR通常自動被設定位有效。

  • CTS - Clear To Send

    CTS則通常來自連結線的另一端。Space電壓表示你可以從工作站送出更多的資料。CTS通常用來協調你的工作站和另一端之間的序列資料流。

  • RTS - Request To Send

    如果RTS訊號被設定成space電壓,這表示你準備好了一些資料需要傳送。和CTS一樣,RTS也被用來協調工作站和另一端的電腦或者裝置之間的資料流。有些工作站上會一直將這個訊號設定位space。

非同步通訊 

計算機為了弄懂傳給它的序列資料,它需要確定每個字元開始和結束的位置。這通常是用非同步序列資料來完成的。

在非同步模式中,除非有字元被傳輸,否則序列資料線總是處於mark(1)狀態。有一個start位會被加入傳輸字元的各個位之前,在字元本身的位之後會有一個可選的parity位和一個或者多個stop位。Start位總是space(0)並且它會告訴計算機新的序列資料過來了。資料可以隨時被送出或者接收,這就是所謂的非同步。

#ref(): File not found: "async.gif" at page "Linux串列埠程式設計詳解"

那個可選的parity位僅僅是所有傳輸位的和,這個和用以表示傳輸字元中有奇數個1還是偶數個1。在偶數parity中,如果有傳輸字元中有偶數個1,那麼parity位被設定成0,而傳輸字元中有奇數個1,那麼parity位被設定成1。在奇數parity中,位設定與此相反。還有一些術語,比如space parity, mark parity和no parity。Space parity是指parity位會一直被設定位0,而mark parity正好與此相反,parity會一直是1。No parity的意思就是根本不會傳輸parity位。 剩餘的位叫做stop位。傳輸字元之間可以有1個,1.5個或者2個stop位,而且,它們的值總是1。傳統上,Stop位式用給計算機一些時間處理前面的字元的,但是它只是被用來同步接收資料的計算機和接受的字元。 非同步資料通常被表示成"8N1","7E1",或者與此類似的形式。這表示“8資料位,no parity和1個stop bit”,還有相應得,“7資料位,even parity和1個stop bit”。

什麼是全雙工和半雙工 

全雙工(Full duplex)是說計算機可以同時接受和傳送資料——也就是它有兩個分開的資料傳輸通道(一個傳入,一個傳出)。

半雙工(Half duplex)表示計算機不能同時接受和傳送資料,而在某一時刻它只能單一的傳送或者接收。這通常意味著,它只有一個數據通道。半雙工並不是說RS-232的某些訊號不能使用,而是,它通常是使用了有別於RS-232的其他不支援全雙工的標準。

什麼是流控制 

兩個序列介面之間的傳輸資料流通常需要協調一致才行。這可能是由於用以通訊的某個序列介面或者某些儲存介質的中間序列通訊鏈路的限制造成的。對於非同步資料這裡有兩個方法做到這一點。

第一種方法通常被叫做“軟體”流控制。這種方法採用特殊字元來開始(XON,DC1,八進位制數021)或者結束(XOFF,DC3或者八進位制數023)資料流。而這些字元都在ASCII中定義好了。雖然這些編碼對於傳輸文字資訊非常有用,但是它們卻不能被用於在特殊程式中的其他型別的資訊。

第二種方法叫做“硬體”流控制。這種方法使用RS-232標準的CTS和RTS訊號來取代之前提到的特殊字元。當準備就緒時,接受一方會將CTS訊號設定成為space電壓,而尚未準備就緒時它會被設定成為mark電壓。相應得,傳送方會在準備就緒的情況下將RTS設定成space電壓。正因為硬體流控制使用了於資料分隔的訊號,所以與需要傳輸特殊字元的軟體流控制相比它的速度很快。但是,並不是所有的硬體和作業系統都支援CTS/RTS流控制。

什麼是BREAK 

通常,直到有資料傳輸時,接收和傳輸訊號會保持在mark電壓。如果一個訊號掉到space電壓並且持續了很長時間,一般來說是1/4到1/2秒,那麼就說有一個break條件存在了。

BREAK經常被用來重置一條資料線或者用來改變像調變解調器這樣的裝置的通訊模式。

同步通訊 

與非同步資料不同,同步資料是一個穩定的位元組流。為了能夠線上路上讀取到資料,計算機必須提供或者接受一個時鐘,這樣才能保證傳送端和接收端同步。儘管已經有同步時鐘,計算機還是必須以某種方式標誌資料流的開端。做這件事情最常見的辦法就是使用像Serial Data Link Control("SDLC")或者High-Speed Data Link Control("HDLC")這樣的資料包通訊協議。

這些協議每個都定義了一個確定的位元序列來表示資料包的開始和結束。當然,它們也定義了一個用來表示沒有資料傳輸的位元序列。這些位元序列可以幫助計算機識別資料包的開端。

因為同步協議可以不使用每個字元的同步位元位,所以通常它們的效能比非同步通訊快最少25%,而且一般比較適用於遠距離的網路連結或者有兩個串列埠介面的配置的情況。儘管同步通訊的速度有優勢,大部分RS-232硬體卻不支援它,因為同步通訊需要其他的硬體和軟體。

使用者看到的串列埠和使用者空間的串列埠程式設計 

和其他裝置一樣,Linux也是通過裝置檔案來提供訪問串列埠的功能。當需要訪問串列埠的時候,你只需要open相應的檔案。

串列埠的裝置檔案 

Linux系統上一般有一個或者多個串列埠,而這些串列埠裝置檔名字比較奇怪,如比下面這樣

串列埠裝置檔名

作業系統 串列埠1 串列埠2 USB/RS-232轉換器
Windows COM1 COM2 -
Linux /dev/ttyS0 /dev/ttyS1 /dev/ttyUSB0

開啟串列埠 

因為串列埠和其他裝置一樣,在類Unix系統中都是以裝置檔案的形式存在的,所以,理所當然得你可以使用open(2)系統呼叫/函式來訪問它。但Linux系統中卻有一個稍微不方便的地方,那就是普通使用者一般不能直接訪問裝置檔案。你可以選擇以下方式做一些調整,以便你編寫的程式可以訪問串列埠。

  • 改變裝置檔案的訪問許可權設定 [#cd9bd1e0]
  • 以root超級使用者的身份執行程式 [#kdd0e577]
  • 將你的程式編寫位setuid程式,以串列埠裝置所有者的身份執行程式 [#s7b703ff]

OK.假如你已經準備好了讓串列埠裝置檔案可以被所有使用者訪問,你可以在Linux系統中實驗一下下面這個程式,它可以開啟計算機的串列埠1。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h> /* File control definitions */
#include <errno.h>
#include <termios.h> /* POSIX terminal control definitions */

/*
 * 'open_port()' - Open serial port 1
 * Returns the file descriptor on success or -1 on error.
 */

int open_port(void)
{
	int fd; /* File descriptor for the port */

	fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
	if (fd == -1)
	{
		/*
		 * Could not open the port.
		 */
		perror("open_port: Unable to open /dev/ttyS0 -");
	}
	else
	{
		fcntl(fd, F_SETFL, 0);
		return (fd);
	}
}

開啟檔案的選項 

開啟串列埠連線的時候,程式在open函式中除了Read+Write模式以外還指定了兩個選項;

fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);

標誌O_NOCTTY可以告訴UNIX這個程式不會成為這個埠上的“控制終端”。如果不這樣做的話,所有的輸入,比如鍵盤上過來的Ctrl+C中止訊號等等,會影響到你的程序。而有些程式比如getty(1M/8)則會在開啟登入程序的時候使用這個特性,但是通常情況下,使用者程式不會使用這個行為。

O_NDELAY標誌則是告訴UNIX,這個程式並不關心DCD訊號線的狀態——也就是不關心埠另一端是否已經連線。如果不指定這個標誌的話,除非DCD訊號線上有space電壓否則這個程式會一直睡眠。

給埠上寫資料 

給埠上寫入資料也很簡單,使用write(2)系統呼叫就可以傳送資料了:

n = write(fd, "ATZ\r", 4);
if (n < 0)
        fputs("write() of 4 bytes failed!\n", stderr);

和寫入其他裝置檔案的方式相同,write函式也會返回傳送資料的位元組數或者在發生錯誤的時候返回-1。通常,傳送資料最常見的錯誤就是EIO,當調變解調器或者資料鏈路將Data Carrier Detect(DCD)訊號線弄掉了,就會發生這個錯誤。而且,直至關閉埠這個情況會一直持續。

從埠上讀取資料 

從串列埠上讀取資料的時候就得耍花招了。因為,如果你在原資料模式(raw data mode)操作埠的話,每個read(2)系統呼叫都會返回從串列埠輸入緩衝區中實際得到的字元的個數。在不能得到資料的情況下,read(2)系統呼叫就會一直等著,只到有埠上新的字元可以讀取或者發生超時或者錯誤的情況發生。如果需要read(2)函式迅速返回的話,你可以使用下面這個方式:

fcntl(fd, F_SETFL, FNDELAY);

標誌FNDELAY可以保證read(2)函式在埠上讀不到字元的時候返回0。需要回到正常(阻塞)模式的時候,需要再次在不帶FNDELAY標誌的情況下呼叫fcntl(2)函式:

fcntl(fd, F_SETFL, 0);

當然,如果你最初就是以O_NDELAY標誌開啟串列埠的,你也可在之後使用這個方法改變讀取的行為方式。

關閉串列埠 

可以使用close(2)系統呼叫關閉串列埠:

close(fd);

關閉串列埠會將DTR訊號線設定成low,這會導致很多調變解調器掛起。

配置串列埠 

POSIX終端介面 

很多系統都支援POSIX終端(串列埠)介面。程式可以利用這個介面來改變終端的引數,比如,波特率,字元大小等等。要使用這個埠的話,你必須將<termios.h>標頭檔案包含到你的程式中。這個標頭檔案中定義了終端控制結構體和POSIX控制函式。

與串列埠操作相關的最重要的兩個POSIX函式可能就是tcgetattr(3)和tcsetattr(3)。顧名思義,這兩個函式分別用來取得設設定終端的屬性。呼叫這兩個函式的時候,你需要提供一個包含著所有串列埠選項的termios結構體:

termios結構體成員

成員 描述
c_cflag 控制選項
c_lflag 行選項
c_iflag 輸入選項
c_oflag 輸出選項
c_cc 控制字元
c_ispeed 輸入波特率(NEW)
c_ospeed 輸出波特率(NEW)

控制選項 

通過termios結構體的c_cflag成員可以控制波特率,資料的位元數,parity,停止位和硬體流控制。下面這張表列出了所有可以使用的常數。

c_cflag常數

常量 描述
CBAUD Bit mask for baud rate
B0 0 baud (drop DTR)