Skip to content

Contributed by bzhou830, curated by Ai-Thinker

1. Introduction

In order to understand the USB protocol, I've written two posts about the basics of the USB protocol before. If you're not familiar with it, you can check out the following basic content.

Exploring USB 01. USB protocol basics - Xiao An Pai S1 & M61 tutorial collection - IoT Developer Community - Ai-Thinker Forum

Exploring USB 02. Device connection and enumeration - Xiao An Pai S1 & M61 tutorial collection - IoT Developer Community - Ai-Thinker Forum

Starting from this post, we'll use the M62 board for USB device development, beginning with the simplest USB HID keyboard. The firmware SDK uses the CherryUSB open-source USB stack. CherryUSB is a small, elegant and highly portable USB host/device protocol stack for embedded systems. The device-side protocol flow is shown below:

2. Descriptors

CherryUSB implements a complete set of USB device protocols for us, so we only need to call the interfaces provided by CherryUSB to implement the USB device we want. First, we should fill in the descriptors for our USB device. There are many kinds of descriptors, and we need to understand what they mean first.

Device: a real USB device, such as a USB mouse or USB flash drive.

Configuration: a USB device can have multiple configurations. For example, a 4G dongle has 2 configurations: USB storage and network card. The first time you plug a 4G dongle into a computer, it appears as a USB drive so you can install its software. After installing, plugging it in again makes it a network card. The driver chooses which configuration it works in; only one configuration can be active at a time. Most USB devices have only one configuration.

Interface: each configuration can have multiple interfaces. This interface is not a hardware interface; think of it as a function — one interface represents one function the device currently supports.

Endpoint: each interface can have multiple endpoints. The USB host and device exchange data through endpoints. Each endpoint address corresponds to a direction, e.g., endpoint 2-IN and endpoint 2-OUT have completely different meanings.

For HID devices, there are also the HID descriptor and the HID report descriptor.

HID descriptor: a fixed byte array describing the device's data packets, including how many packets the device supports, how large they are, and the meaning of each byte and bit in the packets.

HID report descriptor: a hardcoded byte array that describes the device's data packets. This includes how many data packets the device supports, how large they are, and the purpose of each byte and bit in the packets.

For the report formats of USB keyboards and mice, see the following post:

USB-HID keyboard/mouse communication data format - Technical - IoT Developer Community - Ai-Thinker Forum

Let's look at each of them one by one.

2.1 Device Descriptor

CherryUSB provides macros to define the device descriptor:

cpp
USB_DEVICE_DESCRIPTOR_INIT(USB_2_0,
                           0x00,
                           0x00,
                           0x00,
                           USBD_VID,
                           USBD_PID,
                           0x0002,
                           0x01)

Let's explain in detail what these fields mean:

FieldDescription
bLengthIndicates the length of this descriptor. The device descriptor is 18 bytes long, which is 0x12 in hexadecimal.
bDescriptorTypeThe type of the descriptor. The specific values are listed in Table 355. The device descriptor type code is 0x01.
bcdUSBThe USB protocol version used by the device, such as 20 or 11. Note that it is represented in BCD; for example, USB 2.0 is 0x0200 and USB 1.1 is 0x0110. As mentioned before, the USB protocol uses little-endian, so the low byte is transmitted first — the bcdUSB of USB 2.0 splits into 0x00 and 0x20, while USB 1.1's bcdUSB splits into 0x10 and 0x01.
bDeviceClassThe class code used by the device. Device class codes are defined by the USB-IF; see the USB documentation for specific codes. For most standard USB device classes, this field is usually 0, and the function implemented by the interface is specified in bInterfaceClass in the interface descriptor. When bDeviceClass is 0, bDeviceSubClass below must also be 0. bDeviceClass of 0xFF indicates a vendor-defined device class.
bDeviceSubClassThe subclass code used by the device. When the class code is neither 0 nor 0xFF, the subclass code is defined by the USB protocol. When bDeviceClass is 0, bDeviceSubClass must also be 0.
bDeviceProtocolThe protocol code used by the device, defined by the USB-IF. When this field is 0, the device does not use a class-defined protocol. When it is 0xFF, the device uses a vendor-defined protocol. bDeviceProtocol is only meaningful together with the device class and subclass, so when the class code is 0, bDeviceProtocol should also be 0.
bMaxPacketSize0The maximum packet size of endpoint 0. It can be 8, 16, 32 or 64.
idVendorThe vendor ID. This ID is assigned by the USB-IF and cannot be used arbitrarily. You can apply to the USB-IF for a vendor ID.
idProductThe product ID. Unlike the vendor ID, it is assigned by the manufacturer freely according to the product.
bcdDeviceThe device version number. After a product is upgraded (e.g., firmware changes adding features), the version number can be changed to distinguish it.
iManufacturerThe index of the string descriptor describing the manufacturer. When 0, there is no manufacturer string. When the host gets the device descriptor, it puts the index in the first byte of wValue to select different strings.
iProductThe index of the string descriptor describing the product. When 0, there is no product string. When a USB device is first plugged in, Windows shows a popup in the bottom-right corner saying new hardware found and displaying the device name. That information actually comes from the product string. To display the information you want, modify the product string.
iSerialNumberThe index of the device's serial number string. It's best to give every product a unique serial number, just like every Intel Pentium 4 processor has an ID. The serial number may be used by the host together with VID and PID to distinguish devices; sometimes connecting multiple devices with the same VID, PID and serial number may cause the device to be unrecognized. When 0, there is no serial number string.
bNumConfigurationsIndicates how many configurations the device has. Each configuration has a configuration descriptor; the host selects a configuration by sending a Set Configuration request. Most USB devices have only one configuration, i.e., this field is 1.

