clibusb

Libusb race condition using asynchronous I/O


I'm using libusb to communicate with a Philips ISP1362 configured as a USB device. I am able to successfully loopback data using Synchronous I/O without any problems. For some reason when using Asynchronous I/O there appears to be a race condition.

I am transferring 64-byte packets using back-to-back OUT-IN transfers. Occasionally when I run my program libusb throws a timeout error and some of the loopback data is lost. When analyzing the USB bus using my Beagle 12 I can see the OUT-IN transactions are out of order (i.e. OUT-OUT-IN-TIMEOUT) when it should be (OUT-IN-OUT-IN).

Update The transfers are appearing out of order in the callback function which is strange because they are not coinciding with what is actually on the bus analyzer.

Example 1: (IN-OUT-IN-OUT)

main(): submitting transfer 0, endpoint 1
main(): submitting transfer 1, endpoint 82
main(): submitting transfer 2, endpoint 1
main(): submitting transfer 3, endpoint 82
xfr_cb(): count 0, status = 0, endpoint = 82, actual_length = 64, completed = 0
xfr_cb(): count 1, status = 0, endpoint = 1, actual_length = 64, completed = 0
xfr_cb(): count 2, status = 0, endpoint = 82, actual_length = 64, completed = 0
xfr_cb(): count 3, status = 0, endpoint = 1, actual_length = 64, completed = 0
completed

Example 2: (OUT-IN-IN-OUT)

main(): submitting transfer 0, endpoint 1
main(): submitting transfer 1, endpoint 82
main(): submitting transfer 2, endpoint 1
main(): submitting transfer 3, endpoint 82
xfr_cb(): count 0, status = 0, endpoint = 1, actual_length = 64, completed = 0
xfr_cb(): count 1, status = 0, endpoint = 82, actual_length = 64, completed = 0
xfr_cb(): count 2, status = 0, endpoint = 82, actual_length = 64, completed = 0
xfr_cb(): count 3, status = 0, endpoint = 1, actual_length = 64, completed = 0
completed

Below is a screenshot from the analyzer:

Beagle 12 Capture

Below is the code:

#include <stdlib.h>
#include <stdio.h>

#include <libusb-1.0/libusb.h>

/* Specify VENDOR_ID and PRODUCT_ID for device */
#define VENDOR_ID   0x0471
#define PRODUCT_ID  0x3630

/* Define number of bytes to transfer */
#define EP_SIZE 64               // bytes
#define TRANSFERS 4              // number of transfers
#define BYTES EP_SIZE*TRANSFERS
#define TIMEOUT 3*1000           // milliseconds

/* Use a global variable to keep the device handle */
static struct libusb_device_handle *devh = NULL;

/* use a global variable to keep the context */
static struct libusb_context *usb_context = NULL;

/* count variable */
int count = 0;

/* The Endpoint addresses are hard-coded.  You should use libusb -v to find
 * the values corresponding to device
 */
static int ep_in  = 0x82;
static int ep_out = 0x01;

void xfr_cb(struct libusb_transfer *transfer )
{
  int *completed = transfer->user_data;

  /* callback - This is called after the transfer has been received by libusb */
  fprintf(stderr, "xfr_cb(): count %d, status = %d, endpoint = %x, actual_length = %d, completed = %d\n",
          count,
          transfer->status,
          transfer->endpoint,
          transfer->actual_length,
          *completed);
  if (transfer->status != LIBUSB_TRANSFER_COMPLETED)
  {
    /* Error! */
    fprintf(stderr, "Error: %s\n", libusb_error_name((int)transfer->status));
  }

  if (count == TRANSFERS-1)
    *completed = 1;
  count++;
}

