March 29, 2024

USB Adventures Part 2: Custom HID and USB Composite Devices

So in a previous post I’ve discussed how to communicate with a custom HID device using libhid and a Raspberry Pi running linux.

This post is a sort of sequel. I’ll talk about some of the issues and nuances I found when working on a more complex (but related) project; In this case a  Composite USB Device that I had to implement on a PIC 18F4550 microcontroller.

Some context

Before we begin I’d like to clarify what the purpose of this is, and what exactly is the difference with what I’ve done in the past. I’d also like to provide a missing piece of information that should’ve been part of my previous post. So let’s start with definitions, first:

A Custom HID Device is basically a peripheral that does not fit any of the common device definitions that conform the Human Interface Device standard (Mouse, Keyboard, Gamepad, Webcam, etc) but still use the same interfacing framework that all HID devices use, which is based in exchanging data with the host through “reports” via  “endpoints”. With custom HID devices the operating system doesn’t know what their function is, but it does knows how to speak with them since they comply with the HID specification. They are normally highly-specific hardware devices that require special software and vendor-provided tools to interact with them.

A Composite USB Device on the other hand, is a piece of USB hardware that is recognized by your operating system as multiple “devices”. Wireless mouse+keyboard combos are an example of this; you plug the wireless receiver on your PC and your system recognizes both a mouse and a keyboard. Some digital cameras are seen by your system as both a storage unit and a video/image capture device. Most multi-card readers are also recognized as several storage drives as well.  They are all examples of composite devices. For your computer they are different devices/interfaces and can act independently, but they are physically one peripheral.

For this particular project I was tasked with creating a Composite USB device that would be recognized by the computer as 4 different devices, one of them being a Custom HID device.

In the past I’ve made both things separately: I’ve created simple composite devices (2 interfaces; both of the same kind) and Custom HID Devices (with their respective “config” tools and libraries so they could be used in our projects), but I’ve never combined both things before, especially with this many interfaces, all of different kind.

The Composite Device

There are many ways of implementing a Composite device, but the simplest method is through what is called a “TLC” (Top Level Collection). This basically consists of a single report descriptor for the whole device that contains all the descriptors for the sub-devices. Adding a special “Report ID” to each sub descriptor allows your computer to address each separate device in the collection using a single interface and a single set of endpoints. To achieve this an “id” needs to be sent with every report that tells your system what sub-device is sending or receiving the data. The details of this will be explained later.

To illustrate, here is what the structure of a regular keyboard report descriptor looks like:

 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 0x09, 0x06, // USAGE (Keyboard)
 0xa1, 0x01, // COLLECTION (Application)
 ...
 (Keyboard Data Scheme and Specs)
 ...
 0xc0 // END_COLLECTION

And this is the skeleton of a mouse report descriptor:

 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 0x09, 0x02, // USAGE (Mouse)
 0xa1, 0x01, // COLLECTION (Application)
 ... 
 (Mouse Data Scheme and Specs)
 ...
 0xc0 // END_COLLECTION

Now this is a composite keyboard+mouse:

 // KEYBOARD REPORT DESCRIPTOR ---------
 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 0x09, 0x06, // USAGE (Keyboard)
 0xa1, 0x01, // COLLECTION (Application)
 0x85, 0x01, // REPORT_ID (1)
 ...
 (Keyboard Data Scheme and Specs)
 ...
 0xc0 // END_COLLECTION
 // MOUSE REPORT DESCRIPTOR ------------
 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 0x09, 0x02, // USAGE (Mouse)
 0xa1, 0x01, // COLLECTION (Application)
 0x85, 0x02, //   REPORT_ID (2)
 ... 
 (Mouse Data Scheme and Specs)
 ...
 0xc0 // END_COLLECTION

Notice how it’s basically just chaining the individual reports together and adding a Report ID to each one of them. To avoid inconsistencies during device enumeration I’d advice you to set the report ids of each device to the “position” they have inside your collection. So the ID of the first device is 1, the second one is 2, and so on…

Device-Side Firmware

If you are using a PIC microprocessor you can use any of the USB examples that come with the MLA and modify the Report Descriptor (and report size) to look like the example above. You’ll also need to set your input and output endpoint sizes to the largest size among the devices in the collection (adding an extra byte for the Report ID).

If your device includes a Custom HID device please note that in a regular single-device HID descriptor you can have an output data buffer of up to 64 bytes because that’s apparently the max size for the endpoint data buffer. But when you are making a composite device you have a max size of 63 bytes, as the report will need an extra byte for the ID. I  spent a number of hours debugging my composite device because communication wasn’t working and then I found that my custom HID sub-device had a 64 byte output (just as the one I described in my previous post) which was an impossible configuration now that an ID was needed in every report. Changing that to 63 while keeping the endpoint data size at 64 fixed my communication problems.

Talking about device ID; Implementation-wise telling devices apart in the firmware is pretty easy; whenever you are sending or receiving data from/to your device, the first byte in your endpoint buffer will always be the report id, which specifies the target (or source) sub-device for a particular report. if you are  using C you can use explicit casts to “parse” the endpoint buffer as a data structure that makes sense to the specific device that is handling the report.

This is an example of what your code will most likely look like, for a composite device:

// Sub-devices report ID:
#define HID_KEYBOARD_REPORT_ID 1
#define HID_MOUSE_REPORT_ID    2
#define HID_CUSTOM_REPORT_ID   3
 ...
    
    uint8_t reportId;
    MouseDataStructure *mouseData;
    KeyboardDataStructure *keyboardData;

    ReceiveUSBReport(&ReceivedDataBuffer);
    reportId = ReceivedDataBuffer[0];
    if (reportId == HID_CUSTOM_REPORT_ID){
        HandleCustomHIDInputReport (&ReceivedDataBuffer[1]);
    }
     ...
    OutputDataBuffer[0] = HID_MOUSE_REPORT_ID;
    mouseData = (MouseDataStructure *)&OutputDataBuffer[1];
    FillMouseReportData (mouseData);
    SendUSBReport(OutputDataBuffer);
     ...
    OutputDataBuffer[0] = HID_KEYBOARD_REPORT_ID;
    keyboardData = (KeyboardDataStructure *)&OutputDataBuffer[1];
    FillKeyboardReportData (keyboardData);
    SendUSBReport(OutputDataBuffer);
     ...

 

Host-Side Software

Opening the device

If you follow the examples provided by Microchip you’ll see that the first step (in Windows) to communicate with a HID device is to obtain a sort of “file” handle that enables us to read and write data to it, almost as if it was an ordinary file. In order to do that, you use SetupDiGetDeviceRegistryProperty to enumerate hardware devices. Then you need to request vendor and product information from the device, looking for a specific VID and PID (Through a “Vid_VVVV&Pid_PPPP” search string). When you have a match, you have found your device  (The code to do this is also found in Microchip’s MLA collection).

The problem is that this won’t work with a composite device because Windows will actually create a bunch of entries (one per sub-device in your hardware) with the same vendor and product ID. The CheckIfPresentAndGetUSBDevicePath() method -as it appears in the MLA examples- will always return a handle to the first sub-device. To differentiate between the entries you’ll need to look for an extra parameter called COL. Our mouse+keyboard+custom HID device above would have generated 3 entries “Vid_VVVV&Pid_PPPP&Col_01”, “Vid_VVVV&Pid_PPPP&Col_02” and “Vid_VVVV&Pid_PPPP&Col_03”. You’ll need to modify the code so whenever it finds a matching PID &VID it also checks if the “&Col_XX” substring is present, using the ID of the actual sub-device you are interested in talking to. In our example that would be “&Col_03”, the custom HID device.

The code needed to recognize our device, applied to the CheckIfPresentAndGetUSBDevicePath() function in Microchip’s MLA  custom HID example, would look like this:

'//Now check if the hardware ID we are looking at contains the correct VID/PID
MatchFound = DeviceIDFromRegistry.Contains(DeviceIDToFind)

'MODIFIED CODE: ------------------------------------------------------
'For a TLC composite device we need to match the report ID that belongs
'to the custom HID Device.
' Old code was simply "If MatchFound Then ..."
'---------------------------------------------------------------------
If MatchFound And DeviceIDFromRegistry.Contains("&col03") Then
...
...

Talking to the device

Curiously enough even when we have obtained a handle to the sub-device itself you’ll still need to add the report ID at the beginning of each data frame you send.  This is interesting because even for single-interface HID devices (that don’t need a report ID) you are still required to add an extra byte (set to zero) which is never actually sent. The USB layer in Windows doesn’t seem to be smart nor aware of the report descriptor structure of your device. Otherwise it would be able to tell if the device you opened belongs to a composite device (so it could automatically append the report ID) or if it’s a device with a single interface and doesn’t have a report ID (so no extra bytes are needed).

In linux (with libusb at least) you only need the PID & VID to find and open the device, and then you specify the proper sub-device you want to talk to by adding the report ID as the first byte in the data you send. You know, the way it actually makes sense.

With this you should have your composite device working. You should be able to apply the changes described here to any of the existing USB code examples in Microchip’s MLA or Application notes. In fact, the information here should be generic enough to be applied to other microcontrollers and frameworks.

 

4 thoughts on “USB Adventures Part 2: Custom HID and USB Composite Devices

    1. That’s an interesting question. I haven’t tried that, but I don’t see why it wouldn’t work!

  1. Hi Mr. E
    Do you have any example source using the latest version of MLA working with 2 composite HID devices?
    I dont see that they use REPORT_ID in the latest MLA composite example?
    Thanks
    Mark

    1. Hi Mark,
      The REPORT_ID and related constants in my posts are placeholders for the IDs you specify when you define your custom device report descriptor, which is something you normally create using tools like the Descriptor Tool from USB.org. They are not constants inside the MLA code unless you define them as such when modifying the examples (as I did).

      Bear in mind that not all HID descriptors require Report IDs. As I wrote here, adding them to the descriptor is one way of building a composite device (the simplest, IMHO), but there are other techniques and the composite example in the MLA shows one way that probably does not involve Report IDs. You should be able to start from any basic (non-composite) HID device example from the MLA and customize the descriptor as outlined in the post, adding your subdevices to the structure, and giving them their own report ids, and it should work.

      Hope that clears things up.

Leave a Reply to Mark Cancel reply

Your email address will not be published. Required fields are marked *