Sunday, November 26, 2017

Using multiple USB HID interfaces for increased data throughput

USB devices implementing an HID class are convenient as they do not require custom driver development on the host.  Modern operating systems incorporate a standard HID driver and provide a an API for interfacing to the device.  The major downside commonly noted with HID class devices is that communication is performed using interrupt endpoints which are limited to a bandwidth of 64 kB/sec.  This limitation can be overcome by using multiple interfaces with separate HID and endpoint types.  This blog post explains how to build a dual interface USB HID firmware on the Silicon Labs 8051-based EFM8UB2 and presents test software to verify operation.


Background

Universal Serial Bus (USB) is a ubiquitous protocol allowing host systems to communicate with a wide variety of hardware device types.  As a protocol, it's considerably more complex then simple point-to-point hardware protocols like UART, SPI, and I2C.  With USB, the host controller manages all input and output communication between the various devices on the bus.  The host periodically polls the USB device endpoint (an input or output buffer) based on an interval provided by the device during enumeration.

The USB protocol supports four endpoint types: control, bulk, isochronous, and interrupt.  Control transfers are primarily used in the initial device enumeration for communication of descriptors.  Bulk transfers provide no guaranteed latency but are useful for high bandwidth with error checking (think TCP).  Isochronous have a guaranteed latency and are also high bandwidth but provide no error checking (think UDP).  Lastly, interrupt transfers have a guaranteed latency but are confined to a low bandwidth.  The USB HID device class uses interrupt endpoints for communication with applications in mice, keyboards, and other devices requiring real-time updates.

The HID device class allows report messages to be exchanged between host and device.  During enumeration, the device provides one or more interface descriptor with an HID class code.  During this time, the device also sends the endpoint descriptor tied to the particular interface which specifies that it's the endpoint address and that it's a interrupt endpoint type.


Development

Single Interface Verification

We will start by validating a single interface USB HID example before forking the project to create the multiple interface example.

git clone --recursive https://github.com/shraken/efm8_hidmulti

Build the firmware image in the fw/single directory using Keil uVision.  Flash the firmware image to the SLSTK2001A development kit.

Build the software read_test binary in the sw/ directory by following the README.md directions with CMake.  Launch the read_test executable and record the path for the device that matches a Manufacturer of 'SLAB' and a Product name of 'HIDtoUART example'.

➜  build git:(master) ./read_test 
Device Found
  type: 80ee 0021
  path: /dev/hidraw0
  serial_number: 
  Manufacturer: VirtualBox
  Product:      USB Tablet
  Release:      100
  Interface:    0

Device Found
  type: 10c4 8468
  path: /dev/hidraw1
  serial_number: 
  Manufacturer: SLAB
  Product:      HIDtoUART example
  Release:      0
  Interface:    0

ERROR: device to open needs to be passed as first argument
➜  build git:(master)

Execute the binary with the path noted above and verify that an incrementing byte stream is being received.

➜  build git:(master) sudo ./read_test /dev/hidraw1
Device Found
  type: 80ee 0021
  path: /dev/hidraw0
  serial_number: 
  Manufacturer: VirtualBox
  Product:      USB Tablet
  Release:      100
  Interface:    0

Device Found
  type: 10c4 8468
  path: /dev/hidraw1
  serial_number: 
  Manufacturer: SLAB
  Product:      HIDtoUART example
  Release:      0
  Interface:    0

Manufacturer String: SLAB
Product String: HIDtoUART example
Serial Number String: (0) 
Unable to read indexed string 1
Indexed String 1: 
received new USB packet 0
Data read:
   01 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef 00 
received new USB packet 1
Data read:
   01 f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 00 
received new USB packet 2

This output shows that HID reports are being received with a Report ID of 0x01.  In the firmware's main execution block, a flag 'SendPacketBusy' is continuously polled to determine if the HID IN endpoint has been transmitted.  If the 'SendPacketBusy' becomes a boolean false then the IN endpoint buffer is loaded with the next incrementing byte buffer.


Multi Interface Modifications

A full implementation of the dual HID interface is provided in the fw/multi directory.

Start by defining the report ID's and report size for the secondary HID interface.  In the F3xx_HIDtoUART.h file, add

#define IN_DATA2  0x03
#define IN_DATA_SIZE2 63

#define OUT_DATA2 0x04
#define OUT_DATA_SIZE2 60

in the same file, also make an external declaration for the IN and OUT temporary endpoint buffers for secondary endpoint.

