cserial-porttermios

Serial read in C returning no data


I am using termios in C to write a simple program to write to a serial port and read the returned data. The communication with the device on the serial line terminates with a carriage return. The program is simple and currently looks like:

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <termios.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{

  struct termios s_alicat;
  int tty_fd, count;

  // Ports connected in USB as sudo
  if ((tty_fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
  {
    printf("Line failed to open with %d\n", tty_fd);
    return -1;
  }
  else
  {
    printf("fd is %d\n", tty_fd);
  }

  s_alicat.c_cflag = B19200 | CS8 | CREAD | CLOCAL;

  //No parity 8N1:
  s_alicat.c_cflag &= ~PARENB;
  s_alicat.c_cflag &= ~CSTOPB;
  s_alicat.c_cflag &= ~CSIZE;

  s_alicat.c_iflag = IGNPAR | ICRNL; // Ignore parity errors

  //Disable hardware flow control
  s_alicat.c_cflag &= ~CRTSCTS;

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

  //Raw output
  s_alicat.c_oflag &= ~OPOST;

  //Raw input
  s_alicat.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

  tcflush(tty_fd, TCIFLUSH);
  tcsetattr(tty_fd, TCSANOW, &s_alicat);


  unsigned char transmit[2] = "C\r";
  if ((count = write(tty_fd, &transmit, 2)) < 0)
  {
    printf("Failed to write to device");
  }

  printf("Transmited %d characters\n", count);

  usleep(500000);

  unsigned char receive[255];

  if ((count = read(tty_fd, &receive, 255) < 0))
  {
    printf("Error receiving text %d", count);
  }
  else
  {
    if (count == 0)
    {
      printf("No data read in...\n");
    }
    else
    {
      printf("%s", receive);
    }
  }
  printf("Closting port...\n");
  close(tty_fd);

  return 0;
}

So:

If I send the same command (C\r) through another program set up with 19.2, 8N1, no flow control, I get the following string ( or something very similar) back

C\s+012.05\s+031.73\s+000.01\s+000.01\s010.24\s\s\s\s\sAir\r

So, what am I doing wrong here? Does this have something to do with the fact that the IO is carriage return terminated? Or is my configuration incorrect?

EDIT: So, it appears that if I watch the character device (/dev/ttyUSB0) I can actually see the data coming back - see snap shot below. So, it looks like my issue is in reading into and getting information from the read buffer.

enter image description here


Solution

  • Or is my configuration incorrect?

    Yes.
    The problem is that your program uses non-blocking mode

      open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY)
    

    and combines that with non-canonical mode

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

    even though you state that the input are lines terminated "with (a) carriage return."

    A "read returns with 0 characters" is predictable and normal for non-blocking raw reads (like your configuration) whenever there is simply no data available. See this answer for the full details.

    To correct your program, use blocking mode.
    Insert the following statement after the file descriptor has been obtained:

    fcntl(tty_fd, F_SETFL, 0);    /* set blocking mode */
    

    See this for an explanation.

    As for the termios configuration, your program has a serious bug: it uses the termios structure s_alicat uninitialized.
    The proper method is to use tcgetattr().
    Refer to Setting Terminal Modes Properly and Serial Programming Guide for POSIX Operating Systems .

    #include <errno.h>
    #include <string.h>
    
    ...
    
      if (tcgetattr(tty_fd, &s_alicat) < 0) {
          printf("Error from tcgetattr: %s\n", strerror(errno));
          return -1;
      }
      cfsetospeed(&s_alicat, B19200);
      cfsetispeed(&s_alicat, B19200);
    
      s_alicat.c_cflag |= (CLOCAL | CREAD);
      s_alicat.c_cflag &= ~CSIZE;
      s_alicat.c_cflag |= CS8;         /* 8-bit characters */
      s_alicat.c_cflag &= ~PARENB;     /* no parity bit */
      s_alicat.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
    
      s_alicat.c_iflag |= ICRNL;       /* CR is a line terminator */
      s_alicat.c_iflag |= IGNPAR;      // Ignore parity errors
    
      // no flow control
      s_alicat.c_cflag &= ~CRTSCTS;
      s_alicat.c_iflag &= ~(IXON | IXOFF | IXANY);
    
      // canonical input & output
      s_alicat.c_lflag |= ICANON;
      s_alicat.c_lflag &= ~(ECHO | ECHOE | ISIG);
      s_alicat.c_oflag |= OPOST;
    
      if (tcsetattr(tty_fd, TCSANOW, &s_alicat) != 0) {
          printf("Error from tcsetattr: %s\n", strerror(errno));
          return -1;
      }
    

    Additional bugs in your code include use of pointer to array addresses (i.e. address of address) when the array address would suffice.

    write(tty_fd, &transmit, 2)
    read(tty_fd, &receive, 255)
    

    should simply be respectively

    write(tty_fd, transmit, 2)
    read(tty_fd, receive, 255)
    

    The read() syscall does not return or store a string, yet your program assumes it does.
    The code (with the precedence bug corrected) should be:

    if ((count = read(tty_fd, receive, sizeof(receive) - 1)) < 0) {
        printf("Error receiving text %s\n", strerror(errno));
    } else {
        receive[count] = 0;  /* terminate string */
        printf("Received %d: \"%s\"\n", count, receive);
    }
    

    Note that the read request length is one-less-than the buffer size to reserve space for a terminating null byte.


    ADDENDUM

    Your code has a precedence/parentheses bug, which carried over into my code. The offending statement is:

    if ((count = read(tty_fd, &receive, 255) < 0))
    

    The assignment to variable count should be the return code from the read() syscall, rather than the evaluation of the logical expression read() < 0.
    Without proper parentheses, the comparison is performed first since the less-than operator has higher precedence than the assignment operator.
    This bug causes count, when there's a good read (i.e. a positive, non-zero return code), to always be assigned the value 0 (i.e. the integer value for false).


    The revised code from this answer merged with your code was tested, and confirmed to function as expected with text terminated with a carriage return.