使用STM32 H7实现HID组合设备(键盘、鼠标)
分类: 硬件 发布于: 2023-08-27

网上的stm32 usb教程,大多数都是基于F1系列或者是单设备的。这里寻找了一个方法实现组合设备,在一个usb device上模拟键盘和鼠标,同时可以与主机传输数据。此例定义了Custom HID设备,包括3个接口,第一个接口为自定义的HID输入输出设备,包含两个端点。另外两个接口分别为键盘和鼠标,分别包含1个端点。如果移除第一个接口,根据实际测试,需要对默认的CUSTOMHIDInEpAdd等配置做较多配置,较为麻烦,因此不再讨论。

此文仅用作备忘录,不介绍原理。

硬件

使用wio lite ai,基于stm32H725AE,内置USB OTG HS,GPIO映射如下:

USB
  PA11, PA12
USART3    // 板子丝印为TX、RX
  RX -> PD9
  TX -> PD8
LED
  RED    -> PC13
  ORANGE -> PF0
KEY
  PF1

创建工程

配置以下内容:

  • LED
  • KEY 设置为中断触发
  • USART3,设置为中断触发
  • 外部晶振
  • 单线调试

然后配置USB_HS_OTG。在“Connectivity”组中,选择内部phy,"Device_Only"组中保持默认参数,中断不用勾选。然后在“Middleware”组中,配置“USB_DEVICE”,选择“Custom Human Interface ...(HID)”。关键配置项如下:

// 报告描述符大小,在这里暂不修改。等在代码中编写完成之后,替换为实际大小
USBD_CUSTOM_HID_REPORT_DESC_SIZE

// 输出端点的报告buff大小,设置为64字节
USBD_CUSTOMHID_OUTREPORT_BUF_SIZE

// 接口数量,这里设置为3,第一个为默认的HID设备,另外两个分别为键盘、鼠标
USBD_MAX_NUM_INTERFACES

// 设备描述中,修改VID/PID,厂商描述符等。不影响功能

最后配置时钟,修改project中堆和栈的大小(可选)。

编写配置描述符

配置描述符的位置在 Middleware->ST->STM32_USB..Library->Class->CustomHID->Src下的usbd_customhid.c文件里,关键字USBD_CUSTOM_HID_CfgDesc。这个数组下面,每一组第一个字节为该组配置的大小,单位为字节;第二个字节为类型;3、4个字节为配置描述大小。其他数值参照注释。

在这里需要修改接口数量,将其改为3。电源部分采用总线供电,支持唤醒,因此设置为0xA0. 具体内容:

__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_CfgDesc[USB_CUSTOM_HID_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09,                                               /* bLength: Configuration Descriptor size */
  USB_DESC_TYPE_CONFIGURATION,                        /* bDescriptorType: Configuration */
  LOBYTE(USB_CUSTOM_HID_CONFIG_DESC_SIZ),             /* wTotalLength: Bytes returned */
  HIBYTE(USB_CUSTOM_HID_CONFIG_DESC_SIZ),
  0x03,                                               // 3个接口
  0x01,                                               /* bConfigurationValue: Configuration value */
  0x00,                                               /* iConfiguration: Index of string descriptor
                                                         describing the configuration */
  0xA0,                                               // 总线供电,支持唤醒
  USBD_MAX_POWER,                                     /* MaxPower (mA) */
  // ==================== 第0个接口 (默认)  ====================//
  /************** Descriptor of CUSTOM HID interface ****************/
  0x09, USB_DESC_TYPE_INTERFACE, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00,
  /******************** Descriptor of CUSTOM_HID *************************/
  0x09, CUSTOM_HID_DESCRIPTOR_TYPE, 0x11, 0x01, 0x00, 0x01, 0x22, LOBYTE(USBD_CUSTOM_HID_REPORT_DESC_SIZE), HIBYTE(USBD_CUSTOM_HID_REPORT_DESC_SIZE),
  /******************** Descriptor of Custom HID endpoints ********************/
  0x07, USB_DESC_TYPE_ENDPOINT, CUSTOM_HID_EPIN_ADDR, 0x03, LOBYTE(CUSTOM_HID_EPIN_SIZE), HIBYTE(CUSTOM_HID_EPIN_SIZE), CUSTOM_HID_FS_BINTERVAL,
  0x07, USB_DESC_TYPE_ENDPOINT, CUSTOM_HID_EPOUT_ADDR, 0x03, LOBYTE(CUSTOM_HID_EPOUT_SIZE), HIBYTE(CUSTOM_HID_EPOUT_SIZE), CUSTOM_HID_FS_BINTERVAL,
  /* 9 + 9 + 9 + 7 + 7 = 41 */

  // ==================== 第1个接口 (鼠标)  ====================//
  /************** Descriptor of CUSTOM HID interface ****************/
  0x09,
  USB_DESC_TYPE_INTERFACE,
  0x01,                                               // 接口编号,按顺序指定Interface */
  0x00,                                               /* bAlternateSetting: Alternate setting */
  0x01,                                               // 端点个数
  0x03,                                               /* bInterfaceClass: CUSTOM_HID */
  0x00,                                               /* bInterfaceSubClass : 1=BOOT, 0=no boot */
  0x02,                                               /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
  0x00,                                               /* iInterface: Index of string descriptor */
  /* 50 */
  /******************** Descriptor of CUSTOM_HID *************************/
  0x09,
  CUSTOM_HID_DESCRIPTOR_TYPE,
  0x11,
  0x01,
  0x00,                                               // HID为键盘时设置国家, 0x21为美国。鼠标不用修改  0x00,
  0x01,                                               /* bNumDescriptors: Number of CUSTOM_HID class descriptors
														 to follow */
  0x22,                                               // 描述符类型: 报告描述符
  LOBYTE(USBD_CUSTOM_HID_MOUSE_REPORT_DESC_SIZE),     // 报告描述符的大小
  HIBYTE(USBD_CUSTOM_HID_MOUSE_REPORT_DESC_SIZE),
  /* 59 */
  /******************** Descriptor of Custom HID endpoints ********************/
  0x07,
  USB_DESC_TYPE_ENDPOINT,
  // CUSTOM_HID_EPIN_ADDR D0~D3端点编号,D4~D6保留。D7为传输方向,主机为out时为0, 主机为in时为1,即键盘鼠标类是以0b1xxx_xxxx开头。
  // 这里使用2号 端点,MOUSE_EP 应为0x82。
  MOUSE_EP,
  0x03,                                               // 4种控制方式,11: 中断; 10: 批量; 01: 同步; 00: 控制。批量需要自己编写驱动
  LOBYTE(MOUSE_EP_SIZE),
  HIBYTE(MOUSE_EP_SIZE),
  CUSTOM_HID_FS_BINTERVAL,
  /* 66 */
  // ==================== 第2个接口 (键盘)  ====================//
  /************** Descriptor of CUSTOM HID interface ****************/
  0x09,
  USB_DESC_TYPE_INTERFACE,
  0x02,                                               // modified: 接口编号,前面为1,这里用2
  0x00,                                               /* bAlternateSetting: Alternate setting */
  0x01,                                               // 端点个数,只有一个输入端点
  0x03,                                               /* bInterfaceClass: CUSTOM_HID */
  0x00,                                               /* bInterfaceSubClass : 1=BOOT, 0=no boot */
  0x01,                                               /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */
  0x00,                                               /* iInterface: Index of string descriptor */
  /* 75 */
  /******************** Descriptor of CUSTOM_HID *************************/
  0x09,
  CUSTOM_HID_DESCRIPTOR_TYPE,
  0x11,                                               /* bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number */
  0x01,
  0x21,                                               // 键盘设置国家, 0x21为美国
  0x01,                                               /* bNumDescriptors: Number of CUSTOM_HID class descriptors
														 to follow */
  0x22,                                               // 0x22: 报告描述符
  LOBYTE(USBD_CUSTOM_HID_KEYBOARD_REPORT_DESC_SIZE),  // 报告描述符的大小
  HIBYTE(USBD_CUSTOM_HID_KEYBOARD_REPORT_DESC_SIZE),
   /* 84 */
   /******************** Descriptor of Custom HID endpoints ********************/
   0x07,
   USB_DESC_TYPE_ENDPOINT,
   KEYBOARD_EP,                                       // CUSTOM_HID_EPIN_ADDR, 上述已使用了2号端点,KEYBOARD_EP 应该为0x83
   0x03,                                              // modified: 4种控制方式,11: 中断; 10: 批量; 01: 同步; 00: 控制
   LOBYTE(KEYBOARD_EP_SIZE),
   HIBYTE(KEYBOARD_EP_SIZE),
   CUSTOM_HID_FS_BINTERVAL
   /* 91 */
};

修改 USB_CUSTOM_HID_CONFIG_DESC_SIZ为91,并在对应头文件中定义:

#define MOUSE_EP                                     0x82U
#define MOUSE_EP_SIZE                                0x0AU

#define KEYBOARD_EP                                  0x83U
#define KEYBOARD_EP_SIZE                             0x0AU

编写报告描述符

