HID report descriptors and Linux
HID Devices
USB HID (Human Interface Device) device class is the type of computer peripherals that human interacts with, such as keyboards, mice, game controllers and touchscreens. The protocol is probably one of the most simple protocols in the USB specification. Even if HID was originally written for USB in mind, it works with several other transport layers. Your mouse and keyboard do probably use HID over USB, the touchscreen in your smartphone could use HID over I2C. Even Bluetooth/BLE make use of the same protocol for this type of devices.
The protocol is popular as it's so simple. The HID descriptor could be stored in ROM and the peripheral could be implemented using only a small 8-bit MCU.
Despite how simple and well defined [1] the HID specification is, implementers will still get it wrong as we will see later on. Those non compliant errors has to be fixed up in order to use the device. It's quite sad as it requires a separate driver for this even if the HID should be able to be handled in a generic way.
We will continue to focus on the HID implementation over USB.
HID Descriptors
The device itself has to identify itself and that information is stored in segments of its ROM (Read Only Memory). This segments, or descriptors as they're called, describe what type of device it's and which interface (Interface Descriptor) it exposes. Such interfaces are called classes and all devices belongs to one USB class. Such class [2] could be Printer, Video, or the one we will focus on - the HID class.
The HID class device descriptor defines and identifies which other descriptors present. Such other descriptors could be report descriptors and physical descriptors.
Report Descriptors
The report descriptor describes how the data that the device generates should be interpreted. For example, the report descriptor describes how to determine the button state of your mouse or the position of the touchscreen click on your smartphone.
Physical Descriptors
The physical descriptor on the other hand provides information about the physical layout of the device, e.g. what and how your body interact with the device to activate certain functions.
The big picture
There are of cause more descriptors, but most of them are part of the USB specification rather than specific for the HID devices.
There are much to say about the HID class, sub classes, interfaces and protocols and we will not cover them all. But just to give you a hint of what it's:
HID class
USB devices are grouped into USB classes depending on what type of device and transport requirement it's. For example an Audio device requires isohcronous data pipes which HID devices does not. HID Devices has different and much simpler data transport requirements.
Subclasses
The only subclass for the HID class is the Boot Interface Subclass. It's a small subset of the report descriptor that is easier to parse for code that does not want (or have resource to) parse a full report descriptor. BIOS is one example of such code that want to keep the complexity and footprint as small as possible, but still want to be able to use a keyboard.
Protocol
The HID protocol only has a meaning if subclass is a Boot Interface, then it's used to determine if the device is a mouse or keyboard. Otherwise the Protocol is not used.
Interfaces
The interface is a way for the device and host to communicate.HID devices use either the Control pipe or the Interrupt pipe.
Control pipes are used for:
- Receive and respond to requests for USB control and class data
- Transmit data when asked by the HID class driver
- Receive data from the host
The interrupt pipe are used for:
- Receiving asynchronous data from the device
- Transmitting low-latency
Report descriptors
The information passed to and from the device is encapsulated in reports. These reports are organized data where the data layout is described in a report descripor. This report descriptor is one of the first items that the host requests from the device and describes how the data should be interpreted. For example, a mouse could have several buttons represented by one bit each and a wheel represented from -127 to +128. The report descriptor will give you details about which bits are mapped to which button and also which 8 bits should be used for the wheel.
All report descriptors are available to read out from sysfs. This is the report from my keyboard
[15:15:02]marcus@goliat:~$ od -t x1 -Anone /sys/bus/usb/devices/3-12.3.2.1/3-12.3.2.1:1.0/0003:045E:00DB.0009/report_descriptor 05 01 09 06 a1 01 05 08 19 01 29 03 15 00 25 01 75 01 95 03 91 02 09 4b 95 01 91 02 95 04 91 01 05 07 19 e0 29 e7 95 08 81 02 75 08 95 01 81 01 19 00 29 91 26 ff 00 95 06 81 00 c0
Here is a another (parsed) example of what the report descriptor may look like
0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x04, // Usage (Joystick) 0xA1, 0x01, // Collection (Application) 0xA1, 0x00, // Collection (Physical) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x07, // Logical Maximum (2047) 0x35, 0x00, // Physical Minimum (0) 0x46, 0xFF, 0x00, // Physical Maximum (255) 0x75, 0x10, // Report Size (16) 0x95, 0x02, // Report Count (2) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection 0x75, 0x08, // Report Size (8) 0x95, 0x03, // Report Count (3) 0x81, 0x03, // Input (Cnst,Var,Abs) 0xC0, // End Collection
The descriptor describes a 2 axis (X/Y) joystick controller where each axis could have an absolute value between 0 and 2047.
If we walk through the descriptor step by step.
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
As there are a many different types of devices, these are grouped into pages. This to know how the following (Usage) entry should be intepreted.
There are a plenty of groups as seen in the reference manual [3] :
Our joystick belongs to the Generic Desktop Ctrls group.
The next line is the usage:
0x09, 0x04, // Usage (Joystick)
Even mouse, keyboard and gamepads are example of Generic Desktop Ctrls as seen in the table below:
The next entry is the application collection:
0xA1, 0x01, // Collection (Application)
The application collection is to make a meaningful grouping of Input, Output and Feature items. Each such grouping has at least Report Size and Report Count defined to determine how big in terms of bytes the collections is.
The next entry is the physical collection:
0xA1, 0x00, // Collection (Physical)
This provides information about the part or parts of the human body used to activate the controls on the device. In other words - button and knobs.
Now to the more fun part:
0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x07, // Logical Maximum (2047) 0x35, 0x00, // Physical Minimum (0) 0x46, 0xFF, 0x00, // Physical Maximum (255) 0x75, 0x10, // Report Size (16) 0x95, 0x02, // Report Count (2) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
Here we can see that there are two axis, X/Y, that is represented by a value between 0 and 2047 (11 bits). A single axis does occupy 16 bits and is of the type Input. That is pretty much all what we need to know in order to parse this information.
What is so hard then?
These report descriptors is not that hard to follow and there is no black magic around them. Despite that, many vendors does not get these report descriptors right and keep deliver a custom driver along with their product.
I used to build and fly tri- and quadcopters, and on way to be better at flying is to use your radio transmitter connected to a simulator as training. The crashes is not that fatal nor costly that way...
I've never seen such a flight controller that actually follow the HID specification, and that makes them useless without a custom driver that can parse the mess. It's not uncommon that the actual reports from the device looks good, it's just that the report descriptor is messed up.
In that case we can write a pretty small Linux Kernel Driver that only fixup the report descriptor and then let the HID layer create and manage the device in a generic way. This is what I did for the VRC2 and HID-PXRC driver [4] which will be available in Linux 6.1.
Such driver could be as simple as (cut out from my VRC2-driver):
static __u8 vrc2_rdesc_fixed[] = { 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 0x09, 0x04, // Usage (Joystick) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x07, // Logical Maximum (2047) 0x35, 0x00, // Physical Minimum (0) 0x46, 0xFF, 0x00, // Physical Maximum (255) 0x75, 0x10, // Report Size (16) 0x95, 0x02, // Report Count (2) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 0xC0, // End Collection 0x75, 0x08, // Report Size (8) 0x95, 0x03, // Report Count (3) 0x81, 0x03, // Input (Cnst,Var,Abs) 0xC0, // End Collection }; static __u8 *vrc2_report_fixup(struct hid_device *hdev, __u8 *rdesc, unsigned int *rsize) { hid_info(hdev, "fixing up VRC-2 report descriptor\n"); *rsize = sizeof(vrc2_rdesc_fixed); return vrc2_rdesc_fixed; } static int vrc2_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret; /* * The device gives us 2 separate USB endpoints. * One of those (the one with report descriptor size of 23) is just bogus so ignore it */ if (hdev->dev_rsize == 23) return -ENODEV; ret = hid_parse(hdev); if (ret) { hid_err(hdev, "parse failed\n"); return ret; } ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); if (ret) { hid_err(hdev, "hw start failed\n"); return ret; } return 0; } static const struct hid_device_id vrc2_devices[] = { { HID_USB_DEVICE(USB_VENDOR_ID_VRC2, USB_DEVICE_ID_VRC2) }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(hid, vrc2_devices); static struct hid_driver vrc2_driver = { .name = "vrc2", .id_table = vrc2_devices, .report_fixup = vrc2_report_fixup, .probe = vrc2_probe, }; module_hid_driver(vrc2_driver); MODULE_AUTHOR("Marcus Folkesson <marcus.folkesson@gmail.com>"); MODULE_DESCRIPTION("HID driver for VRC-2 2-axis Car controller"); MODULE_LICENSE("GPL");
BPF for HID drivers
Benjamin Tissoires, One of the maintiners for the HID core layer, has posted his work [6] to introduce eBPF (extended Berkely Packet Filter) support for HID devics which is a really cool thing. As many devices just lack a proper report descriptor, the eBPF let you write such fixup in userspace and simply load the program into the kernel. There is still some parts missing before we can see full support for this feature, but the main part is merged and will be available in 6.1.
See the LWN article [5] for further reading.
Conclusion
HID report descriptors has been a fun subject to dig into. It's still hard to see why different vendors has so hard to follow the specification though.
I also have to thank Benjamin Tissoires for great help and support in understanding how the HID layer and HID devices works.
References
[1] | https://www.usb.org/sites/default/files/hid1_11.pdf |
[2] | https://www.usb.org/defined-class-codes |
[3] | https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf |
[4] | https://www.phoronix.com/news/Linux-6.1-New-HID-Flight-Car |
[5] | https://lwn.net/Articles/909109/ |
[6] | https://lwn.net/ml/linux-kernel/20220824134055.1328882-1-benjamin.tissoires@redhat.com/ |