int main(int argc, char **argv)
{
  int ep_addr;
  int completed = 0;
  unsigned char *buf;
  size_t length = 64;
  int n;
  int i;
  int rc;

  /* Initialize libusb */
  rc = libusb_init(NULL);
  if (rc < 0)
  {
    fprintf(stderr, "Error Initializing libusb: %s\n", libusb_error_name(rc));
    exit(1);
  }

  /* Set debugging output to max level */
  libusb_set_debug(NULL, 3);

  /* Look for a specific device and open it */
  devh = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);
  if (!devh)
  {
    fprintf(stderr, "Error finding USB device\n");
    goto out;
  }

  /* allocate memory */
  buf = malloc(length);

  /* start with OUT transfer */
  ep_addr = ep_out;

  /* queue up alternating OUT-IN transfers */
  for (i = 0; i < TRANSFERS; i++)
  {
    /* fill the buffer with incrementing data */
    for (n = 0; n < EP_SIZE; n++)
    {
      buf[n] = i+n;
    }

    /* Set up the transfer object */
    struct libusb_transfer *transfer;
    transfer = libusb_alloc_transfer(0);
    libusb_fill_bulk_transfer(transfer, devh, ep_addr, buf, EP_SIZE, xfr_cb, &completed, TIMEOUT); /* callback data = &completed */

    /* Submit the transfer object */
    libusb_submit_transfer(transfer);
    fprintf(stderr, "main(): submitting transfer %d, endpoint %x\n", i, ep_addr);

    /* alternate writing and reading for loopback */
    ep_addr = (ep_addr == ep_out) ? ep_in : ep_out;
  }

  /* Handle Events */
  while (!completed)
  {
    rc = libusb_handle_events_completed(NULL, &completed);
    if (rc < 0)
    {
      if (rc == LIBUSB_ERROR_INTERRUPTED)
        continue;
      fprintf(stderr, "Transfer Error: %s", libusb_error_name(rc));
      continue;
    }
  }

  fprintf(stderr, "completed\n");

  /* Release the interface */
  libusb_release_interface(devh, 0);


  /* Close the device handle */
  if (devh)
    libusb_close(devh);


out:
  if (devh)
  {
    libusb_close(devh);
  }
  libusb_exit(NULL);

  return rc;
}

Update 2 I successfully eliminated the timeout. The cause of the libusb timeout is because the Host was sending two consecutive OUT transactions intermittently on the bus.

Analyzer screenshot:

enter image description here

The following is the working code (no timeouts). Ran these thousands of times with no issues

static void LIBUSB_CALL xfr_cb(struct libusb_transfer *transfer )
{
  int *completed = transfer->user_data;
  unsigned char *wbuf, *rbuf;
  size_t length = 64;

  fprintf(stderr, "xfr_cb(): status = %d, endpoint = %x, actual_length = %d\n",
          transfer->status,
          transfer->endpoint,
          transfer->actual_length);

  *completed = 1;
}

int main(int argc, char **argv)
{
  const struct libusb_version *version;
  int ep_addr;
  int completed = 0;
  unsigned char *buf, *wbuf1, *wbuf2, *rbuf1, *rbuf2;
  size_t length = 64;
  int n;
  int m;
  int i;
  int rc;

  /* Get libusb version */
  version = libusb_get_version();
  fprintf(stderr, "libusb version: %d.%d.%d.%d\n", version->major, version->minor, version->micro, version->nano);

  /* Initialize libusb */
  rc = libusb_init(NULL);
  if (rc < 0)
  {
    fprintf(stderr, "Error Initializing libusb: %s\n", libusb_error_name(rc));
    exit(1);
  }

  /* Set debugging output to max level */
  libusb_set_debug(NULL, 3);

  /* Look for a specific device and open it */
  handle = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);
  if (!handle)
  {
    fprintf(stderr, "Error finding USB device\n");
    goto out;
  }

  /* claim interface */
  rc = libusb_claim_interface(handle, 0);
  if (rc < 0)
  {
    fprintf(stderr, "Error claiming interface.\n");
    goto out;
  }

  /* allocate memory */
  wbuf1 = malloc(length);
  wbuf2 = malloc(length);
  rbuf1 = malloc(length);
  rbuf2 = malloc(length);

  /* fill the buffer with incrementing data */
  for (n = 0; n < EP_SIZE; n++)
    wbuf1[n] = n;

  for (m = 0; m < EP_SIZE; m++)
    wbuf2[m] = m+1;

  struct libusb_transfer *transfer1;
  struct libusb_transfer *transfer2;
  struct libusb_transfer *transfer3;
  struct libusb_transfer *transfer4;

  /* Set up the transfer object */
  transfer1 = libusb_alloc_transfer(0);
  transfer2 = libusb_alloc_transfer(0);
  transfer3 = libusb_alloc_transfer(0);
  transfer4 = libusb_alloc_transfer(0);
  libusb_fill_bulk_transfer(transfer1, handle, ep_out, wbuf1, EP_SIZE, xfr_cb, NULL, TIMEOUT);
  libusb_fill_bulk_transfer(transfer2, handle, ep_in, rbuf1, EP_SIZE,  xfr_cb, NULL, TIMEOUT);
  libusb_fill_bulk_transfer(transfer3, handle, ep_out, wbuf2, EP_SIZE, xfr_cb, NULL, TIMEOUT);
  libusb_fill_bulk_transfer(transfer4, handle, ep_in, rbuf2, EP_SIZE,  xfr_cb, &completed, TIMEOUT); /* callback data = &completed */

  /* Submit the transfers */
  libusb_submit_transfer(transfer1);
  libusb_submit_transfer(transfer2);
  libusb_submit_transfer(transfer3);
  libusb_submit_transfer(transfer4);

  /* Handle Events */
  while (!completed)
  {
    rc = libusb_handle_events_completed(NULL, &completed);
    if (rc != LIBUSB_SUCCESS)
    {
      fprintf(stderr, "Transfer Error: %s\n", libusb_error_name(rc));
      break;
    }
  }

  fprintf(stderr, "completed\n");

  //* Release the interface */
  libusb_release_interface(handle, 0);

  /* Close the device handle */
  if (handle)
    libusb_close(handle);

