2

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:

  • The port opens correctly
  • I am able to write the data (can physically see that it is going across the line via LEDs that light up when data is transmitted and received)
  • The read returns with 0 characters

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

cirrusio
  • 580
  • 5
  • 28
  • 1
    I don't get it. You write and read the same port? Do you have a loopback connection on it? – Eugene Sh. Jun 14 '18 at 18:06
  • 1
    I have a device connected on the other end that is responding. I send a request `C\r` with the expectation that I will receive data back based on the request - basic RS232 communication. – cirrusio Jun 14 '18 at 19:02
  • Is the data expected binary or string? Are you seeing the message "No data read in..." ? – Eugene Sh. Jun 14 '18 at 19:15
  • A string that looks like what is given above. Since I am getting no characters back, I get the message from the `if` block where `count` is 0 - "No data read in...". – cirrusio Jun 14 '18 at 19:39
  • +1 for simply posting code that is reasonably formatted, i.e. the keyword `if` is followed by a space so that it doesn't look like a procedure name. – sawdust Jun 14 '18 at 21:27

2 Answers2

3

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.

sawdust
  • 16,103
  • 3
  • 40
  • 50
  • Thanks, @sawdust - I have implemented these changes but this does not seem to solve the problem. But, maybe I am missing something? If you look at the screenshot above you see what is actually in the character read device (`\dev\ttyUSB0`) - the data that I expect. When I implement this with blocking as with your code above, the system hangs and there is nothing in the character device. – cirrusio Jun 14 '18 at 21:30
  • So, I can say without a doubt that without this line `s_alicat.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);` the system simply hangs and nothing hits the character device (but thanks for pointing out these other bugs). – cirrusio Jun 14 '18 at 21:36
  • *"the system hangs"* -- Really? The system!? Or just the program? If you haven't rebooted your PC since the last time you ran the broken version, then issue the command `stty -F /dev/ttyUSB0 sane`. Note the direction of the slashes (apparently you're a DOS/Windows user.) If that doesn't help, then update your post with the revised code for review. – sawdust Jun 14 '18 at 21:43
  • No - I am a linux user (just mistyped the slash). And by hangs I mean the program is waiting for input that never comes. (not system - but I think you understood from the response). And I wouldn't call it "broken", just not producing the functionality required (builds, runs, sends commands as expected just not reading from `/dev/ttyUSB0` which actually has the data visible as can be seen in the screenshot above). – cirrusio Jun 14 '18 at 21:54
  • OK...so I get a similar response if I use the `O_NDELAY` flag and set `VMIN` to 1 with `VTIME` = 5. – cirrusio Jun 14 '18 at 22:07
  • 1
    @cirrusio -- See addendum . – sawdust Jun 15 '18 at 04:30
  • Will look at this tomorrow - seems the device I was testing this on has become inaccessible via ssh. Should I just remove `count=...` and test the `read` function? – cirrusio Jun 15 '18 at 04:48
  • @cirrusio *"Should I just remove `count=...` "* -- Leave **count** unassigned, and not retain the return code?? That introduces bugs if not logic problems. The simplification would be to extract the assignment from the conditional, and make two statements. FYI the precedence bug has been fixed in my answer. – sawdust Jun 15 '18 at 19:44
1

@sawdust - thanks for your help. I will post my working code here with a few initial comments.

The problem is that your program uses non-blocking mode

This is actually not a problem and is exactly what I want. I don't want reads to block as this could cause the program to hang if the device is not responding. This code is just for testing whether my serial interface is good. So, setting the flags to 0 with fcntl(tty_fd, F_SETFL, 0) is what I don't want to do.

The assignment to variable count should be the return code from the read() syscall

I think that I am following you here but the wording is odd. Yes, the parentheses were improperly placed - thank you for pointing that out. But, by "return code" I assume that you mean -1 or the number of bytes received? Based on your updated response I am assuming so.

So, here is the final code. I believe that I incorporated the feedback that you provided into it. Feel free to provide more if you see anything odd. The return from the function when this run looks like