2.2 Configuration Descriptor

Similarly, CherryUSB provides macros to define the configuration descriptor:

cpp
USB_CONFIG_DESCRIPTOR_INIT(USB_HID_CONFIG_DESC_SIZ,
                           0x01,
                           0x01,
                           USB_CONFIG_BUS_POWERED,
                           USBD_MAX_POWER),
FieldDescription
bLengthIndicates the length of this descriptor. A standard USB configuration descriptor is 9 bytes long.
bDescriptorTypeIndicates the type of the descriptor. The configuration descriptor type code is 0x02.
wTotalLengthIndicates the total length of the whole configuration descriptor set, including the configuration descriptor, interface descriptors, class-specific descriptors (if any) and endpoint descriptors. Note the low byte comes first.
bNumInterfacesIndicates the number of interfaces supported by this configuration. Usually a single-function device has only one interface (e.g., a mouse), while composite devices have multiple interfaces (e.g., audio devices).
bConfigurationIndicates the value of this configuration. A USB device can support multiple configurations; bConfiguration is the identifier of each configuration. When a Set Configuration request is sent with a configuration value, the configuration whose bConfiguration matches becomes active.
iConfiguration1 byte, the index of the string descriptor describing this configuration. If 0, there is no string.
bmAttributes1 byte describing some characteristics of the device. D7 is reserved and must be 1. D6 indicates the power source: 1 means self-powered, 0 means bus-powered. D5 indicates remote wakeup support: 1 means supported. D4~D0 are reserved and set to 0.
bMaxPower1 byte, the maximum current the device draws from the bus, in units of 2mA. For example, 200mA maximum current means this byte is 100.

2.3 Interface Descriptor

The interface descriptor needs to be written as an array by ourselves:

cpp
0x09,                       /* bLength: Interface Descriptor size */
USB_DESCRIPTOR_TYPE_INTERFACE, /* bDescriptorType: Interface descriptor type */
0x00,                       /* bInterfaceNumber: Number of Interface */
0x00,                       /* bAlternateSetting: Alternate setting */
0x01,                       /* bNumEndpoints */
0x03,                       /* bInterfaceClass: HID */
0x01,                       /* bInterfaceSubClass: 1=BOOT, 0=no boot */
0x01,                       /* nInterfaceProtocol: 0=none, 1=keyboard, 2=mouse */
0,                          /* iConfiguration: Index of string descriptor */
FieldDescription
bLength1 byte, the length of this descriptor. A standard USB interface descriptor is 9 bytes long.
bDescriptorType1 byte, the type of the descriptor. The interface descriptor type code is 0x04.
bInterfaceNumber1 byte, the number of this interface. When a configuration has multiple interfaces, each interface has a different number, starting from 0 and increasing.
bAlternateSetting1 byte, the alternate setting of this interface. Numbering follows the same rule as bInterfaceNumber; this field is rarely used and set to 0.
bNumEndpoints1 byte, the number of endpoints used by this interface (excluding endpoint 0). If 0, there are no non-zero endpoints and only the default control endpoint is used.
bInterfaceClass, bInterfaceSubClass, bInterfaceProtocolThe class, subclass and protocol used by the interface respectively; their codes are defined by the USB-IF, similar to those in the device descriptor. Usually the device's function is defined in the interface, while the class, subclass and protocol fields in the device descriptor are set to 0.
iConfiguration1 byte, the index of the string descriptor describing this interface. If 0, there is no string.

