前言¶
概述
本文档描述了BS2X USB HID复合设备的两种实现方式。
读者对象
本文档主要适用于以下工程师:
产品软件开发工程师
技术支持工程师
符号约定
在本文中可能出现下列标志,它们所代表的含义如下。
符号 |
说明 |
|---|---|
|
表示如不避免则将会导致死亡或严重伤害的具有高等级风险的危害。 |
|
表示如不避免则可能导致死亡或严重伤害的具有中等级风险的危害。 |
|
表示如不避免则可能导致轻微或中度伤害的具有低等级风险的危害。 |
|
用于传递设备或环境安全警示信息。如不避免则可能会导致设备损坏、数据丢失、设备性能降低或其它不可预知的结果。 “须知”不涉及人身伤害。 |
|
对正文中重点信息的补充说明。 “说明”不是安全警示信息,不涉及人身、设备及环境伤害信息。 |
修改记录
文档版本 |
发布日期 |
修改说明 |
|---|---|---|
02 |
2024-08-20 |
更新“实现方式”章节内容。 |
01 |
2024-05-15 |
第一次正式版本发布。 |
00B01 |
2024-03-08 |
第一次临时版本发布。 |
概述¶
USB设备的功能是由接口来承载的,对应到代码即是接口描述符,一般一个接口就是一个功能,这个功能可能是鼠标、键盘、手柄等,也可能是自定义功能。HID复合设备是一类多功能设备,例如鼠标+键盘、手柄+键盘等。
实现方式¶
添加任何类型的HID设备都需要调用接口hid_add_report_descriptor。“hid_add_report_descriptor”定义在“f_hid.c”,其参数及功能如下:
参数 |
说明 |
|---|---|
report_desc |
报告描述符的首地址。 |
report_desc_len |
报告描述符的长度。 |
protocol |
报告描述符的协议类型(0=none, 1=keyboard, 2=mouse),其对应接口描述符的bInterfaceProtocol字段。 |
函数功能:添加报告描述符并和接口关联,会自动创建接口描述符、hid描述符和端点描述符。
函数返回值:设备功能对应的id号。
一个接口实现多个功能¶
HID设备一个接口对应一个报告描述符,由报告描述符来描述接口的功能,而报告描述符可通过report_id项来区分同一个报告描述符的不同功能。
报告描述符1如下:
// 键盘设备
usage_page(1), 0x01,
usage(1), 0x06,
collection(1), 0x01,
report_id(1), 0x01,
usage_page(1), 0x07,
usage_minimum(1), 0xE0,
usage_maximum(1), 0xE7,
logical_minimum(1), 0x00,
logical_maximum(1), 0x01,
report_size(1), 0x01,
report_count(1), 0x08,
input(1), 0x02,
report_count(1), 0x01,
report_size(1), 0x08,
input(1), 0x01,
report_count(1), 0x05,
report_size(1), 0x01,
usage_page(1), 0x08,
usage_minimum(1), 0x01,
usage_maximum(1), 0x05,
output(1), 0x02,
report_count(1), 0x01,
report_size(1), 0x03,
output(1), 0x01,
report_count(1), 0x06,
report_size(1), 0x08,
logical_minimum(1), 0x00,
logical_maximum(1), 0x65,
usage_page(1), 0x07,
usage_minimum(1), 0x00,
usage_maximum(1), 0x65,
input(1), 0x00,
end_collection(0),
// 自定义数据接收
usage_page(2), 0xB1, 0xFF,
usage(1), 0x1,
collection(1), 0x01,
report_id(1), 0x08,
collection(1), 0x00,
report_count(1), 0xc,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
output(1), 2,
end_collection(0),
end_collection(0),
// 自定义数据收发
usage_page(2), 0xB2, 0xFF,
usage(1), 0x1,
collection(1), 0x01,
report_id(1), 0x09,
collection(1), 0x00,
report_count(1), 0x3f,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
output(1), 2,
usage(1), 0x2,
report_count(1), 0x3f,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
input(1), 0,
end_collection(0),
end_collection(0),
上述报告描述符定义了一个键盘设备+自定义数据接收+自定义数据收发三个功能的复合设备,他们通过report_id+数据的方式来区分功能,即只有一个功能的报告描述符,在发送数据时直接发送数据即可,拥有多个功能的报告描述符在发送数据时需要在数据的头部加上report_id。
多个接口实现多个功能¶
HID设备一个接口对应一个报告描述符,由报告描述符来描述接口的功能,那么多个接口多个报告描述符也可以实现复合设备功能,BS2X芯片除了端点0之外,一共有三个IN端点,三个OUT端点,因此最多支持三个接口。
多个接口多个报告描述符的示例如下。
报告描述符1如下:
usage_page(1), 0x01,
usage(1), 0x06,
collection(1), 0x01,
report_id(1), 0x01,
usage_page(1), 0x07,
usage_minimum(1), 0xE0,
usage_maximum(1), 0xE7,
logical_minimum(1), 0x00,
logical_maximum(1), 0x01,
report_size(1), 0x01,
report_count(1), 0x08,
input(1), 0x02,
report_count(1), 0x01,
report_size(1), 0x08,
input(1), 0x01,
report_count(1), 0x05,
report_size(1), 0x01,
usage_page(1), 0x08,
usage_minimum(1), 0x01,
usage_maximum(1), 0x05,
output(1), 0x02,
report_count(1), 0x01,
report_size(1), 0x03,
output(1), 0x01,
report_count(1), 0x06,
report_size(1), 0x08,
logical_minimum(1), 0x00,
logical_maximum(1), 0x65,
usage_page(1), 0x07,
usage_minimum(1), 0x00,
usage_maximum(1), 0x65,
input(1), 0x00,
end_collection(0),
// 自定义数据接收
usage_page(2), 0xB1, 0xFF,
usage(1), 0x1,
collection(1), 0x01,
report_id(1), 0x08,
collection(1), 0x00,
report_count(1), 0xc,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
output(1), 2,
end_collection(0),
end_collection(0),
// 自定义数据收发
usage_page(2), 0xB2, 0xFF,
usage(1), 0x1,
collection(1), 0x01,
report_id(1), 0x09,
collection(1), 0x00,
report_count(1), 0x3f,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
output(1), 2,
usage(1), 0x2,
report_count(1), 0x3f,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
input(1), 0,
end_collection(0),
end_collection(0),
上述报告描述符定义了一个键盘设备+自定义数据接收+自定义数据收发三个功能的复合设备,他们通过report_id+数据的方式来区分功能,即只有一个功能的报告描述符,在发送数据时直接发送数据即可,拥有多个功能的报告描述符在发送数据时需要在数据的头部加上“report_id”。此描述符可通过“hid_add_report_descriptor”函数注册到接口0。
报告描述符2如下:
usage_page(1), 0x01,
usage(1), 0x02,
collection(1), 0x01,
usage(1), 0x01,
report_count(1), 0x03,
report_size(1), 0x01,
usage_page(1), 0x09,
usage_minimum(1), 0x1,
usage_maximum(1), 0x3,
logical_minimum(1), 0x00,
logical_maximum(1), 0x01,
input(1), 0x02,
report_count(1), 0x01,
report_size(1), 0x05,
input(1), 0x01,
usage_page(1), 0x01,
usage(1), 0x38,
report_count(1), 0x01,
report_size(1), 0x08,
logical_minimum(1), 0x81,
logical_maximum(1), 0x7f,
input(1), 0x06,
usage(1), 0x30,
usage(1), 0x31,
report_count(1), 0x02,
report_size(1), 0x10,
logical_minimum(2), 0x01, 0x80,
logical_maximum(2), 0xff, 0x7f,
input(1), 0x06,
end_collection(0)
此描述符描述了鼠标功能。通过再一次调用“hid_add_report_descriptor”函数可将此描述符注册到接口1,会自动创建接口描述符、hid描述符和端点描述符,不需要改“f_hid.c”。
此时已实现两个接口的复合设备,其中接口0实现的是键盘设备+自定义数据接收+自定义数据收发三个功能,接口1实现的是鼠标功能。示例代码可查看“application/samples/products/usb_mouse/mouse_usb/usb_init_app.c”。
如果需要更深程度的自定义,比如需要添加out端点/需要添加空的interface,需要在kconfig里开启HID Custom(如下图示):

