跳轉到內容

序列程式設計/termios

來自華夏公益教科書,開放的書籍,開放的世界

termios 是較新的 (現在已經幾十歲了) Unix API,用於終端 I/O。使用 termios 執行序列 I/O 的程式的結構如下

  • 使用標準 Unix 系統呼叫 open(2) 開啟序列裝置
  • 使用特定的 termios 函式和資料結構配置通訊引數和其他介面屬性(線路紀律等)。
  • 使用標準 Unix 系統呼叫 read(2) 和 write(2) 從序列介面讀取資料,並寫入序列介面。也可以使用相關的系統呼叫,如 readv(2) 和 writev(2)。多種 I/O 技術也是可能的,例如阻塞、非阻塞、非同步 I/O (select(2) 或 poll(2),或訊號驅動 I/O (SIGIO 訊號))。選擇 I/O 技術是應用程式設計的重要組成部分。序列 I/O 需要與應用程式執行的其他 I/O 型別(如網路)協同工作,並且不能浪費 CPU 週期。
  • 完成後,使用標準 Unix 系統呼叫 close(2) 關閉裝置。

開始序列 I/I 程式時,一個重要部分是確定要部署的 I/O 技術。

termios 所需的宣告和常量可以在標頭檔案 <termios.h> 中找到。因此,序列或終端 I/O 程式碼通常從以下開始

#include <termios.h>

一些額外的函式和宣告也可以在 <stdio.h><fcntl.h><unistd.h> 標頭檔案中找到。

termios I/O API 支援兩種不同的模式:老的 termio 也支援這個嗎?如果是,將段落移至關於 Unix 中序列和終端 I/O 的通用部分)。

1. 規範模式。
這在處理真正的終端或提供逐行通訊的裝置時非常有用。終端驅動程式逐行返回資料。

2. 非規範模式。
在此模式下,不會進行任何特殊處理,終端驅動程式會返回單個字元。

在 BSD 類系統上,存在三種模式

1. 熟練模式。
輸入被組裝成行,並且處理特殊字元。

2. 原始模式。
輸入不被組裝成行,並且不處理特殊字元。

3. Cbreak 模式。
輸入不被組裝成行,但處理一些特殊字元。

除非另行設定,否則規範模式(或 BSD 下的熟練模式)是預設模式。在相應模式下處理的特殊字元是控制字元,例如行尾或退格符。特定 Unix 版本的完整列表可以在相應的 termios 手冊頁 中找到。對於序列通訊,通常建議使用非規範模式(BSD 下的原始模式或 cbreak 模式),以確保傳輸的資料不會被終端驅動程式解釋。因此,在設定通訊引數時,還應透過設定/清除相應的 termios 標誌將裝置配置為原始/非規範模式。還可以單獨啟用或停用特殊字元的處理。

此配置是使用 struct termios 資料結構完成的,該結構在 termios.h 標頭檔案中定義。此結構是序列裝置配置和查詢其設定的核心。它至少包含以下欄位

struct termios {
  tcflag_t c_iflag;    /* input specific flags (bitmask) */
  tcflag_t c_oflag;    /* output specific flags (bitmask) */
  tcflag_t c_cflag;    /* control flags (bitmask) */
  tcflag_t c_lflag;    /* local flags (bitmask) */
  cc_t     c_cc[NCCS]; /* special characters */
};

需要注意的是,真正的 struct termios 宣告通常要複雜得多。這源於 Unix 供應商對 termios 的實現,使它向後相容 termio,並將 termio 和 termios 的行為整合到同一個資料結構中,以便他們避免兩次實現相同的程式碼。在這種情況下,應用程式程式設計師可能能夠混合使用 termio 和 termios 程式碼。

可以使用 struct termios 設定 (透過 tcsetattr()) 或獲取 (透過 tcgetattr()) 超過 45 個不同的標誌。大量標誌以及它們有時難以理解和病態的含義和行為,是 Unix 下序列程式設計很困難的原因之一。在裝置配置中,必須注意不要出錯。