2.4 Endpoint Descriptor

cpp
0x07,                       /* bLength: Endpoint Descriptor size */
USB_DESCRIPTOR_TYPE_ENDPOINT, /* bDescriptorType */
HID_INT_EP,                 /* bEndpointAddress: Endpoint Address (IN) */
0x03,                       /* bmAttributes: Interrupt endpoint */
HID_INT_EP_SIZE,            /* wMaxPacketSize: 4 Byte max */
0x00,
HID_INT_EP_INTERVAL,        /* bInterval: Polling Interval */

/* 34 */

///////////////////////////////////////
/// string0 descriptor
///////////////////////////////////////

USB_LANGID_INIT(USBD_LANGID_STRING),

///////////////////////////////////////
/// string1 descriptor
///////////////////////////////////////

0x14,                       /* bLength */
USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
'C', 0x00,                  /* wcChar0 */
'h', 0x00,                  /* wcChar1 */
'e', 0x00,                  /* wcChar2 */
'r', 0x00,                  /* wcChar3 */
'r', 0x00,                  /* wcChar4 */
'y', 0x00,                  /* wcChar5 */
'U', 0x00,                  /* wcChar6 */
'S', 0x00,                  /* wcChar7 */
'B', 0x00,                  /* wcChar8 */

///////////////////////////////////////
/// string2 descriptor
///////////////////////////////////////

0x26,                       /* bLength */
USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
'C', 0x00,                  /* wcChar0 */
'h', 0x00,                  /* wcChar1 */
'e', 0x00,                  /* wcChar2 */
'r', 0x00,                  /* wcChar3 */
'r', 0x00,                  /* wcChar4 */
'y', 0x00,                  /* wcChar5 */
'U', 0x00,                  /* wcChar6 */
'S', 0x00,                  /* wcChar7 */
'B', 0x00,                  /* wcChar8 */
' ', 0x00,                  /* wcChar9 */
'H', 0x00,                  /* wcChar10 */
'I', 0x00,                  /* wcChar11 */
'D', 0x00,                  /* wcChar12 */
' ', 0x00,                  /* wcChar13 */
'D', 0x00,                  /* wcChar14 */
'E', 0x00,                  /* wcChar15 */
'M', 0x00,                  /* wcChar16 */
'O', 0x00,                  /* wcChar17 */

///////////////////////////////////////
/// string3 descriptor
///////////////////////////////////////

0x16,                       /* bLength */
USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
'2', 0x00,                  /* wcChar0 */
'0', 0x00,                  /* wcChar1 */
'2', 0x00,                  /* wcChar2 */
'2', 0x00,                  /* wcChar3 */
'1', 0x00,                  /* wcChar4 */
'2', 0x00,                  /* wcChar5 */
'3', 0x00,                  /* wcChar6 */
'4', 0x00,                  /* wcChar7 */
'5', 0x00,                  /* wcChar8 */
'6', 0x00,                  /* wcChar9 */
0x00
FieldDescription
bLength1 byte, the length of this descriptor. A standard USB endpoint descriptor is 5 bytes long.
bDescriptorType1 byte, the type of the descriptor. The endpoint descriptor type code is 0x05.
bEndpointAddress1 byte, the address of this endpoint. The highest bit D7 is the transfer direction: 1 means IN (like the first letter of Input), 0 means OUT (like the first letter of Output). D3-D0 are the endpoint number. D6-D4 are reserved and set to 0.
bmAttributes1 byte, the attributes of this endpoint. The lowest two bits D1-D0 indicate the transfer type: 0 control, 1 isochronous, 2 bulk, 3 interrupt. For non-isochronous endpoints, D7-D2 are reserved and set to 0. For isochronous endpoints, D3-D2 indicate the synchronization type (0 no sync, 1 asynchronous, 2 adaptive, 3 synchronous); D5-D4 indicate usage (0 data endpoint, 1 feedback endpoint, 2 implicit feedback data endpoint, 3 reserved). D7-D6 are reserved.
wMaxPacketSize1 byte, the maximum packet size supported by this endpoint. Note the low byte comes first. For full-speed and low-speed modes, D10-D0 indicate the maximum packet size and other bits are reserved as 0. For high-speed mode, D12-D11 are the additional transactions per frame; see the USB 2.0 specification.
bInterval1 byte, the polling interval of this endpoint. For interrupt endpoints, it indicates the number of frame intervals between polls. For isochronous transfers and high-speed interrupt/bulk transfers, see the USB 2.0 specification.

