Serial port loopback/duplex test, in Bash or C? (process substitution)

sdaau picture sdaau · Sep 13, 2010 · Viewed 31.6k times · Source

I have a serial device set up as loopback (meaning it will simply echo back any character it receives), and I'd like to measure effective throughput speed. For this, I hoped I could use time, as in

time bash -c '...'

where '...' would be some command I could run.

Now, the first problem is that I want to use the device at 2000000 bps, so I cannot use ttylog or screen (they both seem to go up to 115200 bps only). However, working with /dev/ttyUSB0 as a file (using file redirection and cat) seems to work fine:

# initialize serial port
stty 2000000 -ixon icanon </dev/ttyUSB0

# check settings
stty -a -F /dev/ttyUSB0

# in one terminal - read from serial port
while (true) do cat -A /dev/ttyUSB0 ; done

# in other terminal - write to serial port
echo "1234567890" > /dev/ttyUSB0

# back to first terminal, I now have:
# $ while (true) do cat -A /dev/ttyUSB0 ; done
# 1234567890$
# ...

Now, I'd like to do something similar - I'd like to cat a file to a serial port, and have the serial port read back - but from a single terminal command (so I could use it as argument to time).

I thought that I could use a Bash process substitution, to have the "writing" and "reading" part go, sort of, in "parallel" - if I try it with named pipes, it works:

# mkfifo my.pipe # same as below:
$ mknod my.pipe p

$ comm <(echo -e "test\ntest\ntest\n" > my.pipe) <(cat my.pipe)
    test
    test
    test
comm: file 2 is not in sorted order

Up there, I'm not using comm for any other purpose, than to (sort of) merge the two processes into a single command (I guess, I could have just as well used echo instead).

Unfortunately, that trick does not seem to work with a serial port, because when I try it, I sometimes get:

$ comm <(echo "1234567890" > /dev/ttyUSB0) <(while (true) do cat -A /dev/ttyUSB0 ; done)
cat: /dev/ttyUSB0: Invalid argument

..., however, usually I just get no output whatsoever. This tells me that: either there is no control of which process starts first, and so cat may start reading before the port is ready (however, that doesn't seem to be a problem in the first example above); or in Linux/Bash, you cannot both read and write to a serial port at the same time, and so the "Invalid argument" would occur in those moments when both read and write seem to happen at the same time.

So my questions are:

  • Is there a way to do something like this (cat a file to a serial port configured as loopback; read it back and see how long it takes) only in Bash, without resorting to writing a dedicated C program?
  • If I need a dedicated C program, any source examples out there on the net I could use?

Thanks a lot for any responses,

Cheers!

 

EDIT: I am aware that the while loop written above does not exit; that command line was for preliminary testing, and I interrupt it using Ctrl-C. ( I could in principle interrupt it with something like timeout -9 0.1 bash -c 'while (true) do echo AA ; done', but that would defeat the purpose of time, then :) )

The reason that while is there, is that for the time being, reading via cat from the device exits immediately; at times, I have set up the device, so that when cat is issued, it in fact blocks and waits for incoming data; but I cannot as of yet figure what's going on (and partially that is why I'm looking for a way to test from the command line).

In case I didn't use the while, I imagine for timing, I'd use something like:

time bash -c 'comm <(echo "1234567890" > /dev/ttyUSB0) <(cat -A /dev/ttyUSB0)'