開啟/關閉序列裝置 0% 開發 截至 2005 年 7 月 23 日

[編輯 | 編輯原始碼]

開啟序列裝置時,需要做出一些決定。應該只為讀取、只為寫入還是同時讀取和寫入開啟裝置?應該以阻塞還是非阻塞 I/O 模式開啟裝置(建議使用非阻塞模式)?應該以獨佔模式開啟裝置,以便其他程式在開啟後無法訪問該裝置嗎?

雖然 open(2) 可以使用大量不同的標誌來控制這些屬性和其他屬性,但以下是一個典型的示例

#include <fcntl.h>
...

const char device[] = "/dev/ttyS0";
fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
if(fd == -1) {
  printf( "failed to open port\n" );
}

其中

裝置
序列埠的路徑(例如 /dev/ttyS0)
fd
返回的裝置檔案控制代碼。如果發生錯誤,則為 -1
O_RDWR
以讀寫方式開啟埠
O_NOCTTY
該埠永遠不會成為程序的控制終端。
O_NDELAY
使用非阻塞 I/O。在某些系統上,這也意味著忽略 RS232 DCD 訊號線。

注意:如果存在,O_EXCL 標誌在開啟序列裝置(如調變解調器)時會由核心靜默忽略。

在現代 Linux 系統上,像 ModemManager 這樣的程式有時會讀取和寫入您的裝置,並可能破壞您的程式狀態。為避免出現像 ModemManager 這樣的程式所導致的問題,您應該在將終端與裝置關聯後在終端上設定 TIOCEXCL。您不能使用 O_EXCL 開啟,因為它會被靜默忽略。

給定一個開啟的檔案控制代碼 fd,您可以使用以下系統呼叫關閉它

close(fd);

序列裝置配置

[編輯 | 編輯原始碼]

開啟序列裝置後,通常需要執行兩到三個任務來配置裝置。首先,您需要驗證裝置確實是一個序列裝置。其次,您需要為特定的硬體配置終端設定。此步驟包括波特率或線路規程等設定。第三,您可以選擇在終端上設定獨佔模式。配置可能是一項具有挑戰性的任務,因為介面支援許多硬體裝置,並且有超過 60 個 termios 標誌。以下示例程式碼演示了最重要的標誌。

TTY 裝置

[edit | edit source]

配置的第一步是驗證裝置是否為 tty。您可以使用 isatty 來驗證裝置是否為 tty,如下所示。

#include <termios.h>
#include <unistd.h>

//
// Check if the file descriptor is pointing to a TTY device or not.
//
if(!isatty(fd)) { ... error handling ... }

終端配置

[edit | edit source]

配置的第二步是設定終端屬性,例如波特率或線路規程。這是使用一個相當複雜的資料結構,使用 tcgetattr(3) 和 tcsetattr(3) 函式完成的。

#include <termios.h>
#include <unistd.h>

struct termios  config;

//
// Get the current configuration of the serial interface
//
if(tcgetattr(fd, &config) < 0) { ... error handling ... }

//
// Input flags - Turn off input processing
//
// convert break to null byte, no CR to NL translation,
// no NL to CR translation, don't mark parity errors or breaks
// no input parity check, don't strip high bit off,
// no XON/XOFF software flow control
//
config.c_iflag &= ~(IGNBRK | BRKINT | ICRNL |
                    INLCR | PARMRK | INPCK | ISTRIP | IXON);

//
// Output flags - Turn off output processing
//
// no CR to NL translation, no NL to CR-NL translation,
// no NL to CR translation, no column 0 CR suppression,
// no Ctrl-D suppression, no fill characters, no case mapping,
// no local output processing
//
// config.c_oflag &= ~(OCRNL | ONLCR | ONLRET |
//                     ONOCR | ONOEOT| OFILL | OLCUC | OPOST);
config.c_oflag = 0;

