读取串口时阻塞的原因未知。

5

我正在尝试在Linux下使用termios框架通过UART(usbserial)接口与一种非接触式智能卡读卡器进行通信。代码在PC上运行良好,但是当我将其交叉编译并在ARM9目标上尝试执行时,它可以打开设备并向设备写入命令,但读取命令会无限期地阻塞。以下是代码片段:

int mifare_rdr_init(struct mifare_1K * ptr, char *rdr_devnode)
{   
    bzero(ptr, sizeof(struct mifare_1K));           // zero the entire structure
    // open serial device
    int fd = open(rdr_devnode, O_RDWR|O_NOCTTY );
    if (fd == -1) {
    perror("Failed to open serial device ");
    return 1;
    }
    ptr->serialfd = fd;                 // save file descriptor

    ptr->serialdev.c_iflag = 0;                 // no i/p flags
    ptr->serialdev.c_oflag = 0;                 // o/p flags
    ptr->serialdev.c_cflag = ( CS8 | CREAD | B38400 );      // 8 bits, receive enable, baud for rdr
    ptr->serialdev.c_lflag = ( ICANON );                // CANONICAL mode, means read till newline char '\n'.
    // control chars 
        // commented below line as suggested by A.H below, since it's not needed in CANONICAL mode
    // ptr->serialdev.c_cc[VMIN] = 1;               // read unblocks only after at least one received char.

    // flush all i/o garbage data if present
    tcflush(ptr->serialfd,TCIOFLUSH);

    int ret = 0;
    // apply settings
    ret = tcsetattr(ptr->serialfd,TCSANOW,&ptr->serialdev);
    if (ret == -1) {
        perror("tcsetattr() failed ");
        return 2;
    }
    return 0;
    }

int get_mifare_rdr_version(struct mifare_1K *ptr, char *data)
{
    // flush all i/o garbage data if present
    tcflush(ptr->serialfd,TCIOFLUSH);

    int chars_written = write(ptr->serialfd,"$1V\n",4);
    if( chars_written < 0 ) {
        perror("Failed to write serial device ");
        return 1;
    }
        printf("cmd sent, read version...\n");   // this prints, so I know cmd sent...
    int chars_read = read(ptr->serialfd,ptr->data_buf,14);
    if( chars_read < 0 ) {
        perror("Failed to read serial device ");
        return 2;
    }
    // copy data to user buffer
        printf("reading done.\n");    // this doesn't print...
    return 0;
}

mifare_1K结构包含串行设备的文件描述符、termios结构和各种缓冲区。我在使用的设备是usb转串口模块(ftdi_sio)。它以termios的规范模式配置在38400@8-N-1。因为读卡器的响应以'\n'结尾,所以最好采用规范模式处理。因为采用该模式可以等待接收到'\n'之后再读取设备(如果我错了,请纠正我)。
首先我调用init()函数,然后再调用get_rdr_version()函数。当打印出"cmd sent, read version..."时,我知道它能够写入,但是在此之后并未打印出字符串"reading done."。
另外一件事情是,如果我移除读卡器并将该端口连接到另一台PC上的gtkterm(串口终端程序),我就无法在gtkterm上接收到"$1V\n"。经过一些研究后,我发现只有在重启连接读卡器的系统后,我才会在另一台Gtkterm上收到该命令"$1V\n"。如果我不重新启动系统,那么该命令就不会出现在Gkterm上。这是个线索,但我还没有搞清楚原因。
难道是命令被写入设备文件中,但没有被传输到实际设备中吗?有没有办法检查这个问题?
非常感谢任何帮助,因为我已经卡在这里一段时间了......谢谢。
更新:
好的,我通过修改代码稍微调整了一下,现在它正常工作了。
// open serial device
    int fd = open(rdr_devnode, O_RDWR|O_NOCTTY|O_NDELAY );  // O_NDELAY ignores the status of DCD line, all read/write calls after this will be non-blocking
    fcntl(fd,F_SETFL,0);   // restore read/write blocking behavior
    if (fd == -1) {
    perror("Failed to open serial device ");
    return 1;
    }

这是我在init()函数中打开端口的修改部分代码。有两个更改:

1)在open()调用的标志中添加了O_NDELAY,它忽略数据载波检测(DCD)线以查看另一端是否连接并准备好通信。这最初用于MODEM,但实际上我不需要,事实上,我根本没有使用usbserial。但是,此标志还使进一步的read()和write()调用为非阻塞。需要注意的是,我曾认为将CLOCAL添加到termios结构体的cflag中会处理此问题,但尝试后没有成功。

2)fcntl(fd,F_SETFL,0)恢复了进一步的read()和write()调用的阻塞行为。