... however for this to be working, sort of, assumes that cat -A /dev/ttyUSB0 starts first and blocks; then the echo writes to the serial port (and exits); and then cat -A outputs whatever it read from the serial port - and then exits. (And I'm not really sure neither if a serial port can behave this way at all, nor if cat can be made to block and exit arbitrarily like that).

The exact method really doesn't matter; if at all possible, I'd just like to avoid coding my own C program to do this kind of testing - which is why my primary interest is if it is somehow possible to run such a "full-duplex test" using basic Bash/Linux (i.e. coreutils); (and if not, if there is a ready-made code I can use for something like this).

EDIT2: Also possibly relevant:

Answer

sdaau picture sdaau · Sep 14, 2010

Well, here is something like a partial answer - although the question about the use of bash is still open. I tried to look a little bit in some C code solutions - and that, it seems, isn't trivial either! :)

First, let's see what possibly doesn't work for this case - below is an example from "between write and read:serial port. - C":

// from: between write and read:serial port. - C - http://www.daniweb.com/forums/thread286634.html
// gcc -o sertest -Wall -g sertest.c

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

int main(int argc, char *argv[])
{
    char line[1024];
    int chkin;
    char input[1024];
    char msg[1024];
    char serport[24];

    // argv[1] - serial port
    // argv[2] - file or echo 

    sprintf(serport, "%s", argv[1]);

    int file= open(serport, O_RDWR | O_NOCTTY | O_NDELAY);

    if (file == 0)
    {
        sprintf(msg, "open_port: Unable to open %s.\n", serport);
        perror(msg);
    }
    else
        fcntl(file, F_SETFL, FNDELAY); //fcntl(file, F_SETFL, 0);

    while (1)
    {

        printf("enter input data:\n");
        scanf("%s",&input[0]);

        chkin=write(file,input,sizeof input);

        if (chkin<0)
        {
            printf("cannot write to port\n");
        }

        //chkin=read(file,line,sizeof line);

        while ((chkin=read(file,line,sizeof line))>=0)
        {
            if (chkin<0)
            {
                printf("cannot read from port\n");
            }
            else
            {
                printf("bytes: %d, line=%s\n",chkin, line);
            }
        }

        /*CODE TO EXIT THE LOOP GOES HERE*/
        if (input[0] == 'q') break;
    }

    close(file);
    return 0;
}

The problem with the above code is that it doesn't explicitly initialize the serial port for character ("raw") operation; so depending on how the port was set previously, a session may look like this:

$ ./sertest /dev/ttyUSB0 
enter input data:
t1
enter input data:
t2
enter input data:
t3
enter input data:
^C

... in other words, there is no echoing of the input data. However, if the serial port is set up properly, we can get a session like:

$ ./sertest /dev/ttyUSB0 
enter input data:
t1
enter input data:
t2
bytes: 127, line=t1
enter input data:
t3
bytes: 127, line=t2
enter input data:
t4
bytes: 127, line=t3
enter input data:
^C

... (but even then, this sertest code fails on input words greater than 3 characters.)

Finally, through some online digging, I managed to find "(SOLVED) Serial Programming, Write-Read Issue", which offers a writeread.cpp example. However, for this byte-by-byte "duplex" case, not even that was enough - namely, "Serial Programming HOWTO" notes: "Canonical Input Processing ... is the normal processing mode for terminals ... which means that a read will only return a full line of input. A line is by default terminated by a NL (ASCII LF) ..." ; and thus we have to explicitly set the serial port to "non-canonical" (or "raw") mode in our code via ICANON (in other words, just setting O_NONBLOCK via open is not enough) - an example for that is given at "3.2 How can I read single characters from the terminal? - Unix Programming Frequently Asked Questions - 3. Terminal I/O". Once that is done, calling writeread will "correctly" set the serial port for the serport example (above), as well.

So I changed some of that writeread code back to C, added the needed initialization stuff, as well as time measurement, possibility to send strings or files, and additional output stream (for 'piping' the read serial data to a separate file). The code is below as writeread.c and serial.h, and with it, I can do something like in the following Bash session:

$ ./writeread /dev/ttyUSB0 2000000 writeread.c 3>myout.txt
stdalt opened; Alternative file descriptor: 3
Opening port /dev/ttyUSB0;
Got speed 2000000 (4107/0x100b);
Got file/string 'writeread.c'; opened as file (4182).

+++DONE+++
Wrote: 4182 bytes; Read: 4182 bytes; Total: 8364 bytes. 
Start: 1284422340 s 443302 us; End: 1284422347 s 786999 us; Delta: 7 s 343697 us. 
2000000 baud for 8N1 is 200000 Bps (bytes/sec).
Measured: write 569.47 Bps, read 569.47 Bps, total 1138.94 Bps.

$ diff writeread.c myout.txt 

$ ./writeread /dev/ttyUSB0 2000000 writeread.c 3>/dev/null 
stdalt opened; Alternative file descriptor: 3
Opening port /dev/ttyUSB0;
Got speed 2000000 (4107/0x100b);
Got file/string 'writeread.c'; opened as file (4182).

+++DONE+++
Wrote: 4182 bytes; Read: 4182 bytes; Total: 8364 bytes. 
Start: 1284422380 s -461710 us; End: 1284422388 s 342977 us; Delta: 8 s 804687 us. 
2000000 baud for 8N1 is 200000 Bps (bytes/sec).
Measured: write 474.97 Bps, read 474.97 Bps, total 949.95 Bps.

Well:

  • First surprise - it goes faster if I'm writing to a file, than if I'm piping to /dev/null!
  • Also, getting around 1000 Bps - whereas the device is apparently set for 200000 BPS!!

At this point, I'm thinking that the slowdown is because after each written byte in writeread.c, we wait for a flag to be cleared by the read interrupt, before we proceed to read the serial buffer. Possibly, if the reading and writing were separate threads, then both reading and writing could try to use bigger blocks of bytes in single read or write calls, and so bandwidth would be used better ?! (Or, maybe the interrupt handler does act, in some sense, like a "thread" running in parallel - so maybe something similar could be achieved by moving all read related functions to the interrupt handler ?!)

Ah well - at this point, I am very open to suggestions / links for existing code like writeread.c, but multithreaded :) And, of course, for any other possible Linux tools, or possibly Bash methods (although it seems Bash will not be able to exert this kind of control...)