//
// No line processing
//
// echo off, echo newline off, canonical mode off,
// extended input processing off, signal chars off
//
config.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);

//
// Turn off character processing
//
// clear current char size mask, no parity checking,
// no output processing, force 8 bit input
//
config.c_cflag &= ~(CSIZE | PARENB);
config.c_cflag |= CS8;

//
// One input byte is enough to return from read()
// Inter-character timer off
//
config.c_cc[VMIN]  = 1;
config.c_cc[VTIME] = 0;

//
// Communication speed (simple version, using the predefined
// constants)
//
if(cfsetispeed(&config, B9600) < 0 || cfsetospeed(&config, B9600) < 0) {
    ... error handling ...
}

//
// Finally, apply the configuration
//
if(tcsetattr(fd, TCSAFLUSH, &config) < 0) { ... error handling ... }

獨佔訪問

[edit | edit source]

如果您希望確保對序列裝置的獨佔訪問,請使用 ioctl 設定 TIOCEXCL。如果您的系統包含 ModemManager 等程式,則應設定此屬性。

if (ioctl(fd, TIOCEXCL, NULL) < 0) {
    ... error handling ...
}

請注意:設定 TIOCEXCL 後,其他程式將無法開啟序列裝置。如果您的程式架構包含單獨的讀取器和寫入器,那麼您應該 fork/exec 繼承檔案描述符的唯一例項。

線路控制函式 25% developed  as of Jul 23, 2005

[edit | edit source]

termios 包含許多線路控制函式。這些函式允許在某些特殊情況下更精細地控制序列線路。它們都針對由 open(2) 呼叫開啟序列裝置返回的檔案描述符 fildes 工作。如果發生錯誤,可以在全域性 errno 變數中找到詳細原因(請參見 errno(2))。

tcdrain

[edit | edit source]
#include <termios.h>
int tcdrain(int fildes);

等待之前寫入由 fildes 指示的序列線路的所有資料都被髮送。這意味著,當 UART 的傳送緩衝區清除時,該函式將返回。
如果成功,該函式將返回 0。否則,它將返回 -1,全域性變數 errno 包含錯誤的確切原因。

不使用 tcdrain()

如今的計算機速度很快,擁有更多核心,程式碼也經過了很多最佳化。它們一起會導致奇怪的結果。在以下示例中

set_rts();
write();
clr_rts();

您會期望看到一個訊號上升,然後寫入,然後再下降。但實際上並沒有發生,儘管程式設計師的本意是如此。可能是最佳化導致核心在資料真正寫入之前報告寫入成功。

使用 tcdrain()

現在使用 tcdrain() 的同一程式碼

set_rts();
write();
tcdrain();
clr_rts();

現在程式碼的行為符合預期,因為 clr_rts(); 只在資料真正寫入時才執行。一些程式設計師透過使用 sleep()/usleep() 來解決問題,儘管這可能不是您想要的。

tcflow

[edit | edit source]
#include <termios.h>
int tcflow(int fildes, int action);

此函式暫停/重啟由 fildes 指示的序列裝置上的資料傳輸和/或接收。確切的功能由 action 引數控制。action 應該是以下常量之一

TCOOFF
暫停輸出。
TCOON
重啟之前暫停的輸出。
TCIOFF
傳輸 STOP (xoff) 字元。如果遠端裝置收到此字元,它們應該停止傳輸資料。這要求序列線路另一端的遠端裝置支援此軟體流控制。
TCION
傳輸 START (xon) 字元。如果遠端裝置收到此字元,它們應該重啟傳輸資料。這要求序列線路另一端的遠端裝置支援此軟體流控制。

如果成功,該函式將返回 0。否則,它將返回 -1,全域性變數 errno 包含錯誤的確切原因。

tcflush

[edit | edit source]
#include <termios.h>
int tcflush(int fildes, int queue_selector);

