1

I am writing some software to handle serial port read/writing for a Beaglebone system. The OS is Debian 9. I am writing code in C with --std=gnu99.

Here is my code:

// reference
// https://www.cmrr.umn.edu/~strupp/serial.html

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>

int open_port(void)
{    
    int fd;

    fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY | O_NDELAY);
    // removing O_NDELAY no difference
    if(fd == -1)
    {
        perror("open_port: Unable to open /dev/ttyS1 - ");
    }
    else
    {
        fcntl(fd, F_SETFL, 0);
    }

    return fd;
}

int main()
{

    // open fd for serial port
    int fd = open_port();


    // set baud rate
    struct termios options;
    // get current options
    tcgetattr(fd, &options);
    // set input and output baud
    cfsetispeed(&options, B115200);
    cfsetospeed(&options, B115200);
    // enable reciever and set local mode
    // CLOCAL: ignore modem control lines
    // CREAD: enable reciever
    options.c_cflag |= (CLOCAL | CREAD);

    // set partity bit
    //options.c_cflag &= PARENB;
    options.c_cflag &= ~PARENB;
    // use even parity
    //options.c_cflag &= ~PARODD;
    // use only 1 stop bit
    options.c_cflag &= ~CSTOPB;
    // set character size to 8 bit
    options.c_cflag &= ~CSIZE;
    options.c_cflag &= CS8;
    // disable flow control
    //options.c_cflag &= ~CNEW_RTSCTS; // does not work?

    // note: check local options, some may be required
    // disable canonical input (use raw)
    // disable echoing of characters
    // disable echoing of erase characters
    // disable signals SIGINTR SIGSUSP SIGDSUSP, SIGQUIT
    // input characters are passed through exactly as recieved
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    // disable parity check
    options.c_iflag &= IGNPAR;

    // disable flow control (software)
    options.c_iflag &= ~(IXON | IXOFF | IXANY);

    // set raw output, no output processing
    options.c_oflag &= ~OPOST;

    // set options
    tcsetattr(fd, TCSANOW, &options);

    char data[] = {0x01, 0x03, 0x00, 0x00};
    int n = write(fd, data, 4);
    if(n =! 4)
    {
        printf("write fail %d\n", n);
    }

    char buffer[40];
    n = read(fd, buffer, 2);
    if(n != 2)
    {
        printf("read fail %d\n", n);
    }
    if(buffer[0] == 0x03 && buffer[1] == 0x00)
    {

    }
    else
    {
        printf("uart error\n");
    }

    char data2[] = {0x00, 0x00, 0x00, 0x01};
    n = write(fd, data2, 4);
    if(n != 4)
    {
        printf("write fail %d\n", n);
    }

    n = read(fd, buffer, 2);
    if(n != 2)
    {
        printf("read fail %d\n", n);
    }
    if(buffer[0] == 0x03 && buffer[1] == 0x00)
    {

    }
    else
    {
        printf("uart error\n");
    }

    printf("process complete\n");

    // to close
    close(fd);
}

The issue I have is calls to read() do not block. This is not the desired behaviour. These calls should block until 2 bytes of data are received.

My guess would be I have misconfigured an option somewhere. However I don't know where the mistake is, and from what I have researched, this should be reading in blocking mode. (fcntl(fd, F_SETFL, 0);)

sawdust
  • 16,103
  • 3
  • 40
  • 50
FreelanceConsultant
  • 13,167
  • 27
  • 115
  • 225
  • 1
    You specified `O_NDELAY`, so you got it. – Ben Voigt Jul 07 '19 at 21:12
  • @BenVoigt The issue persists regardless of whether I specify `O_NDELAY`. I tried removing it. I don't fully understand the difference between this option and `O_NONBLOCK`. – FreelanceConsultant Jul 07 '19 at 21:31
  • Your program actually has more (unreported) problems than *"read() [that] do not block'*. The write operations are not accurately transmitted, and the 2nd write can return an errno 5, input/output error. The 1st read can retrieve one byte, but it's a garbage value. Program behavior is dependent on prior termios settings. See my expanded answer. – sawdust Jul 08 '19 at 02:48

1 Answers1

2

The issue I have is calls to read() do not block.

That's actually a conclusion, rather than an observation.
Presumably your program is reporting zero bytes read, which technically is not an error.

