Implementing USB as a Custom HID Device Using STM32 (Part – 2)

In the first part of this blog, we laid the groundwork by discussing the fundamentals of USB communication and the concepts behind Custom HID devices. With that foundation in place, it’s time to delve deeper into the practical aspect of USB communication—packet flow during HID enumeration.

Enumeration is the process where the USB host detects, identifies, and configures a newly connected device. During this phase, various packets are exchanged between the host and the device, each serving a specific purpose. Understanding these packets, their formats, and their roles is crucial for debugging, optimizing, and creating USB devices from scratch.

In this part, we’ll dissect the packet flow step by step, starting with the control transfer requests sent by the host, followed by the device’s responses. We’ll explore the purpose of each packet type, analyze their structure, and explain how they contribute to the HID enumeration process. By the end of this section, you’ll gain a clear understanding of how the USB host and device communicate at the protocol level, paving the way for implementing your own Custom HID device.

USB Enumeration Process in Brief

When the STM32 microcontroller powers on (POR), it initializes the USB peripheral and waits for the device to be connected. Upon plugging in, the host issues a USB reset to prepare the device for communication, followed by a series of standard requests: the Device Descriptor Request to identify basic device properties, the Set Address Request to assign a unique address, the Device Qualifier Request to query high-speed support (optional for full-speed devices), and the Device Configuration Request to activate the device. The host may also request String Descriptors for human-readable information(This is basically 4 requests and their responses. Language ID, Product Name, Manufacture Name and Serial number) and the Set Configuration Descriptor to finalize the setup. Additionally, during HID-specific enumeration, the host sends Set Idle Request to configure idle behavior and HID Report Descriptor Request to fetch the HID report format.

The enumeration process, as described above, works in the sequence shown in the diagram on the right. In the diagram, I have outlined the flow of each request and its corresponding response, providing a visual representation of how the host and device communicate during enumeration. Each step, from the device descriptor request to the set configuration request, is illustrated to help you understand the sequence of interactions that occur.

Fig – 1: USB Enumeration Process

USB 2.0 Specification

The USB specification documents are available on the official USB website, usb.org. Since we are working with USB 2.0, I have already downloaded the document for this version(Download). You can access it directly from the official site as well. In this document, every requests and responses are described in detail. The request I got from the host and the response I have sent for each requests are shown below.

The USB Request Format

A USB request is sent by the host to the device during communication, particularly during the enumeration process. It consists of an 8-byte setup packet, which is the first stage of a control transfer. This setup packet contains important fields that help define the specific operation the host wishes to perform on the device.

Name of FieldSizeDescription
bmRequestType1 byteBit 7: Data Direction
0 – OUT (Host to device)
1 – IN (Device to host)
Bit 6-5: Type of request
00 – Standard Request
01 – Interface Request
10 – Vendor Request
11 – Reserved
D4-0: Recipient
0 – Device
1 – Interface
2 – Endpoint
3 – Other
4..31 – Reserved
bRequest1 bytespecifies the specific request that the host is making to the device
wValue2 byteThe meaning of this field depends on the specific request.
It typically holds the index of the descriptor or a value related to the request.
wIndex2 byteThis field provides additional context for the request.
It can be used to specify an interface or endpoint, depending on the type of request.
wLength2 byteDefines the length of the data to be transferred in the response
More details about these field can be found in this link.

Type of USB requests

  1. GET_DESCRIPTOR – Retrieves descriptors such as Device, Configuration, String, Interface, Endpoint, and HID descriptors.
  2. SET_ADDRESS – Assigns a unique address to the device for communication.
  3. SET_CONFIGURATION – Selects a specific configuration for the device.
  4. GET_CONFIGURATION – Retrieves the currently active configuration.
  5. GET_STATUS – Fetches the status of the device, interface, or endpoint (e.g., self-powered or remote-wakeup support).
  6. CLEAR_FEATURE – Clears a specific feature (e.g., endpoint halt).
  7. SET_FEATURE – Sets a specific feature (e.g., enable remote wakeup or halt an endpoint).
  8. GET_INTERFACE – Retrieves the currently active alternate setting for an interface.
  9. SET_INTERFACE – Selects an alternate setting for an interface.

Requests used for custom HID device enumeration

Device Descriptor Request

The device descriptor request is a type of GET_DESCRIPTOR. The device descriptor is the data structure that provides essential information about a USB device to the host during the enumeration process. This is the initial request with maximum packet size (64 bytes) that the endpoint 0 can support since the host doesn’t know about the descriptor size. The host may use some standard size during this request like 8, 16, 32.. based on the USB version. A sample request and response are shown below.