重新整理(丟棄)未傳送的資料(仍在 UART 傳送緩衝區中的資料)和/或重新整理(丟棄)已接收的資料(已在 UART 接收緩衝區中的資料)。確切的操作由 queue_selector 引數定義。queue_selector 的可能常量是

TCIFLUSH
重新整理已接收但未讀取的資料。
TCOFLUSH
重新整理已寫入但未傳送的資料。
TCIOFLUSH
重新整理兩者。

如果成功,該函式將返回 0。否則,它將返回 -1,全域性變數 errno 包含錯誤的確切原因。

tcsendbreak

[edit | edit source]
#include <termios.h>
int tcsendbreak(int fildes, int duration_flag);

傳送持續一定時間的斷開訊號。duration_flag 控制斷開訊號的持續時間

0
傳送至少 0.25 秒,不超過 0.5 秒的斷開訊號。
任何其他值
對於 0 以外的值,行為是實現定義的。一些實現將該值解釋為某些時間規範,而另一些則讓該函式的行為類似於 tcdrain()

斷開訊號是故意產生的序列資料幀(時序)錯誤 - 訊號時序透過傳送一系列零位來違反,這也包括起始/停止位,因此幀被明確刪除。

如果成功,該函式將返回 0。否則,它將返回 -1,全域性變數 errno 包含錯誤的確切原因。

讀取和設定引數

[edit | edit source]

由於介面支援不同的硬體,Unix 和 Linux 序列介面具有超過 60 個引數。這種大量的引數以及由此產生的不同的介面配置是 Unix 和 Linux 中序列程式設計具有挑戰性的原因。不僅引數如此之多,而且它們的含義對於現代駭客來說通常是未知的,因為它們起源於計算的黎明時期,那時人們以不同的方式做事,現在不再為人所知或在小駭客學校教授。

然而,Unix 中序列介面的大多數引數僅透過兩個函式控制

tcgetattr()
用於讀取當前屬性。

tcsetattr()
用於設定序列介面屬性。

有關序列介面配置的所有資訊都儲存在 struct termios 資料型別的例項中。tcgetattr() 需要指向已預先分配的 struct termios 的指標,它將寫入該指標。tcsetattr() 需要指向已預先分配並初始化的 struct termios 的指標,它將從該指標讀取值。

此外,速度引數透過一組單獨的函式設定

cfgetispeed()
獲取線路輸入速度。
cfgetospeed()
獲取線路輸出速度。
cfsetispeed()
設定線路輸入速度。
cfsetospeed()
設定線路輸出速度。

以下小節將更詳細地解釋提到的函式。

屬性更改

[edit | edit source]

可以使用單個函式讀取 Unix 中序列介面的 50 多個屬性:tcgetattr()。這些引數中包括所有選項標誌,以及例如有關應用了哪種特殊字元處理的資訊。該函式的簽名如下

#include <termios.h>
int tcgetattr(int fd, struct termios *attribs);

其中引數是

fd
指向已開啟的終端裝置的檔案控制代碼。該裝置通常透過 open(2) 系統呼叫開啟。但是,Unix 中還有幾種其他機制可以獲得合法檔案控制代碼(例如,透過 fork(2)/exec(2) 組合繼承它)。只要控制代碼指向已開啟的終端裝置,一切正常。
*attribs
指向已預先分配的 struct termios 的指標,tcgetattr() 將寫入該指標。

tcgetattr() 返回一個整數,該整數指示 Unix 系統呼叫中常見的成功或失敗

0
表示成功完成
-1
表示失敗。有關問題的更多資訊可以在全域性(或執行緒區域性)errno 變數中找到。請參見 errno(2)、intro(2) 和/或 perror(3C) 手冊頁,瞭解有關 errno 值含義的資訊。
注意;不檢查返回值並假設一切都會正常工作是典型的初學者和駭客錯誤。

以下是一個簡單示例,演示了tcgetattr()的使用。它假設標準輸入已被重定向到終端裝置。

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>

