Search code examples
cmacosserial-porttermiosstty

Opening a serial port on OS X hangs forever without O_NONBLOCK flag


I have a serial to USB converter (FTDI, drivers installed from http://www.ftdichip.com/Drivers/VCP.htm) connecting a serial device to a MacBook Air. It shows up on the MacBook as both /dev/cu.usbserial-A4017CQY and /dev/tty.usbserial-A4017CQY. All behaviour I describe is identical regardless of which of these two I use.

Edit: Using /dev/cu.* did solve the problem. I'm not sure why it seemed not to work when I first posted this question. Thanks to duskwuff for pointing me in the right direction, though he has his TTY names backwards: /dev/tty.* will wait for flow control, while /dev/cu.* will not.

The first problem I encountered was that the syscall to open() would block forever if I did not use the O_NONBLOCK flag. Using the flag, I get a good file descriptor, but write() does not seem to actually write (though it returns just fine claiming to have written the bytes), and read() fails with the error "Resource temporarily unavailable".

stty -af /dev/cu.usbserial-A4017CQY shows the settings just fine, but if I try to change them with a command like stty -f /dev/cu.usbserial-A4017CQY -clocal, they do appear to be changed when displayed with a successive call to stty.

If I use select() to wait for the device to become ready before reading/writing, it reports after a short time that it is ready to write, but never to read. This gels with how write() completes without complaint, while read() fails. Note that the data written never does actually make it to the device.

The entire test program I wrote to debug this is below:

#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>

#define SYSCALL(A) do { ret = A; if (ret == -1) { perror(#A); return -1; } else printf("%s returned %d\n", #A, ret); } while (0)
int ret; /* necessary for SYSCALL */

int main()
{
    struct termios tio;
    char buf[256];
    int fd = open("/dev/cu.usbserial-A4017CQY", 
            O_RDWR | O_NOCTTY | O_NONBLOCK);
    fd_set rfds, wfds, xfds;
    struct timeval to;
    to.tv_sec = 5;
    to.tv_usec = 0;

    SYSCALL(tcgetattr(fd, &tio));

    cfmakeraw(&tio);

    tio.c_cflag = CS8|CREAD|CLOCAL;
    tio.c_cc[VMIN] = 1;
    tio.c_cc[VTIME] = 1;

    cfsetispeed(&tio, B115200);
    cfsetospeed(&tio, B115200);

    SYSCALL(tcsetattr(fd, TCSANOW, &tio));

    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    FD_ZERO(&xfds);
    FD_SET(fd, &rfds);
    FD_SET(fd, &wfds);
    FD_SET(fd, &xfds);

    int ret = select(fd+1, &rfds, NULL, &xfds, &to);
    if (ret == -1) perror("select");
    else if (ret > 0)
    {
        if(FD_ISSET(fd, &rfds))
            puts("Ready to read");
        if(FD_ISSET(fd, &wfds))
            puts("Ready to write");
        if(FD_ISSET(fd, &xfds))
            puts("Exception!");
    }
    else puts("Timed out!");

    SYSCALL(write(fd, "/home\n", 5));
    SYSCALL(read(fd, buf, 256));

    return 0;
}

Solution

  • You have a flow control issue. Either loop back join RTS/CTS and DTR/DSR/CD on your cable, have the other end provide control signals, or, as @duskwuff suggests, use the device that ignores flow control.

    I see that you are setting CLOCAL -- that should work, but some USB devices do not do the right thing. Your description is consistent with the device waiting for modem control signals.