2. USB HID 键盘基础描述符实现
任务说明
本案例实现一个USB HID鼠标设备,使主机能够识别该设备,并支持光标移动(X/Y轴)以及左键、右键、中键的点击操作。设备采用HID Boot Protocol鼠标规范,通过控制传输完成枚举,通过中断输入端点上报鼠标状态。
实现方案
基于STM32F103系列MCU的USB全速外设,使用自建USB描述符实现HID鼠标类设备。代码包含完整的设备描述符、配置描述符(含HID描述符)、字符串描述符,以及端点0控制传输处理和端点1中断上报逻辑。鼠标状态通过全局变量mouse_report维护,上层应用只需修改该变量并调用HID_Mouse_Send即可触发上报。
代码实现
/* usb_hid_mouse.h */
#ifndef __USB_HID_MOUSE_H
#define __USB_HID_MOUSE_H
#include "stm32f1xx_hal.h"
/* HID鼠标报告格式(Boot Protocol) */
typedef struct {
uint8_t buttons; /* bit0:左键 bit1:右键 bit2:中键 */
int8_t x; /* X轴位移(-127~127) */
int8_t y; /* Y轴位移(-127~127) */
int8_t wheel; /* 滚轮(本案例未使用,置0) */
} __attribute__((packed)) mouse_report_t;
extern mouse_report_t mouse_report;
void HID_Mouse_Init(void);
void HID_Mouse_Send(void);
#endif
/* usb_hid_mouse.c */
#include "usb_hid_mouse.h"
#include "usbd_core.h"
#include "usbd_desc.h"
#include "usbd_hid.h"
mouse_report_t mouse_report = {0, 0, 0, 0};
/* USB设备描述符 */
static const uint8_t device_desc[] = {
0x12, /* bLength */
0x01, /* bDescriptorType: Device */
0x00, 0x02, /* bcdUSB 2.00 */
0x00, /* bDeviceClass: 由接口描述符定义 */
0x00, /* bDeviceSubClass */
0x00, /* bDeviceProtocol */
0x40, /* bMaxPacketSize0: 64 */
0x34, 0x12, /* idVendor: 0x1234 */
0x56, 0x78, /* idProduct: 0x7856 */
0x00, 0x01, /* bcdDevice: 1.00 */
0x01, /* iManufacturer */
0x02, /* iProduct */
0x03, /* iSerialNumber */
0x01 /* bNumConfigurations */
};
/* HID报告描述符:鼠标(Boot Protocol) */
static const uint8_t hid_report_desc[] = {
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x02, /* Usage (Mouse) */
0xA1, 0x01, /* Collection (Application) */
0x09, 0x01, /* Usage (Pointer) */
0xA1, 0x00, /* Collection (Physical) */
0x05, 0x09, /* Usage Page (Button) */
0x19, 0x01, /* Usage Minimum (1) */
0x29, 0x03, /* Usage Maximum (3) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (1) */
0x95, 0x03, /* Report Count (3) */
0x75, 0x01, /* Report Size (1) */
0x81, 0x02, /* Input (Data,Var,Abs) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x05, /* Report Size (5) */
0x81, 0x01, /* Input (Const) */
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x30, /* Usage (X) */
0x09, 0x31, /* Usage (Y) */
0x09, 0x38, /* Usage (Wheel) */
0x15, 0x81, /* Logical Minimum (-127) */
0x25, 0x7F, /* Logical Maximum (127) */
0x75, 0x08, /* Report Size (8) */
0x95, 0x03, /* Report Count (3) */
0x81, 0x06, /* Input (Data,Var,Rel) */
0xC0, /* End Collection */
0xC0 /* End Collection */
};
/* 配置描述符(含HID描述符和端点描述符) */
static const uint8_t config_desc[] = {
/* 配置描述符 */
0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,
/* 接口描述符 */
0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x01, 0x02, 0x00,
/* HID描述符 */
0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x34, 0x00,
/* 端点描述符(中断输入) */
0x07, 0x05, 0x81, 0x03, 0x04, 0x00, 0x0A
};
/* 字符串描述符 */
static const uint8_t lang_id_desc[] = {
0x04, 0x03, 0x09, 0x04
};
static const uint8_t *strings[] = {
lang_id_desc,
(const uint8_t[]){0x0E, 0x03, 'S', 0, 'T', 0, 'M', 0, '3', 0, '2', 0, 'F', 0},
(const uint8_t[]){0x10, 0x03, 'H', 0, 'I', 0, 'D', 0, ' ', 0, 'M', 0, 'o', 0, 'u', 0, 's', 0, 'e', 0},
(const uint8_t[]){0x0A, 0x03, '1', 0, '.', 0, '0', 0, '0', 0}
};
/* USB回调函数声明 */
static int8_t HID_Init(void);
static int8_t HID_DeInit(void);
static int8_t HID_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req);
static int8_t HID_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum);
/* USB设备实例 */
USBD_HandleTypeDef hUsbDevice;
USBD_ClassTypeDef usb_hid_mouse_class = {
HID_Init,
HID_DeInit,
HID_Setup,
NULL, /* EP0_RxReady */
HID_DataIn,
NULL, /* EP0_TxSent */
NULL, /* SOF */
NULL, /* IsoINIncomplete */
NULL, /* IsoOUTIncomplete */
NULL, /* Resume */
NULL, /* Disconnect */
NULL, /* GetDeviceDescriptor */
NULL, /* GetConfigDescriptor */
NULL /* GetStringDescriptor */
};
static uint8_t hid_ready = 0;
static int8_t HID_Init(void) {
hid_ready = 1;
return 0;
}
static int8_t HID_DeInit(void) {
hid_ready = 0;
return 0;
}
static int8_t HID_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) {
switch (req->bRequest) {
case USB_REQ_GET_DESCRIPTOR:
if (req->wValue >> 8 == 0x22) {
/* 返回HID报告描述符 */
USBD_CtlSendData(pdev, (uint8_t*)hid_report_desc,
sizeof(hid_report_desc));
return 0;
}
break;
case USB_REQ_SET_IDLE:
/* 接受任意Idle设置 */
USBD_CtlSendStatus(pdev);
return 0;
case USB_REQ_GET_PROTOCOL:
/* Boot Protocol = 0 */
USBD_CtlSendData(pdev, (uint8_t[]){0}, 1);
return 0;
case USB_REQ_SET_PROTOCOL:
USBD_CtlSendStatus(pdev);
return 0;
default:
break;
}
return -1;
}
static int8_t HID_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum) {
return 0;
}
/* 发送鼠标报告 */
void HID_Mouse_Send(void) {
if (hid_ready) {
USBD_LL_Transmit(&hUsbDevice, 0x81, (uint8_t*)&mouse_report,
sizeof(mouse_report));
}
}
/* 初始化USB并连接 */
void HID_Mouse_Init(void) {
USBD_Init(&hUsbDevice, (USBD_DescriptorsTypeDef*)&(USBD_DescriptorsTypeDef){
.GetDeviceDescriptor = NULL, /* 使用默认描述符获取逻辑 */
.GetConfigDescriptor = NULL,
.GetStringDescriptor = NULL
}, 0);
/* 注册自定义描述符获取函数 */
USBD_RegisterClass(&hUsbDevice, &usb_hid_mouse_class);
USBD_Start(&hUsbDevice);
}
关键实现说明
上述代码中,HID_Mouse_Init完成USB外设初始化和连接。实际工程中需将device_desc、config_desc、strings[]通过描述符回调函数提供给USB协议栈。HID报告描述符定义了3个按键(左/右/中键)和X/Y/Wheel三个相对位移轴,符合Boot Protocol鼠标规范。
SVG:HID鼠标枚举与上报时序
验证方法
编译与烧录:使用STM32CubeIDE创建工程,将上述代码合并到USB设备栈中(需适配具体HAL库版本),编译后通过ST-Link烧录至STM32F103开发板。
功能测试:
- 将开发板USB口连接PC,打开设备管理器,确认识别到“HID兼容鼠标”设备,且无黄色感叹号。
- 在代码中设置
mouse_report.x = 10; mouse_report.y = 0; mouse_report.buttons = 0;并调用HID_Mouse_Send(),观察光标是否向右移动。 - 设置
mouse_report.buttons = 0x01;(左键按下)并发送,确认产生左键点击效果。
判定标准:设备枚举无错误,光标能按设定的X/Y值移动,按键动作能正确触发,即判定本案例实现通过。