Search code examples
linuxserial-portusbcdctermios

Serial port reads are delayed (and bytes missing)


I have a Rasperry Pi and a Raspberry Pi Pico communicating together via serial. I was using the UART pins on both, and everything was fine in terms of communications, except some environments were noisy, so I switched to USB CDC (USB using a differential pair...)

On the Pico side, I've had to do some changes to my code, but it seems now OK, after adding this:

stdio_set_translate_crlf(&stdio_usb, false);

(My protocol is binary, so translation was interfereing)

Now on the "Regular Pi", things have gone a bit wrong. Using the UART, all reads were non-buffered/instantaneous. Now that I'm over USB, it seems I have to wait for a fair bit of data to come before anything is transferred to my program. Note that other programs (like minicom) do not have this issue. I also seem to miss a fair few bytes.

I made a special program to run on the Pico that continuously outputs bytes from 0x00 to 0xFF. If I connect the Pico to a Windows host, run Putty and log the session, I get all the bytes, no problem.

My own software on the Raspberry Pi misses a lot of bytes.

I also tried this command:

(stty raw; cat > received.log) < /dev/ttyACM0

Most of the times, bytes between 0x0A and 0x16 are missing (I presume 0x0A triggers something related to CR/LF, but I'm not sure why characters till 0x16 (not included) get skipped. 0x7E and 0x7F are missing fairly often too. Occasionally, the full 256 bytes seems captures in a raw. I added a 10ms sleep in my loop on the Pico Pi, and the command above captures data properly. (The original UART was meant to run at 9600-115200 bps, nothing higher, my loop currently outputs >200 kbps, so that's quite enough for my needs).

Here's the initialisation code:

uart0_fd = ::open("/dev/ttyACM0", O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK);       //Open in non blocking read/write mode

struct termios options;
tcgetattr(uart0_fd, &options);
cfmakeraw(&options);
options.c_cflag = B115200 | CS8 | CLOCAL | CSTOPB | CREAD;      //<Set baud rate
options.c_iflag = IGNPAR | IGNBRK;
options.c_oflag = 0;
options.c_lflag = ICANON | NOFLSH;
options.c_cc[VTIME] = 10;
tcflush(uart0_fd, TCIFLUSH);
tcsetattr(uart0_fd, TCSANOW, &options);

struct termios tty;
memset (&tty, 0, sizeof tty);
if (tcgetattr (uart0_fd, &tty) != 0)
{
  printf ("error %d from tggetattr\r\n", errno);
  return;
}

tty.c_cc[VMIN]  = 0;
tty.c_cc[VTIME] = 5;            // 0.5 seconds read timeout

if (tcsetattr (uart0_fd, TCSANOW, &tty) != 0) { ... }

And here's the reading code:

void printHex(const uint8_t *data, const size_t size)
{
  for (uint32_t index = 0; index < size; ++index)
  {
    printf("%.2X",data[index]);
  }
}

void execute()
{
  printf("UART thread started\r\n");

  sendOverUart(COMMAND_RESEND_ALL, 0xFFFF);

  int local_uart0_fd = uart0_fd;

  unsigned char buffer[100];
  memset(buffer, 0, sizeof(buffer));

  while (!shouldFinish)
  {
    int bytesRead = read(local_uart0_fd, &buffer, sizeof(buffer) - 1);
    buffer[bytesRead + 1] = '\0';
    if (-1 == bytesRead)
    {
      if (errno != EAGAIN)
      {
        printf("UART... error reading %d\r\n", errno);
      }
    }
    else
    {
      printHex(buffer, bytesRead);
      fflush(stdout);
      continue;
    }
    if (EFAULT == errno)
    {

    }
  }
  ::close(local_uart0_fd);
  printf("UART thread finished\r\n");
}

As I said, it was working fine with the UART pins (/dev/serial0 on my Pi), did I miss anything, do I need to make something different due to USB? I need it to be "quick" (i.e. be able get the bytes without buffering) and reliable (i.e. not miss part of the bytes received).

The current output of my program on the Raspberry Pi is consistently 00-0A, then DE-FF, consistenly capturing 88 out of 256 byte [I suppose, I haven't time, maybe it captures 88 out of 512 or more...].


Solution

  • As I said, it was working fine with the UART pins (/dev/serial0 on my Pi), did I miss anything, do I need to make something different due to USB?

    With Linux, your program accesses a serial terminal rather than a UART or USB CDC. Your initialization for that serial terminal is poorly written and not reliable. See Setting Terminal Modes Properly and Serial Programming Guide for POSIX Operating Systems.

    I'm skeptical that the code you posted actually passed binary data with the UART. After the cfmakeraw() call to configure non-canonical mode, your code proceeds with superfluous, hard termios assignments, including the disastrous

    options.c_lflag = ICANON | NOFLSH;
    

    that puts the serial terminal in canonical mode. This is the simple explanation for why ASCII control characters are not appearing in your read buffer.

    For an example of proper use of cfmakeraw(), see this answer.


    Even though you specify a timed read with

    tty.c_cc[VMIN]  = 0;
    tty.c_cc[VTIME] = 5;            // 0.5 seconds read timeout
    

    this termios specification is overridden by the non-blocking mode of the uart0_fd file descriptor. That mode was (redundantly) specified when the serial terminal was opened (and commented):

    uart0_fd = ::open("/dev/ttyACM0", O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK);       //Open in non blocking read/write mode
    

    Refer to this answer for more details. Note the mention of the EAGAIN errno, which is dealt with properly in your code.

    If you're skeptical about this precedence, then simply try VMIN=0 and VTIME=250 for a timed read of 25 seconds. The actual duration of the read() syscall should be rather obvious, and indicate whether the termios or file-descriptor mode has precedence.


    I meant something like "I want to get the data less than 100ms after it arrived".

    Latency when reading a serial terminal can be problematic. There was a good discussion on a now-deleted forum where a developer tweaked his Linux SBC down to just a few milliseconds latency. The time was measured with an oscilloscope capturing the data on the wire and a GPIO flipping when the read() completes. The key tweaks IIRC were using the ASYNC_LOW_LATENCY flag with the TIOC[GS]SERIAL ioctl, and increasing the priority of the serial driver's tasklet or workqueue.

    ... when there's data in the buffer, get all of it. System buffer is too small?

    The termios buffer is typically 4K bytes.
    With non-canonical termios reads, you have to compromise between efficient reads (i.e. fetch as many bytes as possible per syscall) and low latency. You can get read() to return (with some data) in less than 0.1 seconds by specifying VMIN=1 and VTIME>0.