Why Do You Want A HID Report Descriptor?
The USB specification includes a section on Human Interface Devices (HID). These devices range from keyboards, mice, joysticks, audio controls to medical controls, eye tracking and LED lighting. CircuitPython has support for USB HID with built-in defaults for a keyboard, mouse and consumer control and associated libraries. Dan Halbert has an excellent guide to get started with this.
A USB report descriptor tells the host machine the how to talk to your device. But what if you want to communicate to a device that no one else has written a report descriptor for? Perhaps you are building a new joystick, LED indicator or medical ultrasound device. Then you will have to write your own report descriptor.
Simple Descriptor Example
Initially when you look at report descriptors they seem complicated but it is best to think of them as a description of one or more reports that can be sent to or from your device. Each report gives the current state you wish to communicate. For example a joystick report may give the X and Y axis, a throttle value and if any of a dozen buttons are pressed. The joystick may also have a second report defined that allows the host machine to light up one or more indicator lights.
Below is an example report descriptor for a joystick that has 5 buttons. This descriptor only defines one report with each button taking up 1 bit, and 3 padding bits. While this is readable it is hard to create. There is an easier way.
JOYSTICK_REPORT_DESCRIPTOR = bytes(( 0x05, 0x01, # UsagePage(Generic Desktop[0x0001]) 0x09, 0x04, # UsageId(Joystick[0x0004]) 0xA1, 0x01, # Collection(Application) 0x85, 0x01, # ReportId(1) 0x05, 0x09, # UsagePage(Button[0x0009]) 0x19, 0x01, # UsageIdMin(Button 1[0x0001]) 0x29, 0x05, # UsageIdMax(Button 5[0x0005]) 0x15, 0x00, # LogicalMinimum(0) 0x25, 0x01, # LogicalMaximum(1) 0x95, 0x05, # ReportCount(5) 0x75, 0x01, # ReportSize(1) 0x81, 0x02, # Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0x95, 0x01, # ReportCount(1) 0x75, 0x03, # ReportSize(3) 0x81, 0x03, # Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0xC0, # EndCollection() ))
USB HID Specification
The USB HID specification is large, there are a lot of possible interface devices. The area we are most interested in is the HID Usage Tables. HID usages cover the parts of your device from generic to the detailed elements and tell you the predefined usage IDs that are used.
Following those documents you could make a descriptor by hand, but it is tedious. Luckily there is a collection of Microsoft HID Tools that contains Waratah. This tool takes a TOML formatted report descriptor and converts it to the hex required.
Waratah
The Waratah repository has both samples and a wiki to help teach about creating descriptors, the following is just an overview.
Report Descriptor TOML Example
[[applicationCollection]]
usage = ['Generic Desktop', 'Joystick']
The start of the file defines the type of device you are describing reports for. This is where the usage tables are your guide. The first element of usage defines the page while the second declares a device from that page. This guides the host operating system as to what type of device you are describing.
[[applicationCollection.inputReport]]
Next you start declaring the reports your device can send or receive. In this case there is only one report.
[[applicationCollection.inputReport.physicalCollection]]
usage = ['Generic Desktop', 'Pointer']
For a joystick you may have composite items, such as where the stick is pointing. So you declare a collection of items.
[[applicationCollection.inputReport.physicalCollection.variableItem]]
usage = ['Generic Desktop', 'X']
sizeInBits = 8
[[applicationCollection.inputReport.physicalCollection.variableItem]]
usage = ['Generic Desktop', 'Y']
sizeInBits = 8
After declaring the collection you need to define the actual items. You will notice the same pattern for usage, defining the page and then the detailed item. The usage tables continue to be your guide. Here both the X and Y variables are declared as 8 bits.
[[applicationCollection.inputReport.variableItem]]
usageRange = ['Button', 'Button 1', 'Button 4']
logicalValueRange = [0, 1]
A joystick needs buttons, so finally 4 buttons are added to the report. This is still part of the same report as the pointer. Each button takes 1 bit.
After the TOML file is finished you run it through waratah and it will produce a C .h file, but that is easily converted to python or any other language you require.
[[applicationCollection]] usage = ['Generic Desktop', 'Joystick'] [[applicationCollection.inputReport]] [[applicationCollection.inputReport.physicalCollection]] usage = ['Generic Desktop', 'Pointer'] [[applicationCollection.inputReport.physicalCollection.variableItem]] usage = ['Generic Desktop', 'X'] sizeInBits = 8 [[applicationCollection.inputReport.physicalCollection.variableItem]] usage = ['Generic Desktop', 'Y'] sizeInBits = 8 [[applicationCollection.inputReport.variableItem]] usageRange = ['Button', 'Button 1', 'Button 5'] logicalValueRange = [0, 1]
// AUTO-GENERATED by WaratahCmd.exe (https://github.com/microsoft/hidtools) // HID Usage Tables: 1.4.0 // Descriptor size: 50 (bytes) // +----------+-------+-------------------+ // | ReportId | Kind | ReportSizeInBytes | // +----------+-------+-------------------+ // | 1 | Input | 3 | // +----------+-------+-------------------+ static const uint8_t hidReportDescriptor[] = { 0x05, 0x01, // UsagePage(Generic Desktop[0x0001]) 0x09, 0x04, // UsageId(Joystick[0x0004]) 0xA1, 0x01, // Collection(Application) 0x85, 0x01, // ReportId(1) 0x09, 0x01, // UsageId(Pointer[0x0001]) 0xA1, 0x00, // Collection(Physical) 0x09, 0x30, // UsageId(X[0x0030]) 0x09, 0x31, // UsageId(Y[0x0031]) 0x15, 0x80, // LogicalMinimum(-128) 0x25, 0x7F, // LogicalMaximum(127) 0x95, 0x02, // ReportCount(2) 0x75, 0x08, // ReportSize(8) 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0xC0, // EndCollection() 0x05, 0x09, // UsagePage(Button[0x0009]) 0x19, 0x01, // UsageIdMin(Button 1[0x0001]) 0x29, 0x05, // UsageIdMax(Button 5[0x0005]) 0x15, 0x00, // LogicalMinimum(0) 0x25, 0x01, // LogicalMaximum(1) 0x95, 0x05, // ReportCount(5) 0x75, 0x01, // ReportSize(1) 0x81, 0x02, // Input(Data, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0x95, 0x01, // ReportCount(1) 0x75, 0x03, // ReportSize(3) 0x81, 0x03, // Input(Constant, Variable, Absolute, NoWrap, Linear, PreferredState, NoNullPosition, BitField) 0xC0, // EndCollection() };
And that is it, you have a report descriptor. But what do you send for the report?
If you look at the generated code you will see ReportSize and ReportCount lines. The size is in bits and the count is how many that size appears in a row. For example above you see a count of 2 with a size of 8. So 2 bytes (16 bits). Later you see a report a count of 5 with a size of 1 (5 bits) followed by a count of 1 with a size of 3 (3 bits).
From there you can extrapolate your report is:
- 8 bits - X
- 8 bits - Y
- 5 bits - buttons
- 3 bits - padding
How you create the structure to send the report will depend on the language you are using.
What's Next?
This is a brief view into creating your own report. The references are long but worth flipping through to get an idea of what is possible. The above example only handles an input report allowing the device to communicate with the host computer. There are also output reports to control things like LEDs in a keyboard.
As well the samples and wiki for Waratah provides valuable information to help create your own descriptors without having to build the hex by hand.