Cheers!

 

writeread.c:

/*
    writeread.c - based on writeread.cpp
    [SOLVED] Serial Programming, Write-Read Issue - http://www.linuxquestions.org/questions/programming-9/serial-programming-write-read-issue-822980/

    build with: gcc -o writeread -Wall -g writeread.c
*/

#include <stdio.h>
#include <string.h>
#include <stddef.h>

#include <stdlib.h>
#include <sys/time.h>

#include "serial.h"


int serport_fd;

void usage(char **argv)
{
    fprintf(stdout, "Usage:\n"); 
    fprintf(stdout, "%s port baudrate file/string\n", argv[0]); 
    fprintf(stdout, "Examples:\n"); 
    fprintf(stdout, "%s /dev/ttyUSB0 115200 /path/to/somefile.txt\n", argv[0]); 
    fprintf(stdout, "%s /dev/ttyUSB0 115200 \"some text test\"\n", argv[0]); 
}


int main( int argc, char **argv ) 
{

    if( argc != 4 ) { 
        usage(argv);
        return 1; 
    }

    char *serport;
    char *serspeed;
    speed_t serspeed_t;
    char *serfstr;
    int serf_fd; // if < 0, then serfstr is a string
    int bytesToSend; 
    int sentBytes; 
    char byteToSend[2];
    int readChars;
    int recdBytes, totlBytes; 

    char sResp[11];

    struct timeval timeStart, timeEnd, timeDelta;
    float deltasec; 

    /* Re: connecting alternative output stream to terminal - 
    * http://coding.derkeiler.com/Archive/C_CPP/comp.lang.c/2009-01/msg01616.html 
    * send read output to file descriptor 3 if open, 
    * else just send to stdout
    */
    FILE *stdalt;
    if(dup2(3, 3) == -1) {
        fprintf(stdout, "stdalt not opened; ");
        stdalt = fopen("/dev/tty", "w");
    } else {
        fprintf(stdout, "stdalt opened; ");
        stdalt = fdopen(3, "w");
    }
    fprintf(stdout, "Alternative file descriptor: %d\n", fileno(stdalt));

    // Get the PORT name
    serport = argv[1];
    fprintf(stdout, "Opening port %s;\n", serport);

    // Get the baudrate
    serspeed = argv[2];
    serspeed_t = string_to_baud(serspeed);
    fprintf(stdout, "Got speed %s (%d/0x%x);\n", serspeed, serspeed_t, serspeed_t);

    //Get file or command;
    serfstr = argv[3];
    serf_fd = open( serfstr, O_RDONLY );
    fprintf(stdout, "Got file/string '%s'; ", serfstr);
    if (serf_fd < 0) {
        bytesToSend = strlen(serfstr);
        fprintf(stdout, "interpreting as string (%d).\n", bytesToSend);
    } else {
        struct stat st;
        stat(serfstr, &st);
        bytesToSend = st.st_size;
        fprintf(stdout, "opened as file (%d).\n", bytesToSend);
    }


    // Open and Initialise port
    serport_fd = open( serport, O_RDWR | O_NOCTTY | O_NONBLOCK );
    if ( serport_fd < 0 ) { perror(serport); return 1; }
    initport( serport_fd, serspeed_t );

    sentBytes = 0; recdBytes = 0;
    byteToSend[0]='x'; byteToSend[1]='\0';
    gettimeofday( &timeStart, NULL );

    // write / read loop - interleaved (i.e. will always write 
    // one byte at a time, before 'emptying' the read buffer ) 
    while ( sentBytes < bytesToSend )
    {
        // read next byte from input...
        if (serf_fd < 0) { //interpreting as string
            byteToSend[0] = serfstr[sentBytes];
        } else { //opened as file 
            read( serf_fd, &byteToSend[0], 1 );
        }

        if ( !writeport( serport_fd, byteToSend ) ) { 
            fprintf(stdout, "write failed\n"); 
        }
        //~ fprintf(stdout, "written:%s\n", byteToSend );

        while ( wait_flag == TRUE );

        if ( (readChars = readport( serport_fd, sResp, 10)) >= 0 ) 
        {
            //~ fprintf(stdout, "InVAL: (%d) %s\n", readChars, sResp);
            recdBytes += readChars;
            fprintf(stdalt, "%s", sResp);
        }

        wait_flag = TRUE; // was ==
        //~ usleep(50000);
        sentBytes++;
    }

    gettimeofday( &timeEnd, NULL );

    // Close the open port
    close( serport_fd );
    if (!(serf_fd < 0)) close( serf_fd );

    fprintf(stdout, "\n+++DONE+++\n");

    totlBytes = sentBytes + recdBytes;
    timeval_subtract(&timeDelta, &timeEnd, &timeStart);
    deltasec = timeDelta.tv_sec+timeDelta.tv_usec*1e-6;

    fprintf(stdout, "Wrote: %d bytes; Read: %d bytes; Total: %d bytes. \n", sentBytes, recdBytes, totlBytes);
    fprintf(stdout, "Start: %ld s %ld us; End: %ld s %ld us; Delta: %ld s %ld us. \n", timeStart.tv_sec, timeStart.tv_usec, timeEnd.tv_sec, timeEnd.tv_usec, timeDelta.tv_sec, timeDelta.tv_usec);
    fprintf(stdout, "%s baud for 8N1 is %d Bps (bytes/sec).\n", serspeed, atoi(serspeed)/10);
    fprintf(stdout, "Measured: write %.02f Bps, read %.02f Bps, total %.02f Bps.\n", sentBytes/deltasec, recdBytes/deltasec, totlBytes/deltasec);

    return 0;
}

