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.
in the F380_HIDtoUART.c source file add a global for the IN and OUT temporary endpoint buffers.
modify the typedef structure hid_configuration_descriptor to add the additional fields for the secondary interface, class, and two endpoint descriptors.
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.
now add the secondary interface, class, and the two endpoint descriptors to the hid_configuration_descriptor declaration in F3xx_USB0_Descriptor.c
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
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.
Next, add the handlers for the new IN and OUT endpoints.
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:
And add the following for the report handlers:
Add the extern declarations for the buffer structure and the report handlers in F3xx_USB0_ReportHandler.h header file.
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.
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.
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.
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.