在生成的代码当中,有一个默认的报告描述符,在USB_DEVICE->App->usbd_custom_hid_if.c中:

__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_HS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
  /* USER CODE BEGIN 1 */
  0x00,
  /* USER CODE END 1 */
   0xC0    /*     END_COLLECTION             */
};

将函数体内的数组改为下述值(给默认的HID设备使用):

0x06, 0x00, 0xFF,
0x09, 0x01,
0xA1, 0x01,
0x09, 0x01,
0x15, 0x00,
0x26, 0xFF, 0x00,
0x75, 0x08,
0x95, 0x40,
0x81, 0x02,
0x09, 0x01,
0x15, 0x00,
0x26, 0xFF, 0x00,
0x75, 0x08,
0x95, 0x40,
0x91, 0x02,
0xC0

并将USBD_CUSTOM_HID_REPORT_DESC_SIZE修改为34。

然后定义鼠标和键盘的描述符。将上述函数拷贝到配置描述符的usbd_customhid.c文件中@defgroup USBD_CUSTOM_HID_Private_Variables下(可选其他位置),并替换为键盘和鼠标的报告描述符:

__ALIGN_BEGIN static uint8_t CUSTOM_HID_MouseReportDesc_HS[USBD_CUSTOM_HID_MOUSE_REPORT_DESC_SIZE] __ALIGN_END = {
  /* USER CODE BEGIN 1 */
	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 (Button 1)
	0x29, 0x03,                    // Usage Maximum (Button 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, Variable, Absolute)
	0x95, 0x01,                    // Report Count (1)
	0x75, 0x05,                    // Report Size (5)

	0x81, 0x03,                    // Input (Constant, Var, abs)
	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, Variable, Relative)
	0xC0,                          // 需注意此处的0xC0不是END_COLLECTION
	0xC0                           /*     END_COLLECTION             */
};

__ALIGN_BEGIN static uint8_t CUSTOM_HID_KeyboardReportDesc_HS[USBD_CUSTOM_HID_KEYBOARD_REPORT_DESC_SIZE] __ALIGN_END = {
	0x05, 0x01,
	0x09, 0x06,
	0xA1, 0x01,
	0x05, 0x07,
	0x19, 0xE0,

	0x29, 0xE7,
	0x15, 0x00,
	0x25, 0x01,
	0x75, 0x01,
	0x95, 0x08,

	0x81, 0x02,
	0x95, 0x01,
	0x75, 0x08,
	0x81, 0x01,
	0x95, 0x05,

	0x75, 0x01,
	0x05, 0x08,
	0x19, 0x01,
	0x29, 0x05,
	0x91, 0x02,

	0x95, 0x01,
	0x75, 0x03,
	0x91, 0x01,
	0x95, 0x06,
	0x75, 0x08,

	0x15, 0x00,
	0x25, 0x65,
	0x05, 0x07,
	0x19, 0x00,
	0x29, 0x65,

	0x81, 0x00,
	0xC0
};

并在对应头文件添加上报告描述符的长度:

#define USBD_CUSTOM_HID_MOUSE_REPORT_DESC_SIZE        52U
#define USBD_CUSTOM_HID_KEYBOARD_REPORT_DESC_SIZE     63U

修改HID初始化函数

usbd_customhid.c中,HID初始化通过USBD_CUSTOM_HID_Init完成,在其中添加

...
  /* Open EP IN */
  (void)USBD_LL_OpenEP(pdev, CUSTOMHIDInEpAdd, USBD_EP_TYPE_INTR,
                       CUSTOM_HID_EPIN_SIZE);

  pdev->ep_in[CUSTOMHIDInEpAdd & 0xFU].is_used = 1U;

  // 以下是添加自定义的EP
  (void)USBD_LL_OpenEP(pdev, MOUSE_EP, USBD_EP_TYPE_INTR, MOUSE_EP_SIZE);
  pdev->ep_in[MOUSE_EP & 0xFU].is_used = 1U;

  (void)USBD_LL_OpenEP(pdev, KEYBOARD_EP, USBD_EP_TYPE_INTR, KEYBOARD_EP_SIZE);
  pdev->ep_in[KEYBOARD_EP & 0xFU].is_used = 1U;
...