2.5 HID Descriptor

cpp
0x09,                       /* bLength: HID Descriptor size */
HID_DESCRIPTOR_TYPE_HID,    /* bDescriptorType: HID */
0x11,                       /* bcdHID: HID Class Spec release number */
0x01,
0x00,                       /* bCountryCode: Hardware target country */
0x01,                       /* bNumDescriptors: Number of HID class descriptors to follow */
0x22,                       /* bDescriptorType */
HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength: Total length of Report descriptor */
0x00,
FieldDescription
bLength1 byte, the total length of this descriptor. Its size depends on the number of subordinate descriptors. For example, with only one subordinate descriptor, the total length is 1+1+2+1+1+1+2 = 9 bytes.
bDescriptorType1 byte, the type code of this descriptor. The HID descriptor type code is 0x21.
bcdHID2 bytes, the version number of the HID protocol used by the device.
bCountryCode1 byte, the country the device applies to. Usually our keyboards are US keyboards, code 33, i.e., 0x21.
bNumDescriptors1 byte, the number of subordinate descriptors. This value must be at least 1, i.e., at least one report descriptor. Subordinate descriptors can be report descriptors or physical descriptors.
bDescriptorType1 byte, the type of the subordinate descriptor. The report descriptor type is 0x22 and the physical descriptor type is 0x23.
bDescriptorLength2 bytes, the length of the subordinate descriptor. When there are multiple subordinate descriptors, bDescriptorType and bDescriptorLength repeat alternately.

2.6 HID Report Descriptor

cpp
static const uint8_t hid_keyboard_report_desc[HID_KEYBOARD_REPORT_DESC_SIZE] = {
    0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
    0x09, 0x06, /* USAGE (Keyboard) */
    0xa1, 0x01, /* COLLECTION (Application) */
    0x05, 0x07, /* USAGE_PAGE (Keyboard) */
    0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
    0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
    0x15, 0x00, /* LOGICAL_MINIMUM (0) */
    0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
    0x75, 0x01, /* REPORT_SIZE (1) */
    0x95, 0x08, /* REPORT_COUNT (8) */
    0x81, 0x02, /* INPUT (Data,Var,Abs) */
    0x95, 0x01, /* REPORT_COUNT (1) */
    0x75, 0x08, /* REPORT_SIZE (8) */
    0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
    0x95, 0x05, /* REPORT_COUNT (5) */
    0x75, 0x01, /* REPORT_SIZE (1) */
    0x05, 0x08, /* USAGE_PAGE (LEDs) */
    0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
    0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
    0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
    0x95, 0x01, /* REPORT_COUNT (1) */
    0x75, 0x03, /* REPORT_SIZE (3) */
    0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
    0x95, 0x06, /* REPORT_COUNT (6) */
    0x75, 0x08, /* REPORT_SIZE (8) */
    0x15, 0x00, /* LOGICAL_MINIMUM (0) */
    0x25, 0xFF, /* LOGICAL_MAXIMUM (255) */
    0x05, 0x07, /* USAGE_PAGE (Keyboard) */
    0x19, 0x00, /* USAGE_MINIMUM (Reserved (no event indicated)) */
    0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
    0x81, 0x00, /* INPUT (Data,Ary,Abs) */
    0xc0        /* END_COLLECTION */
};

3. Implementing the Device

When the device is connected, the device side reports various descriptors to the host; the host then recognizes the device and assigns it an address (device enumeration in the USB protocol basics).

After connection, the device can report data to the host through the usbd_ep_start_write function (an API provided by CherryUSB).

cpp
#include "usbd_core.h"
#include "usbd_hid.h"

#define USBD_VID           0xffff
#define USBD_PID           0xffff
#define USBD_MAX_POWER     100
#define USBD_LANGID_STRING 1033

#define HID_INT_EP          0x81
#define HID_INT_EP_SIZE     8
#define HID_INT_EP_INTERVAL 10
#define USB_HID_CONFIG_DESC_SIZ       34
#define HID_KEYBOARD_REPORT_DESC_SIZE 63

