1

I have implemented a blocking read from serial port in C. My aim is to have a read which blocks until new data arrives.

Here is how I have implemented the serial pseudo-object (I've removed multithread protections for the code to be clearer).

typedef struct
{
    int fd;
    se_serial_speed_t speed;
    se_serial_parity_t parity;
    bool flow_control;
}se_serial_t;

int se_serial_constructor(se_serial_t** self, char* serial_port)
{
    int fd;

    if(NULL != *self)
    {
        return ERR_NNULL_PTR;
    }
    else if(0 != access(serial_port, F_OK))
    {
        ERRNO("Serial port is not available");
        return ERR_ILLEGAL_PARAM;
    }
    else
    {
        if(-1 == (fd = open(serial_port, O_RDWR | O_NOCTTY)))
        {
            ERRNO("Error opening %s in rw mode", serial_port);
            return ERR_OFILE_FAIL;
        }
        else if(NULL == (*self = malloc(sizeof(se_serial_t))))
        {
            ERROR("Error allocating memory for Serial");
            return ERR_MALLOC_FAIL;
        }
        (*self)->fd = fd;
    }
    return ERR_OK;
}

int se_serial_configure_interface(se_serial_t* self, se_serial_speed_t speed, se_serial_parity_t parity, bool flow_control)
{
    struct termios options;

    if(NULL == self)
    {
        return ERR_NULL_PTR;
    }
    else
    {
        if(0 != tcgetattr(self->fd,  &options))
        {
            ERRNO("Unable to get serial port current configuration");
        }
        if(0 != cfsetospeed(&options, speed))
        {
            ERRNO("Unable to set serial port output speed");
        }
        if(0 != cfsetispeed(&options, speed))
        {
            ERRNO("Unable to set serial port input speed");
        }

        switch(parity)
        {
        case SE_SERIAL_PARITY_8N1:
            options.c_cflag &= ~PARENB;
            options.c_cflag &= ~CSTOPB;
            options.c_cflag &= ~CSIZE;
            options.c_cflag |= CS8;
            break;
        case SE_SERIAL_PARITY_7E1:
            options.c_cflag |= PARENB;
            options.c_cflag &= ~PARODD;
            options.c_cflag &= ~CSTOPB;
            options.c_cflag &= ~CSIZE;
            options.c_cflag |= CS7;
            break;
        case SE_SERIAL_PARITY_7O1:
            options.c_cflag |= PARENB;
            options.c_cflag |= PARODD;
            options.c_cflag &= ~CSTOPB;
            options.c_cflag &= ~CSIZE;
            options.c_cflag |= CS7;
            break;
        case SE_SERIAL_PARITY_7S1:
            options.c_cflag &= ~PARENB;
            options.c_cflag &= ~CSTOPB;
            options.c_cflag &= ~CSIZE;
            options.c_cflag |= CS8;
            break;
        default:
            WARNING("Unable to set serial port parity");
            break;
        }

        if(flow_control)
            options.c_cflag |= CRTSCTS;
        else
            options.c_cflag &= ~CRTSCTS;

        options.c_cc[VMIN] = 1;
        options.c_cc[VTIME] = 0;

        if(0 != tcsetattr(self->fd, TCSANOW, &options))
        {
            ERRNO("Error configuring serial port");
            return ERR_SERIAL_CONF_FAIL;
        }
        self->speed = speed;
        self->parity = parity;
        self->flow_control = flow_control;
    }
    return ERR_OK;
}

int se_serial_read(se_serial_t* self, uint8_t* buffer, int size)
{
    int bytes_read = 0;
    int ret;

    if(NULL == self)
    {
        return ERR_NULL_PTR;
    }
    else
    {
        while(bytes_read < size)
        {
            if(0 > (ret = read(self->fd, &(buffer[bytes_read]), size - bytes_read)))
            {
                ERROR("Error reading from %s : %d\n", self->serial_port, ret);
                return ERR_RFILE_FAIL;
            }
            bytes_read += ret;
        }
        size = bytes_read;
    }

    return size;
}

The device I'm communicating with send a frame of 11 bytes every second after it has booted.

So I'm receiving frames in an infinite loop after initializing the serial port and I then print them.

se_serial_t* serial = NULL;
uint8_t buffer[1024] = {0};
int ret = 0;
int i;

if(0 > (ret = se_serial_constructor(&serial, "/dev/ttyUSB0")))
{
    ERROR("Error creating serial : %d", ret);
    return ERR_SERIAL_CREATION_FAIL;
}
else if(0 > (ret = se_serial_configure_interface(serial, SE_SERIAL_SPEED_B115200, SE_SERIAL_PARITY_8N1, false)))
{
    ERROR("Error configuring serial interface : %d", ret);
    return ERR_SERIAL_CONFIG_FAIL;
}

while(1)
{
    if(0 > (ret = se_serial_read(serial, buffer, 11)))
    {
        ERROR("Error reading from serial : %d", ret);
        return ret;
    }
    else
    {
        for(i=0;i<ret;i++)
        {
            printf("%02x ", buffer[i]);
        }
        printf("\n");
    }
}

What is strange with the result I get is that the read blocks forever even if I know that the device is sending frames.

However if I open the port with another program like minicom, I can receive the frames inside of it. Once the port has been opened using minicom and that I have exited it, my program works well until next reboot of my computer.

If I reboot the device, the code blocks until it begins to send frames and receive them well.

I have also tried on a Raspberry Pi 3 to be sure it wasn't a configuration problem on my laptop but I get the same result.

Does anybody has a clue on why I get this behaviour ?

1 Answer 1

2

What is strange with the result I get is that the read blocks forever even if I know that the device is sending frames.
...
Once the port has been opened using minicom and that I have exited it, my program works well until next reboot of my computer.

Not strange at all.
That's a clear indication that your program's initialization of the serial terminal is incomplete and depends on a pre-existing initialization to be suitable.
(BTW "strange" is an opinion-based description that conveys no technical information to aid debugging.)

The default mode of the serial terminal is typically canonical mode (for transferring text) after a system boot.
Therefore an (unintentional) canonical read() of a serial terminal will block until a line termination character (e.g. a new-line character or 0x0A) is encountered.
Your program would block forever if the source never sends any line termination character.
You can confirm this by using the stty -F /dev/ttyUSB0 -a command, and find that the icanon attribute has no hyphen preceding it.

Minicom configures the serial terminal for non-canonical mode, which is the mode that your program apparently also expects the serial terminal to operate in.
However your program only configures the termios parameters for baudrate, framing, and flow-control.
It is missing several salient terms for reliable operation of a serial terminal.

If your program requires non-canonical mode, then it must explicitly configure that mode rather than rely on a preexisting configuration.
Since numerous other related attributes should also be set or cleared for non-canonical mode, the macro cfmakeraw() is the simplest edit to your code.
Insert

cfmakeraw(&options);

in between your baudrate and "parity" configuration.
Note that use of a 7-bit data frame will likely cause corruption if the data is not exclusively ASCII text, so supporting those three modes in your program is incongruous.

The other salient omission is enabling the receiver and setting local mode:

options.c_cflag |= (CLOCAL | CREAD);

The device I'm communicating with send a frame of 11 bytes

BTW your use of "frame" in the context of a serial terminal is inappropriate. In asynchronous serial communication each character or byte is framed. Your references to a "frame" would be more appropriately called a message or packet or datagram.

Not the answer you're looking for? Browse other questions tagged or ask your own question.