Rethinking HID Report Descriptors

One of the biggest pains left in the USB world that LUFA is yet to solve is the formidable HID Report Descriptor. This is a large, opaque table of values which describes the functionality and report data structure for Human Interfaces Devices (HID). The HID specification is rather complex, but neccesarily so; it is designed to support literally any possible input device, from a keyboard to a flight simulator. To do this, the reports given by HID devices are flexible, given in the device’s HID Report Descriptor so that the host knows how to communicate with the device correctly.

Here’s a HID report descriptor, that described a mouse with one report, containing two 8-bit signed delta values for the two movement axis, plus three digital buttons:

USB_Descriptor_HIDReport_Datatype_t PROGMEM MouseReport[] =
{
        0x05, 0x01,          /* Usage Page (Generic Desktop)             */
        0x09, 0x02,          /* Usage (Mouse)                            */
        0xA1, 0x01,          /* Collection (Application)                 */
        0x09, 0x01,          /*   Usage (Pointer)                        */
        0xA1, 0x00,          /*   Collection (Application)               */
        0x95, 0x03,          /*     Report Count (3)                     */
        0x75, 0x01,          /*     Report Size (1)                      */
        0x05, 0x09,          /*     Usage Page (Button)                  */
        0x19, 0x01,          /*     Usage Minimum (Button 1)             */
        0x29, 0x03,          /*     Usage Maximum (Button 3)             */
        0x15, 0x00,          /*     Logical Minimum (0)                  */
        0x25, 0x01,          /*     Logical Maximum (1)                  */
        0x81, 0x02,          /*     Input (Data, Variable, Absolute)     */
        0x95, 0x01,          /*     Report Count (1)                     */
        0x75, 0x05,          /*     Report Size (5)                      */
        0x81, 0x01,          /*     Input (Constant)                     */
        0x75, 0x08,          /*     Report Size (8)                      */
        0x95, 0x02,          /*     Report Count (2)                     */
        0x05, 0x01,          /*     Usage Page (Generic Desktop Control) */
        0x09, 0x30,          /*     Usage X                              */
        0x09, 0x31,          /*     Usage Y                              */
        0x15, 0x81,          /*     Logical Minimum (-127)               */
        0x25, 0x7F,          /*     Logical Maximum (127)                */
        0x81, 0x06,          /*     Input (Data, Variable, Relative)     */
        0xC0,                /*   End Collection                         */
        0xC0                 /* End Collection                           */
};

Which, to be frank, is damn scary to most people.

We can thank HID for the vast amount of interoperability between USB input devices today – this is why you can plug in any keyboard, mouse, joystick or DJ mixer desk and have the OS work with it out-of-the-box. However, all this functionality is brought about by the crucial report descriptor which is, at best, a pain in the proverbial rear foot. So much so in fact that the USBIF have released a tool designed to construct these HID descriptors, to make the job a little easier. I’ve been pondering the creation of a drag-drop modern interface to take all the pain of HID report descriptor creation away, but haven’t had the time to sit down and properly design it. In the meantime, I’d like to work on a way to make the descriptors a little easier to work with without the need for such tools, by making the values hand-editable in the code. This won’t help those who aren’t well versed in how HID descriptors work, but it *will* make the lives of myself and others easier when changes need to be made.

So here’s what I’m proposing. I’m think I’ll replace the current “magic value” table with defines from an internal library header file, which makes the descriptors a little more human readable. So far I’ve brainstormed two options, and I’d like some feedback (votes or other suggestions) on the two syntaxes.

First is option A, which uses currently private constants from the HID descriptor parser to construct the report. Here’s a sample of what this would look like:

USB_Descriptor_HIDReport_Datatype_t PROGMEM MouseReport[] =
{
        TYPE_GLOBAL | TAG_GLOBAL_USAGEPAGE   | DATA_SIZE_1, 0x01, /* Usage Page (Generic Desktop) */
        TYPE_LOCAL  | TAG_LOCAL_USAGE        | DATA_SIZE_1, 0x02, /* Usage (Mouse)                */
        TYPE_MAIN   | TAG_MAIN_COLLECTION    | DATA_SIZE_1, 0x01, /* Collection (Application)     */
        TYPE_LOCAL  | TAG_LOCAL_USAGE        | DATA_SIZE_1, 0x01, /*   Usage (Pointer)            */
        TYPE_MAIN   | TAG_MAIN_COLLECTION    | DATA_SIZE_1, 0x01, /*   Collection (Application)   */
        TYPE_GLOBAL | TAG_GLOBAL_REPORTCOUNT | DATA_SIZE_1, 0x03, /*     Report Count (3)         */
        // ...
        TYPE_MAIN | TAG_MAIN_INPUT | DATA_SIZE_1, IOF_DATA | IOF_VARIABLE | IOF_RELATIVE /*     Input (Data, Variable, Relative)     */

It’s not pretty, but it’s very flexible, and despite the awkwardness it’s hand-editable. One option here would be to remove some of the redundancy and make it instead combine the TYPE and TAG values, so that both don’t need to be specified (since they are intrinsicly linked) – although this would move it slightly away from the HID specification which seperates out the three elements explicitly.

Option B is a little more abstract, combining all three into a new set of HID defines that read much like the comments in the original version:

USB_Descriptor_HIDReport_Datatype_t PROGMEM MouseReport[] =
{
	HID_USAGE_PAGE(1), 0x01,
	HID_USAGE(1), 0x02,
	HID_COLLECTION(1), 0x01,
	HID_USAGE(1), 0x01,
	HID_COLLECTION(1), 0x01,
	HID_REPORT_COUNT(1), 0x03,
        // ....
	HID_INPUT(1), HID_DATA | HID_VARIABLE | HID_RELATIVE,

This would be written so that the defines are named just like in the HID specification, except with a HID_ prefix, all constants in ALL CAPS, and spaces replaced with underscores. The value passed to the HID constant would be the data size, so that multiple byte data values (useful in situations like maximum coordinate value descriptions, which can be quite large) can be specified. This is easy to read and to edit, yet is still flexible.

Finally, option C is the most abstract:

** HID class report descriptor. This is a special descriptor constructed with values from the
 *  USBIF HID class specification to describe the reports and capabilities of the HID device. This
 *  descriptor is parsed by the host and its contents used to determine what data (and in what encoding)
 *  the device will send, and what it may be sent back from the host. Refer to the HID specification for
 *  more details on HID report descriptors.
 */
USB_Descriptor_HIDReport_Datatype_t PROGMEM MouseReport[] =
{
	HID_USAGE_PAGE(0x01),
	HID_USAGE(0x02),
	HID_COLLECTION(0x01),
	HID_USAGE(0x01),
	HID_COLLECTION(0x01),
	HID_REPORT_COUNT(0x03),
        // ....
	HID_INPUT(HID_DATA | HID_VARIABLE | HID_RELATIVE),

Which is a modified B that inferrs the data size from the value passed to it. This is the most abstract version with the least user control, however I can’t think of any real downsides to that – although I’d need to write out some test cases to ensure that the encoding works correctly. Behind the scenes the macros would test the value passed in with 8-bit, 16-bit, 24-bit and 32-bit maximums to determine the data size.

Anyone got a preference or any feedback on which way to go on this?

 

Comments: 10

Leave a reply »

 
 
 

I like option C best. But if you have to specify data sizes explicitly, maybe use the number of bits instead of the number of bytes? The numbers 8 and 16 seem more suggestive than 1 and 2. Maybe something like HID_USAGE_PAGE_8(0x01)? Alternatively, what about using multiple arguments to the macro, like HID_USAGE_PAGE(0x01, 0x02)?

 

Your example was: “a mouse with two 8-bit signed delta axis values, plus three buttons.” What’s the cleanest way to code that statement without any extraneous info?

Everything else is HID report metadata (meta-metadata?), and hopefully can be generated by your tool/macros/API.

Option C is closest. The challenge might be how to count things in the preprocessor, so the user doesn’t need to specify things like HID_REPORT_COUNT(0x03).

Might be worth checking to see if some clever coding and use of inline results in gcc generating data not code even if your “macros” are written as actual functions.

And don’t worry if you can’t generate every possible valid HID report. Bring the learning curve down a bit and people can graduate to writing their own report.

 

I put my vote on alternative C.
Seems clean and efficient.
But I must con-cure with David A. Mellis, useing something like HID_USAGE_PAGE_8(0×01) would make the macro coding ALOT simpler and not very much harder for the user. You could could check that the values used later in the Logical maximum and (minimum)isn’t to high for that particular page macro and issue a error (or warning) if it is.

 

All three seem to be a significant improvement.

I think my criteria would be:
1. What do error messages look like when there is a mistake?
2. Which has fewer ways to make mistakes? For example, casting parameters to appropriate types.
3. Which could most easily be augmented to detect defects at compile time? For example testing parameters for appropriate size.
4. Which could most easily be ‘reverse engineered’ into a future tool? I.e. which should be easy to parse, which still being valid C?

It appears that option C, which captures all of it’s parameters within parentheses might be better for 3 and 4, and maybe 2.

 

Thanks for the feedback guys. Option C seems to be the concensus. Rather than having a million macros, I think it would be better to have one macro per HID report item, and have a second (internal) macro to figure out the size mask from the value passed into the macro. That means that a single “HID_REPORT_COUNT()” macro can be used for example with a value of 1, 1000 or 10000 and the code would be able to figure it out automatically, like thus:

#define HID_SIZE_MASK(X) ((X > 0xFFFF) ? 3 : (X > 0xFF) ? 2 : 1)
#define HID_REPORT_COUNT(X) (0x12 | HID_SIZE_MASK(X)), X

char HIDData[] =
{
HID_REPORT_COUNT(20)
};

Leading to some compact and nice code, where you don’t have to worry about how it works internally, you just have to supply the right value to the macros.

– Dean

 

Late to the discussion, but as a newbie to USB descriptor coding that just struggled (is struggling) with this:

Option A does nothing for me. Having just learned the syntax, I’m scratching my head what that syntax even means.

Option B and C are equally big steps forward. Sure, it would be nice for the parser to calculate the size bits, but it is also very easy for me to do that and specify it as in example B.

I assume you are aware of the HID descriptor tool from usb.org http://www.usb.org/developers/hidpage/dt2_4.zip Useful but missing the ability to generate user defined codes and some of the more obscure usage tables. I think it is also buggy, but my inexperience may have been in play.

 

I concur with David Mellis’s ideas. As far as I can see they retain any additional flexibility you’d get with A or B.

One thing I’d really like to see is a HID Descriptor Tool in Python, but that’s a whole other project.

 

Hey all,

I’ve been hoping to run into a set of HID Macros for a while and after reading this post I decided to just do it. This implementation is based on option C.

This isn’t close to complete – please contribute if you more ideas!

https://github.com/rfb/HIDMacros

 

I have to say, this is a great idea. I’ve been spending a lot of time on HID devices recently and by far the learning curve is the biggest adversary. These descriptor Macros would be so useful to abstract with a simple library as you describe and would be a tremendous aid to all newcomers and those interested in more clarity. I will definitely support and link to you if you publish a library for this.

 

Leave a Reply

 
(will not be published)
 
 
Comment
 
 

 

Vital Stats

  • 35 Years Old
  • Australian
  • Lover of embedded systems
  • Firmware engineer
  • Self-Proclaimed Geek

Latest Blog Posts

RSS