这种组合对我来说完美地工作。唯一的原因是我还不理解为什么它在PC上没有进行此修改也能够工作,因为硬件是相同的。实际上,我能够使用minicom从ARM9目标读取智能卡读卡器中的数据,但无法使用我的程序。我将查看FT232BL文档,以查看DCD的默认状态。

无论如何,我在POSIX操作系统串行编程指南上找到了这些信息。有人能解释一下吗?当然,我找到答案后会发布答案。

干杯 :)


2
可能不是问题,但您可能想将 CLOCAL 添加到 c_cflag。最好使用 tcgetattr 获取当前设置(例如控制字符),而不是用 0 覆盖这些设置。您还可以尝试使用 tcdrain 确保程序等待数据传输完成。 - Hasturkun
嗨,Hasturkun...我没有检查,因为我认为没有人表现出兴趣。无论如何,我按照你的建议进行了检查(tcdrain(),CLOCAL)...结果相同。但是这个程序在PC上运行,而在目标设备上却不能运行,这让我感到困惑。欢迎提出更多建议...我会更新后发布。 - aditya
“但事实是,这个程序可以在PC上运行,但不能在目标设备上运行…”——简单来说,你的代码不符合POSIX标准,所以不要指望它具有可移植性。请参阅正确设置终端模式 - sawdust
2个回答

4

我刚在树莓派上使用 Telegesis USB 模块时遇到了相同的问题,因此我将其添加为另一个数据点。

我的情况是缺少 RTS 标志。Telegesis 需要 CRTSCTS 流控,并且在看到 RTS 之前不会向树莓派发送任何数据。这里令人困惑的地方是,a)相同的代码在 PC 上运行得很好,b)第一次插入 Telegesis 时,在树莓派上也能正常工作,但在后续打开 /dev/ttyUSB0 时,树莓派将无法接收到任何数据。

由于某种原因,在 ARM 上关闭设备时 RTS 标志被清除,但未再次设置,而在 x86/x64 上 RTS 标志保持设置状态。解决方法很简单,如果 RTS 标志尚未设置,则设置它 - 例如:

#include <sys/ioctl.h>
//...
int rtscts = 0;
if (ioctl (fd, TIOCMGET, &rtscts) != 0)
{
  // handle error
}
else if (!(rtscts & TIOCM_RTS))
{
  rtscts |= TIOCM_RTS;
  if (ioctl (fd, TIOCMSET, &rtscts) != 0)
  {
    // handle error
  }
}

我注意到您的情况下没有使用流控制,因此上述内容可能并不适用。然而,正是由于您的问题和提到minicom工作的情况,我们才发现了解决我们问题的方法 - 所以非常感谢您!


你是回答者,所以非常感谢你:)很高兴我的问题能帮到你。我现在有点时间紧迫,所以我会尽快验证这个答案。 - aditya
有没有一种简单的方法来设置这个标志?而不需要编译C代码 :) https://github.com/raspberrypi/linux/issues/1900#issuecomment-286782906 - Ray Foss

1

你可能需要检查以下三件事情:

规范模式/非规范模式混合1

你正在混合使用规范模式和非规范模式的内容:

ptr->serialdev.c_lflag = ( ICANON );
// ...
ptr->serialdev.c_cc[VMIN] = 1;

termios(3)手册关于VMIN的说明如下:

VMIN 非规范读取的最小字符数。

因此,很明显,您的超时不会按照您想象的方式工作。

规范模式/非规范模式混淆2

此外,手册在下面进一步说明:

这些符号下标值都是不同的,除了VTIME、VMIN可能与VEOL、VEOF的值相同。在非规范模式下,特殊字符的含义被超时的含义所替代。有关VMIN和VTIME的说明,请参见下面的非规范模式描述。

因此,请检查这些常量的定义是否在您的两个平台上不同。可能是EOL/EOF逻辑可能会被错误的设置破坏。EOL和EOF都可能导致从read返回。

未初始化的c_cc

您的代码没有正确初始化c_cc数组。您既没有读取现有设置,也没有为规范模式所需的值提供适当的默认值。到目前为止,所显示的代码甚至没有清除这些值。因此可能会使用不可预测的值。


嗨A.H.,你对VMIN的理解是正确的。在CANONICAL模式下不需要它,所以我已经在评论中编辑了问题。但是关于初始化结构体,你可以看到我将包含termios结构的结构体清零,因此我正在将整个结构体初始化为零,而不是让它悬而未决。 - aditya
1
@aditya:抱歉,我没有注意到bzero。但这加强了这一点,即规范模式所需的控制字符未配置。 - A.H.
非常正确,它们应该被初始化。只是想指出,在我的一次尝试中,我实际上初始化了VEOL、VEOL2和VEOF,因为我认为read()是阻塞的,因为它没有得到默认终端'\n',但是没有成功。 - aditya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接