USB-STM32实现HID+CDC复合设备

本文介绍了如何使用STM32的USB库+HAL实现HID+CDC复合USB设备,使用STM32F103RCT6开发板。

项目代码

一,介绍

开发平台

STM32F103RCT6内部集成了一个USB-FS设备,每个端点最大可使用512字节缓冲区,最多可用于16个单向或8个双向端点。

具体开发板为绿深电子的核心板,其USB电路图如下:
image

具体外设信息详见数据手册,本次不涉及,由CubuMX生成的库代码完成底层的操作。

STM32USB库

本次使用STM32Cube_FW_F1_V1.8.6固件库,其中包含STM32 USB Device Library V2.5.3版本。

ST公司提供的USB库分为两个层级:一个是底层驱动,包含在HAL库中(PCD驱动和LL驱动),该驱动将不同芯片的USB外设功能抽象出来,具有统一接口;另一个是应用库,包含Device设备库和OTG库,用于处理USB传输的数据信息。本平台只支持DeviceUSB,因此只使用Device设备库。
image

USB基础知识

本文涉及到USB基本知识,包括USB传输过程、USB描述符、USB枚举过程、USB请求等等,这里不再展开,在阅读本文前可以先阅读上几篇文章:

Reversed

二,使用USB库

CubuMX生成代码结构

在CubuMX中,可以直接生成ST官方准备好的模板工程,这些工程已经将USB Device库移植到了HAL库上,可以直接使用。这里以CustomHID模板工程为例。

相比与简单的工程,模板工程会包含以下文件:

  • usb_device.c——作用同gpio.c,usart.c,是CubuMX生成的初始化功能的代码。

该文件会依次调用USBD_Init()初始化库和外设(初始化外设通过调用移植时实现的MspInit函数),USBD_RegisterClass()注册USB设备类。根据设备类的实现代码不同,可能会有RegisterInterface()注册设备类的回调。最后调用USBD_Start()开启USB功能。

  • usbd_desc.c——实现设备描述符和通用的字符串描述符

该代码中通过实现一个USBD_DescriptorsTypeDef结构体,注册了设备描述符的返回函数以及LangIDManufacturerProductSerialConfigInterface字符串描述符。其中,Serial ID实现为STM32芯片的UID,其余参数如VID、PID、ProductString(设备列表中显示)等可由用户自定义。

  • usbd_XXX_if.c——用户自定义的设备类的回调代码

该文件并非必须,是用户在实现自己的设备类时为了方便更灵活的使用而创建。在CustomHID模板中,该文件用于实现实际的报告描述符以及实现接收到主机发送的报告后的处理回调函数。一般会将回调函数作为结构体,将指针保存在pdev->pUserData中。

  • usbd_XXX.c——设备类核心实现代码

用户使用USB库必须实现的代码。包含了USB设备各种时间的处理回调函数实现,以及对应用程序开放调用的函数(如HID的SendReport())。设备类回调函数以结构体指针形式保存在pdev->pClass中。

  • usbd_conf.c/h——移植代码

实现了USB Device库移植需要的实现的函数,以及对USB库功能的设置宏定义。

  • usbd_core.c,usbd_ctlreq.c,usbd_ioreq.c——USB Device库代码

实现USB设备类

从模板工程的代码可以知道,要实现自己的功能,除了通用的设备描述符和字符串描述符,只需要用户实现一个USB设备类即可。

USB库整体基于中断运行,当发生USB事务时,外设产生中断,由中断函数完成相应的USB功能。因此,要实现用户的USB类功能,需要向USB库注册用户回调函数。

需要实现的回调函数结构体如下:

typedef struct _Device_cb
{
  uint8_t (*Init)(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx);
  uint8_t (*DeInit)(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx);
  /* Control Endpoints*/
  uint8_t (*Setup)(struct _USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef  *req);
  uint8_t (*EP0_TxSent)(struct _USBD_HandleTypeDef *pdev);
  uint8_t (*EP0_RxReady)(struct _USBD_HandleTypeDef *pdev);
  /* Class Specific Endpoints*/
  uint8_t (*DataIn)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*DataOut)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*SOF)(struct _USBD_HandleTypeDef *pdev);
  uint8_t (*IsoINIncomplete)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*IsoOUTIncomplete)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);

  uint8_t  *(*GetHSConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetFSConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
  uint8_t  *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev, uint8_t index,  uint16_t *length);