int main(void) {
    struct termios attribs;
    speed_t speed;
    if(tcgetattr(STDIN_FILENO, &attribs) < 0) {
        perror("stdin");
        return EXIT_FAILURE;
    }

    /*
     * The following mess is to retrieve the input
     * speed from the returned data. The code is so messy,
     * because it has to take care of a historic change in
     * the usage of struct termios. Baud rates were once
     * represented by fixed constants, but later could also
     * be represented by a number. cfgetispeed() is a far
     * better alternative.
     */

    if(attribs.c_cflag & CIBAUDEXT) {
        speed = ((attribs.c_cflag & CIBAUD) >> IBSHIFT)
                 + (CIBAUD >> IBSHIFT) + 1;
    }
    else
    {
        speed = (attribs.c_cflag & CIBAUD) >> IBSHIFT;
    }
    printf("input speed: %ul\n", (unsigned long) speed);

    /*
     * Check if received carriage-return characters are
     * ignored, changed to new-lines, or passed on
     * unchanged.
     */
    if(attribs.c_iflag & IGNCR) {
        printf("Received CRs are ignored.\n");
    }
    else if(attribs.c_iflag & ICRNL)
    {
        printf("Received CRs are translated to NLs.\n");
    }
    else
    {
        printf("Received CRs are not changed.\n");
    }
    return EXIT_SUCCESS;
}

編譯並連結上述程式後,假設程式名為example,可以按以下方式執行。

 ./example < /dev/ttya

假設/dev/ttya是一個有效的序列裝置。可以使用stty命令來驗證輸出是否正確。

tcsetattr()

#include <termios.h>
tcsetattr( int fd, int optional_actions, const struct termios *options );

options中定義的選項設定檔案控制代碼fd的termios結構體。optional_actions指定更改何時生效。

TCSANOW
配置立即生效。
TCSADRAIN
在寫入fd的所有輸出都傳輸完畢後,配置生效。這可以防止更改破壞正在傳輸的資料。
TCSAFLUSH
與上述相同,但任何已接收但未讀取的資料將被丟棄。

波特率設定

[編輯 | 編輯原始碼]

可以透過tcgetattr()和tcsetattr()函式讀取和設定波特率(線路速度)。這可以透過讀取或寫入必要資料到struct termios來實現。前面的tcgetattr()示例展示了直接訪問結構成員的程式碼可能很混亂。

建議使用以下函式之一,而不是直接訪問結構成員。

cfgetispeed()
獲取線路輸入速度。
cfgetospeed()
獲取線路輸出速度。
cfsetispeed()
設定線路輸入速度。
cfsetospeed()
設定線路輸出速度。

函式簽名如下:

#include <termios.h>
speed_t cfgetispeed(const struct termios *attribs);
speed
輸入波特率。
attribs
用於提取速度的struct termios
#include <termios.h>
speed_t cfgetospeed(const struct termios *attribs);
speed
輸出波特率。
attribs
用於提取速度的struct termios
#include <termios.h>
int cfsetispeed(struct termios *attribs, speed_t speed);
attribs
用於設定輸入波特率的struct termios
speed
要設定的輸入波特率。

speed引數應為預定義值之一,例如B115200B57600B9600

函式返回值:

0
如果速度可以設定(已編碼)。
-1
如果速度無法設定(例如,如果它不是有效的或支援的速度值)。
#include <termios.h>
int cfsetospeed(struct termios *attribs, speed_t speed);
attribs
用於設定輸出波特率的struct termios
speed
要設定的輸出波特率。

函式返回值:

0
如果速度可以設定(已編碼)。
-1
如果速度無法設定(例如,如果它不是有效的或支援的速度值)。

注意
如果您想知道為什麼函式名不以tc開頭,而以cf開頭,那麼您就看到了Unix API的許多特性之一。顯然,有人想顯得聰明,認為由於函式需要處理struct termiosc_flags成員,因此應該以cf開頭。這樣完全忽略了這些函式的整個目的是隱藏實現細節(c_flags),而不是公開它。這談論的是錯誤的重點。

