FTDI D2XX Windows to Linux

418 views Asked by At

I have a simple polling loop for a serial port that receives continuous bit bang data, and I'm seeing big performance losses moving from Windows to Linux.

It's a really simple application using FTDI's D2XX driver to run a USB UART in async bit bang mode to continuously read incoming data.

It's just always polling for a minimum number of incoming bytes, if quantity is met read entire receive buffer into memory, repeat.

My issue is that I originally wrote this in Windows. Each loop is completed in 2 milliseconds or less, with about 0.01% of loops straying out of range into the 2-2.5 millisecond zone. This was effectively perfect for my application.

However, I'm using Ubuntu on my server, which is where this application is meant to run. The problem is that using the exact same code, Linux is significantly slower. Effectively 1% of loops exceed 2 milliseconds, and I've seen peaks as high as 10 milliseconds. Even more oddly, when these peaks happen, it is usually in a pattern like below (in microseconds):

1500 1490 1550 5000 500 430 450 50 1500 1400

So on average a loop takes 1.5 milliseconds, but there will be times when there's a big lag spike on one sample, and a handful of subsequent samples go way faster than average before returning to normal.

I'm currently playing with the kernel. I tried the stock low latency kernel from the repository, but it didn't seem to have a clear effect. It might have reduced the total number of loops that timeout from 1% to 0.7%, but it's not clear. I'm building a modified kernel with some optimizations for the Intel CPU I'm using, but I'm not optimistic.

I've read there's an ASYNC_LOW_LATENCY flag you can throw onto a serial port to improve performance, but the FTDI doesn't have a /dev/ttyUSB device associated with it when using the D2XX driver so I don't know how to proceed with that.

Does anybody know what it is about Linux that makes it slower than Windows reading this UART and is there anything I can do about it? Minimizing timeouts is kind of mission critical to me. I've even set real time priority for the thread running this code on a dedicated core and I am not seeing any improvement.

Any help would be appreciated.

1

There are 1 answers

3
Rick64 On
  1. First question are your running Ubuntu on a VM or on a dedicated CPU? Virtual Machine definetly slows down the reading process.
  2. In the thread loop of your serial port how often do you read? In C language I open the port with these settings:
    int iResult;
    int iParity_flag;
    int RTS_flag;
    int DTR_flag;
    int fd;                             // File descriptor
    struct termios PortSettings;

    // Open port
    fd = open(in_DeviceName, O_RDWR | O_NOCTTY | O_NDELAY);