extern unsigned char xdata IN_PACKET2[];
extern unsigned char xdata OUT_PACKET2[];

in the F380_HIDtoUART.c source file add a global for the IN and OUT temporary endpoint buffers.

unsigned char xdata IN_PACKET2[64];
unsigned char xdata OUT_PACKET2[64];

modify the typedef structure hid_configuration_descriptor to add the additional fields for the secondary interface, class, and two endpoint descriptors.

typedef code struct {
  configuration_descriptor   hid_configuration_descriptor;
  interface_descriptor       hid_interface_descriptor;
  class_descriptor           hid_descriptor;
  endpoint_descriptor        hid_endpoint_in_descriptor;
  endpoint_descriptor        hid_endpoint_out_descriptor;
 
  interface_descriptor       hid_interface_descriptor2;
  class_descriptor           hid_descriptor2;
  endpoint_descriptor        hid_endpoint_in_descriptor2;
  endpoint_descriptor        hid_endpoint_out_descriptor2;
}
hid_configuration_descriptor;

modify the hid_configuration_descriptor initialization where the hid_configuration_descriptor is set in the F3xx_USB0_Descriptor.c source file.  For the hid_configuration_descriptor we must change the NumInterfaces field to 0x02 and adjust the Totallength to 0x4900 (big endian) to accommodate the additional fields added to the hid_configuration_descriptor above.

{ // configuration_descriptor hid_configuration_descriptor
   0x09,                               // Length
   0x02,                               // Type
   0x4900,                             // Totallength (= 9+9+9+7+7+9+9+7+7)
   0x02,                               // NumInterfaces
   0x01,                               // bConfigurationValue
   0x00,                               // iConfiguration
   0x80,                               // bmAttributes
   0x20                                // MaxPower (in 2mA units)
},

now add the secondary interface, class, and the two endpoint descriptors to the hid_configuration_descriptor declaration in F3xx_USB0_Descriptor.c

{ // interface_descriptor hid_interface_descriptor
   0x09,                               // bLength
   0x04,                               // bDescriptorType
   0x01,                               // bInterfaceNumber
   0x00,                               // bAlternateSetting
   0x02,                               // bNumEndpoints
   0x03,                               // bInterfaceClass (3 = HID)
   0x00,                               // bInterfaceSubClass
   0x00,                               // bInterfaceProcotol
   0x00                                // iInterface
},

{ // class_descriptor hid_descriptor
  0x09,                               // bLength
  0x21,                               // bDescriptorType
  0x0101,                             // bcdHID
  0x00,                               // bCountryCode
  0x01,                               // bNumDescriptors
  0x22,                               // bDescriptorType
  HID_REPORT_DESCRIPTOR_SIZE_LE       // wItemLength (tot. len. of report
                                      // descriptor)
},

// IN endpoint (mandatory for HID)
{ // endpoint_descriptor hid_endpoint_in_descriptor
   0x07,                               // bLength
   0x05,                               // bDescriptorType
   0x82,                               // bEndpointAddress
   0x03,                               // bmAttributes
   EP2_PACKET_SIZE_LE,                 // MaxPacketSize (LITTLE ENDIAN)
   1                                   // bInterval
},

// OUT endpoint (optional for HID)
{ // endpoint_descriptor hid_endpoint_out_descriptor
   0x07,                               // bLength
   0x05,                               // bDescriptorType
   0x02,                               // bEndpointAddress
   0x03,                               // bmAttributes
   EP2_PACKET_SIZE_LE,                 // MaxPacketSize (LITTLE ENDIAN)
   1                                   // bInterval
}

We also need to allocate the secondary hid_report_descriptor representing the second HID interface we intend to communicate on.  Add the following to the F3xx_USB0_Descriptor.c source file

code const hid_report_descriptor HIDREPORTDESC2 =
{
    0x06, 0x00, 0xff,              // USAGE_PAGE (Vendor Defined Page 1)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
  
    0x85, IN_DATA2,                // Report ID
    0x95, IN_DATA_SIZE2,           //   REPORT_COUNT ()
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)

    0x85, OUT_DATA2,               // Report ID
    0x95, OUT_DATA_SIZE2,          //   REPORT_COUNT ()
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)

    0xC0                           //   end Application Collection
};

The interrupt service source file, F3xx_USB0_InterruptServiceRoutine.c must be modified to add handlers for the the second IN and OUT endpoints.  Start by adding a global flag variable used to indicate when the second IN endpoint has been serviced.


