STM32H5系列作为STMicroelectronics最新推出的高性能微控制器,在工业控制、消费电子等领域有着广泛应用。这次我们要探讨的是如何在STM32H5上基于USBx外设裸机驱动,通过添加OUT端点实现HID类设备的双向通信功能。
在实际项目中,我们经常遇到这样的需求:设备既要接收主机下发的控制指令(OUT传输),又要向主机上报状态数据(IN传输)。传统HID设备通常只配置IN端点用于上报数据,而要实现双向通信就必须添加OUT端点。这个技术点在工业HID设备(如编程器、调试工具)、人机交互设备(带反馈功能的控制面板)等场景中尤为重要。
HID(Human Interface Device)类设备在USB协议中属于一类特殊的设备类型,其最大特点是可以通过USB协议内置的HID驱动实现免驱使用。HID设备通常采用中断传输方式,具有以下特性:
STM32H5系列搭载的USBx外设是ST最新一代USB控制器,相比前代产品主要改进包括:
在裸机开发中,我们需要直接操作USBx外设的寄存器来实现端点配置和数据传输。
要实现USB HID通信,硬件上需要确保:
推荐使用以下工具链:
在CubeMX中需要启用:
在STM32H5上配置USB端点需要操作以下寄存器组:
关键配置步骤:
以下代码展示了如何添加一个OUT端点:
c复制#define HID_OUT_EP 0x02 // 端点2作为OUT端点
#define HID_OUT_EP_SIZE 64 // 最大包大小为64字节
void USB_ActivateEndpoint(uint8_t epnum, uint8_t direction, uint8_t eptype, uint16_t ep_mps)
{
if (direction == EP_OUT)
{
USBx_OUTEP(epnum)->DOEPCTL = (eptype << 18) |
(1 << 15) | // EP Enable
(ep_mps << 0);
}
// ... IN端点配置省略
}
// 在USB初始化函数中调用
USB_ActivateEndpoint(HID_OUT_EP, EP_OUT, EP_TYPE_INTR, HID_OUT_EP_SIZE);
OUT端点数据传输完成后会触发中断,需要在USB全局中断服务例程中处理:
c复制void OTG_FS_IRQHandler(void)
{
// 检查OUT端点中断标志
if (USBx_DEVICE->DAINT & (1 << (HID_OUT_EP + 16)))
{
uint32_t doepint = USBx_OUTEP(HID_OUT_EP)->DOEPINT;
// 处理传输完成中断
if (doepint & (1 << 3)) // XFRC
{
// 读取接收到的数据
uint32_t pktcnt = (USBx_OUTEP(HID_OUT_EP)->DOEPTSIZ & 0x1FF) >> 19;
uint32_t xfrsiz = USBx_OUTEP(HID_OUT_EP)->DOEPTSIZ & 0x7FFFF;
// 从FIFO读取数据
USB_ReadPacket(USBx_FIFO(HID_OUT_EP), HID_Out_Buffer, xfrsiz);
// 清除中断标志
USBx_OUTEP(HID_OUT_EP)->DOEPINT = (1 << 3);
// 处理接收到的数据
HID_ProcessOutData(HID_Out_Buffer, xfrsiz);
}
}
// ... 其他中断处理
}
以下是一个支持双向通信的HID报告描述符示例:
c复制__ALIGN_BEGIN static uint8_t HID_ReportDesc[] __ALIGN_END = {
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined)
0x09, 0x01, // Usage (Vendor Defined)
0xA1, 0x01, // Collection (Application)
// Input Report (设备到主机)
0x09, 0x02, // Usage (Vendor Defined)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x81, 0x02, // Input (Data,Var,Abs)
// Output Report (主机到设备)
0x09, 0x03, // Usage (Vendor Defined)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x91, 0x02, // Output (Data,Var,Abs)
0xC0 // End Collection
};
向主机发送数据的典型流程:
c复制void HID_SendReport(uint8_t *report, uint16_t len)
{
// 等待上一次传输完成
while (HID_TxState != 0);
// 复制数据到发送缓冲区
memcpy(HID_Tx_Buffer, report, len);
HID_Tx_Length = len;
// 启动传输
USB_WritePacket(USBx_FIFO(HID_IN_EP), HID_Tx_Buffer, len);
USBx_INEP(HID_IN_EP)->DIEPTSIZ = (1 << 19) | len;
USBx_INEP(HID_IN_EP)->DIEPCTL |= (1 << 31) | (1 << 26); // CNAK & EP Enable
HID_TxState = 1; // 标记为发送中
}
主机下发数据的处理流程已在中断处理部分展示,这里补充数据处理函数:
c复制void HID_ProcessOutData(uint8_t *data, uint16_t len)
{
// 解析接收到的数据
switch(data[0]) // 假设第一个字节为命令字
{
case CMD_SET_CONFIG:
handleSetConfig(&data[1], len-1);
break;
case CMD_GET_STATUS:
prepareStatusReport();
break;
// ... 其他命令处理
default:
// 未知命令处理
break;
}
// 准备接收下一包数据
USBx_OUTEP(HID_OUT_EP)->DOEPTSIZ = (1 << 19) | HID_OUT_EP_SIZE;
USBx_OUTEP(HID_OUT_EP)->DOEPCTL |= (1 << 31) | (1 << 26); // CNAK & EP Enable
}
完整的USB设备需要提供以下描述符:
关键配置示例:
c复制__ALIGN_BEGIN static uint8_t USBD_HID_CfgDesc[] __ALIGN_END = {
// 配置描述符
0x09, // bLength
0x02, // bDescriptorType (Configuration)
0x22, 0x00, // wTotalLength (34 bytes)
0x01, // bNumInterfaces
0x01, // bConfigurationValue
0x00, // iConfiguration
0x80, // bmAttributes (Bus powered)
0x32, // bMaxPower (100mA)
// 接口描述符
0x09, // bLength
0x04, // bDescriptorType (Interface)
0x00, // bInterfaceNumber
0x00, // bAlternateSetting
0x02, // bNumEndpoints
0x03, // bInterfaceClass (HID)
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0x00, // iInterface
// HID描述符
0x09, // bLength
0x21, // bDescriptorType (HID)
0x11, 0x01, // bcdHID (1.11)
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType (Report)
0x34, 0x00, // wDescriptorLength (52 bytes)
// IN端点描述符
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x81, // bEndpointAddress (IN endpoint 1)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize (64 bytes)
0x0A, // bInterval (10ms)
// OUT端点描述符
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x02, // bEndpointAddress (OUT endpoint 2)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize (64 bytes)
0x0A, // bInterval (10ms)
};
在USB设备连接时,主机会发起枚举过程,需要正确处理各种标准请求:
c复制void USBD_HID_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
{
switch (req->bRequest)
{
case USB_REQ_GET_DESCRIPTOR:
handleGetDescriptor(pdev, req);
break;
case USB_REQ_SET_INTERFACE:
USBD_CtlSendStatus(pdev);
break;
case USB_REQ_GET_INTERFACE:
USBD_CtlSendData(pdev, (uint8_t *)&hid_iface, 1);
break;
case USB_REQ_SET_CONFIGURATION:
handleSetConfiguration(pdev);
break;
// ... 其他请求处理
default:
USBD_CtlError(pdev, req);
break;
}
}
设备无法被识别
OUT端点无法接收数据
数据传输不稳定
枚举成功后通信失败
对于高速传输需求,考虑使用双缓冲机制:
c复制// 配置端点时启用双缓冲
USBx_OUTEP(epnum)->DOEPCTL |= (1 << 31) | (1 << 30) | (1 << 29);
合理设置端点FIFO大小,避免溢出或浪费内存:
c复制// 设置端点2的FIFO大小为128字(512字节)
USBx->DIEPTXF2 = (128 << 16) | (2 << 0);
对于实时性要求高的应用,可以缩短端点间隔时间(bInterval),但要注意不要超过USB规范限制。
HID协议支持一个接口包含多个报告,可以通过以下方式扩展:
将HID接口与其他USB类接口(如CDC、MSC)组合:
针对电池供电设备的优化措施:
在实际测试中,我们使用STM32H563ZI开发板实现了上述方案,测试环境如下:
测试结果:
性能瓶颈分析:
优化后的实现已经成功应用于工业HID编程器项目,稳定支持了超过10万次编程操作。