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.
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:
Let's look at each of them one by one.
2.1 Device Descriptor
CherryUSB provides macros to define the device descriptor:
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:
| Field | Description |
|---|---|
| bLength | Indicates the length of this descriptor. The device descriptor is 18 bytes long, which is 0x12 in hexadecimal. |
| bDescriptorType | The type of the descriptor. The specific values are listed in Table 355. The device descriptor type code is 0x01. |
| bcdUSB | The 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. |
| bDeviceClass | The 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. |
| bDeviceSubClass | The 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. |
| bDeviceProtocol | The 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. |
| bMaxPacketSize0 | The maximum packet size of endpoint 0. It can be 8, 16, 32 or 64. |
| idVendor | The 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. |
| idProduct | The product ID. Unlike the vendor ID, it is assigned by the manufacturer freely according to the product. |
| bcdDevice | The device version number. After a product is upgraded (e.g., firmware changes adding features), the version number can be changed to distinguish it. |
| iManufacturer | The 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. |
| iProduct | The 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. |
| iSerialNumber | The 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. |
| bNumConfigurations | Indicates 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:
USB_CONFIG_DESCRIPTOR_INIT(USB_HID_CONFIG_DESC_SIZ,
0x01,
0x01,
USB_CONFIG_BUS_POWERED,
USBD_MAX_POWER),| Field | Description |
|---|---|
| bLength | Indicates the length of this descriptor. A standard USB configuration descriptor is 9 bytes long. |
| bDescriptorType | Indicates the type of the descriptor. The configuration descriptor type code is 0x02. |
| wTotalLength | Indicates 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. |
| bNumInterfaces | Indicates 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). |
| bConfiguration | Indicates 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. |
| iConfiguration | 1 byte, the index of the string descriptor describing this configuration. If 0, there is no string. |
| bmAttributes | 1 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. |
| bMaxPower | 1 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:
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 */| Field | Description |
|---|---|
| bLength | 1 byte, the length of this descriptor. A standard USB interface descriptor is 9 bytes long. |
| bDescriptorType | 1 byte, the type of the descriptor. The interface descriptor type code is 0x04. |
| bInterfaceNumber | 1 byte, the number of this interface. When a configuration has multiple interfaces, each interface has a different number, starting from 0 and increasing. |
| bAlternateSetting | 1 byte, the alternate setting of this interface. Numbering follows the same rule as bInterfaceNumber; this field is rarely used and set to 0. |
| bNumEndpoints | 1 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, bInterfaceProtocol | The 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. |
| iConfiguration | 1 byte, the index of the string descriptor describing this interface. If 0, there is no string. |
2.4 Endpoint Descriptor
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| Field | Description |
|---|---|
| bLength | 1 byte, the length of this descriptor. A standard USB endpoint descriptor is 5 bytes long. |
| bDescriptorType | 1 byte, the type of the descriptor. The endpoint descriptor type code is 0x05. |
| bEndpointAddress | 1 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. |
| bmAttributes | 1 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. |
| wMaxPacketSize | 1 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. |
| bInterval | 1 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
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,| Field | Description |
|---|---|
| bLength | 1 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. |
| bDescriptorType | 1 byte, the type code of this descriptor. The HID descriptor type code is 0x21. |
| bcdHID | 2 bytes, the version number of the HID protocol used by the device. |
| bCountryCode | 1 byte, the country the device applies to. Usually our keyboards are US keyboards, code 33, i.e., 0x21. |
| bNumDescriptors | 1 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. |
| bDescriptorType | 1 byte, the type of the subordinate descriptor. The report descriptor type is 0x22 and the physical descriptor type is 0x23. |
| bDescriptorLength | 2 bytes, the length of the subordinate descriptor. When there are multiple subordinate descriptors, bDescriptorType and bDescriptorLength repeat alternately. |
2.6 HID Report Descriptor
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).
#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.
#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:
// 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:
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.
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

