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鼠标枚举与上报时序

USB主机 HID鼠标设备 SETUP: Get_Descriptor(Device) 返回设备描述符 SETUP: Set_Address SETUP: Get_Descriptor(Config) 返回配置+HID+端点 SETUP: Set_Configuration(1) SETUP: Set_Idle(0) IN: 鼠标报告(4字节) USB枚举阶段(控制传输) 运行阶段(中断传输)

验证方法

编译与烧录:使用STM32CubeIDE创建工程,将上述代码合并到USB设备栈中(需适配具体HAL库版本),编译后通过ST-Link烧录至STM32F103开发板。

功能测试:

  1. 将开发板USB口连接PC,打开设备管理器,确认识别到“HID兼容鼠标”设备,且无黄色感叹号。
  2. 在代码中设置mouse_report.x = 10; mouse_report.y = 0; mouse_report.buttons = 0;并调用HID_Mouse_Send(),观察光标是否向右移动。
  3. 设置mouse_report.buttons = 0x01;(左键按下)并发送,确认产生左键点击效果。

判定标准:设备枚举无错误,光标能按设定的X/Y值移动,按键动作能正确触发,即判定本案例实现通过。