同理,在USBD_CUSTOM_HID_DeInit中也添加上关闭IN端口的逻辑:

  /* Close CUSTOM_HID EP IN */
  (void)USBD_LL_CloseEP(pdev, CUSTOMHIDInEpAdd);
  pdev->ep_in[CUSTOMHIDInEpAdd & 0xFU].is_used = 0U;
  pdev->ep_in[CUSTOMHIDInEpAdd & 0xFU].bInterval = 0U;

  // 以下关闭自定义的端点
  (void)USBD_LL_CloseEP(pdev, MOUSE_EP);
  pdev->ep_in[MOUSE_EP & 0xFU].is_used = 0U;
  pdev->ep_in[MOUSE_EP & 0xFU].bInterval = 0U;

  (void)USBD_LL_CloseEP(pdev, KEYBOARD_EP);
  pdev->ep_in[KEYBOARD_EP & 0xFU].is_used = 0U;
  pdev->ep_in[KEYBOARD_EP & 0xFU].bInterval = 0U;

最后,在USBD_CUSTOM_HID_Setup中寻找到case USB_REQ_GET_DESCRIPTOR,在判断条件if ((req->wValue >> 8) == CUSTOM_HID_REPORT_DESC)的代码块中,修改为根据端点序号返回描述:

// 以下判断自定义的端点,进行setup
if (req->wIndex == 0) {
  len = MIN(USBD_CUSTOM_HID_REPORT_DESC_SIZE, req->wLength);
  pbuf = ((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData[pdev->classId])->pReport;
} else if (req->wIndex == 1) {
  len = MIN(USBD_CUSTOM_HID_MOUSE_REPORT_DESC_SIZE, req->wLength);
  pbuf = CUSTOM_HID_MouseReportDesc_HS;
} else if (req->wIndex == 2) {
  len = MIN(USBD_CUSTOM_HID_KEYBOARD_REPORT_DESC_SIZE, req->wLength);
  pbuf = CUSTOM_HID_KeyboardReportDesc_HS;
}

修改发送缓冲

USB_DEVICE->Target->usbd_conf.c中,USBD_LL_Init函数最后几行用于配置fifo。

  /* USER CODE BEGIN TxRx_HS_Configuration */
  HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_HS, 0x200);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 0, 0x80);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 1, 0x174);

  // 以下为端点2、3设置TxFIFO。由于H7有足够大的内存,地址直接加0x100
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 2, 0x274);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_HS, 3, 0x374);

添加发送函数

usbd_customhid.c中,参照USBD_CUSTOM_HID_SendReport函数编写键盘和鼠标的发送函数:

// ========================= Send report ===================================
uint8_t USBD_KEYBOADR_HID_SendReport(USBD_HandleTypeDef  *pdev, uint8_t *report, uint16_t len) {

	USBD_CUSTOM_HID_HandleTypeDef     *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;

	if (pdev->dev_state == USBD_STATE_CONFIGURED) {
		if (hhid->state == CUSTOM_HID_IDLE) {
			hhid->state = CUSTOM_HID_BUSY;
			USBD_LL_Transmit(pdev, KEYBOARD_EP, report, len);
		} else {
			return USBD_BUSY;
		}
	}
	return USBD_OK;
}

// ========================= Send report ===================================
uint8_t USBD_MOUSE_HID_SendReport(USBD_HandleTypeDef  *pdev, uint8_t *report, uint16_t len) {

	USBD_CUSTOM_HID_HandleTypeDef     *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;

	if (pdev->dev_state == USBD_STATE_CONFIGURED) {
		if (hhid->state == CUSTOM_HID_IDLE) {
			hhid->state = CUSTOM_HID_BUSY;
			USBD_LL_Transmit(pdev, MOUSE_EP, report, len);
		} else {
			return USBD_BUSY;
		}
	}
	return USBD_OK;
}

最后,需要把函数定义放在头文件中。

main函数中的测试

在main.c中,需要import USB实例所在的头文件,定义键鼠缓冲,操作如下:

#include "usb_device.h"

extern USBD_HandleTypeDef hUsbDeviceHS;
uint8_t MouseBuff[4] = {0, 10, 10, 0};
uint8_t KeyboardBuff[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

// 在main loop中:
...
	  // 鼠标往右下移10个单位
	  USBD_MOUSE_HID_SendReport(&hUsbDeviceHS, MouseBuff, 4);
	  HAL_Delay(20);

	  // 发送 z
	  KeyboardBuff[2] = 0x1D;
	  USBD_KEYBOADR_HID_SendReport(&hUsbDeviceHS, KeyboardBuff, 8);
	  HAL_Delay(20);

	  // 抬起按键
	  KeyboardBuff[2] = 0x00;
	  USBD_KEYBOADR_HID_SendReport(&hUsbDeviceHS, KeyboardBuff, 8);

	  // 休眠4秒
	  HAL_Delay(4000);
...

烧录到设备,即可观察到屏幕中发送“z”并伴随鼠标移动。

参考资料:

  1. 花椰菜杀手-基于STM32复合USB设备教程