這是一個關於cfgetispeed()的簡單示例。cfgetospeed()的工作方式非常類似。

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>

int main(void) {
    struct termios attribs;
    speed_t speed;

    if(tcgetattr(STDIN_FILENO, &attribs) < 0) {
        perror("stdin");
        return EXIT_FAILURE;
    }

    speed = cfgetispeed(&attribs);
    printf("input speed: %lu\n", (unsigned long) speed);

    return EXIT_SUCCESS;
}

cfsetispeed()和cfsetospeed()的工作方式也很簡單。以下示例將標準輸入的輸入速度設定為9600波特。請注意,此設定不會永久生效,因為裝置可能在程式結束時重置。

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>

int main(void) {
    struct termios attribs;
    speed_t speed;

    /*
     * Get the current settings. This saves us from
     * having to initialize a struct termios from
     * scratch.
     */
    if(tcgetattr(STDIN_FILENO, &attribs) < 0)
    {
        perror("stdin");
        return EXIT_FAILURE;
    }

    /*
     * Set the speed data in the structure
     */
    if(cfsetispeed(&attribs, B9600) < 0)
    {
        perror("invalid baud rate");
        return EXIT_FAILURE;
    }

    /*
     * Apply the settings.
     */
    if(tcsetattr(STDIN_FILENO, TCSANOW, &attribs) < 0)
    {
        perror("stdin");
        return EXIT_FAILURE;
    }

    /* data transmision should happen here */

    return EXIT_SUCCESS;
}

特殊輸入字元

[編輯 | 編輯原始碼]

規範模式

[編輯 | 編輯原始碼]

所有內容都會儲存到緩衝區中,並在輸入回車或換行符之前進行編輯。按下回車或換行符後,緩衝區將被髮送。

options.c_lflag |= ICANON;

其中

ICANON
啟用規範輸入模式

非規範模式

[編輯 | 編輯原始碼]

此模式將處理固定數量的字元,並允許使用字元計時器。在此模式下,輸入不會被組裝成行,並且不會發生輸入處理。這裡我們必須設定兩個引數:時間和最小字元數,以在讀取滿足條件之前接收這些字元,例如,如果我們必須將最小字元數設定為 4,並且我們不想使用任何計時器,那麼我們可以按照以下方式進行設定:-

options.c_cc[VTIME]=0;
options.c_cc[VMIN]=4;

有一些 C 函式對終端和序列 I/O 程式設計很有用,但它們不屬於終端 I/O API。這些函式是:

#include <stdio.h>
char *ctermid(char *s);

此函式將當前程序的控制終端裝置名稱作為字串返回(例如,“/dev/tty01”)。這對想要直接開啟該終端裝置以與其通訊的程式很有用,即使控制終端關聯後來被刪除了(因為例如,程序fork/exec成為守護程序)。
*s可以是NULL,也可以指向至少L_ctermid位元組的字元陣列(該常量也在stdio.h中定義)。如果*sNULL,則使用一些內部靜態字元陣列,否則使用提供的陣列。在這兩種情況下,都將返回指向字元陣列第一個元素的指標。

#include <unistd.h>
int isatty(int fildes)

檢查提供的檔案描述符是否代表終端裝置。例如,這可以用於確定裝置是否能理解透過終端 I/O API 傳送的命令。

#include <unistd.h>
char *ttyname (int fildes);

此函式將由檔案描述符表示的終端裝置的裝置名稱作為字串返回。

#include <sys/ioctl.h>
ioctl(int fildes, TIOCGWINSZ, struct winsize *);
ioctl(int fildes, TIOCSWINSZ, struct winsize *);

這些 I/O 控制允許獲取和設定終端模擬的視窗大小,例如,以畫素和字元大小表示的xterm。通常情況下,get 變體(TIOCGWINSZ)與 SIGWINCH 訊號處理程式結合使用。當大小發生變化時(例如,由於使用者調整了終端模擬視窗的大小),訊號處理程式會被呼叫,應用程式會使用 I/O 控制來獲取新大小。