serial.h:

/* serial.h
    (C) 2004-5 Captain http://www.captain.at

    Helper functions for "ser"

    Used for testing the PIC-MMC test-board
    http://www.captain.at/electronic-index.php
*/

#include <stdio.h>   /* Standard input/output definitions */
#include <string.h>  /* String function definitions */
#include <unistd.h>  /* UNIX standard function definitions */
#include <fcntl.h>   /* File control definitions */
#include <errno.h>   /* Error number definitions */
#include <termios.h> /* POSIX terminal control definitions */
#include <sys/signal.h>
#include <sys/stat.h>
#include <sys/types.h>

#define TRUE    1
#define FALSE   0

int wait_flag = TRUE;   // TRUE while no signal received

// Definition of Signal Handler
void DAQ_signal_handler_IO ( int status )
{
    //~ fprintf(stdout, "received SIGIO signal %d.\n", status);
    wait_flag = FALSE;
}


int writeport( int fd, char *comm ) 
{
    int len = strlen( comm );
    int n = write( fd, comm, len );

    if ( n < 0 ) 
    {
        fprintf(stdout, "write failed!\n");
        return 0;
    }

    return n;
}


int readport( int fd, char *resp, size_t nbyte ) 
{
    int iIn = read( fd, resp, nbyte );
    if ( iIn < 0 ) 
    {
        if ( errno == EAGAIN ) 
        {
            fprintf(stdout, "SERIAL EAGAIN ERROR\n");
            return 0;
        } 
        else 
        {
            fprintf(stdout, "SERIAL read error: %d = %s\n", errno , strerror(errno));
            return 0;
        }
    }

    if ( resp[iIn-1] == '\r' )
        resp[iIn-1] = '\0';
    else
        resp[iIn] = '\0';

    return iIn;
}