bit SendPacketBusy2 = 0;

Next, add the handlers for the new IN and OUT endpoints.

void Handle_In2 (void)
{
   EP_STATUS[2] = EP_IDLE;
   SendPacketBusy2 = 0;
}

void Handle_Out2 (void)
{
   unsigned char Count = 0;
   unsigned char ControlReg;

   POLL_WRITE_BYTE (INDEX, 2);         // Set index to endpoint 1 registers
   POLL_READ_BYTE (EOUTCSR1, ControlReg);

   if (EP_STATUS[2] == EP_HALT)        // If endpoint is halted, send a stall
   {
      POLL_WRITE_BYTE (EOUTCSR1, rbOutSDSTL);
   }

   else                                // Otherwise read received packet
                                       // from host
   {
      if (ControlReg & rbOutSTSTL)     // Clear sent stall bit if last
                                       // packet was a stall
      {
         POLL_WRITE_BYTE (EOUTCSR1, rbOutCLRDT);
      }

      Setup_OUT_BUFFER ();             // Configure buffer to save
                                       // received data
      Fifo_Read(FIFO_EP2, OUT_BUFFER.Length, OUT_BUFFER.Ptr);

      // Process data according to received Report ID.
      // In systems with Report Descriptors that do not define report IDs,
      // the host will still format OUT packets with a prefix byte
      // of '0x00'.

      ReportHandler_OUT2 (OUT_BUFFER.Ptr[0]);
   POLL_WRITE_BYTE (EOUTCSR1, 0);   // Clear Out Packet ready bit
   }
}

The endpoint handlers are called inside the USB ISR.  The IN1INT register specifies the currently active IN endpoints that are pending.  Likewise the OUT1INT register specifies the currently active OUT endpoints that are pending.  We add checks for the secondary endpoints that call into the newly defined handlers above.


if (bIn & rbIN2)
{
   Handle_In2();
}

if (bOut & rbOUT2)
{
   Handle_Out2();
}

Finally, we need to define a new send packet routine for the new IN endpoint.  This routine checks if the endpoint is idle and call the HID interface report handler.  In this trivial case, the report handler will fill the temporary IN buffer with an byte increment stream.  Following the report handler, the temporary buffer is written to the USB endpoint FIFO buffer and the INPRDY bit of the specific endpoint EINCSR is set to arm the endpoint for future host IN transfers.

void SendPacket2(unsigned char ReportID)
{
   bit EAState;
   unsigned char ControlReg;
   EAState = EA;
   SendPacketBusy2 = 1;

   POLL_WRITE_BYTE (INDEX, 2);         // Set index to endpoint 2 registers
 
   // Read contol register for EP 2
   POLL_READ_BYTE (EINCSR1, ControlReg);

   if (EP_STATUS[2] == EP_HALT)        // If endpoint is currently halted,
                                       // send a stall
   {
      POLL_WRITE_BYTE (EINCSR1, rbInSDSTL);
   }

   else if(EP_STATUS[2] == EP_IDLE)
   {
      // the state will be updated inside the ISR handler
      EP_STATUS[2] = EP_TX;

      if (ControlReg & rbInSTSTL)      // Clear sent stall if last
                                       // packet returned a stall
      {
         POLL_WRITE_BYTE (EINCSR1, rbInCLRDT);
      }

      if (ControlReg & rbInUNDRUN)     // Clear underrun bit if it was set
      {
         POLL_WRITE_BYTE (EINCSR1, 0x00);
      }

      EIE1 &= ~0x02;                   // Disable USB0 Interrupts
   
      ReportHandler_IN_Foreground2 (ReportID);

      Fifo_Write_Foreground (FIFO_EP2, IN_BUFFER2.Length,
                    (unsigned char *)IN_BUFFER2.Ptr);
      POLL_WRITE_BYTE (EINCSR1, rbInINPRDY);
                                       // Set In Packet ready bit,
                                       // indicating fresh data on FIFO 2
      
      EIE1 |= 0x02;                    // Enable USB0 Interrupts
   }
}

In the F3xx_USB0_InterruptServiceRoutine.h header file we need to add declarations for the functions and global variables previously defined. Add the following:

void Handle_In2(void);
void Handle_Out2(void);
void SendPacket2(unsigned char);

extern bit SendPacketBusy2;

We must define the report handler structures and routines called upon by the handler and SendPacket2 function in the F3xx_USB0_ReportHandler.c source file.  Start by adding the support structure and macro defines:

