【问题标题】:Unable to write to serial device but able to read from it无法写入串行设备但能够从中读取
【发布时间】:2021-02-24 15:15:30
【问题描述】:

我正在使用非阻塞 IO,我只是尝试对串行端口进行写入和读取。串行端口的读取按预期工作,而写入则没有。

这就是我设置串行线路的方式。如您所见,它设置为非阻塞和规范。也许有些标志是多余的,但它们大多取自这里的示例:Serial setup example

tSerial::tSerial(const char *serialPort, bool echo)
{
    // Open the serial port. Change device path as needed (currently set to an standard FTDI USB-UART cable type device)
    m_serialPort = open(serialPort, O_RDWR);

    // Create new termios struct, we call it 'tty' for convention
    struct termios tty;

    // Read in existing settings, and handle any error
    if (tcgetattr(m_serialPort, &tty) != 0)
    {
        spdlog::error("[tSerial] error {} from tcgetattr: {}", errno, strerror(errno));
        throw std::runtime_error("Failed to get existing serial settings");
    }

    tty.c_cflag &= ~PARENB;        // Clear parity bit, disabling parity (most common)
    tty.c_cflag &= ~CSTOPB;        // Clear stop field, only one stop bit used in communication (most common)
    tty.c_cflag &= ~CSIZE;         // Clear all bits that set the data size
    tty.c_cflag |= CS8;            // 8 bits per byte (most common)
    tty.c_cflag &= ~CRTSCTS;       // Disable RTS/CTS hardware flow control (most common)
    tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

    // tty.c_lflag &= ~ICANON;
    tty.c_cflag |= ICANON;
    if (!echo)
    {
        tty.c_lflag &= ~ECHO; // Disable echo
    }
    else
    {
        tty.c_lflag |= ECHO; // Enable echo
    }

    // tty.c_lflag &= ~ECHOE;                                                       // Disable erasure
    // tty.c_lflag &= ~ECHONL;                                                      // Disable new-line echo
    // tty.c_lflag &= ~ISIG;                                                        // Disable interpretation of INTR, QUIT and SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                                      // Turn off s/w flow ctrl
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); // Disable any special handling of received bytes

    tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed

    // Set in/out baud rate to be 115200
    cfsetispeed(&tty, B115200);
    cfsetospeed(&tty, B115200);

    // Save tty settings, also checking for error
    if (tcsetattr(m_serialPort, TCSANOW, &tty) != 0)
    {
        spdlog::error("[tSerial] error {} from tcsetattr: {}", errno, strerror(errno));
        throw std::runtime_error("Failed to set new serial settings");
    }

    // Set non-blocking
    int flags;
    if ((flags = fcntl(m_serialPort, F_GETFL, 0)) == -1)
    {
        flags = 0;
    }
    fcntl(m_serialPort, F_SETFL, flags | O_NONBLOCK);
}

这就是我尝试写作的方式。我正在使用select 等待IO 资源可用,因为尝试直接写入它会给出错误Resource temporarily unavailable。但即使等待 20 秒,设备仍然不可用。

void tSerial::writeSerial(std::string message)
{
    int n;
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(m_serialPort, &rfds);
    spdlog::debug("[tSerial] writing command to serial {}", message);
    struct timeval tv;
    tv.tv_sec = 20;
    tv.tv_usec = 0;
    int retval = select(1, NULL, &rfds, NULL, &tv);
    if (retval == -1)
    {
        spdlog::error("[tSerial] select error");
    }
    else if (retval)
    {

        n = write(m_serialPort, message.c_str(), message.length());
        tcflush(m_serialPort, TCIOFLUSH);
        if (n < 0)
        {
            spdlog::error("[tSerial] error writing: {}", strerror(errno));
        }
    }
    else
    {

        spdlog::error("[tSerial] Resource unavailable after 20 seconds wait");
    }
}

【问题讨论】:

  • 所以调用 write 返回 -1 ?在这种情况下,errno 是什么?根本不需要选择,没有时间设备“不可用”。直接从 cmd 行写入有效吗?
  • 没有select write 如你所说返回-1。 errno 是 11,EAGAIN。从这个link,在第二个答案中建议使用selectechoing 到设备按预期工作。 microcom 也可以使用它。
  • 嗯,我看不出代码有什么问题。您确定设备没有被其他程序保持忙碌吗?如果将其设置为原始模式会发生什么(有关标志,请参见 termios 的手册页)?为什么不直接在open 中设置为 no-block 呢?如果至少设置了一个设置,tcsetattr 会成功,请再次阅读以查看所有设置是否已真正应用。尝试this answer 创建一个虚拟串口设备并尝试它是否适用。使用stty检查真实设备。

标签: c++ c linux serial-port


【解决方案1】:

我正在使用select等待IO资源变为可用...但是即使等待20秒,设备仍然不可用。

您的程序未按预期运行,因为您没有正确编程 select() 调用:

int retval = select(1, NULL, &rfds, NULL, &tv);

根据 ma​​n 页面,第一个参数“应设置为三个集合中任何一个中编号最高的文件描述符,再加上 1。
由于您传递值1select() 可以检查的唯一文件描述符是stdin(它是只读的,永远不会准备好输出)并且永远不会(文件描述符)打开的串行终端。
相反,系统调用需要是

select(m_serialPort + 1, ...);

您的代码还有其他问题。

(1) 使用非阻塞模式是有问题的。
似乎您更喜欢在程序中添加额外的代码以等待而不是让操作系统为您完成。如果您的程序没有有效且高效地利用其时间片,那么您最好让操作系统管理系统资源。

(2) 在 write() 之后使用 tcflush() 是荒谬和错误的!
您可能打算使用 tcdrain()
研究 ma​​n 页面。

(3) 当您复制和修改代码时,您至少引入了一个错误。
ICANONc_lflag 成员中,而不是在c_cflag 中。

(4) 良好的编码习惯是使用有意义的变量名。
重用用于读取参数的变量名作为写入参数是草率且令人困惑的:

fd_set rfds;  
...
int retval = select(..., NULL, &rfds, NULL, &tv);

您的程序应该使用wfds 之类的东西而不是rfds 作为第三个参数。


...尝试直接写入它会给出错误Resource temporarily unavailable

使用 select() 时的问题很容易解释。
对于上述问题,您需要提供一个最小的、可重现的示例,即一个完整的程序。

【讨论】:

  • 谢谢,我会一一解决问题。 (1) 用例是写入可以阻塞一小段时间,而读取不应该阻塞,因为有更多的“入口点”会导致程序唤醒。我正在以一种我知道不会占用太多资源的简单方式写这篇文章。也许我可以在任何地方都使用 select,但是当我开始写这个时我不知道那个函数,我不确定如何使用它来编写解耦代码。
  • (2) 将其删除。在另一个堆栈溢出帖子中看到了这一点,我习惯于在其他语言中将其称为刷新,但无论哪种方式都可能没有必要尝试刷新。 (3) 很好,不确定我实际上设置了什么作为标志,但也许这是“破坏”串行端口的问题的一部分。 (4) 好点,wfds 更明智。
  • 昨天发现串口实际上根本没有工作。我今天尝试了这些更改,并且成功了。不确定是由于某些标志,我将串行端口置于错误状态还是由于其他原因。