开启后会编译f_hid_custom.c而不是f_hid.c, 此时需要自行修改f_hid_custom.c里的f_hid_desc_array数组来自定义config描述符(只需要改config描述符,端点初始化之类的操作都会自动完成)。注意: 自定义了几个interface就应该调用几次hid_add_report_descriptor函数,如果是空的interface,可以调用hid_add_report_descriptor并传入NULL。
例:实现4个interface,第一个interface使用1个in端点,第二个interface是个空interface,后两个interface都用2个端点,1in 1out。
若要实现4个interface需要在Kconfig更改最大报告描述符数量为4,并使能custom HID 如下图示,

然后修改配置描述符如下,4个interface需要在应用层调用4次hid_add_report_descriptor来将报告描述符注册到对应的interface上,第二个是空interface,故在第二次调用hid_add_report_descriptor时传入NULL即可,其他三次则需要传入对应功能的报告描述符。
static struct usb_cfgdesc_s g_fhid_config_desc =
{
.len = sizeof(struct usb_cfgdesc_s),
.type = USB_DESC_TYPE_CONFIG,
HSETW(.totallen, 0), /* Size of all descriptors, set later */
.ninterfaces = 0x1, /* Number of Interfaces */
.cfgvalue = 0x1, /* ID of this configuration */
.icfg = 0x0, /* Index of string descriptor */
.attr = 0xa0, /* Bus-powered and remote wakeup */
.mxpower = 0x32 /* Maximum power consumption from the bus */
};
static struct usb_ifdesc_s g_fhid_intf_desc =
{
.len = sizeof(struct usb_ifdesc_s),
.type = USB_DESC_TYPE_INTERFACE,
.ifno = 0, /* Index number of this interface */
.alt = 0, /* Index of this settings */
.neps = 1, /* Number of endpoint */
.classid = 0x03, /* bInterfaceClass: HID */
.subclass = 1, /* bInterfaceSubClass : 1=BOOT, 0=no boot */
.protocol = 0, /* bInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
.iif = 0 /* Index of string descriptor */
};
static struct usb_hid_desc g_fhid_desc =
{
.bLength = sizeof(struct usb_hid_desc),
.bDescriptorType = USB_DESC_TYPE_HID, /* HID type is 0x21 */
HSETW(.bcdHID, 0x0110), /* bcdHID: HID Class Spec release number HID 1.1 */
.bCountryCode = 0x00, /* bCountryCode: Hardware target country */
.bNumDescriptors = 0x01, /* bNumDescriptors: Number of HID class descriptors to follow */
{
{
.bDescriptorType = 0x22, /* bDescriptorType */
}
}
};
static struct usb_epdesc_s g_fhid_in_ep_desc =
{
.len = sizeof(struct usb_epdesc_s),
.type = USB_DESC_TYPE_ENDPOINT,
.addr = USB_DIR_IN | 0x01,
.attr = 0x03, /* bmAttributes = 00000011b */
HSETW(.mxpacketsize, HID_IN_DATA_SIZE), /* wMaxPacketSize = 64 */
.interval = 1 /* bInterval = 125us */
};
static struct usb_ifdesc_s g_fhid_intf_desc_null =
{
.len = sizeof(struct usb_ifdesc_s),
.type = USB_DESC_TYPE_INTERFACE,
.ifno = 0, /* Index number of this interface */
.alt = 0, /* Index of this settings */
.neps = 0, /* Number of endpoint */
.classid = 0, /* bInterfaceClass: HID */
.subclass = 0, /* bInterfaceSubClass : 1=BOOT, 0=no boot */
.protocol = 0, /* bInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
.iif = 0 /* Index of string descriptor */
};
static struct usb_ifdesc_s g_fhid_intf2_desc =
{
.len = sizeof(struct usb_ifdesc_s),
.type = USB_DESC_TYPE_INTERFACE,
.ifno = 0, /* Index number of this interface */
.alt = 0, /* Index of this settings */
.neps = 2, /* Number of endpoint */
.classid = 0x03, /* bInterfaceClass: HID */
.subclass = 1, /* bInterfaceSubClass : 1=BOOT, 0=no boot */
.protocol = 0, /* bInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
.iif = 0 /* Index of string descriptor */
};
static struct usb_hid_desc g_fhid2_desc =
{
.bLength = sizeof(struct usb_hid_desc),
.bDescriptorType = USB_DESC_TYPE_HID, /* HID type is 0x21 */
HSETW(.bcdHID, 0x0110), /* bcdHID: HID Class Spec release number HID 1.1 */
.bCountryCode = 0x00, /* bCountryCode: Hardware target country */
.bNumDescriptors = 0x01, /* bNumDescriptors: Number of HID class descriptors to follow */
{
{
.bDescriptorType = 0x22, /* bDescriptorType */
}
}
};
static struct usb_epdesc_s g_fhid_in_ep2_desc =
{
.len = sizeof(struct usb_epdesc_s),
.type = USB_DESC_TYPE_ENDPOINT,
.addr = USB_DIR_IN | 0x02,
.attr = 0x03, /* bmAttributes = 00000011b */
HSETW(.mxpacketsize, HID_IN_DATA_SIZE), /* wMaxPacketSize = 64 */
.interval = 1 /* bInterval = 125us */
};
static struct usb_epdesc_s g_fhid_out_ep_desc =
{
.len = sizeof(struct usb_epdesc_s),
.type = USB_DESC_TYPE_ENDPOINT,
.addr = USB_DIR_OUT | 0x01,
.attr = 0x03, /* bmAttributes = 00000011b */
HSETW(.mxpacketsize, HID_OUT_DATA_SIZE), /* wMaxPacketSize */
.interval = 1 /* bInterval = 125us */
};
static struct usb_ifdesc_s g_fhid_intf3_desc =
{
.len = sizeof(struct usb_ifdesc_s),
.type = USB_DESC_TYPE_INTERFACE,
.ifno = 0, /* Index number of this interface */
.alt = 0, /* Index of this settings */
.neps = 2, /* Number of endpoint */
.classid = 0x03, /* bInterfaceClass: HID */
.subclass = 1, /* bInterfaceSubClass : 1=BOOT, 0=no boot */
.protocol = 0, /* bInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
.iif = 0 /* Index of string descriptor */
};
static struct usb_hid_desc g_fhid3_desc =
{
.bLength = sizeof(struct usb_hid_desc),
.bDescriptorType = USB_DESC_TYPE_HID, /* HID type is 0x21 */
HSETW(.bcdHID, 0x0110), /* bcdHID: HID Class Spec release number HID 1.1 */
.bCountryCode = 0x00, /* bCountryCode: Hardware target country */
.bNumDescriptors = 0x01, /* bNumDescriptors: Number of HID class descriptors to follow */
{
{
.bDescriptorType = 0x22, /* bDescriptorType */
}
}
};
static struct usb_epdesc_s g_fhid_in_ep3_desc =
{
.len = sizeof(struct usb_epdesc_s),
.type = USB_DESC_TYPE_ENDPOINT,
.addr = USB_DIR_IN | 0x03,
.attr = 0x03, /* bmAttributes = 00000011b */
HSETW(.mxpacketsize, HID_IN_DATA_SIZE), /* wMaxPacketSize = 64 */
.interval = 1 /* bInterval = 125us */
};
static struct usb_epdesc_s g_fhid_out_ep2_desc =
{
.len = sizeof(struct usb_epdesc_s),
.type = USB_DESC_TYPE_ENDPOINT,
.addr = USB_DIR_OUT | 0x02,
.attr = 0x03, /* bmAttributes = 00000011b */
HSETW(.mxpacketsize, HID_OUT_DATA_SIZE), /* wMaxPacketSize */
.interval = 1 /* bInterval = 125us */
};
/* fhid desc array includes:
* 1. config_desc (for all report map)
* 2. iface_desc、hid_desc、in ep_desc (report map 0)
* 3. iface_desc、hid_desc、in ep_desc (report map 1)
* ...
* n+1. iface_desc、hid_desc、in ep_desc (report map n-1)
*
* g_fhid_desc_array is like this:
*
* config_desc iface_desc(0) hid_desc(0) ep_desc(0) [ep_desc(0)] iface_desc(1) hid_desc(1) ep_desc(1) [ep_desc(1)].. NULL
* | | | |
* |_ _ _ _ _ _ report map 0 _ _ _ _ _ _ | |_ _ _ _ _ _ report map 1 _ _ _ _ _ _ |
*
* Total Length : 2 + (4) * n
*/
#define HID_SINGLE_PROTOCOL_DESC_NUM 4
#define HID_DESC_ARRAY_MAX_NUM (1 + (HID_SINGLE_PROTOCOL_DESC_NUM) * HID_REPORT_MAP_NUM)
static const uint8_t *g_fhid_desc_array[HID_DESC_ARRAY_MAX_NUM] =
{
(const uint8_t *)&g_fhid_config_desc,
(const uint8_t *)&g_fhid_intf_desc,
(const uint8_t *)&g_fhid_desc,
(const uint8_t *)&g_fhid_in_ep_desc,
(const uint8_t *)&g_fhid_intf_desc_null,
(const uint8_t *)&g_fhid_intf2_desc,
(const uint8_t *)&g_fhid2_desc,
(const uint8_t *)&g_fhid_in_ep2_desc,
(const uint8_t *)&g_fhid_out_ep_desc,
(const uint8_t *)&g_fhid_intf3_desc,
(const uint8_t *)&g_fhid3_desc,
(const uint8_t *)&g_fhid_in_ep3_desc,
(const uint8_t *)&g_fhid_out_ep2_desc,
NULL,
};
实现虚拟串口功能¶
虚拟串口设备类型是DEV_SERIAL,但通常情况下,我们使用的是HID+ACM的符合设备,即类型DEV_SER_HID。因为ACM设备并非HID设备,所以报告描述符并不需要进行修改,可以使用2.1和2.2中的报告描述符传入
hid_add_report_descriptor,再调用usbd_set_device_info(DEV_SER_HID, &str_manufacturer, &str_product, &str_serial_number, dev_id)和usb_init(DEVICE, DEV_SER_HID)即可枚举HID设备和ACM串口。
ACM串口需要新建一个线程进行数据接收,若单板端写数据发送到USB主机,则直接调用write接口;若单板端读取从USB主机发来的数据,open串口后先调用ioctl接口,然后再调用read接口,ioctl用法如下:
usb_serial_ioctl(0, CONSOLE_CMD_RD_BLOCK_SERIAL, 1);
创建一个大小4096字节的buffer来存储读取的值(虚拟串口最大支持读写4096字节数据)
usb_serial_read(0, g_usb_serial_recv_data, SERIAL_RECV_DATA_MAX_LEN);usb_serial_read的返回值是读取到的数据长度。
usb_serial_write(0, g_usb_serial_recv_data, recv_len);
示例代码可查看“application/samples/products/sle_dongle/sle_dongle_hid_serial.c”。
实现升级功能(DFU)¶
DFU功能依赖于Burntool工具,建议下载和使用尽可能新版本的Burntool。
Burntool工具可以对Usage Page设置为0xFFB1的设备进行DFU升级,如下文描述符:
usage_page(2), 0xB1, 0xFF,
usage(1), 0x1,
collection(1), 0x01,
report_id(1), 0x08,
collection(1), 0x00,
report_count(1), 0xc,
report_size(1), 0x8,
usage_minimum(1), 0x0,
usage_maximum(1), 0xFF,
output(1), 2,
end_collection(0),
end_collection(0),
在hid接收数据的线程中添加处理Burntool发送的升级消息的分支,当有升级包通过Burntool发送时,Burntool会下发frame_type为0x1e的消息:
if (command.frame_type == 0x1e) {
osal_printk("start dfu\n");
usb_deinit();
osal_msleep(USB_DEINIT_DELAY);
usb_dfu_init();
usb_dfu_wait_ugrade_done_and_reset();
break;
}
middleware/utils/usb_class/f_dfu.c中定义了名为usb_dfu_download_callback的弱函数,在开始升级时会调用该函数,需要用户自己将该函数实现以完成升级流程,具体实现可参考“application/samples/products/sle_dongle/sle_dongle_hid_serial.c”。
实现音频数据接口(UAC)¶
音频数据接口设备类型是DEV_UAC,初始化完成后需要调用uac_wait_host接口等待主机发送识别到UAC设备的信号,通常入参传入1表示WAIT_HOST_FOREVER,即在声音设置->输入中选择该设备,才能完成UAC的初始化。UAC1.0最高支持双声道192Khz 16b的音源,而LiteOS提供的UAC驱动为16Khz 16b,将想要发送的16bit音频数据填充到buffer中,使用vdt_usb_uac_send_data(buffer, buffer_len)接口发送,具体实现可参考“application/samples/products/usb_amic_vdt/vdt_usb/vdt_usb.c”。