#define IN2_VECTORTABLESize 1
#define OUT2_VECTORTABLESize 1

const VectorTableEntry IN_VECTORTABLE2[IN2_VECTORTABLESize] =
{
   // FORMAT: Report ID, Report Handler
   IN_DATA2, IN_Data2,
};

//*****************************************************************************
// Link all Report Handler functions to corresponding Report IDs
//*****************************************************************************
const VectorTableEntry OUT_VECTORTABLE2[OUT2_VECTORTABLESize] =
{
   // FORMAT: Report ID, Report Handler
   OUT_DATA2, OUT_Data2,
};

BufferStructure IN_BUFFER2, OUT_BUFFER2;

The last modification required in the F3xx_USB0_ReportHandler.c file is to add the report handler functions and function pointers specified in the  VectorTableEntry initialized above for the IN_Data2 and Out_Data2 functions.  The report handler functions are templates copied from the single interface and the IN_Data2 function performs the same action as the IN1 endpoint by filling the temporary buffer with an incrementing byte stream.  Add the following code for IN_Data2 and OUT_Data2 functions:

void IN_Data2 (void)
{
   static unsigned char count = 0;
   unsigned char i;
 
   IN_PACKET2[0] = IN_DATA2;
 
   for (i = 1; i < IN_DATA_SIZE2; i++) {
      IN_PACKET2[i] = count;
      count++;
   }
 
   IN_BUFFER2.Ptr = IN_PACKET2;
   IN_BUFFER2.Length = IN_DATA_SIZE2 + 1;
}

void OUT_Data2 (void)
{

}

And add the following for the report handlers:

void ReportHandler_IN_ISR2(unsigned char R_ID)
{
   unsigned char index;

   index = 0;

   while(index <= IN2_VECTORTABLESize)
   {
      // Check to see if Report ID passed into function
       // matches the Report ID for this entry in the Vector Table
      if(IN_VECTORTABLE2[index].ReportID == R_ID)
      {
         IN_VECTORTABLE2[index].hdlr();
         break;
      }

      // If Report IDs didn't match, increment the index pointer
      index++;
   }

}

void ReportHandler_IN_Foreground2(unsigned char R_ID)
{
   unsigned char index;

   index = 0;

   while(index <= IN2_VECTORTABLESize)
   {
      // Check to see if Report ID passed into function
      // matches the Report ID for this entry in the Vector Table
      if(IN_VECTORTABLE2[index].ReportID == R_ID)
      {
         IN_VECTORTABLE2[index].hdlr();
         break;
      }

      // If Report IDs didn't match, increment the index pointer
      index++;
   }

}

void ReportHandler_OUT2(unsigned char R_ID){

   unsigned char index;

   index = 0;

   while(index <= OUT2_VECTORTABLESize)
   {
      // Check to see if Report ID passed into function
      // matches the Report ID for this entry in the Vector Table
      if(OUT_VECTORTABLE2[index].ReportID == R_ID)
      {
         OUT_VECTORTABLE2[index].hdlr();
         break;
      }

      // If Report IDs didn't match, increment the index pointer
      index++;
   }
}

Add the extern declarations for the buffer structure and the report handlers in F3xx_USB0_ReportHandler.h header file.


extern void ReportHandler_IN_ISR2(unsigned char);
extern void ReportHandler_IN_Foreground2(unsigned char);
extern void ReportHandler_OUT2(unsigned char);

extern BufferStructure IN_BUFFER2, OUT_BUFFER2;

The last thing we need to do is modify the F3xx_USB0_Standard_Requests.c source file to send the HID report descriptor and report when a request is made by the host.  Modify the Get_Descriptor function for the DSC_HID_REPORT case with the following.

   case DSC_HID_REPORT:                // HID Specific (HID report descriptor)
      if (SETUP.wIndex.U8[LSB] == 0x00) {
         DATAPTR = (unsigned char*)&HIDREPORTDESC;
         DATASIZE = HID_REPORT_DESCRIPTOR_SIZE;
         break; 
      } else if (SETUP.wIndex.U8[LSB] == 0x01) {
         DATAPTR = (unsigned char*)&HIDREPORTDESC2;
         DATASIZE = HID_REPORT_DESCRIPTOR_SIZE;
         break;
      }

Next, modify the Get_Report function to send the new report type to the host.  Add the following as a replacement for the single call to ReportHandler_IN_ISR and setting DATAPTR and DATASIZE.