// Save File Descriptor
    int= fd;
    *out_FileDescriptor = fd;

    // Read in existing settings, and handle any error
    // NOTE: This is important! POSIX states that the struct passed to tcsetattr()
    // must have been initialized with a call to tcgetattr() overwise behaviour
    // is undefined
    if(tcgetattr(fd, &PortSettings) != 0) {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
    }

    // Open the device in nonblocking mode
    fcntl(fd, F_SETFL, FNDELAY);

    // Set parameters   
    bzero(&PortSettings, sizeof(PortSettings));    // Clear all the options

    // [♦] BAUDRATE [♦]
   speed_t  Speed;
    switch (in_Baudrate)
    {
        case 110  :     Speed=B110; break;
        case 300  :     Speed=B300; break;
        case 600  :     Speed=B600; break;
        case 1200 :     Speed=B1200; break;
        case 2400 :     Speed=B2400; break;
        case 4800 :     Speed=B4800; break;
        case 9600 :     Speed=B9600; break;
        case 19200 :    Speed=B19200; break;
        case 38400 :    Speed=B38400; break;
        case 57600 :    Speed=B57600; break;
        case 115200 :   Speed=B115200; break;
        default     :   m_bOpened = false; return(m_bOpened);
    }

    cfsetispeed(&PortSettings, Speed);                   // Set INPUT the baud rate at 115200 bauds
    cfsetospeed(&PortSettings, Speed);                   // Set OUTPUT the baud rate at 115200 bauds

    //  [♦] BITS (c_cflag) [♦]
        // PortSettings.c_cflag |= ( CLOCAL | CREAD |  CS8);    // Configure the device : 8 bits, no parity, no control
        PortSettings.c_cflag &= ~CSIZE; // Clear all the size bits, then use one of the statements below

        switch (in_Bits)
        {
            case 8  :
                PortSettings.c_cflag |= CS8;
                break;
            case 7  :
                PortSettings.c_cflag |= CS7;
                break;
            case 6  :
                PortSettings.c_cflag |= CS6;
                break;
            case 5  :
                PortSettings.c_cflag |= CS5;
                break;

            default:
                m_bOpened = false;
                return(m_bOpened);
        }

    //  [♦] PARITY (c_cflag) [♦]
        iParity_flag = 0;
        // Check NONE
        iResult = memcmp (in_Parity, "N", strlen("N"));
        if (iResult == 0){
            PortSettings.c_cflag &= ~PARENB;
            iParity_flag++;
        }
        // Check EVEN
        iResult = memcmp (in_Parity, "E", strlen("E"));
        if (iResult == 0){
            PortSettings.c_cflag |= PARENB;
            PortSettings.c_cflag &= ~PARODD;
            iParity_flag++;
        }
        // Check ODD
        iResult = memcmp (in_Parity, "O", strlen("O"));
        if (iResult == 0){
            PortSettings.c_cflag |= PARENB;
            PortSettings.c_cflag |= PARODD;
            iParity_flag++;
        }
        // Check if we found the Parity
        if (iParity_flag != 1){
            m_bOpened = false;
            return(m_bOpened);
        }


    //  [♦] STOP BITS (c_cflag) [♦]
        switch (in_StopBits)
        {
            case 1  :
                PortSettings.c_cflag &= ~CSTOPB; // Clear stop field, only 1 stop bit used in communication
                break;
            case 2  :
                PortSettings.c_cflag |= CSTOPB;  // Set stop field, 2 stop bits used in communication
                break;
            default:
                m_bOpened = false;
                return(m_bOpened);
        }

    //  [♦] DTR [♦]
        DTR_flag = TIOCM_DTR;
        if (in_DTR == 0){
            ioctl(fd, TIOCMBIC, &DTR_flag); //Clear RTS pin
        }
        else{
            ioctl(fd,TIOCMBIS, &DTR_flag);   //Set RTS pin
        }

    //  [♦] RTS [♦]
        RTS_flag = TIOCM_RTS;
        if (in_RTS == 0){
            ioctl(fd, TIOCMBIC, &RTS_flag); //Clear RTS pin
        }
        else{
            ioctl(fd,TIOCMBIS,&RTS_flag);   //Set RTS pin
        }

    /*
      [♦] INPUT MODES (c_iflag) [♦]
        Clearing all of the following bits disables any special handling of the bytes as they are received by the serial port, before they are passed to the application.
        We just want the raw data thanks!
    */
        //PortSettings.c_iflag |= ( IGNPAR | IGNBRK );         // input mode flags
        PortSettings.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

    /*
      [♦] OUTPUT MODES (c_oflag) [♦]
        The c_oflag member of the termios struct contains low-level settings for output processing.
        When configuring a serial port, we want to disable any special handling of output chars/bytes.
    */

        PortSettings.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
        PortSettings.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/l


    /*
      [♦] VMIN - VTIME (c_cc) [♦]
        When VMIN is 0, VTIME specifies a time-out from the start of the read() call.
        When VMIN is > 0, VTIME specifies the time-out from the start of the first received character.
        Let’s explore the different combinations:
        VMIN = 0, VTIME = 0: No blocking, return immediately with what is available
        VMIN > 0, VTIME = 0: This will make read() always wait for bytes (exactly how many is determined by VMIN), so read() could block indefinitely.
        VMIN = 0, VTIME > 0: This is a blocking read of any number of chars with a maximum timeout (given by VTIME). read() will block until either any amount of data is available, or the timeout occurs. This happens to be my favourite mode (and the one I use the most).
        VMIN > 0, VTIME > 0: Block until either VMIN characters have been received, or VTIME after first character has elapsed. Note that the timeout for VTIME does not begin until the first character is received.
        VMIN and VTIME are both defined as the type cc_t, which I have always seen be an alias for unsigned char (1 byte). This puts an upper limit on the number of VMIN characters to be 255 and the maximum timeout of 25.5 seconds (255 deciseconds).
    */

        PortSettings.c_cc[VTIME]=0;                          // Timer unused
        PortSettings.c_cc[VMIN]=0;                           // At least on character before satisfy reading

   // Save tty settings, also checking for error
    if (tcsetattr(fd, TCSANOW, &PortSettings) != 0) {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));       
        return 1;
    }
    
    return 1;

and then on a thread read:

    while (1)
    {
        memset (ReadBuffer, 0, sizeof(ReadBuffer));
        bytes_read = read(in_fd, &ReadBuffer, sizeof(ReadBuffer));
        if (bytes_read > 0){
            memcpy (LocalBuffer+BytesCounter, ReadBuffer, bytes_read);
            BytesCounter = BytesCounter + bytes_read;
            SystemTick = GetSystem_getTick(); // Adapt your function here
        }
        if (BytesCounter == 0){
            break;
        }
        if (bytes_read == 0 && BytesCounter > 0){
            ElapsedTime = GetSystem_getTick() - SystemTick; // Adapt your function here
            if ((int)ElapsedTime > in_BytesDelay){
                break;
            }
        }
    }

    // Copy in out Buffer
    if (BytesCounter > LINUX_SERIAL_BUFFER){
        return -1;
    }
    else{
        memcpy ((unsigned char*)out_buffer, LocalBuffer, BytesCounter);
    }

You can adapt the code at your needs.

if you want to test it better I use a software called SerialTool www.serialtool.com which beautifully works on Windows, Linux (Ubuntu) and MacOS. It allows you to set the read timeout between each byte (take a look at the consifiguration somewhere in the software) and you won't loose any byte. At least you can compare the two version in Windows and Linux. I use it on Ubuntu and it runs faster than Windows on FTDI USB converter. Take a look at it!

I hope this helped your task.

I hope this helped