out:
  if (handle)
  {
    libusb_close(handle);
  }
  libusb_exit(NULL);

  return rc;
}

Changing the code as follows (i.e. callback = NULL for transfer 1-3) re-creates intermittent duplicate transactions, as shown in the screenshots.

  libusb_fill_bulk_transfer(transfer1, handle, ep_out, wbuf1, EP_SIZE, NULL, NULL, TIMEOUT);
  libusb_fill_bulk_transfer(transfer2, handle, ep_in, rbuf1, EP_SIZE,  NULL, NULL, TIMEOUT);
  libusb_fill_bulk_transfer(transfer3, handle, ep_out, wbuf2, EP_SIZE, NULL, NULL, TIMEOUT);
  libusb_fill_bulk_transfer(transfer4, handle, ep_in, rbuf2, EP_SIZE,  xfr_cb, &completed, TIMEOUT); /* callback data = &completed */

I honestly don't understand why the loop would cause race conditions based on their documentation and examples. Queueing up multiple transfers is actually suggested in one of the libusb examples (sam3u_benchmark.c) and also demonstrated (using loops) in the following .pdfs.

See asynchronous I/O sections:

https://www.socallinuxexpo.org/sites/default/files/presentations/scale_2017_usb.pdf http://www.signal11.us/oss/elc2014/elc_2014_usb_0.pdf

From my understanding, the use of libusb_handle_events_completed(NULL, &completed) is supposed to resolve synchronization issues. Am I misunderstanding something? See libusb_handle_events() from multiple threads http://libusb.sourceforge.net/api-1.0/libusb_mtasync.html

-"This is why libusb-1.0.9 introduces the new libusb_handle_events_timeout_completed() and libusb_handle_events_completed() functions, which handles doing the completion check for you after they have acquired the lock:"

What they need are crystal clear examples of how to use their API if this is the case.

I can add more event checking but something does not seem right here.

Update 3: See accepted answer.