Request: {
  80,           // (0x80) Requesting an IN, standard, Device request
  06,           // (0x06) Request is GET_DESCRIPTOR
  00, 01,       // (0x0100) Request for Device descriptor
  00, 00,       // (0x0000) Not used here
  40, 00        // (0x0040) Expected length 64 bytes
}

Response: {
  12,           // Length of response - 18 bytes (1 byte)
  01,           // Type of descriptor - device descriptor (1 byte)
  00, 20,       // What version of USB supports - (0x0200) USB2.0 (2 bytes)
  00,           // Device class - Device class defined in interface descriptor (1 byte)
  00,           // Device subclass
  00,           // Device protocol
  40,           // Maximum packet size - 64 bytes (1 byte)
  83, 04,       // Vendor ID - 16 bit number assigned for the manufacture by USB.org (2 bytes)
  5A, 57,       // Product ID - 16 bit number model ID assigned by vendor or product (2 bytes)
  00, 20        // Device release number - eg 0x0200 for 2.0 (2 bytes)
  01,           // Index of string manufacture (1 byte)
  02,           // Index of string product descriptor (1 byte)
  03,           // index of string serial number (1 byte)
  01            // Number of configurations - 1 (1 byte)
}

Set Address Request

After receiving the device descriptor, the host assigns a unique address to the USB device using a SET_ADDRESS request. This is a standard USB control transfer that allows the host to communicate with the device on the USB bus. While the host does not expect any data response for this request, it ensures the device can handle subsequent requests after the address is set.

Request: {
  00,           // (0x00) Requesting an OUT, standard, Device request
  05,           // (0x05) Request is SET_ADDRESS
  02, 00,       // (0x0002) The address to be set is 2
  00, 00,       // (0x0000) Reserved, set as 0
  00, 00        // (0x0000) Expected length is 0 byte
}

Response: {}

Device Descriptor Request

During the first request, the device may respond with either a full or partial descriptor based on the request length. From this response, the USB host determines the actual size of the descriptor and issues a second request to retrieve it. In this stage, the device is expected to send the complete device descriptor. In our case, since we already sent the entire descriptor during the first request, we simply send the same response again for the second request.

Request: {
  80,           // (0x80) Requesting an IN, standard, Device request
  06,           // (0x06) Request is GET_DESCRIPTOR
  00, 01,       // (0x0100) Request for Device descriptor
  00, 00,       // (0x0000) Not used here
  12, 00        // (0x0012) Expected length 18 bytes
}

Response: {
  12,           // Length of response - 18 bytes (1 byte)
  01,           // Type of descriptor - device descriptor (1 byte)
  00, 20,       // What version of USB supports - (0x0200) USB2.0 (2 bytes)
  00,           // Device class - Device class defined in interface descriptor (1 byte)
  00,           // Device subclass
  00,           // Device protocol
  40,           // Maximum packet size - 64 bytes (1 byte)
  83, 04,       // Vendor ID - 16 bit number assigned for the manufacture by USB.org (2 bytes)
  5A, 57,       // Product ID - 16 bit number model ID assigned by vendor or product (2 bytes)
  00, 20        // Device release number - eg 0x0200 for 2.0 (2 bytes)
  01,           // Index of string manufacture (1 byte)
  02,           // Index of string product descriptor (1 byte)
  03,           // index of string serial number (1 byte)
  01            // Number of configurations - 1 (1 byte)
}

Qualifier Descriptor Request

The device qualifier descriptor describes information about a high-speed capable device that would change if the device were operating at the other speed. For example, if the device is currently operating at full-speed, the device_qualifier returns information about how it would operate at high-speed and vice-versa.

If a full-speed only device receives a GET_DESCRIPTOR request for a device qualifier, it must respond with a STALL handshake.

Request: {
  80,           // (0x80) Requesting an IN, standard, Device request
  06,           // (0x06) Request is GET_DESCRIPTOR
  00, 06,       // (0x0600) Request for device qualifier
  00, 00,       // (0x0000) Not used here
  0A, 00        // (0x000A) Expected length 10 bytes
}

Response: {
  0A,           // (0x0A) Length of response is 10 bytes
  06,           // (0x06) This is qualifier response
  00, 02,       // (0x0002) USB spec version number (0x0200 for 2.0)
  00,           // (0x00) Class code
  00,           // (0x00) Sub class code
  00,           // (0x00) Protocol code
  40,           // (0x40) Maximum packet size 64 bytes
  01,           // (0x01) Number of other speed configurations
  00            // (0x00) Reserved
}

Configuration Descriptor Request