#endif

} USBD_ClassTypeDef;

其中,struct _USBD_HandleTypeDef就是USBD_HandleTypeDef

  • Init(uint8_t cfgidx)——在配置使能时调用

该回调在主机发送设置配置请求时调用,一般用于使能该配置下所有使用到的端点并设置模式。同时做一些类功能初始化(比如在CustomHID中初始化pdev->pClassData用于维护内部状态标志和报告缓冲区)。

  • DeInit(uint8_t cfgidx)——在切换(清除)配置时调用

该回调在主机发送设置配置请求且设备以及处于配置状态时(即切换配置时)调用,一般用于关闭当前配置的端点同时做一些类功能反初始化(如清空状态、释放内存等)。

  • Setup(USBD_SetupReqTypedef *req)——在控制传输收到Setup调用

该回调在控制端点0收到主机包,进入控制传输Setup阶段时调用(并且只会在USB库不能自行处理时调用(非标准请求),其他如设置地址、请求描述符、设置配置等库会处理)。该函数用于处理各种和用户应用相关的请求,调用场合有很多种,需要用户自行解析req参数判断。

调用场合包括:

  1. 请求发送到设备时的类请求(SB_REQ_TYPE_CLASS)和厂商请求(USB_REQ_TYPE_VENDOR)。
  2. 请求发送到接口时。
  3. 请求发送到端点时的类请求(SB_REQ_TYPE_CLASS)和厂商请求(USB_REQ_TYPE_VENDOR)。

若请求有效,返回USB_OK;若请求无效,调用USBD_CtlError(pdev, req)并返回USBD_FAIL

  • EP0_TxSent()——在控制传输的发送数据过程结束后调用

该回调在控制端点0的控制传输的数据过程结束,设备发送完全部数据后调用。

  • EP0_RxReady()——在控制传输的接收数据过程结束后调用

该回调在控制端点0的控制传输的数据过程结束,设备接收完全部数据后调用。用于处理控制传输接收到的来自主机的数据,比如HID设备中处理通过Set Report请求发送的报告,CDC设备中处理通过控制请求发送的控制命令。

  • DataIn( uint8_t epnum)——其他端点在设备向主机发送完全部数据后调用

该回调用于在普通端点的数据过程(中断、批量等),通过IN数据包向主机发送完全部的数据后调用。如果发送的数据长度超过最大包长,库会自动进行分包继续发送,直到所有的数据传输完成后再调用该回调。

对于大批量数据传输,如批量传输中包长大于最大包长且为其整数倍的数据,需要在该回调中补发一个0长度数据包表示结束。如下:

PCD_HandleTypeDef *hpcd = pdev->pData;
if (pdev->pClassData != NULL)
{
    if ((pdev->ep_in[epnum].total_length > 0U) && ((pdev->ep_in[epnum].total_length % hpcd->IN_ep[epnum].maxpacket) == 0U))
      {//数据长度 = MaxPacketSize 整数倍时,会再有一个令牌包,设备或主机必须再发一个 0长度DATA包结束
        /* Update the packet total length */
        pdev->ep_in[epnum].total_length = 0U;
        /* Send ZLP */
        USBD_LL_Transmit(pdev, epnum, NULL, 0U);
      }
    ...
}

  • DataOut( uint8_t epnum)——其他端点在主机发送完全部数据后调用

该回调在普通输出端点接收完主机的OUT数据后调用,包括接收到指定长度数据(主机没发完,此时设备返回NAK等待这里处理结束)或者收到了小于最大包长的数据包(主机发送完成)。

可以使用USBD_LL_GetRxDataSize(pdev, epnum)查看实际接收了多少。

在该回调中处理主机发送的数据,包括未传输完成的和传输完成的。同时注意调用USBD_LL_PrepareReceive()重新开始接收。

  • SOF()——当收到 SOF 中断时调用此回调

可使用此回调将一些过程与帧开始同步。这里用不到。

  • IsoINIncomplete( uint8_t epnum)——当最后一个同步 IN 传输未完成时,调用此回调。

  • IsoOUTIncomplete( uint8_t epnum)——当最后一个同步 OUT 传输未完成时,调用此回调。

  • GetHSConfigDescriptor(uint16_t *length)——返回 HS USB 配置描述符

  • GetFSConfigDescriptor(uint16_t *length)——返回 FS USB 配置描述符