static const uint8_t hid_descriptor[] = {
    USB_DEVICE_DESCRIPTOR_INIT(USB_2_0, 0x00, 0x00, 0x00,
                               USBD_VID, USBD_PID, 0x0002, 0x01),

    USB_CONFIG_DESCRIPTOR_INIT(USB_HID_CONFIG_DESC_SIZ, 0x01, 0x01,
                               USB_CONFIG_BUS_POWERED, USBD_MAX_POWER),

    /************** Descriptor of Joystick Mouse interface ****************/

    /* 09 */
    0x09,                       /* bLength: Interface Descriptor size */
    USB_DESCRIPTOR_TYPE_INTERFACE, /* bDescriptorType: Interface descriptor type */
    0x00,                       /* bInterfaceNumber: Number of Interface */
    0x00,                       /* bAlternateSetting: Alternate setting */
    0x01,                       /* bNumEndpoints */
    0x03,                       /* bInterfaceClass: HID */
    0x01,                       /* bInterfaceSubClass: 1=BOOT, 0=no boot */
    0x01,                       /* nInterfaceProtocol: 0=none, 1=keyboard, 2=mouse */
    0,                          /* iInterface: Index of string descriptor */

    /******************** Descriptor of Joystick Mouse HID ********************/

    /* 18 */
    0x09,                       /* bLength: HID Descriptor size */
    HID_DESCRIPTOR_TYPE_HID,    /* bDescriptorType: HID */
    0x11,                       /* bcdHID: HID Class Spec release number */
    0x01,
    0x00,                       /* bCountryCode: Hardware target country */
    0x01,                       /* bNumDescriptors: Number of HID class descriptors to follow */
    0x22,                       /* bDescriptorType */
    HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength: Total length of Report descriptor */
    0x00,

    /******************** Descriptor of Mouse endpoint ********************/

    /* 27 */
    0x07,                       /* bLength: Endpoint Descriptor size */
    USB_DESCRIPTOR_TYPE_ENDPOINT, /* bDescriptorType */
    HID_INT_EP,                 /* bEndpointAddress: Endpoint Address (IN) */
    0x03,                       /* bmAttributes: Interrupt endpoint */
    HID_INT_EP_SIZE,            /* wMaxPacketSize: 4 Byte max */
    0x00,
    HID_INT_EP_INTERVAL,        /* bInterval: Polling Interval */

    /* 34 */

    ///////////////////////////////////////
    /// string0 descriptor
    ///////////////////////////////////////

    USB_LANGID_INIT(USBD_LANGID_STRING),

    ///////////////////////////////////////
    /// string1 descriptor
    ///////////////////////////////////////

    0x14,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    'C', 0x00,                  /* wcChar0 */
    'h', 0x00,                  /* wcChar1 */
    'e', 0x00,                  /* wcChar2 */
    'r', 0x00,                  /* wcChar3 */
    'r', 0x00,                  /* wcChar4 */
    'y', 0x00,                  /* wcChar5 */
    'U', 0x00,                  /* wcChar6 */
    'S', 0x00,                  /* wcChar7 */
    'B', 0x00,                  /* wcChar8 */

    ///////////////////////////////////////
    /// string2 descriptor
    ///////////////////////////////////////

    0x26,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    'C', 0x00,                  /* wcChar0 */
    'h', 0x00,                  /* wcChar1 */
    'e', 0x00,                  /* wcChar2 */
    'r', 0x00,                  /* wcChar3 */
    'r', 0x00,                  /* wcChar4 */
    'y', 0x00,                  /* wcChar5 */
    'U', 0x00,                  /* wcChar6 */
    'S', 0x00,                  /* wcChar7 */
    'B', 0x00,                  /* wcChar8 */
    ' ', 0x00,                  /* wcChar9 */
    'H', 0x00,                  /* wcChar10 */
    'I', 0x00,                  /* wcChar11 */
    'D', 0x00,                  /* wcChar12 */
    ' ', 0x00,                  /* wcChar13 */
    'D', 0x00,                  /* wcChar14 */
    'E', 0x00,                  /* wcChar15 */
    'M', 0x00,                  /* wcChar16 */
    'O', 0x00,                  /* wcChar17 */

    ///////////////////////////////////////
    /// string3 descriptor
    ///////////////////////////////////////

    0x16,                       /* bLength */
    USB_DESCRIPTOR_TYPE_STRING, /* bDescriptorType */
    '2', 0x00,                  /* wcChar0 */
    '0', 0x00,                  /* wcChar1 */
    '2', 0x00,                  /* wcChar2 */
    '2', 0x00,                  /* wcChar3 */
    '1', 0x00,                  /* wcChar4 */
    '2', 0x00,                  /* wcChar5 */
    '3', 0x00,                  /* wcChar6 */
    '4', 0x00,                  /* wcChar7 */
    '5', 0x00,                  /* wcChar8 */
    '6', 0x00,                  /* wcChar9 */

#ifdef CONFIG_USB_HS
    ///////////////////////////////////////
    /// device qualifier descriptor
    ///////////////////////////////////////

    0x0a,
    USB_DESCRIPTOR_TYPE_DEVICE_QUALIFIER,
    0x00,
    0x02,
    0x00,
    0x00,
    0x00,
    0x40,
    0x01,
    0x00,
#endif

    0x00
};

