跳转到内容

串口编程/termios

来自 Wikibooks,开放的书籍,开放的世界

termios 是更新的(现在已有几十年的历史)Unix 终端 I/O API。使用 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% developed  as of Jul 23, 2005

[编辑 | 编辑源代码]

在打开串口设备时,需要做出一些决定。是否应仅为读取、仅为写入或同时为读取和写入打开设备?是否应为阻塞或非阻塞 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);

串行设备配置

[edit | edit source]

串行设备打开后,通常需要执行两到三项任务来配置设备。首先,您需要验证设备确实是一个串行设备。其次,您需要为特定硬件配置终端设置。此步骤包括波特率或线路规程等设置。最后,您可以选择在终端上设置独占模式。配置可能是一项具有挑战性的任务,因为该接口支持许多硬件设备,并且有超过 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 串行编程具有挑战性的原因。不仅有这么多参数,而且它们的含义对于当代黑客来说往往是未知的,因为它们起源于计算的早期,当时做事方式不同,并且不再在 Little-Hacker School 中学习或教授。

但是,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值的含义,请参见 errno(2),intro(2) 和/或 perror(3C) 手册页。
注意;不检查返回值并假设一切都将正常工作是初学者和黑客的典型错误。

以下是一个演示 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);
速度
输入波特率。
属性
要从中提取速度的struct termios
#include <termios.h>
speed_t cfgetospeed(const struct termios *attribs);
速度
输出波特率。
属性
要从中提取速度的struct termios
#include <termios.h>
int cfsetispeed(struct termios *attribs, speed_t speed);
属性
应在其中设置输入波特率的struct termios
速度
应设置的输入波特率。

speed参数应该是预定义值之一,如B115200B57600B9600

函数返回

0
如果速度可以设置(编码)。
-1
如果速度无法设置(例如,如果它不是有效或支持的速度值)。
#include <termios.h>
int cfsetospeed(struct termios *attribs, speed_t speed);
属性
应在其中设置输出波特率的struct termios
速度
应设置的输出波特率。

函数返回

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
启用规范输入模式

非规范模式

[编辑 | 编辑源代码]

此模式将处理固定数量的字符,并允许使用字符计时器。在此模式下,输入不会组装成行,并且不会发生输入处理。在这里,我们必须设置两个参数,时间和在读取满足之前要接收的最小字符数,这些参数通过设置 VTIME 和 VMIN 字符来设置,例如,如果我们必须将最小字符数设置为 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,则使用一些内部静态 char 数组,否则使用提供的数组。在这两种情况下,都会返回指向 char 数组第一个元素的指针

#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。通常,获取变体(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 速度等设置的信息。

振铃计数

[编辑 | 编辑源代码]

在电话网络上运行调制解调器的软件通常会执行诸如检测振铃、收集来电显示信息、接听电话和播放消息之类的任务。该软件通常会维护一个状态机,该状态机取决于振铃计数。

可以通过写入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) 来终止调制解调器命令。C 字符常量用于 CR 是 \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() 及其相关函数。

华夏公益教科书