Tuesday, November 27, 2018

Using robot framework for embedded hardware acceptance testing

I've been developing the Buddy DAQ project over the past few years.  The Buddy DAQ is a low cost and open source data acquisition device that connects to a host computer using the USB HID protocol.  During firmware development I was routinely getting hit with regressions.  Minor modifications in the device firmware were throwing the performance out of specification.

Figure 1: Picture of the Buddy DAQ.  The host computer attaches to the micro USB port and the flexible IO pins are exposed on the right 10-pin male IDC connector. 


The hardware in the Buddy DAQ uses a C8051 micro-controller with a small 4k RAM and 48 MHz system clock.  Most of the computational effort in the DAQ firmware is packing and unpacking unsigned integers to and from a frame buffer.  These codec operations dominate the execution cycles so changes to this section of code are very sensitive.  I needed some kind of quick check during development to ensure that the project was still meeting the performance specification.

The best way to ensure the device is meeting performance spec is to push the device at the rated specification and analyze the performance.  The idea here was to push the sample frequency up on the Buddy in DAC or ADC mode of operation and drive or watch the flex inputs to quantify performance.  In DAC mode, we generate a sinusoidal wave of fixed frequency using the DAC and digitize the signal with another instrument.  In ADC mode, we analyze a sinusoidal waveform being generated by a reference test instrument.  We convert the digitized waveform into the frequency spectrum and look for the frequency delta between the true tone frequency and the digitized and recorded signal.  If the measured delta frequency exceeds some threshold value then the test fails and the ADC or DAC is not performing at the specification.

The communication between host PC and Buddy DAQ uses a common codec library where channel values are packed in a 64 byte frame buffer.  Memory copy operations incur a hefty run penalty so we build the frame in one memory location before performing a single copy operation to the destination USB endpoint buffer.  This leaves a lot that can be get whacked during development as simple thing likes accidentally changing the buffer memory model or adjusting alignment can cause big slowdowns in codec operations leading to degradation in the actual sample rate.

I use Robot Framework as the automated test framework and tool on the host PC.  Using Robot Framework, you write test suites and support routines in an expressive verb syntax that makes clear the test intent.  The framework is python-based and that's awesome because Buddy DAQ already has existing SWIG python bindings.


Test List

The following test cases are implemented.  Some are general and meant to check version and identification information while the others are mode-specific and meant to validate performance specification.  

  • Exercise each channel by cycling through each flex pin and performing an all channel test.
  • Exercise each resolution (8 bit, 16 bit, and 32 bit) modes where appropriate.



Test Name
Test ID
Description
Result
Device
Version
BUDDY_FW_VER_CHECK
PC requests device version information from Buddy device.
Check if application and bootloader version are sane.
Device Information
BUDDY_FW_SERIAL_NUM_CHECK
PC requests device information from Buddy device.
Check build datetime, version, and DAC type.
USB Identification
BUDDY_USB_ID_CHECK
PC returns OS device information on USB device from Buddy device.
Check USB manufacturer and product string.  
External Teensy Version
BUDDY_TEENSY_ID_CHECK
Teensy returns the version information when requested.
Check if Teensy MCU version are sane.
ADC static simple
BUDDY_DAC_STATIC_CHECK
Buddy supplies static DAC output voltages.

A step cycle is repeated for each channel with DAC output voltages (0, 256, 512, 1024, 2048, and 4095).
Check if digitized static voltage are valid on the Teensy.
ADC dynamic
BUDDY_DAC_DYNAMIC_CHECK
Buddy supplies dynamic input voltage waveform.  A sinusoid of fixed generation frequency with a fixed sample rate is sent using stream mode.
Record ADC samples on Teensy.  Take FFT of samples collected and validate tone frequency.  
DAC static
BUDDY_ADC_STATIC_CHECK
Set a series of static output voltages on the Teensy.
Check if digitized output voltage as detected by Buddy are valid.
DAC dynamic
BUDDY_ADC_DYNAMIC_CHECK
Set a dynamic voltage waveform.  Use a sine wave being driven from the Teensy.
Record ADC samples on the Buddy.  Take FFT of samples collected and validate tone frequency.
PWM frequency
BUDDY_PWM_FREQUENCY_CHECK
Set a series of static PWM frequencies over a given clock range on the Buddy.
Check if detected frequency matches expected frequency on the Teensy.
PWM duty cycle
BUDDY_PWM_DUTY_CHECK
Set a series of static PWM duty cycles with a fixed base frequency on the Buddy.  Utilize all base frequencies.
Check if detected duty cycle matches the expected duty cycle on the Teensy.
Tick counter
BUDDY_COUNTER_CHECK
Set a series of static frequencies (1 kHz, 10 kHz, and 100 kHz) on the Buddy.  
Check if detected tick period matches the value expected on the Teensy.  