int getbaud( int fd ) 
{
    struct termios termAttr;
    int inputSpeed = -1;
    speed_t baudRate;
    tcgetattr( fd, &termAttr );
    // Get the input speed
    baudRate = cfgetispeed( &termAttr );
    switch ( baudRate )
    {
        case B0:      inputSpeed = 0; break;
        case B50:     inputSpeed = 50; break;
        case B110:    inputSpeed = 110; break;
        case B134:    inputSpeed = 134; break;
        case B150:    inputSpeed = 150; break;
        case B200:    inputSpeed = 200; break;
        case B300:    inputSpeed = 300; break;
        case B600:    inputSpeed = 600; break;
        case B1200:   inputSpeed = 1200; break;
        case B1800:   inputSpeed = 1800; break;
        case B2400:   inputSpeed = 2400; break;
        case B4800:   inputSpeed = 4800; break;
        case B9600:   inputSpeed = 9600; break;
        case B19200:  inputSpeed = 19200; break;
        case B38400:  inputSpeed = 38400; break;
        case B115200: inputSpeed = 115200; break;
        case B2000000: inputSpeed = 2000000; break; //added
    }
    return inputSpeed;
}


/* ser.c
    (C) 2004-5 Captain http://www.captain.at

    Sends 3 characters (ABC) via the serial port (/dev/ttyS0) and reads
    them back if they are returned from the PIC.

    Used for testing the PIC-MMC test-board
    http://www.captain.at/electronic-index.php

*/


int initport( int fd, speed_t baudRate ) 
{
    struct termios options;
    struct sigaction saio;  // Definition of Signal action

    // Install the signal handler before making the device asynchronous
    saio.sa_handler = DAQ_signal_handler_IO;
    saio.sa_flags = 0;
    saio.sa_restorer = NULL;
    sigaction( SIGIO, &saio, NULL );

    // Allow the process to receive SIGIO
    fcntl( fd, F_SETOWN, getpid() );
    // Make the file descriptor asynchronous (the manual page says only 
    // O_APPEND and O_NONBLOCK, will work with F_SETFL...)
    fcntl( fd, F_SETFL, FASYNC );
    //~ fcntl( fd, F_SETFL, FNDELAY); //doesn't work; //fcntl(file, F_SETFL, 0);

    // Get the current options for the port...
    tcgetattr( fd, &options );
/*       
    // Set port settings for canonical input processing
    options.c_cflag = BAUDRATE | CRTSCTS | CLOCAL | CREAD;
    options.c_iflag = IGNPAR | ICRNL;
    //options.c_iflag = IGNPAR;
    options.c_oflag = 0;
    options.c_lflag = ICANON;
    //options.c_lflag = 0;
    options.c_cc[VMIN] = 0;
    options.c_cc[VTIME] = 0;
*/   
    /* ADDED - else 'read' will not return, unless it sees LF '\n' !!!!
    * From: Unix Programming Frequently Asked Questions - 3. Terminal I/O - 
    * http://www.steve.org.uk/Reference/Unix/faq_4.html 
    */
    /* Disable canonical mode, and set buffer size to 1 byte */
    options.c_lflag &= (~ICANON);
    options.c_cc[VTIME] = 0;
    options.c_cc[VMIN] = 1; 

    // Set the baud rates to...
    cfsetispeed( &options, baudRate );
    cfsetospeed( &options, baudRate );

    // Enable the receiver and set local mode...
    options.c_cflag |= ( CLOCAL | CREAD );
    options.c_cflag &= ~PARENB;
    options.c_cflag &= ~CSTOPB;
    options.c_cflag &= ~CSIZE;
    options.c_cflag |= CS8;

    // Flush the input & output...
    tcflush( fd, TCIOFLUSH );

    // Set the new options for the port...
    tcsetattr( fd, TCSANOW, &options );

    return 1;
}