返回描述符数组地址即可,同时给length赋值描述符的长度。

  • GetOtherSpeedConfigDescriptor(uint16_t *length)——返回HS模式中所用类的其它配置描述符

  • GetDeviceQualifierDescriptor(uint16_t *length)——返回HS模式中设备合格描述符

  • GetUsrStrDescriptor(uint8_t index, uint16_t *length)——返回用户自定义字符串

返回用户定义的其他索引位置的字符串,例如WinUSB字符串。一般不使用。

其他实现

上一节实现的是核心功能,但实际使用时,还需要注意一些地方。

usbd_desc.c中实现的设备描述符结构体:

typedef struct
{
  uint8_t  *(*GetDeviceDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetLangIDStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetManufacturerStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetProductStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetSerialStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetConfigurationStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
  uint8_t  *(*GetInterfaceStrDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
#if (USBD_LPM_ENABLED == 1U)
  uint8_t  *(*GetBOSDescriptor)(USBD_SpeedTypeDef speed, uint16_t *length);
#endif
} USBD_DescriptorsTypeDef;
  • GetDeviceDescriptor:此回调返回设备描述符。

  • GetLangIDStrDescriptor:此回调返回语言 ID 字符串描述符。

  • GetManufacturerStrDescriptor:此回调返回制造商字符串描述符。

  • GetProductStrDescriptor 此回调返回产品字符串描述符。

  • GetSerialStrDescriptor:此回调返回序列号字符串描述符。

  • GetConfigurationStrDescriptor:此回调返回配置字符串描述符。

  • GetInterfaceStrDescriptor:此回调返回接口字符串描述符。

  • GetBOSDescriptor:(USB2.1可选)返回BOS描述符

具体实现方法可以参考模板工程,这里需要注意的是设备描述符中的VID,PID以及设备类型。

除此之外,在usbd_conf.c中还需要注意一下USBD_static_malloc()的实现。

配置USB库

可使用 usbd_conf.h文件配置 USB 设备库。

常见的配置项有:

#define USBD_MAX_NUM_INTERFACES     4 		//所支持接口的最大数
#define USBD_MAX_NUM_CONFIGURATION     1 	//所支持配置的最大数
#define USBD_MAX_STR_DESC_SIZ     512 		//字符串描述符的最大大小
#define USBD_SELF_POWERED     0/1 			//使能自开机特性
#define USBD_SUPPORT_USER_STRING 0/1 		//使能对用户字符串的支持
#define USBD_LPM_ENABLED 0/1				//使能BOS相关

数据发送接收

对非控制端点,调用USBD_LL_Transmit()传输数据;调用USBD_LL_PrepareReceive()接收数据,并在回调中处理接收的数据(使用USBD_LL_GetRxDataSize()获取接收数据的多少)。

对控制传输过程(端点0),可以调用USBD_CtlSendData()在数据过程发送数据;调用USBD_CtlPrepareRx()在数据过程接收数据。这两个函数其实就是封装了上面的LL函数。

更多API见源码或UM1734。

复合设备注意点

对于CDC这种同一个功能涉及多个接口的USB设备,如果想要和其他功能(如CustomHID)一起实现,设备类必须是复合设备(bDeviceClass=0xEF,bDeviceSubClass=0x02,bDeviceProtocol=0x01)。同时,在配置描述符中,使用IAD描述符组合多个接口为一个功能。

三,实现复合设备

详细的回调函数实现规范请自行参考模板工程,这里只讨论如何将CustomHID和CDC合并实现。

STM32的CDC HID模板工程API说明文档见UM1734。

合并的代码详见工程文件:项目代码

核心步骤

  1. 修改设备描述符,将设备设置为复合设备类。
  2. 合并类的内部Handle和ItfTypeDef。
  3. 修改conf设定中最大接口数和内存分配函数。
  4. 修改配置描述符,合并HID和CDC,并使用IAD组合。
  5. 合并类的各种回调函数。

设备描述符

设备描述符主要是修改设备类别,如下:

__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
  0x12,                       /*bLength */
  USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
  0x00,                       /*bcdUSB */
  0x02,
  0xEF,                       /*bDeviceClass*/ //Need be 0xEF
  0x02,                       /*bDeviceSubClass*/
  0x01,                       /*bDeviceProtocol*/
  USB_MAX_EP0_SIZE,           /*bMaxPacketSize*/
  LOBYTE(USBD_VID),           /*idVendor*/
  HIBYTE(USBD_VID),           /*idVendor*/
  LOBYTE(USBD_PID_FS),        /*idProduct*/
  HIBYTE(USBD_PID_FS),        /*idProduct*/
  0x00,                       /*bcdDevice rel. 2.00*/
  0x02,
  USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
  USBD_IDX_PRODUCT_STR,       /*Index of product string*/
  USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
  USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
};

合并回调函数和内部Handle

直接大结构体包小结构体即可。

//Composite if
typedef struct _COMPOSITE_Itf
{
    struct {
      uint8_t *pReport; //报告描述符在这里实现
      int8_t (* Init)(void);
      int8_t (* DeInit)(void);
      int8_t (* OutEvent)(uint8_t event_idx, uint8_t state); // 主机发送的报告的处理回调
    } hid;//HID ItfTypeDef
    
    struct {
      int8_t (* Init)(void);
      int8_t (* DeInit)(void);
      int8_t (* Control)(uint8_t cmd, uint8_t *pbuf, uint16_t length); // 控制端点的控制指令请求处理
      int8_t (* Receive)(uint8_t *Buf, uint32_t *Len); // 数据端点OUT回调,需要处理接收到的数据
    } cdc;//CDC ItfTypeDef

} USBD_COMPOSITE_ItfTypeDef;

typedef struct
{
  struct {
    uint32_t data[CDC_DATA_FS_MAX_PACKET_SIZE / 4U];      /* Force 32bits alignment 命令请求的数据缓冲 */
    uint8_t  CmdOpCode; //控制请求发送的命令
    uint8_t  CmdLength; //控制命令携带的数据长度
    uint8_t  *RxBuffer; //数据端点的接收缓存
    uint8_t  *TxBuffer; //数据端点的发送缓存
    uint32_t RxLength; 
    uint32_t TxLength;

    __IO uint32_t TxState;
    __IO uint32_t RxState;
  } cdc; // CDC Class Handle
  
  struct {
    uint8_t              Report_buf[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE]; //缓存主机发送的报告
    uint32_t             Protocol; //类请求相关
    uint32_t             IdleState; //类请求相关
    uint32_t             AltSetting; //类请求相关
    uint32_t             IsReportAvailable; //标注是否有通过请求发送的报告
    CUSTOM_HID_StateTypeDef     state; 
  } hid; //HID Class Handle
  
} USBD_COMPOSITE_HandleTypeDef; // Composite Class Handle

配置Conf

需要修改USBD_MAX_NUM_INTERFACES为3,包含一个HID接口和两个CDC接口。

同时还需要把CDC、HID模板工程中添加的设置定义写上去。

然后修改内存分配函数:

void *USBD_static_malloc(uint32_t size)
{
  static uint32_t mem[(sizeof(USBD_COMPOSITE_HandleTypeDef)/4+1)];/* On 32-bit boundary */
  return mem;
}

修改配置描述符

修改配置描述符是核心操作,决定了主机如何枚举设备。

在本次修改中,除了注意普通的接口号、输入输出端点不要冲突外,还需要使用IAD描述符。

IAD描述符可以将多个接口绑定在一起,在音频USB设备、CDC等多接口设备与其他类接口一起使用时需要添加在要绑定的接口前。

这里直接给出可行的描述符:

__ALIGN_BEGIN static uint8_t USBD_COMPOSITE_CfgFSDesc[USB_COMPOSITE_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09, /* bLength: Configuration Descriptor size */
  USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
  USB_COMPOSITE_CONFIG_DESC_SIZ,
  /* wTotalLength: Bytes returned */
  0x00,
  0x03,         /*bNumInterfaces: 3 interface,2 for cdc 1 for HID*/
  0x01,         /*bConfigurationValue: Configuration value*/
  0x00,         /*iConfiguration: Index of string descriptor describing
  the configuration*/
  0xC0,         /*bmAttributes: bus powered */
  0x32,         /*MaxPower 100 mA: this current is used for detecting Vbus*/

  /************** Descriptor of CUSTOM HID interface ****************/
  /* 09 */
  0x09,         /*bLength: Interface Descriptor size*/
  USB_DESC_TYPE_INTERFACE,/*bDescriptorType: Interface descriptor type*/
  CUSTOM_HID_INTERFACE,   /*bInterfaceNumber: Number of Interface*/
  0x00,         /*bAlternateSetting: Alternate setting*/
  0x02,         /*bNumEndpoints*/
  0x03,         /*bInterfaceClass: CUSTOM_HID*/
  0x00,         /*bInterfaceSubClass : 1=BOOT, 0=no boot*/
  0x00,         /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
  0,            /*iInterface: Index of string descriptor*/
  /******************** Descriptor of CUSTOM_HID *************************/
  /* 18 */
  0x09,         /*bLength: CUSTOM_HID Descriptor size*/
  CUSTOM_HID_DESCRIPTOR_TYPE, /*bDescriptorType: CUSTOM_HID*/
  0x11,         /*bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number*/
  0x01,
  0x00,         /*bCountryCode: Hardware target country*/
  0x01,         /*bNumDescriptors: Number of CUSTOM_HID class descriptors to follow*/
  0x22,         /*bDescriptorType*/
  USBD_CUSTOM_HID_REPORT_DESC_SIZE,/*wItemLength: Total length of Report descriptor*/
  0x00,
  /******************** Descriptor of Custom HID endpoints ********************/
  /* 27 */
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/

  CUSTOM_HID_EPIN_ADDR,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x03,          /*bmAttributes: Interrupt endpoint*/
  CUSTOM_HID_EPIN_SIZE, /*wMaxPacketSize: 2 Byte max */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,          /*bInterval: Polling Interval */
  /* 34 */

  0x07,          /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
  CUSTOM_HID_EPOUT_ADDR,  /*bEndpointAddress: Endpoint Address (OUT)*/
  0x03, /* bmAttributes: Interrupt endpoint */
  CUSTOM_HID_EPOUT_SIZE,  /* wMaxPacketSize: 2 Bytes max  */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,  /* bInterval: Polling Interval */
  /* 41 */
  /******** /IAD should be positioned just before the CDC interfaces ******
                IAD to associate the two CDC interfaces */
  0x08, /* bLength */
  0x0B, /* bDescriptorType */
  0x01, /* bFirstInterface */
  0x02, /* bInterfaceCount */
  0x02, /* bFunctionClass */
  0x02, /* bFunctionSubClass */
  0x01, /* bFunctionProtocol */
  0x02, /* iFunction (Index of string descriptor describing this function) */
  /*49*/
  /************************  CDC interface ********************************/
  0x09,   /* bLength: Interface Descriptor size */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
  /* Interface descriptor type */
  CDC_INTERFACE0,   /* bInterfaceNumber: Number of Interface */
  0x00,   /* bAlternateSetting: Alternate setting */
  0x01,   /* bNumEndpoints: One endpoints used */
  0x02,   /* bInterfaceClass: Communication Interface Class */
  0x02,   /* bInterfaceSubClass: Abstract Control Model */
  0x01,   /* bInterfaceProtocol: Common AT commands */
  0x00,   /* iInterface: */
  /*58*/
  
  /*Header Functional Descriptor*/
  0x05,   /* bLength: Endpoint Descriptor size */
  0x24,   /* bDescriptorType: CS_INTERFACE */
  0x00,   /* bDescriptorSubtype: Header Func Desc */
  0x10,   /* bcdCDC: spec release number */
  0x01,
  /*63*/

  /*Call Management Functional Descriptor*/
  0x05,   /* bFunctionLength */
  0x24,   /* bDescriptorType: CS_INTERFACE */
  0x01,   /* bDescriptorSubtype: Call Management Func Desc */
  0x00,   /* bmCapabilities: D0+D1 */
  0x01,   /* bDataInterface: 1 */
  /*68*/

  /*ACM Functional Descriptor*/
  0x04,   /* bFunctionLength */
  0x24,   /* bDescriptorType: CS_INTERFACE */
  0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
  0x02,   /* bmCapabilities */
  /*72*/

  /*Union Functional Descriptor*/
  0x05,   /* bFunctionLength */
  0x24,   /* bDescriptorType: CS_INTERFACE */
  0x06,   /* bDescriptorSubtype: Union func desc */
  0x00,   /* bMasterInterface: Communication class interface */
  0x01,   /* bSlaveInterface0: Data Class Interface */
  /*77*/

  /*Endpoint 2 Descriptor*/
  0x07,                           /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,   /* bDescriptorType: Endpoint */
  CDC_CMD_EP,                     /* bEndpointAddress */
  0x03,                           /* bmAttributes: Interrupt */
  LOBYTE(CDC_CMD_PACKET_SIZE),     /* wMaxPacketSize: */
  HIBYTE(CDC_CMD_PACKET_SIZE),
  0x10,                           /* bInterval: */ 
  /*84*/

  /*---------------------------------------------------------------------------*/
  
  /*Data class interface descriptor*/
  0x09,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
  CDC_INTERFACE1,   /* bInterfaceNumber: Number of Interface */
  0x00,   /* bAlternateSetting: Alternate setting */
  0x02,   /* bNumEndpoints: Two endpoints used */
  0x0A,   /* bInterfaceClass: CDC */
  0x00,   /* bInterfaceSubClass: */
  0x00,   /* bInterfaceProtocol: */
  0x00,   /* iInterface: */
  /*93*/

  /*Endpoint OUT Descriptor*/
  0x07,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
  CDC_OUT_EP,                        /* bEndpointAddress */
  0x02,                              /* bmAttributes: Bulk */
  LOBYTE(CDC_DATA_FS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
  HIBYTE(CDC_DATA_FS_MAX_PACKET_SIZE),
  0x00,                              /* bInterval: ignore for Bulk transfer */
  /*100*/

  /*Endpoint IN Descriptor*/
  0x07,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
  CDC_IN_EP,                         /* bEndpointAddress */
  0x02,                              /* bmAttributes: Bulk */
  LOBYTE(CDC_DATA_FS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
  HIBYTE(CDC_DATA_FS_MAX_PACKET_SIZE),
  0x00                               /* bInterval: ignore for Bulk transfer */
  /*107*/
};

合并回调函数

合并回调函数基本没有什么可以说的,将两个模板工程实现的函数合并即可。但有几个函数需要注意。

Setup

对HID和CDC的请求会发送到端点0,用户需要解析req请求结构体,判断该请求说发送到哪个接口的来判断如何处理。

if (req->wIndex == CUSTOM_HID_INTERFACE) // 通过接口区分请求
{
    //HID请求处理
}
else
{
    //CDC请求处理
}

其中,CDC有一个控制命令请求在Setup处理。如果数据过程是IN,需要调用pUserData->Control()准备data缓存的数据然后发送;如果数据过程是OUT,需要将命令存储在CmdOpCode,并将数据长度存储在CmdLength,然后开启接收。最后在EP0_RxReady()中调用pUserData->Control()处理。

EP0_RxReady

这里无法区分是哪个类的过程结束了,但可以通过用户设定的标志位判断(在Setup中标注)。

处理HID,根据IsReportAvailable标志位决定是否调用pUserData->OutEvent()处理报告。

处理CDC,根据CmdOpCode标志位决定是否调用pUserData->Control()处理控制命令。

DataIn

根据传入的epnum判断是哪个类。

处理HID,设置hid.state为空闲,表示上一次报告发送完成,可以发送下一个了。

处理CDC,需要给整数倍的批量传输补一个0长度DATA包结束(ZLP)。

DataOut

根据传入的epnum判断是哪个类。

处理HID,调用pUserData->OutEvent()处理报告,并调用USBD_LL_PrepareReceive开启下次接收。

处理CDC,先获取实际接收到数据长度,调用pUserData->cdc.Receive处理数据并重新开始下次接收。

测试

如果没有该VID、PID的话,可能需要在电脑上清除缓存才能正常识别。

如果HID的报告描述符不合法,Win也会报错无法识别HID设备。

参考文章

STM32配置组合设备(HID+CDC)

GitHub-STM32_CompositeHID_CDC

ST官方文档 UM1734-STM32Cube USB设备库

ST官方文档 UM1725-PCD驱动API说明

CubuMX模板工程CDC 和 CustomHID

posted @ 2026-08-22 13:02  LO_StacNet  阅读(0)  评论(0)    收藏  举报