Table 1: Description of the test cases implemented to test the Buddy DAQ.

Teensy test jig design

The test jig board needed a microcontroller with a fast clock speed, large memory, and rich on-chip peripherals (1x DAC, 8x ADC, 8x PWM).  The Teensy 3.2 was used in the first iteration of the hardware as it is easily the most flexible, documented, easy-to-source, and miniature solution out there.


Figure 2: Picture of the configuration used for test.  The Buddy DAQ board is connected to the Teensy test jig board using a 10 pin IDC ribbon cable. 


For the second iteration of the hardware, I moved to the Teensy 3.6 as it runs at more than 2x the clock frquency (180 MHz v. 72MHz) with 4x the RAM (256k v. 64k).  The additional RAM allows the Teensy software to buffer larger stream waveforms.  This is a major benefit as the ADC or DAC on the Teensy can sample at a higher rate allowing capture and generate of waveforms with better time resolution.

A block diagram showing the hardware design of the test jig is provided in Figure 3.  The Buddy DAQ device is connected to the test jig (FIO_0 - FIO_7).  The Teensy drives a DAC signal into an 8:1 multiplexer (MAX4617) with GPIO outputs from Teensy acting as selectors and an enable line.  Two 8 channel single-pole-single-throw SPST (MAX335) chips are used to route incoming ADC, PWM, and counter into the Teensy.  The Teensy acts as a SPI master and interfaces with the two SPST chips using two GPIO lines as chip enables.





Figure 3: Block diagram of the Teensy test jig board connected to the Buddy DAQ.




Figure 4: (A) Back and (B) Front picture of the Teensy test jig board.  The Buddy DAQ board connects to the test jig board by the IDC ribbon header on the left or the high density mezzanine connector.

FFT Calculation

A sine waveform is generated and output by a DAC.  This test is run with the Buddy operating in ADC and DAC modes with the Teensy configured in the opposing mode to monitor or provide stimulus.  

In the first test (BUDDY_ADC_DYNAMIC_CHECK), the Teensy test jig generates the sine waveform on its DAC output and directs the 8:1 Mux to the desired flex channel. The Buddy DAQ is then instructed to sample on the same flex channel using the Buddy ADC.

In the second test (BUDDY_DAC_DYNAMIC_CHECK), the Buddy generates a sine waveform using the DAC and the waveform is sampled by the test jig board by routing the flex channel output through the SPST circuit and into an ADC channel on the Teensy micro-controller.


The recorded waveform data has an FFT operation run to analyze waveform data in the frequency domain. The FFT magnitude data is then fed into a peak detector to find the frequency of the input tone (Figure 5).


The dynamic frequency test expects the detected tone frequency to fall within the following range, otherwise the test is deemed a failure.
  • BUDDY_DAC_DYNAMIC_CHECK: 10 ± 15 Hz.  
  • BUDDY_ADC_DYNAMIC_CHECK: 50 ± 4 Hz





Figure 5: A block diagram showing the FFT, peak magnitude detection and expected frequency evaluation operation.

(a) A single channel on the Buddy DAC generates a 10 Hz tone and the waveform is recorded by the Teensy.
(b) The Teensy uses it's onboard DAC to generate a 50 Hz sinusoidal tone and the signal is digitized by the Buddy DAQ operating in ADC mode.

Figure 6
: FFT magnitude spectrum plots showing a sampled and recorded sinusoidal time waveform that when transformed shows the tone frequency.


File Logging


I quickly realized I desperately needed logs when running these tests.  When running the FFT analysis I wanted to be able to go back and understand why the FFT and threshold detection was failing.  We also save the matplotlib generated temporal and FFT magnitude-frequency plots.  The CSV files are saved in the `csv` directory in the standard `reports` directory.  The generated plot image files are saved into an images directory on disk.


Caveats

Initially I had the test cases defined in a single robot framework (.robot) file.  I was having issues with intermittent test failures when all the tests were combined in this fashion.  I attribute this mostly to the USB HID backend not getting properly cleaned up after each test suite ran.  I saw occasional timeout errors with the serial link to the Teensy but most of this was memory overruns related to the RingBuffer.  I added the ability to reset both the Buddy and Teensy on a software command from the robot framework tests using the watchdog timer in both firmware.  


Future

  • Refactor teensy software.  Break the functionality into separate source files.  Add error handling and reporting.
  • Refactor robot framework support libraries.  Move all threshold checks into the test suite but keep all the heavy lifting behind the scenes in support *.robot or wrapper python libraries.


References

1. Buddy DAQ Robot Framework tests
2. Buddy DAQ project on github
3. Robot Framework official website
4. Teensy 3.6 pinouts

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