... from what I have researched, this should be reading in blocking mode. (fcntl(fd, F_SETFL, 0);)

Correct, you have the file descriptor configured for blocking mode.

My guess would be I have misconfigured an option somewhere.

Yes, your termios configuration is incomplete. (It uses proper bitwise operations, but unfortunately is incomplete and has errors, see ADDENDUM.)
When configured for non-canonical mode, you must also specify the VMIN and VTIME parameters.
When left unspecified (as in your code), the default values of VMIN = 0 and VTIME = 0 are likely to be in effect, which is equivalent to a non-blocking read.


Note that the byte length in the read() syscall has minimal effect on completion behavior (other than capping the number of bytes to be returned).
From the man page for read(2):

read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.

Therefore the count parameter is effectively a maximum.
The actual number of bytes read can be between zero and count depending on the values configured for VMIN and VTIME (and the actual data available).

See Linux Blocking vs. non Blocking Serial Read for more details.


To read at least two bytes per syscall (with no time limit), include the following in the termios configuration:

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

The above two statements inserted into your program before the tcsetattr() syscall would cause your program to block forever until exactly two bytes can be returned in each read(fd, buffer, 2) statement.
A read(fd, buffer, 4) statement would also block forever, and return either 2, 3, or 4 bytes of data (depending on program timing with respect to data reception & buffering).


ADDENDUM

Turns out there are a few bugs in your termios initialization.
As posted your program neither transmits or receives any data correctly, and you failed to detect these errors or neglect to mention them.

The following statement in your program obliterates all the previous c_cflag operations, and unintentionally reconfigures the baudrate to B0 and the character size to CS5:

    options.c_cflag &= CS8;

The correct syntax is:

    options.c_cflag |= CS8;

The statement

    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

should be expanded with more attributes to match cfmakeraw():

    options.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);

The following statement in your program disables every attribute in c_iflag except IGNPAR, and leaves the IGNPAR attribute in an unknown/ambiguous state:

    // disable parity check
    options.c_iflag &= IGNPAR;

To match cfmakeraw(), it should be changed to:

    options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
                       | INLCR | IGNCR | ICRNL);

With all the corrections mentioned, I can get your program to execute as expected.

sawdust
  • 16,103
  • 3
  • 40
  • 50
  • Is it possible to set the time to be infinite? – FreelanceConsultant Jul 07 '19 at 21:50
  • Yes. If you study the provided link, `VMIN > 0 and VTIME = 0` is a counted read that can wait forever. But your program does not handle the data in a robust manner. A **tcflush()** before each **write()** and a **tcdrain()** after would help synchronize operations a lot. – sawdust Jul 07 '19 at 21:52
  • Ah ok - I'm still a little confused. I wrote something similar in Python and a call to read(2) appears to be "blocking" and appears to wait for infinite time to read 2 bytes. Is it possible to accomplish this? If I set VMIN to 2 that then presumably will not work if I want to read(4) for example. – FreelanceConsultant Jul 07 '19 at 21:56
  • 1
    Forget what Python does. As I already wrote the byte length in **read()** does not control the minimum count; that's what VMIN is for. It is not practical to try to read exactly N bytes in one **read()**, and then read exactly M bytes in the next **read()**, because that would require reprogramming VMIN. You would be better off buffering the data as in this example: https://stackoverflow.com/questions/43280740/parsing-complete-messages-from-serial-port/43287464#43287464 – sawdust Jul 07 '19 at 22:10
  • Ok, can you explain why you changed the c_lflag and c_iflag options? In particular why ECHOE is now missing and ECHONL is added? And why IGNPAR is now neither set nor unset and various other options are unset instead? – FreelanceConsultant Jul 08 '19 at 10:11
  • Those changes are required to get your program to transmit & receive properly. If you think your program can work without those changes, then you are not thoroughly testing your code and analyzing the results. Those changes match the termios settings of **cfmakeraw()**, which can be considered to have the minimal required set of attributes to disable. – sawdust Jul 09 '19 at 00:50
  • Is there a reason to not use `cfmakeraw` instead of doing the bit setting manually? – FreelanceConsultant Jul 09 '19 at 09:23
  • Of course you can use **cfmakeraw()**, but it replaces just a few statements and is only a partial initialization for a serial terminal. If you don't understand what it does (and what it does not), then you could end up misusing it. – sawdust Jul 09 '19 at 20:03