調變解調器

[編輯 | 編輯原始碼]

調變解調器對於許多使用者來說仍然很常見,例如那些在中美洲和南美洲以及非洲使用撥號連線的人。此外,與電話公司服務整合的使用者將使用調變解調器進行傳真和呼叫阻止。調變解調器在管理透過蜂窩網路和其他無線網路通訊的更現代應用程式中也很常見,因為有幾個無線模組供應商為系統開發人員提供解決方案,使他們能夠輕鬆地將無線訪問新增到他們的產品產品中。這些模組通常透過調變解調器 API 進行控制。本節將提供有關調變解調器的配置和操作資訊。

調變解調器配置

[編輯 | 編輯原始碼]

通常有兩種方法可以配置調變解調器,或多或少。首先,您可以修改現有的檔案描述符。其次,您可以使用cfmakeraw並應用新的配置標誌。

修改現有檔案描述符將使用類似於以下程式碼的程式碼。該示例基於 Mike Sweet 的POSIX 作業系統序列程式設計指南

int fd;
struct termios options;

/* open the port */
fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);
fcntl(fd, F_SETFL, 0);

/* get the current options */
tcgetattr(fd, &options);

/* set raw input, 1 character trigger */
options.c_cflag     |= (CLOCAL | CREAD);
options.c_lflag     &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag     &= ~OPOST;
options.c_cc[VMIN]  = 1;
options.c_cc[VTIME] = 0;

/* set the options */
tcsetattr(fd, TCSANOW, &options);

配置調變解調器的第二種方法是使用cfmakeraw,如下所示。

int fd;
struct termios options;

/* open the port */
fd = open("/dev/ttyACM0", O_RDWR | O_NOCTTY | O_SYNC);

/* raw mode, like Version 7 terminal driver */
cfmakeraw(&options);
options.c_cflag |= (CLOCAL | CREAD);

/* set the options */
tcsetattr(fd, TCSANOW, &options);

在這兩種情況下,您都應該根據您的特定調變解調器調整options。您需要查閱調變解調器的文件,瞭解線路規程、起始位和停止位、UART 速度等設定。

振鈴次數

[編輯 | 編輯原始碼]

操作連線到電話網路的調變解調器的軟體通常執行檢測振鈴、收集呼叫者 ID 資訊、接聽電話和播放訊息等任務。該軟體通常會維護一個依賴於振鈴次數的狀態機。

可以透過寫入ATS1?並讀取響應來從S1-parameter暫存器中檢索振鈴次數。響應將類似於以下內容,其中ATE1echo=on)。

ATS1?
001
OK

下面展示了使用振鈴次數的典型工作流程式列。

RING  # unsolicited message from the modem
count = read S1  # program reads ring count
NAME = JOHN DOE
NMBR = 4105551212
DATE = 0101  # month and date
TIME = 1345  # hour and minute
RING
count = read S1
RING
count = read S1
RING
count = read S1
...

一些版本的 US Robotics 調變解調器,比如 USR5637,在讀取 `S1` 時存在韌體錯誤。這個錯誤會導致讀取 `S1` 暫存器會破壞來電顯示訊息。如果你使用的是受影響的 USR 調變解調器,你的程式會觀察到以下情況。

RING  # unsolicited message from the modem
count = read S1  # program reads ring count
RING  # No Caller ID message
count = read S1
...

如果你使用的是受影響的調變解調器,你應該 _不要_ 使用 `S1` 暫存器。相反,維護一個內部時間戳,在 8 秒後重置。程式碼看起來類似於以下內容。

/* Last activity for ring count. We used to read S1 and the modem would  */
/* return 1, 2, 3, etc. Then we learned reading S1 is destructive on     */
/* USR modems. Reading S1 between Ring 1 and Ring 2 destroys Caller ID   */
/* information. Caller ID is never sent to the DTE for USR modems.       */
static time_t s_last = 0;

