I/O 在使用 unix STTY 更改后尝试在 C 程序中更改串行波特时出错
I/O error when trying to change serial baud in C program after changing with unix STTY
我注意到一些奇怪但可重现的东西。
我先检查一下我的串口设置:
bash-3.1# stty -F /dev/ttyS0
speed 0 baud; line = 0;
intr = <undef>; quit = <undef>; erase = <undef>; kill = <undef>; eof = <undef>; start = <undef>;
stop = <undef>; susp = <undef>; rprnt = <undef>; werase = <undef>; lnext = <undef>; flush = <undef>;
min = 1; time = 0;
-cread
-brkint -icrnl -imaxbel
-opost -onlcr
-isig -icanon -iexten -echo -echoe -echok -echoctl -echoke
然后将速度更改为 1200bps:
bash-3.1# stty -F /dev/ttyS0 1200
然后我在函数中执行我的程序片段以更改波特率:
fd=open(dev,O_NOCTTY | O_NONBLOCK | O_RDWR);
struct termios ser[1];
tcflush(fd,TCIFLUSH);
tcflush(fd,TCOFLUSH);
cfmakeraw(ser);
// I call tcsetattr after each terminal setting to make sure its applied.
if (tcsetattr(fd,TCSANOW,ser) < 0){
return -1;
}
cfsetspeed(ser,B9600);
if (tcsetattr(fd,TCSANOW,ser) < 0){
return -2; //returns this after manually setting port via STTY
}
问题是波特率没有正确改变。事实上,我从函数和 strerror(errno) returns "input/output error" 中返回了 -2。
程序执行后,我检查系统端口设置:
bash-3.1# stty -F /dev/ttyS0
speed 0 baud; line = 0;
intr = <undef>; quit = <undef>; erase = <undef>; kill = <undef>; eof = <undef>; start = <undef>;
stop = <undef>; susp = <undef>; rprnt = <undef>; werase = <undef>; lnext = <undef>; flush = <undef>;
min = 1; time = 0;
-cread
-brkint -icrnl -imaxbel
-opost -onlcr
-isig -icanon -iexten -echo -echoe -echok -echoctl -echoke
即使我特别要求 9600bps,它也会重置为零 bps。
为什么要这样做?以及如何以编程方式强制速度达到 9600bps?
你的代码有很多错误。
- 你用
O_NONBLOCK
打开 tty 设备,所以当你发出 ioctl
调用时(tc*attr(3)
调用导致 ioctl(2)
系统调用,这取决于你的 unix 风格正在使用)您不知道设备是否已经打开以便能够进行 tc*attr(3)
调用。这同样适用于 O_NOCTTY
标志。您在不知道 open 系统调用的功能的情况下放置了这些标志。 O_NOCTTY
在来自会话内部的 运行 程序中是无用的,并且 O_NONBLOCK
将使您对 return 的 tc*attr(3)
调用出现错误(EAGAIN
) 如果设备还没有打开,在尝试进行参数调整时。
- 您没有检查
open(2)
调用的结果。如果您尝试使用 -1
作为文件描述符(ENODEV
、ENOTTY
或 EBADF
、EINVAL
、ENXIO
,这可能会出错等等)
- 你没有初始化
struct termios
结构的数据,所以这可能是你得到错误的原因。如您所示(您的示例代码片段不完整,告诉您阅读 How to create a Minimal, Complete, and Verifiable example 的一个原因)您使用的 struct termios
是在自动变量上声明的(因为它的声明嵌入到代码中)所以它肯定是未初始化的并且带有垃圾数据。您通常需要对其执行 tcgetattr()
以初始化为正确的值,并能够在程序结束后恢复设置。
当连接到 tty 设备时,bash(1)
使 ioctl(2)
s 在标准输入描述符上设置和获取 termios 参数。如果你正在使用stdin,你必须考虑bash(1)
的干扰。这使得您获得的值与您使用 stty
. 设置的值不同
- 在一般的 unix 操作系统中(恐怕在 linux 中不是这样)最后一次关闭设备通常会将 tty 的参数重置为标准的固定值,所以当你设置的标志使用 stty 更改非标准输入设备(不是标准输入,stty 完成时未最后关闭),一旦
stty
终止(在 tty 最后关闭时),这些参数将重置为默认值。在发出 stty(1)
命令之前执行 sleep 999999999 </dev/ttyBlaBla &
,以便在使用 stty(1)
. 设置后端口保持打开状态(通过 sleep
命令的重定向)
- 阅读
termios(3)
页面,因此您可以从程序本身设置参数。只有当您的程序通常不处理设置参数时,您才不必以编程方式进行。但是这样修改终端参数就没有意义了,所以最好学会如何编程设备参数。
正确的做法应该是这样的(从您的代码片段中复制并编辑):
#include <string.h> /* for strerror */
#include <errno.h> /* for errno definition */
/* ... */
fd=open(dev,O_NOCTTY | O_NONBLOCK | O_RDWR);
if (fd < 0) {
fprintf(stderr, "OPEN: %s (errno = %d)\n",
strerror(errno), errno);
return -1;
}
struct termios ser; /* why an array? */
tcflush(fd,TCIFLUSH); /* unneeded, you have not used the tty yet */
tcflush(fd,TCOFLUSH); /* idem. */
/******* THIS IS THE MOST IMPORTANT THING YOU FORGOT ***********/
int res = tcgetattr(fd, &ser); /* *****this initializes the struct termios ser***** */
if (res < 0) {
fprintf(stderr, "TCGETATTR: %s (errno = %d)\n",
strerror(errno), errno);
return -2; /* cannot tcgetattr */
}
/***************************************************************/
cfmakeraw(&ser); /* now it is valid to set it */
cfsetspeed(&ser,B9600); /* better do all in only one system call */
// I call tcsetattr after each terminal setting to make sure its applied.
/* nope, tcsetattr and tcgetattr are the only calls that make something
* on the tty, the rest only manipulate bits on the struct termios
* structure, but don't do anything to the terminal, you begun with a
* trashed struct termios, so it's normal you end with an error. */
if ((res = tcsetattr(fd, TCSANOW, &ser)) < 0){
fprintf(stderr, "ERROR: %s (errno = %d)\n",
strerror(errno), errno); /* better to know what happened. */
return -3; /* couldn't tcsetattr */
}
最后,这段代码(首先是你的)还没有经过测试,主要是因为你没有发布一个完整的、最小的和可验证的例子。因此,在将其包含在您的代码中之前,您可能需要对其进行一些修改。并请RTFM(最后的意思完整阅读termios(3)
,最重要的是:How to create a Minimal, Complete, and Verifiable example):)。此外,如果您正在使用 bash(1)
,请不要检查 stdin 上的 tty 设置,因为它通常会在命令退出后恢复 tty
设置,然后再发出提示。
我注意到一些奇怪但可重现的东西。
我先检查一下我的串口设置:
bash-3.1# stty -F /dev/ttyS0
speed 0 baud; line = 0;
intr = <undef>; quit = <undef>; erase = <undef>; kill = <undef>; eof = <undef>; start = <undef>;
stop = <undef>; susp = <undef>; rprnt = <undef>; werase = <undef>; lnext = <undef>; flush = <undef>;
min = 1; time = 0;
-cread
-brkint -icrnl -imaxbel
-opost -onlcr
-isig -icanon -iexten -echo -echoe -echok -echoctl -echoke
然后将速度更改为 1200bps:
bash-3.1# stty -F /dev/ttyS0 1200
然后我在函数中执行我的程序片段以更改波特率:
fd=open(dev,O_NOCTTY | O_NONBLOCK | O_RDWR);
struct termios ser[1];
tcflush(fd,TCIFLUSH);
tcflush(fd,TCOFLUSH);
cfmakeraw(ser);
// I call tcsetattr after each terminal setting to make sure its applied.
if (tcsetattr(fd,TCSANOW,ser) < 0){
return -1;
}
cfsetspeed(ser,B9600);
if (tcsetattr(fd,TCSANOW,ser) < 0){
return -2; //returns this after manually setting port via STTY
}
问题是波特率没有正确改变。事实上,我从函数和 strerror(errno) returns "input/output error" 中返回了 -2。
程序执行后,我检查系统端口设置:
bash-3.1# stty -F /dev/ttyS0
speed 0 baud; line = 0;
intr = <undef>; quit = <undef>; erase = <undef>; kill = <undef>; eof = <undef>; start = <undef>;
stop = <undef>; susp = <undef>; rprnt = <undef>; werase = <undef>; lnext = <undef>; flush = <undef>;
min = 1; time = 0;
-cread
-brkint -icrnl -imaxbel
-opost -onlcr
-isig -icanon -iexten -echo -echoe -echok -echoctl -echoke
即使我特别要求 9600bps,它也会重置为零 bps。
为什么要这样做?以及如何以编程方式强制速度达到 9600bps?
你的代码有很多错误。
- 你用
O_NONBLOCK
打开 tty 设备,所以当你发出ioctl
调用时(tc*attr(3)
调用导致ioctl(2)
系统调用,这取决于你的 unix 风格正在使用)您不知道设备是否已经打开以便能够进行tc*attr(3)
调用。这同样适用于O_NOCTTY
标志。您在不知道 open 系统调用的功能的情况下放置了这些标志。O_NOCTTY
在来自会话内部的 运行 程序中是无用的,并且O_NONBLOCK
将使您对 return 的tc*attr(3)
调用出现错误(EAGAIN
) 如果设备还没有打开,在尝试进行参数调整时。 - 您没有检查
open(2)
调用的结果。如果您尝试使用-1
作为文件描述符(ENODEV
、ENOTTY
或EBADF
、EINVAL
、ENXIO
,这可能会出错等等) - 你没有初始化
struct termios
结构的数据,所以这可能是你得到错误的原因。如您所示(您的示例代码片段不完整,告诉您阅读 How to create a Minimal, Complete, and Verifiable example 的一个原因)您使用的struct termios
是在自动变量上声明的(因为它的声明嵌入到代码中)所以它肯定是未初始化的并且带有垃圾数据。您通常需要对其执行tcgetattr()
以初始化为正确的值,并能够在程序结束后恢复设置。
当连接到 tty 设备时, bash(1)
使ioctl(2)
s 在标准输入描述符上设置和获取 termios 参数。如果你正在使用stdin,你必须考虑bash(1)
的干扰。这使得您获得的值与您使用stty
. 设置的值不同
- 在一般的 unix 操作系统中(恐怕在 linux 中不是这样)最后一次关闭设备通常会将 tty 的参数重置为标准的固定值,所以当你设置的标志使用 stty 更改非标准输入设备(不是标准输入,stty 完成时未最后关闭),一旦
stty
终止(在 tty 最后关闭时),这些参数将重置为默认值。在发出stty(1)
命令之前执行sleep 999999999 </dev/ttyBlaBla &
,以便在使用stty(1)
. 设置后端口保持打开状态(通过 - 阅读
termios(3)
页面,因此您可以从程序本身设置参数。只有当您的程序通常不处理设置参数时,您才不必以编程方式进行。但是这样修改终端参数就没有意义了,所以最好学会如何编程设备参数。
sleep
命令的重定向)
正确的做法应该是这样的(从您的代码片段中复制并编辑):
#include <string.h> /* for strerror */
#include <errno.h> /* for errno definition */
/* ... */
fd=open(dev,O_NOCTTY | O_NONBLOCK | O_RDWR);
if (fd < 0) {
fprintf(stderr, "OPEN: %s (errno = %d)\n",
strerror(errno), errno);
return -1;
}
struct termios ser; /* why an array? */
tcflush(fd,TCIFLUSH); /* unneeded, you have not used the tty yet */
tcflush(fd,TCOFLUSH); /* idem. */
/******* THIS IS THE MOST IMPORTANT THING YOU FORGOT ***********/
int res = tcgetattr(fd, &ser); /* *****this initializes the struct termios ser***** */
if (res < 0) {
fprintf(stderr, "TCGETATTR: %s (errno = %d)\n",
strerror(errno), errno);
return -2; /* cannot tcgetattr */
}
/***************************************************************/
cfmakeraw(&ser); /* now it is valid to set it */
cfsetspeed(&ser,B9600); /* better do all in only one system call */
// I call tcsetattr after each terminal setting to make sure its applied.
/* nope, tcsetattr and tcgetattr are the only calls that make something
* on the tty, the rest only manipulate bits on the struct termios
* structure, but don't do anything to the terminal, you begun with a
* trashed struct termios, so it's normal you end with an error. */
if ((res = tcsetattr(fd, TCSANOW, &ser)) < 0){
fprintf(stderr, "ERROR: %s (errno = %d)\n",
strerror(errno), errno); /* better to know what happened. */
return -3; /* couldn't tcsetattr */
}
最后,这段代码(首先是你的)还没有经过测试,主要是因为你没有发布一个完整的、最小的和可验证的例子。因此,在将其包含在您的代码中之前,您可能需要对其进行一些修改。并请RTFM(最后的意思完整阅读termios(3)
,最重要的是:How to create a Minimal, Complete, and Verifiable example):)。此外,如果您正在使用 bash(1)
,请不要检查 stdin 上的 tty 设置,因为它通常会在命令退出后恢复 tty
设置,然后再发出提示。