串口编程/termios
此页面或部分内容是一个未完成的草稿或提纲。 您可以帮助 完善作品,也可以在 项目室 寻求帮助。 |
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 下串口编程困难的原因之一。在设备配置中,必须注意不要出错。
在打开串口设备时,需要做出一些决定。是否应仅为读取、仅为写入或同时为读取和写入打开设备?是否应为阻塞或非阻塞 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
继承文件描述符的唯一实例。
线路控制函数
[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
包含错误的具体原因。
如今的计算机速度很快,拥有更多内核,并且代码会进行很多优化。它们在一起会导致奇怪的结果。在下面的示例中
set_rts();
write();
clr_rts();
您预计会看到一个信号上升,然后写入,最后信号下降。但尽管程序员的本意如此,但并没有发生这种情况。也许优化导致内核在数据真正写入之前报告写入成功。
现在使用 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()
本节是一个存根。 您可以通过扩展它来帮助 Wikibooks。 |
#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
参数应该是预定义值之一,如B115200
、B57600
或B9600
。
函数返回
- 0
- 如果速度可以设置(编码)。
- -1
- 如果速度无法设置(例如,如果它不是有效或支持的速度值)。
#include <termios.h>
int cfsetospeed(struct termios *attribs, speed_t speed);
- 属性
- 应在其中设置输出波特率的
struct termios
。 - 速度
- 应设置的输出波特率。
函数返回
- 0
- 如果速度可以设置(编码)。
- -1
- 如果速度无法设置(例如,如果它不是有效或支持的速度值)。
这是一个简单的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;
}
本节是一个存根。 您可以通过扩展它来帮助 Wikibooks。 |
本节是一个存根。 您可以通过扩展它来帮助 Wikibooks。 |
所有内容都存储在缓冲区中,并且可以编辑,直到输入回车键或换行符。按下回车键或换行符后,缓冲区将被发送。
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中定义)。如果*s 是NULL
,则使用一些内部静态 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
寄存器中检索振铃计数。该响应将类似于以下内容,其中ATE1
(echo=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 的结构。这是一个旧的 SYSV 接口。该结构比 termios 小。该接口在 POSIX.1-1990 中被替换,不应该出现在较新的程序中。尽可能使用 tcsetattr() 及其相关函数。