/* 
    ripped from 
    http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/stty.c
*/

#define STREQ(a, b)     (strcmp((a), (b)) == 0)

struct speed_map
{
  const char *string;       /* ASCII representation. */
  speed_t speed;        /* Internal form. */
  unsigned long int value;  /* Numeric value. */
};

static struct speed_map const speeds[] =
{
  {"0", B0, 0},
  {"50", B50, 50},
  {"75", B75, 75},
  {"110", B110, 110},
  {"134", B134, 134},
  {"134.5", B134, 134},
  {"150", B150, 150},
  {"200", B200, 200},
  {"300", B300, 300},
  {"600", B600, 600},
  {"1200", B1200, 1200},
  {"1800", B1800, 1800},
  {"2400", B2400, 2400},
  {"4800", B4800, 4800},
  {"9600", B9600, 9600},
  {"19200", B19200, 19200},
  {"38400", B38400, 38400},
  {"exta", B19200, 19200},
  {"extb", B38400, 38400},
#ifdef B57600
  {"57600", B57600, 57600},
#endif
#ifdef B115200
  {"115200", B115200, 115200},
#endif
#ifdef B230400
  {"230400", B230400, 230400},
#endif
#ifdef B460800
  {"460800", B460800, 460800},
#endif
#ifdef B500000
  {"500000", B500000, 500000},
#endif
#ifdef B576000
  {"576000", B576000, 576000},
#endif
#ifdef B921600
  {"921600", B921600, 921600},
#endif
#ifdef B1000000
  {"1000000", B1000000, 1000000},
#endif
#ifdef B1152000
  {"1152000", B1152000, 1152000},
#endif
#ifdef B1500000
  {"1500000", B1500000, 1500000},
#endif
#ifdef B2000000
  {"2000000", B2000000, 2000000},
#endif
#ifdef B2500000
  {"2500000", B2500000, 2500000},
#endif
#ifdef B3000000
  {"3000000", B3000000, 3000000},
#endif
#ifdef B3500000
  {"3500000", B3500000, 3500000},
#endif
#ifdef B4000000
  {"4000000", B4000000, 4000000},
#endif
  {NULL, 0, 0}
};

static speed_t
string_to_baud (const char *arg)
{
  int i;

  for (i = 0; speeds[i].string != NULL; ++i)
    if (STREQ (arg, speeds[i].string))
      return speeds[i].speed;
  return (speed_t) -1;
}



/* http://www.gnu.org/software/libtool/manual/libc/Elapsed-Time.html
Subtract the `struct timeval' values X and Y,
storing the result in RESULT.
Return 1 if the difference is negative, otherwise 0.  */
int timeval_subtract (struct timeval *result, struct timeval *x, struct timeval *y)
{
    /* Perform the carry for the later subtraction by updating y. */
    if (x->tv_usec < y->tv_usec) {
     int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1;
     y->tv_usec -= 1000000 * nsec;
     y->tv_sec += nsec;
    }
    if (x->tv_usec - y->tv_usec > 1000000) {
     int nsec = (x->tv_usec - y->tv_usec) / 1000000;
     y->tv_usec += 1000000 * nsec;
     y->tv_sec -= nsec;
    }

    /* Compute the time remaining to wait.
      tv_usec is certainly positive. */
    result->tv_sec = x->tv_sec - y->tv_sec;
    result->tv_usec = x->tv_usec - y->tv_usec;

    /* Return 1 if result is negative. */
    return x->tv_sec < y->tv_sec;
}