【星闪】Hi2821 | USB HID设备类 + HID键盘例程
1. 简介
USB(Universal Serial Bus),全称通用串行总线,是一个外部总线标准,规范电脑与外部设备的连接和通讯。USB接口具有热插拔功能。USB 接口可枚举成多种外设,如鼠标、键盘等。USB 是在 1994 年底由英特尔等多家公司联合;并在 1996 年推出后,现已成为当今电脑与大量智能设备的必配接口。USB 版本经历了多年的发展,到如今已经发展为 USB4 版本。
关于 USB 协议更详细的讲解在之前的文章——从零开始学GD32单片机 | USB通用串行总线接口 + HID键盘例程有包含,感兴趣的可移步阅读。
2. 例程
hi2821 内部包含一个 USB 2.0 外设,包含 PHY 控制器和 Host 控制器,例程中将基于它来实现一个 HID 键盘例程。
2.1 Kconfig

USB 的 Kconfig 配置项非常多,但我们只需注意以下几项即可:
- Enable USB Controller:使能 USB 控制器;
- Enable USB2.0 Device Controller:使能 USB 2.0 设备控制器;
- Enable USB3.0 Device Controller:使能 USB 3.0 设备控制器,Hi2821 不支持;
- Slave Core Extra Board Configuration:从设备板级配置,里面主要配置输出(主机下发)和输入(从机上报)的端点(endpoint)数,正常来说 HID 键盘只需要 1 个输入端点和 1 个输出端点,SDK 默认是各 3 个;
- Enable USB Gadget Support:USB 设备支持,里面主要配置 USB 设备,打开所有 HID 的编译选项即可;HID Interface 配置里面的 HID Report Map Num 用于配置 HID 描述符的数量,一般来说只需要用 1 个,默认 3 个;下面 Use custom HID 可以使能自定义 HID 设备的支持。
- Enable HID Output Report Transfer:使能输出报告传输;
- select HID output report function:输出报告函数类型;选项一表示通过事件栈报告,即用户要通过轮询(polling)的方式获取报告内容;选项二表示通过回调报告,即用户注册回调函数,报告到达时调用回调获取报告内容;
2.2 代码
#include <stdbool.h>#include "soc_osal.h"
#include "gadget/f_hid.h"
#include "usb_init_keyboard_app.h"#define USB_INIT_APP_MANUFACTURER { 'H', 0, 'i', 0, 's', 0, 'i', 0, 'l', 0, 'i', 0, 'c', 0, 'o', 0, 'n', 0 }
#define USB_INIT_APP_PRODUCT { 'M', 0, 'y', 0, 'K', 0, 'e', 0, 'y', 0, 'b', 0, 'o', 0, 'a', 0, 'r', 0, 'd', 0 }
#define USB_INIT_APP_SERIAL { '2', 0, '0', 0, '2', 0, '0', 0, '0', 0, '6', 0, '2', 0, '4', 0 }static bool g_usb_inited = false;
static uint8_t g_usb_report_index = 0;static const uint8_t g_report_desc_hid[] = {0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */0x09, 0x06, /* USAGE (Keyboard) */0xa1, 0x01, /* COLLECTION (Application) */0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */0x15, 0x00, /* LOGICAL_MINIMUM (0) */0x25, 0x01, /* LOGICAL_MAXIMUM (1) */0x95, 0x08, /* REPORT_COUNT (8) */0x75, 0x01, /* REPORT_SIZE (1) */0x81, 0x02, /* INPUT (Data,Var,Abs) */0x95, 0x01, /* REPORT_COUNT (1) */0x75, 0x08, /* REPORT_SIZE (8) */0x81, 0x03, /* INPUT (Cnst,Var,Abs) */0x95, 0x06, /* REPORT_COUNT (6) */0x75, 0x08, /* REPORT_SIZE (8) */0x15, 0x00, /* LOGICAL_MINIMUM (0) */0x26, 0xFF, 0x00, /* LOGICAL_MAXIMUM (255) */0x05, 0x07, /* USAGE_PAGE (Keyboard/Keypad) */0x19, 0x00, /* USAGE_MINIMUM (Reserved (no event indicated)) */0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */0x81, 0x00, /* INPUT (Data,Ary,Abs) */0xc0 /* END_COLLECTION */
};int usb_keyboard_init(void)
{if (g_usb_inited) {return -1;}const char manufacturer[] = USB_INIT_APP_MANUFACTURER;struct device_string str_manufacturer = {.str = manufacturer,.len = sizeof(manufacturer)};const char product[] = USB_INIT_APP_PRODUCT;struct device_string str_product = {.str = product,.len = sizeof(product)};const char serial[] = USB_INIT_APP_SERIAL;struct device_string str_serial_number = {.str = serial,.len = sizeof(serial)};struct device_id dev_id = {.vendor_id = 0x1111,.product_id = 0x0009,.release_num = 0x0800};g_usb_report_index = hid_add_report_descriptor(g_report_desc_hid, sizeof(g_report_desc_hid), 0);osal_printk("usb report index: %d\r\n", g_usb_report_index);if (usbd_set_device_info(DEV_HID, &str_manufacturer, &str_product, &str_serial_number, dev_id) != 0) {return -1;}if (usb_init(DEVICE, DEV_HID) != 0) {return -1;}g_usb_inited = true;return g_usb_report_index;
}int usb_keyboard_deinit(void)
{if (g_usb_inited == false) {return 0;}(void)usb_deinit();g_usb_inited = false;return 0;
}int usb_keyboard_send_data(const void* data, uint32_t len)
{if (!g_usb_inited) {return -1;}return fhid_send_data(g_usb_report_index, (const char*) data, len);
}
SDK 将 USB HID 部分的代码封装得相当好了,所以只需要调用几个函数就能跑起来。
1. 初始化。
调用 hid_add_report_descriptor 函数添加 HID 报告描述符,它的内容可参考 USB 官方的文档——Device Class Definition for HID 1.11。
函数会返回一个 HID 报告句柄,需要保存起来,后面的通讯需要用到。
调用 usbd_set_device_info 函数去设置设备信息,参数一为设备类,这里选 DEV_HID 其他可选如下:
typedef enum device_type {DEV_START, /* start value of the device type */DEV_SERIAL, /* used for serial */DEV_ETHERNET, /* used for rndis */DEV_SER_ETH, /* used for serial and rndis */DEV_DFU, /* used for DFU */DEV_MASS, /* used for mass */DEV_UVC, /* used for USB video */DEV_UAC, /* used for USB audio */DEV_CAMERA, /* used for USB camera */DEV_HID, /* used for USB hid */DEV_CUSTOM, /* used for USB custom */DEV_UAC_HID, /* used for USB uac and hid */DEV_SER_HID, /* used for USB serial and hid */DEV_END /* end value of the device type */
} device_type;
参数二为厂商名,参数三为产品名,参数四为序列号,这三个参数都要用 device_string 结构体封装,需要注意的是因为 USB 的字符串使用 Unicode 格式,一个字符占 2 个字节,所以每个字符之间都要使用“0”来隔开。
参数五为设备 ID,通过 device_id 结构体定义,如下:
struct device_id
{uint16_t vendor_id; /* Vendor id */uint16_t product_id; /* Product id */uint16_t release_num; /* Device release number */
};
如果不是产品开发,里面的内容随便填也没问题。
最后调用 usb_init 即可使能 USB 功能,如果 USB 插入,驱动会自动与主机握手并通信。
2. 发送数据。
调用 fhid_send_data 函数向主机发送数据,参数一为 HID 报告句柄,参数二为数据数组,参数三为数据长度;返回值为发送的数据长度。
发送的数据根据 HID 的报告描述符的内容而定,对于标准的 HID 键盘数据长度应为 8 字节,格式如下:

其中 Modifier keys 的格式如下:

3. 主函数。
主函数的内容可以参考前面文章——Pinctrl、GPIO + LED灯和按键输入例程,写一个按键驱动,在按键按下时调用发送函数发送键位。
HID 的键盘键位键值可以参考官方文档——HID Usage Tables 1.6
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"
#include "gpio.h"
#include "pinctrl.h"#include "usb_init_keyboard_app.h"#define KEY_GPIO 11static osal_task* task_handle = NULL;static int key_task(void* args)
{unused(args);while (1) {if (uapi_gpio_get_val(KEY_GPIO) == GPIO_LEVEL_LOW) {/* 去抖 */while (uapi_gpio_get_val(KEY_GPIO) == GPIO_LEVEL_LOW) {osal_msleep(10);}/* 发送键位 */uint8_t keycode[8] = {0};keycode[2] = 0x04;int ret = usb_keyboard_send_data(keycode, sizeof(keycode));if (ret < 0) {osal_printk("send key failed\r\n");} else {osal_printk("send key ok\r\n");}keycode[2] = 0x00;usb_keyboard_send_data(keycode, sizeof(keycode));}osal_msleep(10);}return 0;
}static void usb_keyboard_entry(void)
{int ret = 0;/* 初始化GPIO */uapi_pin_set_mode(KEY_GPIO, HAL_PIO_FUNC_GPIO);uapi_pin_set_pull(KEY_GPIO, PIN_PULL_UP);uapi_gpio_set_dir(KEY_GPIO, GPIO_DIRECTION_INPUT);/* 初始化USB */ret = usb_keyboard_init();if (ret) {osal_printk("usb keyboard init failed\r\n");return;}/* 创建任务 */osal_kthread_lock();task_handle = osal_kthread_create(key_task, NULL, "KeyTask", 2048);if (task_handle) {osal_kthread_set_priority(task_handle, OSAL_TASK_PRIORITY_MIDDLE);}osal_kthread_unlock();
}/* Run the usb_keyscan_entry. */
app_run(usb_keyboard_entry);
发送完指定的键位后,需要立即发送全 0 的键位,表示复位键位,不然主机端会认为键位被长按,一直保持输入。
2.3 测试
如果系统正常启动,那么可以在电脑的设备管理器中看到枚举多出了一个 HID 键盘设备。

按下用户按键(IO11接地),那么可以看到输入了对应的键值。

