1
votes

I am using non-blocking IO where I simply try to write and read to a serial port. Reading of the serial port works as expected whereas writing doesn't.

This is how I've set up the serial line. As you can see, it is setup to non-blocking and canonical. Maybe some of the flags are redundant, but they are mostly taken from an example here: 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);
}

This is how I try to write. I am using select to wait for the IO resource to become available since trying to write to it directly gives the error Resource temporarily unavailable. But even waiting 20 seconds, the device is still unavailable.

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");
    }
}
1
So calling write returns -1 ? What is errno in that case? Select should not be needed at all, there is no time when the device "is not available". Writing directly from cmd line works?Quimby
Without select write returns -1 as you say. errno is 11, EAGAIN. From this link it was recommended in the second answer to use select. echoing to the device works as expected. Also microcom with it works.Jesper
Hmm, I do not see anything wrong with the code. Are you sure the device is not kept busy by some other program? What happens if you set it to raw mode(for flags see manpage for termios)? Why do you not set it to no-block in open directly? tcsetattr succeedes if at least one setting was set, read it back again to see that all settings were really applied. Try this answer to create a virtual serial device and try whether it works for it. Use stty to inspect the real device.Quimby

1 Answers

2
votes

I am using select to wait for the IO resource to become available ... But even waiting 20 seconds, the device is still unavailable.

Your program is not behaving as you expect because you have not properly programmed the select() call:

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

Per the man page the first argument "should be set to the highest-numbered file descriptor in any of the three sets, plus 1."
Since you are passing the value 1, the only file descriptor that the select() could check is stdin (which is read-only and will never be ready for output) and never (the file descriptor for) the opened serial terminal.
Instead the syscall needs to be

select(m_serialPort + 1, ...);

Your code has additional issues.

(1) Use of nonblocking mode is questionable.
Seems like you prefer to add extra code to your program to wait instead of letting the OS do it for you. If your program does not utilize its time slice effectively and efficiently, then you would be better off having the OS manage the system resources.

(2) Use of tcflush() after the write() is nonsensical and wrong!
You probably meant to use tcdrain().
Study the man pages.

(3) When you copied & modified the code you introduced at least one bug.
ICANON is in the c_lflag member, and not in c_cflag.

(4) Good coding practice is using variable names that make sense.
Reusing a variable name intended for the read argument as the write argument is sloppy and confusing:

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

Instead of rfds, your program should be using something like wfds for the third argument.


... trying to write to it directly gives the error Resource temporarily unavailable

The problem when using select() is easily explainable.
For the above issue you need to provide a minimal, reproducible example, i.e. a complete program.