/* USB HID device Configuration Descriptor */

static uint8_t hid_desc[9] __ALIGN_END = {
    /* 18 */
    0x09,                       /* bLength: HID Descriptor size */
    HID_DESCRIPTOR_TYPE_HID,    /* bDescriptorType: HID */
    0x11,                       /* bcdHID: HID Class Spec release number */
    0x01,
    0x00,                       /* bCountryCode: Hardware target country */
    0x01,                       /* bNumDescriptors: Number of HID class descriptors to follow */
    0x22,                       /* bDescriptorType */
    HID_KEYBOARD_REPORT_DESC_SIZE, /* wItemLength: Total length of Report descriptor */
    0x00,
};

static const uint8_t hid_keyboard_report_desc[HID_KEYBOARD_REPORT_DESC_SIZE] = {
    0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
    0x09, 0x06, /* USAGE (Keyboard) */
    0xa1, 0x01, /* COLLECTION (Application) */
    0x05, 0x07, /* USAGE_PAGE (Keyboard) */
    0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
    0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
    0x15, 0x00, /* LOGICAL_MINIMUM (0) */
    0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
    0x75, 0x01, /* REPORT_SIZE (1) */
    0x95, 0x08, /* REPORT_COUNT (8) */
    0x81, 0x02, /* INPUT (Data,Var,Abs) */
    0x95, 0x01, /* REPORT_COUNT (1) */
    0x75, 0x08, /* REPORT_SIZE (8) */
    0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
    0x95, 0x05, /* REPORT_COUNT (5) */
    0x75, 0x01, /* REPORT_SIZE (1) */
    0x05, 0x08, /* USAGE_PAGE (LEDs) */
    0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
    0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
    0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
    0x95, 0x01, /* REPORT_COUNT (1) */
    0x75, 0x03, /* REPORT_SIZE (3) */
    0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
    0x95, 0x06, /* REPORT_COUNT (6) */
    0x75, 0x08, /* REPORT_SIZE (8) */
    0x15, 0x00, /* LOGICAL_MINIMUM (0) */
    0x25, 0xFF, /* LOGICAL_MAXIMUM (255) */
    0x05, 0x07, /* USAGE_PAGE (Keyboard) */
    0x19, 0x00, /* USAGE_MINIMUM (Reserved (no event indicated)) */
    0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
    0x81, 0x00, /* INPUT (Data,Ary,Abs) */
    0xc0        /* END_COLLECTION */
};

void usbd_configure_done_callback(void)
{
    /* no out ep, do nothing */
}

#define HID_STATE_IDLE 0
#define HID_STATE_BUSY 1

/*!< hid state ! Data can be sent only when state is idle */

static volatile uint8_t hid_state = HID_STATE_IDLE;

void usbd_hid_int_callback(uint8_t ep, uint32_t nbytes)
{
    hid_state = HID_STATE_IDLE;
}

static struct usbd_endpoint hid_in_ep = {
    .ep_cb = usbd_hid_int_callback,
    .ep_addr = HID_INT_EP
};

struct usbd_interface intf0;

void hid_keyboard_init(void)
{
    usbd_desc_register(hid_descriptor);
    usbd_add_interface(usbd_hid_init_intf(&intf0, hid_keyboard_report_desc,
                                          HID_KEYBOARD_REPORT_DESC_SIZE));
    usbd_add_endpoint(&hid_in_ep);
    usbd_initialize();
}

