跳转到内容

串行编程/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() 及其相关函数。

华夏公益教科书