The Standard Configuration Descriptor is a USB descriptor that provides information about a device’s specific configuration. It is retrieved by the host using the GET_DESCRIPTOR request. This descriptor includes details about the device’s power requirements and the number of interfaces and endpoints it supports in a specific configuration.

This two-step process is standard in USB enumeration to handle devices with varying descriptor sizes. By first requesting 9 bytes, the host avoids retrieving unnecessary data and ensures it only requests the exact amount needed in the second step.

The configuration descriptor for custom HID interface composed of 4 parts

  1. Configuration Descriptor (9 bytes)
  2. Interface Descriptor (9 bytes)
  3. HID Descriptor (9 bytes)
  4. Endpoint Descriptor (7 bytes x 2 endpoints)
Request: {
  80,           // (0x80) Requesting an IN, standard, Device request
  06,           // (0x06) Request is GET_DESCRIPTOR
  00, 02,           // 0x0200 configuration descriptor
  00, 00,           // Not used
  29, 00            // 0x0009 (9 bytes) in the first request and 0x0029 (41 bytes) in the second request
}

Response: {
  // Configuration Descriptor (9 bytes)
  09,               // (0x09) Size of configuration descriptor (9 bytes)
  02,               // (0x02) Descriptor type (Configuration)
  29, 00            // (0x0029) Total size of all descriptors in this configurations (41 bytes)
  01,               // (0x01) Number of interfaces (1)
  01,               // (0x01) Value to select this configuration (1)
  00,               // (0x00) Index of string descriptor for this configuration (0 - means no string descriptor)
  C0,               // (0xC0) Configuration char (6th bit - self powered)
  32,               // (0x32) Maximum power consumption of the USB device from the bus in
                    // this specific configuration when the device is fully operational (100mA).

  // Interface Descriptors (9 bytes)
  09,               // (0x09) Size of interface descriptor (9 bytes)
  04,               // (0x04) Descriptor type (Interface)
  00,               // (0x00) Interface number
  00,               // (0x00) Alternate settings
  02,               // (0x02) Number of endpoints (2)
  03,               // (0x03) Interface Class (3 - HID)
  00,               // (0x00) No subclass
  00,               // (0x00) No protocol
  00,               // (0x00) Index of string descriptor for this interface (0 - means no string descriptor)

  // HID Descriptor
  09,               // (0x09) Length of HID descriptor
  21,               // (0x21) Descriptor type HID
  11, 01,           // (0x0111) HID Class specification version in BCD (1.11)
  00,               // (0x00) Target country code (0 - not localized)
  01,               // (0x01) Number of additional descriptor (1)
  22,               // (0x22) Type of additional descriptor (report descriptor)
  03, 00,           // (0x0003) Length of additional descriptor (3)
  // Type and length of additional descriptor can be repeated multiple times upto number of additional descriptors

  // Endpoint descriptors 1 (IN) (7 bytes)
  07,               // 7 (Size of the Endpoint Descriptor).
  05,               // 5 (Endpoint Descriptor).
  81,               // 0x81 (Endpoint 1, IN).
  03,               // 0x03 (Interrupt transfer).
  02, 00,           // Maximum packet size 
  05,               // 0x05 polling interval 5ms

  // Endpoint descriptors 2 (OUT) (7 bytes)
  07,               // 7 (Size of the Endpoint Descriptor).
  05,               // 5 (Endpoint Descriptor).
  01,               // 0x01 (Endpoint 1, OUT).
  03,               // 0x03 (Interrupt transfer).
  02, 00,           // Maximum packet size 
  05                // 0x05 polling interval 5ms
}

Language ID String Request

The Language ID request and response help the host identify which languages the device can use for string descriptors. After receiving the language ID(s), the host can then request additional string descriptors (e.g., manufacturer name, product name) in the appropriate language.

This mechanism ensures that devices support internationalization and localization of string descriptors, allowing for a customized user experience based on the language preferences of the host system.

Request: {
  80,           // (0x80) Requesting an IN, standard, Device request
  06,           // (0x06) Request is GET_DESCRIPTOR
  00, 03,           // 0x0300 String descriptor
  00, 00,           // Index is 0 (language descriptor)
  FF, 00            // 0x00FF maximum 255 bytes
}

Response: {
  04,           // Length of response (4 bytes)
  03,           // String descriptor
  09, 04        // 0x0409 for english
}

Product String Request

The product string typically identifies the product name of the device (e.g., the name of a keyboard or mouse) and is part of the String descriptors category, which provides human-readable information about the USB device.

Request: {
  80,                       // (0x80) Requesting an IN, standard, Device request
  06,                       // (0x06) Request is GET_DESCRIPTOR
  02, 03,                   // 0x0302 Product string descriptor (index 2 is mapped to product string in the device descriptor)
  09, 04,                   // Index is 0 (language descriptor)
  FF, 00                    // 0x00FF maximum 255 bytes
}