Solution

  • I started reading the documentation in the libusb source code and understood what was happening.

    Particularly the section about how libusb deals with packet sizes: http://libusb.sourceforge.net/api-1.0/libusb_packetoverflow.html

    After reading that it clicked for me and I found two ways to accomplish a loopback test with large data size using asynchronous I/O.

    The first way is submitting two transfers consecutively with transfer->buffer containing the entire data structure (i.e. total bytes to send and receive). The second way is submitting the two transfers with transfer->buffer containing wMaxPacketSize (e.g. 64-bytes) and having the out and in callback functions submit additional transfers to transceive the rest of the data.

    For the second case, extra code needed to be added to keep track of the number of transfers and to set the completed signal when finished. The OUT-IN packet interleaving is handled by libusb and the OS - which was the part I didn't realize. In other words, not every OUT-IN transfer needed to be specified and queued individually.

    Here is the asynchronous code along with the transfer rates to my USB device (ISP1362). My USB device controller is an FPGA coded in pure SystemVerilog.

    Note: Regarding the transfer rates, I only have double-buffering enabled on BULK_EP_IN. I am assuming the IN-NAK's (# POLL) and transfer rate would improve in the second approach if double-buffering was enabled on BULK_EP_OUT. So this may not be a fair comparison due to device configuration.

    First approach: ~1.161 MB/s (~9.288 Mb/s)

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    #include </usr/include/libusb-1.0/libusb.h>
    
    /* Specify VENDOR_ID and PRODUCT_ID for device */
    #define VENDOR_ID   0x0471
    #define PRODUCT_ID  0x3630
    
    /* Define number of bytes to transfer */
    #define EP_SIZE 64                       // bytes
    #define TRANSFERS 1024*768*3/EP_SIZE     // number of transfers
    #define TIMEOUT 10*1000                  // milliseconds
    
    /* Use a global variable to keep the device handle */
    static struct libusb_device_handle *handle = NULL;
    
    /* count variables */
    unsigned int count = 0;
    unsigned int count_in = 0;
    unsigned int count_out = 0;
    
    /* The Endpoint addresses are hard-coded.  You should use libusb -v to find
     * the values corresponding to device
     */
    static int ep_in  = 0x82;
    static int ep_out = 0x01;
    
    /* Write and Read buffers */
    unsigned char wbuf[EP_SIZE*TRANSFERS];
    unsigned char wbuf_tmp[EP_SIZE*TRANSFERS];
    unsigned char rbuf[EP_SIZE*TRANSFERS];
    unsigned char rbuf_tmp[EP_SIZE*TRANSFERS];
    
    static void LIBUSB_CALL xfr_cb_out(struct libusb_transfer *transfer )
    {
      memcpy(wbuf+count_out*EP_SIZE, transfer->buffer, EP_SIZE);
    }
    
    static void LIBUSB_CALL xfr_cb_in(struct libusb_transfer *transfer )
    {
      int *completed = transfer->user_data;
      memcpy(rbuf+count_in*EP_SIZE, transfer->buffer, EP_SIZE);
    
      count_in++;  // one transfer complete
      if (count_in < TRANSFERS)
        *completed = 1;
    }
    
    int main(int argc, char **argv)
    {
      const struct libusb_version *version;
      int completed = 0;
      size_t length = 64;
      int n;
      int m;
      int rc;
    
      /* Get libusb version */
      version = libusb_get_version();
      fprintf(stderr, "libusb version: %d.%d.%d.%d\n", version->major, version->minor, version->micro, version->nano);
    
      /* Initialize libusb */
      rc = libusb_init(NULL);
      if (rc < 0)
      {
        fprintf(stderr, "Error Initializing libusb: %s\n", libusb_error_name(rc));
        exit(1);
      }
    
      /* Set debugging output to max level */
      libusb_set_debug(NULL, 3);
    
      /* Look for a specific device and open it */
      handle = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);
      if (!handle)
      {
        fprintf(stderr, "Error finding USB device\n");
        goto out;
      }
    
      /* claim interface */
      rc = libusb_claim_interface(handle, 0);
      if (rc < 0)
      {
        fprintf(stderr, "Error claiming interface.\n");
        goto out;
      }
    
      /* fill the buffer with incrementing data */
      for (n = 0; n < TRANSFERS; n++)
      {
        for (m = 0; m < EP_SIZE; m++)
        {
          wbuf_tmp[m+n*EP_SIZE] = m+n;
        }
      }
    
      struct libusb_transfer *transfer;
      transfer = libusb_alloc_transfer(0);
      libusb_fill_bulk_transfer(transfer, handle, ep_out, wbuf_tmp, EP_SIZE*TRANSFERS, xfr_cb_out, NULL, TIMEOUT);
      libusb_submit_transfer(transfer);
    
      transfer = libusb_alloc_transfer(0);
      libusb_fill_bulk_transfer(transfer, handle, ep_in, rbuf_tmp, EP_SIZE*TRANSFERS, xfr_cb_in, &completed, TIMEOUT);
      libusb_submit_transfer(transfer);
    
      /* Handle Events */
      while (!completed)
      {
        rc = libusb_handle_events_completed(NULL, &completed);
        if (rc != LIBUSB_SUCCESS)
        {
          fprintf(stderr, "Transfer Error: %s\n", libusb_error_name(rc));
          break;
        }
      }
    
      fprintf(stderr, "completed\n");
    
      int res;
      res = memcmp(rbuf, wbuf, sizeof(wbuf));
      if (res != 0)
        fprintf(stderr, "miscompare\n");
      else
        fprintf(stderr, "success\n");
    
      //* Release the interface */
      libusb_release_interface(handle, 0);
    
      /* Close the device handle */
      if (handle)
        libusb_close(handle);
    
    out:
      if (handle)
      {
        libusb_close(handle);
      }
      libusb_exit(NULL);
    
      return rc;
    }
    

    enter image description here

    Second approach: ~755.9 KB/s (~6.047 Mb/s)

    include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    #include </usr/include/libusb-1.0/libusb.h>
    
    /* Specify VENDOR_ID and PRODUCT_ID for device */
    #define VENDOR_ID   0x0471
    #define PRODUCT_ID  0x3630
    
    /* Define number of bytes to transfer */
    #define EP_SIZE 64                       // bytes
    #define TRANSFERS 1024*768*3/EP_SIZE     // number of transfers
    #define TIMEOUT 10*1000                  // milliseconds
    
    /* Use a global variable to keep the device handle */
    static struct libusb_device_handle *handle = NULL;
    
    /* count variables */
    unsigned int count = 0;
    unsigned int count_in = 0;
    unsigned int count_out = 0;
    
    /* The Endpoint addresses are hard-coded.  You should use libusb -v to find
     * the values corresponding to device
     */
    static int ep_in  = 0x82;
    static int ep_out = 0x01;
    
    /* Write and Read buffers */
    unsigned char wbuf[EP_SIZE*TRANSFERS];
    unsigned char *wbuf_tmp;
    unsigned char rbuf[EP_SIZE*TRANSFERS];
    unsigned char rbuf_tmp[EP_SIZE*TRANSFERS];
    
    static void LIBUSB_CALL xfr_cb_out(struct libusb_transfer *transfer )
    {
      memcpy(wbuf+count_out*EP_SIZE, transfer->buffer, EP_SIZE);
    
      count_out++;  // one transfer complete
      if (count_out < TRANSFERS)
      {
        transfer->buffer = ++wbuf_tmp;
        libusb_submit_transfer(transfer);
      }
    }
    
    static void LIBUSB_CALL xfr_cb_in(struct libusb_transfer *transfer )
    {
      int *completed = transfer->user_data;
      memcpy(rbuf+count_in*EP_SIZE, transfer->buffer, EP_SIZE);
    
      count_in++;  // one transfer complete
      if (count_in < TRANSFERS)
        libusb_submit_transfer(transfer);
      else
        *completed = 1;
    }
    
    int main(int argc, char **argv)
    {
      const struct libusb_version *version;
      int completed = 0;
      size_t length = 64;
      int n;
      int rc;
    
      /* Get libusb version */
      version = libusb_get_version();
      fprintf(stderr, "libusb version: %d.%d.%d.%d\n", version->major, version->minor, version->micro, version->nano);
    
      /* Initialize libusb */
      rc = libusb_init(NULL);
      if (rc < 0)
      {
        fprintf(stderr, "Error Initializing libusb: %s\n", libusb_error_name(rc));
        exit(1);
      }
    
      /* Set debugging output to max level */
      libusb_set_debug(NULL, 3);
    
      /* Look for a specific device and open it */
      handle = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);
      if (!handle)
      {
        fprintf(stderr, "Error finding USB device\n");
        goto out;
      }
    
      /* claim interface */
      rc = libusb_claim_interface(handle, 0);
      if (rc < 0)
      {
        fprintf(stderr, "Error claiming interface.\n");
        goto out;
      }
    
      /* allocate memory */
      wbuf_tmp = malloc(length*TRANSFERS);
    
      /* fill the buffer with incrementing data */
      for (n = 0; n < EP_SIZE*TRANSFERS; n++)
      {
        wbuf_tmp[n] = n;
      }
    
      struct libusb_transfer *transfer;
      transfer = libusb_alloc_transfer(0);
      libusb_fill_bulk_transfer(transfer, handle, ep_out, wbuf_tmp, EP_SIZE, xfr_cb_out, NULL, TIMEOUT);
      libusb_submit_transfer(transfer);
    
      transfer = libusb_alloc_transfer(0);
      libusb_fill_bulk_transfer(transfer, handle, ep_in, rbuf_tmp, EP_SIZE, xfr_cb_in, &completed, TIMEOUT);
      libusb_submit_transfer(transfer);
    
      /* Handle Events */
      while (!completed)
      {
        rc = libusb_handle_events_completed(NULL, &completed);
        if (rc != LIBUSB_SUCCESS)
        {
          fprintf(stderr, "Transfer Error: %s\n", libusb_error_name(rc));
          break;
        }
      }
    
      fprintf(stderr, "completed\n");
    
      int res;
      res = memcmp(rbuf, wbuf, sizeof(wbuf));
      if (res != 0)
        fprintf(stderr, "miscompare\n");
      else
        fprintf(stderr, "success\n");
    
      //* Release the interface */
      libusb_release_interface(handle, 0);
    
      /* Close the device handle */
      if (handle)
        libusb_close(handle);
    
    out:
      if (handle)
      {
        libusb_close(handle);
      }
      libusb_exit(NULL);
    
      return rc;
    }
    

    enter image description here