/* Conexant modems reset the ring count after 8 seconds of inactivity.  */
/* USR modems reset the ring count after 6 seconds of inactivity.       */
/* We now do this manually to track ring state due to USR modems.       */
#define INACTIVITY 8
static int s_count = 0;

int get_ring_count(const char* msg)
{
    /* Only increment ring count on a RING message */
    /* Otherwise, return the current count         */
    if (strstr(msg, "RING") ==  NULL) {
        return s_count;
    }

    /* Number of seconds since epoch */
    time_t now = time(NULL);
    if (now >= s_last + INACTIVITY) {
        /* 8 seconds have passed since the last ring. */
        /* This is a new call. Set count to 0.        */
        s_count = 0;
    }

    /* Only increment ring count on a RING message */
    s_count++;
    s_last = now;
    return s_count;
}

常見問題

[編輯 | 編輯原始碼]

Mike Sweet 在 POSIX 作業系統序列程式設計指南 中提供了以下建議。

  • 不要忘記停用輸入回顯。輸入回顯會導致調變解調器和計算機之間的反饋迴圈。
  • 你必須使用回車符 (CR) 而不是換行符 (NL) 來結束調變解調器命令。CR 的 C 字元常量是 `\r`。
  • 確保你使用的是調變解調器支援的波特率。雖然許多調變解調器都支援自動波特率檢測,但有些調變解調器有你必須遵守的限制(19.2kbps 很常見)。

示例終端程式

[編輯 | 編輯原始碼]

一個簡單的使用 termios.h 的終端程式看起來像這樣。

警告:在這個程式中,VMIN 和 VTIME 標誌被忽略,因為設定了 O_NONBLOCK 標誌。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h> // needed for memset

int main(int argc,char** argv)
{
        struct termios tio;
        struct termios stdio;
        int tty_fd;
        fd_set rdset;

        unsigned char c='D';

        printf("Please start with %s /dev/ttyS1 (for example)\n",argv[0]);
        memset(&stdio,0,sizeof(stdio));
        stdio.c_iflag=0;
        stdio.c_oflag=0;
        stdio.c_cflag=0;
        stdio.c_lflag=0;
        stdio.c_cc[VMIN]=1;
        stdio.c_cc[VTIME]=0;
        tcsetattr(STDOUT_FILENO,TCSANOW,&stdio);
        tcsetattr(STDOUT_FILENO,TCSAFLUSH,&stdio);
        fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK);       // make the reads non-blocking

        memset(&tio,0,sizeof(tio));
        tio.c_iflag=0;
        tio.c_oflag=0;
        tio.c_cflag=CS8|CREAD|CLOCAL;           // 8n1, see termios.h for more information
        tio.c_lflag=0;
        tio.c_cc[VMIN]=1;
        tio.c_cc[VTIME]=5;

        tty_fd=open(argv[1], O_RDWR | O_NONBLOCK);        // O_NONBLOCK might override VMIN and VTIME, so read() may return immediately.
        cfsetospeed(&tio,B115200);            // 115200 baud
        cfsetispeed(&tio,B115200);            // 115200 baud

        tcsetattr(tty_fd,TCSANOW,&tio);
        while (c!='q')
        {
                if (read(tty_fd,&c,1)>0)        write(STDOUT_FILENO,&c,1);              // if new data is available on the serial port, print it out
                if (read(STDIN_FILENO,&c,1)>0)  write(tty_fd,&c,1);                     // if new data is available on the console, send it to the serial port
        }

        close(tty_fd);
}

termio vs termios

[編輯 | 編輯原始碼]

有時你可能會遇到一個名為 _termio_ 的結構體。這是一箇舊的 SYSV 介面。該結構體比 _termios_ 小。該介面在 POSIX.1-1990 中被替換,不應該出現在較新的程式中。儘可能使用 tcsetattr() 及其相關函式。

華夏公益教科書