root@cirrus /h/m/D/s/F/C/c/src# ./main
fd is 3
count is 49
String is C +012.17 +030.85 +000.00 +000.00 010.24     Air
Closing port...

The code:

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

#define PORT "/dev/ttyUSB0"

int main(void)
{

  struct termios s_alicat;
  int tty_fd, count;
  char receive[255], transmit[2];

  // Ports connected in USB as sudo
  if ((tty_fd = open(PORT, 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);
  }

  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);

  // Set up receiver and set to local mode
  s_alicat.c_cflag |= (CLOCAL | CREAD | CS8);

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

  tcflush(tty_fd, TCIFLUSH); //discard file information not transmitted
  if (tcsetattr(tty_fd, TCSANOW, &s_alicat) != 0)
  {
    printf("Error from tcsetattr: %s\n", strerror(errno));
    return -1;
  }

  // Clear the port before kicking off communications
  strcpy(transmit, "\r\r");
  write(tty_fd, transmit, 2);

  strcpy(transmit, "C\r");

  if ((count = write(tty_fd, transmit, 2)) < 0)
  {
     printf("Failed to write to device");
  }

  int j = 0;
  count = 0;

  /* Attempt to read data at most 3 times if there is no data 
   * coming back.
   */

  while (count == 0 && j < 3)
  {

    usleep(100000);
    if ((count = read(tty_fd, receive, sizeof(receive) - 1)) < 0)
    {
      printf("Error receiving text %d", count);
    }
    else
    {
      printf("count is %d\n",  count);
      receive[count] = 0;
      printf("String is %s", receive);
    }
    j++;
  }

  printf("Closing port...\n");
  int p = 0;
  if ((p = close(tty_fd)) < 0)
  {
    printf("Port failed to close %d\n", p);
    return -1;
  }

  return 0;
}

Edit

Added explicit setting of baud rate via cfsetospeed and cfsetispeed.

Another Edit

Got rid of redundant tcgetattr call.

cirrusio
  • 580
  • 5
  • 28
  • Since you want to post your own answer, I'm not going to point out the numerous errors in this "answer". Apparently you have not studied the references provided. I've already posted POSIX-compliant code that is portable and functionally correct. Your code is neither. It may do what you want today, but under other circumstances it will not. BTW executing a program as **sudo** simply to access a serial terminal is a very bad practice. – sawdust Jun 16 '18 at 00:17
  • @sawdust - not sure what you are shooting for - you want me to mark your code as the correct answer? I incorporated all the changes you suggested but **it did not work** . I have explained clearly why and posted code that I know to be functional. Yes, I understand not to run as sudo (as I said before, this is strictly for testing); yes, I understand this may have bugs; and yes, I studied the material you provided. How about explaining to me who is not as practiced what is wrong? (I have explained why blocking is not appropriate - if you need more info I can try to explain further) – cirrusio Jun 16 '18 at 03:34
  • One more thing about your answer - below you and in [this post](https://stackoverflow.com/questions/25996171/linux-blocking-vs-non-blocking-serial-read/26006680#26006680) you state "nonblocking is requested by opening the serial port with the O_NONBLOCK or O_NDELAY option". But, according to the [document you point to](https://www.cmrr.umn.edu/~strupp/serial.html) "The O_NDELAY flag tells UNIX that this program doesn't care what state the DCD signal line is in" - my DCD line is *not* wired up - I have only the transmit, receive and ground wired - so this is a must. – cirrusio Jun 16 '18 at 03:45
  • *"you want me to mark your code as the correct answer?"* -- Yes please, since the crux of your problem was the precedence/parentheses issue that causes false reports of "read 0 char". And I had to advise you on your improper fix. I can't explain why canonical mode doesn't work for you (do you have a modified line discipline?) when it should and does for me. Apparently you can use raw mode because of the crude delay. The rest of your coding errors seem to not break execution; you're lucky for now. BTW I use 3-wire connections with FTDI and Prolific adapters all the time without O_NDELAY. – sawdust Jun 20 '18 at 00:11