Response: {
  3A,                       // (0x3A) Total length of response 
  03,                       // (0x03) String descriptor
  53, 00, 54,               // Corresponding to "STM32 Custom Human Interface"
  00, 4D, 00, 33, 00,
  32, 00, 20, 00, 43,
  00, 75, 00, 73, 00,
  74, 00, 6F, 00, 6D,
  00, 20, 00, 48, 00,
  75, 00, 6D, 00, 61,
  00, 6E, 00, 20, 00,
  69, 00, 6E, 00, 74, 
  00, 65, 00, 72, 00, 
  66, 00, 61, 00, 63, 
  00, 65, 00
}

Manufacturer String Request

Request: {
  80,                       // (0x80) Requesting an IN, standard, Device request
  06,                       // (0x06) Request is GET_DESCRIPTOR
  01, 03,                   // 0x0301 Manufacture string descriptor (index 1 is mapped to manufacture string in the device descriptor)
  09, 04,                   // Index is 0 (language descriptor)
  FF, 00                    // 0x00FF maximum 255 bytes
}

Response: {
  26,                       // (0x26) Total length of response 
  03,                       // (0x03) String descriptor
  53, 00, 54,               // Corresponding to "STMicroelectronics"
  00, 4D, 00, 69, 00,
  63, 00, 72, 00, 6F,
  00, 65, 00, 6C, 00,
  65, 00, 63, 00, 74,
  00, 72, 00, 6F, 00,
  6E, 00, 69, 00, 63,
  00, 73, 00
}

Serial Number String Request

is used to retrieve the serial number of a USB device. It is part of the String descriptors mechanism, allowing the host to request additional identifying information about a device, such as its serial number, which is often unique to each device. This is useful for identifying and distinguishing devices of the same type or model.

Request: {
  80,                       // (0x80) Requesting an IN, standard, Device request
  06,                       // (0x06) Request is GET_DESCRIPTOR
  03, 03,                   // 0x0303 Serial number string descriptor (index 3 is mapped to serial number in the device descriptor)
  09, 04,                   // Index is 0 (language descriptor)
  FF, 00                    // 0x00FF maximum 255 bytes
}

Response: {
  10,                       // (0x10) Total length of response 
  03,                       // (0x03) String descriptor
  31, 00, 32,               // Corresponding to "1234567890ABCDEF"
  00, 33, 00, 34, 00,
  35, 00, 36, 00, 37,
  00, 38, 00, 39, 00,
  30, 00, 41, 00, 42,
  00, 43, 00, 44, 00,
  45, 00, 46, 00
}

Set Configuration Request

The Set Configuration Request is a standard USB request sent by the host to configure a USB device after it has been enumerated. This request allows the host to select one of the available configurations provided by the device, which defines the device’s power consumption, interfaces, and endpoint descriptors. It is crucial because the device may have multiple configurations (e.g., different modes of operation or power settings), and the host uses this request to choose the appropriate configuration for communication. The request is sent on the control endpoint using the bRequest value 0x09 with the wValue specifying the configuration index.

Request: {
  00,                       // Host-to-Device, Standard, Device recipient.
  09,                       // SET_CONFIGURATION request.
  01, 00,                   // Configuration value (0x0001 specified in configuration descriptor) to set.
  00, 00,                   // Not used
  00, 00                    // No data expected
}

Response: {}

Set Idle Request (HID Class specific)

This request is used to limit the reporting frequency of an interrupt in endpoint. Specifically, this request causes the endpoint to NAK any polls on an interrupt in endpoint while its current report remains unchanged. In the absence of a change, polling will continue to be NAKed for a given time-based duration

Request: {
  21,                       // Host-to-Device, Class, Interface recipient.
  0A,                       // SET_IDLE request (HID-specific).
  00, 00,                   // High byte: Idle duration (0 for infinite). Low byte: Report ID (0 for all).
  00, 00,                   // Interface number (usually 0 for single interface devices).
  00, 00                    // No data expected
}

Response: {}

HID Report Request (HID Class Specific)

If we are going to send/receive raw data, we don’t need to define any HID report. So just define as follows

Request: {
  81,                       // Device-to-Host, Standard, Interface recipient.
  06,                       // GET_DESCRIPTOR request.
  00, 22,                   // Descriptor Type (0x2200 = HID Report Descriptor).
  00, 00,                   // Interface number (usually 0 for single interface devices).
  03, 00                    // Expected length 3 bytes
}

Response: {
  A1, 01,                 // Collection (Application)
  C0                        // End collection
}

Leave a comment