前言

概述

本文档描述了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”。