if (SETUP.wIndex.U8[LSB] == 0x00) {
   // call appropriate handler to prepare buffer
   ReportHandler_IN_ISR(SETUP.wValue.U8[LSB]);
   // set DATAPTR to buffer used inside Control Endpoint
   DATAPTR = IN_BUFFER.Ptr;
   DATASIZE = IN_BUFFER.Length;
} else if (SETUP.wIndex.U8[LSB] == 0x01) {
   // call appropriate handler to prepare buffer
   ReportHandler_IN_ISR2(SETUP.wValue.U8[LSB]);
   // set DATAPTR to buffer used inside Control Endpoint
   DATAPTR = IN_BUFFER2.Ptr;
   DATASIZE = IN_BUFFER2.Length;
}

The final modification required is to add a call to SendPacket2 after checking the SendPacketBusy2 flag in the main while block.  The SendPacket2 function calls the ReportHandler_IN_Foreground2 handler which subsequently calls the IN_Data2 function to fill the temporary buffer with byte incremented values.  The temporary buffer is copied into the USB FIFO buffer and the endpoint is then armed.

if (!SendPacketBusy2) {
   SendPacket2 (IN_DATA2);
}

That wraps up the firmware modifications.  Build the firmware and flash to the SLSTK2001A development kit.  The image below shows the the reports for IN_DATA1 (0x01) and IN_DATA2 (0x03) simultaneously being transmitted to the a Windows host after enumeration is complete.


Multi Interface Verification

Test software written using the hidapi library was used to validate that IN_DATA1 (0x01) and IN_DATA2 (0x03) reports could be read simultaneously on the host.  The example hidtest application included with hidapi was modified to open the HID device by path instead of USB VID and PID as each interface registers a unique path.  The path is passed as an argument to the application as shown below.

// open device passed as first argument
handle = hid_open_path(argv[1]);

if (!handle) {
   printf("unable to open device\n");
   return 1;
}

A loop was added to invoke hid_read for reading reports from the interrupt IN endpoint.  On each successful read, the input report is printed to console.

// Read requested state. hid_read() has been set to be
// non-blocking by the call to hid_set_nonblocking() above.
// This loop demonstrates the non-blocking nature of hid_read().
res = 0;
count = 0;
while ((res == 0) || (count < 100000)) {
   res = hid_read(handle, buf, sizeof(buf));
   if (res > 0) {
      printf("received new USB packet %d\n", count);
      printf("Data read:\n   ");
      // Print out the returned buffer.
      for (i = 0; i < res; i++)
         printf("%02hhx ", buf[i]);
      printf("\n");
      count++;
   }
}

The abbreviated output below shows the two input reports streaming to the host when launched as separate applications with paths specifying the unique HID interfaces of the same device.  For example, on a Linux system using hidraw driver backend.

Interface 0: /dev/hidraw1
➜  build git:(master) sudo ./read_test /dev/hidraw1
...
received new USB packet 0
Data read:
   01 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd 00 
received new USB packet 1
Data read:
   01 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 00 

Interface 1: /dev/hidraw2
➜  build git:(master) sudo ./read_test /dev/hidraw2
...
received new USB packet 0
Data read:
   03 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 
received new USB packet 1
Data read:
   03 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 00 

Success!  The report ID is the first byte in the dump presented above.  With interface 0  (/dev/hidraw1) we see the IN_DATA1 (0x01) report ID and with interface 1 (/dev/hidraw2) we see IN_DATA2 (0x02) report ID.  While not shown in this example, the firmware also supports OUT endpoint transfers so you can use hid_write to send output reports.


Conclusion

This entry documented the modifications required to enable multi-interfaces when using a USB HID class-type.  This is a simple approach to overcome the low-bandwidth (64 kB/sec) limitation of interrupt HID endpoints by spreading data communication over multiple interfaces.  We showed a two interface modification here but it can be scaled even further -- on the EFM8UB2 the 3rd endpoint could be used but other platforms like EFM32 support 6 IN/OUT endpoints potentially expanding the up/down data rates to 384 kBytes/sec.


References

  1. USB 2.0 Specification
  2. USB Device Class Definition for HID 1.11
  3. Silicon Labs Human Device Interface Tutorial AN249
  4. Tutorial about USB HID Report Descriptors
  5. Beagle USB 480 Protocol Analyzer
  6. USB Complete: The Developer's Guide
  7. hidapi Host Library
  8. Silicon Labs EFM8UB2 SLSTK2001A Development Kit