void hid_keyboard_test(void)
{
    uint8_t sendbuffer[8] = {0x00, 0x00, HID_KBD_USAGE_A, 0x00,
                             0x00, 0x00, 0x00, 0x00};
    /* A */
    bflb_l1c_dcache_clean_range(sendbuffer, 8);

    int ret = usbd_ep_start_write(HID_INT_EP, sendbuffer, 8);
    if (ret < 0) {
        return;
    }
    hid_state = HID_STATE_BUSY;

    while (hid_state == HID_STATE_BUSY) {
    }
}

Call it in the main function, as shown below, to implement a USB keyboard device that continuously sends the letter 'a' to the host.

cpp
#include <FreeRTOS.h>
#include "task.h"
#include "usbh_core.h"
#include "bflb_mtimer.h"
#include "board.h"

#define KEYBOARD_STACK_SIZE (1536)
#define KEYBOARD_TASK_PRIORITY (16)

extern void hid_keyboard_init(void);
extern void hid_keyboard_test(void);

static TaskHandle_t keyboard_task_hd;

void keyboard_task(void *params)
{
    hid_keyboard_init();
    while (1) {
        hid_keyboard_test();
        bflb_mtimer_delay_ms(500);
    }
}

int main(void)
{
    board_init();
    xTaskCreate(keyboard_task, "keyboard", KEYBOARD_STACK_SIZE,
                NULL, KEYBOARD_TASK_PRIORITY, &keyboard_task_hd);
    vTaskStartScheduler();
    while (1) {
    }
}

To correspond to a real keyboard, a matrix keyboard is connected to the board to read the matrix key values, and the corresponding values are converted into HID reports sent to the computer.

The hardware wiring is as follows:

html
// definitions of the matrix keyboard row and column lines
const uint8_t rows[] = { GPIO_PIN_33, GPIO_PIN_32, GPIO_PIN_31, GPIO_PIN_30};
const uint8_t cols[] = { GPIO_PIN_29, GPIO_PIN_27, GPIO_PIN_25};

In the matrix keyboard scanning task, scan the matrix keyboard in a loop; if a key is pressed, convert it directly to the corresponding HID key value:

html
void matrix_keys_task(void *params)
{
    int key_val = 0;
    matrix_keys_init();
    while(1)
    {
        key_val = get_key_val();
        if(key_val != 0)
        {
            printf("%x\r\n", key_val);
            switch(key_val)
            {
                case 0x0001:
                    key_val = 0x1e; // 1
                    break;
                case 0x0002:
                    key_val = 0x1f; // 2
                    break;
                case 0x0004:
                    key_val = 0x20; // 3
                    break;
                case 0x0008:
                    key_val = 0x21; // 4
                    break;
                case 0x0010:
                    key_val = 0x22; // 5
                    break;
                case 0x0020:
                    key_val = 0x23; // 6
                    break;
                case 0x0040:
                    key_val = 0x24; // 7
                    break;
                case 0x0080:
                    key_val = 0x25; // 8
                    break;
                case 0x0100:
                    key_val = 0x26; // 9
                    break;
                case 0x0200:
                    key_val = HID_KBD_USAGE_A;   // A
                    break;
                case 0x0400:
                    key_val = 0x27; //0
                    break;
                case 0x0800:
                    key_val = HID_KBD_USAGE_A + 1; // B
                    break;
            }

            if (xQueueSend(xQueue, &key_val, portMAX_DELAY) != pdPASS)
            {
            }
        }
        bflb_mtimer_delay_ms(10);
    }
}

The definitions of these key values can be found here:

In the USB keyboard task, receive the messages and send them to the PC.

html
void keyboard_task(void *params)
{
    hid_keyboard_init();
    uint16_t receivedData = 0;
    while(1)
    {
        if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS)
        {
            printf("receivedData : %x\r\n", receivedData);
            // process the received data
            hid_keyboard_test();
		}
        bflb_mtimer_delay_ms(500);
    }
}

4. Viewing USB Descriptors

On Windows, you can use usbview to view USB descriptor information, which helps with debugging and analysis.

5. USB Protocol Debugging Tools

The USB protocol is relatively complex; any wrongly written descriptor can prevent the device from working properly. Good debugging tools are therefore very important.

USBlyzer and WireShark can both be used to capture USB packets for analysis.

Have questions?

For other questions, please visit the unified discussion area: Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23