USB-STM32实现WinUSB

WinUSB是微软推出的一个USB设备,可以在Win8以上的操作系统免驱运行,用户可以通过系统API直接对USB发起事务,传输数据。通过WinUSB可以直接操作批量端点传输数据,也可以向设备发出厂商请求,让用户无须编写驱动也能够直接操作USB设备。
本文章基于STM32F103RCT6,实现WinUSB设备。

一,WinUSB种类和限制

在Linux上开发USB设备是不需要特别的驱动的,Linux内核的USB驱动会将USB设备的基本操作都暴露到应用层,由应用层来完成实际的业务逻辑。libusb就是这样的一种通用USB设备访问程序。

为了在Windows上也实现这样的效果,libusb最初提供了Windows上通用的内核态驱动程序,将基本访问接口暴露到应用层,由应用层来实现访问逻辑。随着发展,微软也提供了这样的驱动程序,那就是WinUSB通用驱动程序。

传统操作系统匹配驱动是通过VID、PID进行的,这样即使驱动程序一样,所有设备还是需要提供一个inf文件告诉系统这个设备使用WinUSB驱动,并给出这个设备的GUID用于标识。

为了实现真正的免驱,微软定义了操作系统描述符,用于操作系统自动从设备获取inf文件中的有效内容,让设备即插即用。要实现一个WinUSB设备,最核心的就是要实现这几个描述符。

微软系统描述符1.0

该描述符用于USB2.0以上,使用OS字符串描述符

当设备描述符中bcdUSB字段大于等于2.0时,系统会尝试请求索引为0xEE的OS字符串描述符

然后,系统会发送一个到设备的bRequest为Vendor码,wIndex为4的厂商请求,获取40字节的扩展兼容ID描述符(Extend Compat ID)。

最后,系统发送一个到接口的bRequest为Vendor码,wIndex为5的厂商请求,获取142字节的扩展属性描述符(Extend Properties)。

因此,其核心步骤就是实现:

  1. 索引为0xEE的用户字符串描述符,包含厂商码,需开启USB库的用户字符串功能
  2. 一个wIndex为4的厂商请求,返回一个描述符包含接口,可在Setup回调实现
  3. 一个wIndex为5的厂商请求,返回一个描述符包含GUID,可在Setup回调实现(无效,需要Inf定义GUID)

OS1.0描述符无法自定义GUID,只能使用默认的{A5DCBF10-6530-11D2-901F-00C04FB951ED}进行枚举

微软系统描述符2.0

该描述符用于USB2.1以上,使用BOS描述符

当设备描述符中bcdUSB字段大于等于0x0210时,系统会尝试通过标准请求获取BOS描述符。BOS是USB标准中的,其中bDeviceCapabilityType字段的5是保留值,微软使用该字段。

系统获取BOS描述符后,会发送一个到设备的厂商请求,bRequest为Vendor码,wIndex为7,请求OS2.0描述符集,一次性获取Compatible ID和注册表属性。

因此,其核心步骤就是实现:

  1. 实现一个BOS描述符,包含uuid,操作系统版本,vendor code信息,需开启USB库的BOS功能
  2. 一个wIndex为7的厂商请求,返回一个包含接口信息、GUID的描述符集,可在Setup回调实现。

注意事项

任何设备系统只会在第一次插入时尝试获取OS描述符,如果该VID+PID之前使用过,系统会在注册表中标注该设备为非WinUSB设备从而不会匹配驱动,系统提示找不到驱动。需要清除注册表信息:

  1. 按下Win+R,输入regedit打开注册表编辑器。
  2. 找到计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\usbflags\位置。
  3. 根据VID+PID+后缀找到对应设备,如048357400200的VID为0483,PID为5740,可以看到其osvc项为0000。
  4. 删除该设备信息,让下次插入时系统再次尝试枚举。

Windows系统对扩展属性描述和前面的兼容ID不太一样,如果设备的扩展属性已经有了,就不会再去获取。如果在开发过程中,不慎将获取扩展属性描述符的代码弄坏了,这个时候设备接在系统上还是一切正常,能够正常使用。但是一旦接入一个没有连接过这个设备的系统,就会出现无法使用的情况。因此在开发USB设备的过程中,一个设备能够正常工作也不一定说明设备没有问题,有可能是使用了原来的配置信息。前面我们还说过,一个设备不能使用,并不能说明设备有问题。

在调试描述符相关功能的过程中,为了消除旧配置的影响,最好是即时删除设备的配置信息,通过卸载设备可以去掉扩展属性的信息。而usbflags中的信息只能通过修改注册表来去掉。如果不知道怎么删除,那就每次都用不同的VID和PID,确保系统会把它当成新设备来处理。当描述符稳定了,就不用这么折腾了。

如果使用了WinUSB的兼容ID,但是没有有效的扩展属性,在任务管理器中会看到设备是正常的,但是没有办法在应用程序中访问。遇到任务管理器中设备正常,但是却无法访问的情况,可以查看一下此设备相关的注册表项中有没有DeviceInterfaceGUID字段,如果没有,说明设备没有正确安装,接口不能使用。

——来自USB中文网的Debug排查

二,实现WinUSB 1.0

这里使用单接口,两个批量端点的配置,由于和CDC虚拟串口的配置比较相似,参考CDC模板工程。

参考工程:#TODO

实现步骤

  1. conf.h中定义USBD_SUPPORT_USER_STRING_DESC,开启自定义用户字符串功能
  2. 修改usbd_desc.c中的设备描述符,类别全为0
  3. 实现USBD_ClassTypeDefGetUsrStrDescriptor函数,处理0xEE索引的字符串
  4. 修改配置描述符,设定为厂商类和包含两个批量端点
  5. 在Setup回调中处理请求,返回两个描述符

定义数据结构

定义一些配置和数据结构。

#define WIN_VENDOR_NUM                              0xA5  

#define WIN_INTERFACE                               0x00   /* Interface 0 for WinUSB */
#define WIN_IN_EP                                   0x81U  /* EP1 for data IN */
#define WIN_OUT_EP                                  0x01U  /* EP1 for data OUT */
#define USB_WIN_CONFIG_DESC_SIZ                     32U

#define WIN_DATA_HS_MAX_PACKET_SIZE                 512U  /* Endpoint IN & OUT Packet size */
#define WIN_DATA_FS_MAX_PACKET_SIZE                 64U  /* Endpoint IN & OUT Packet size */

#define WIN_COMPAT_DESC 4  //Get Windows Extend Compat ID Descriptor
#define WIN_EXTEND_DESC 5  //Get Windows Extend Properties Descriptor

extern USBD_ClassTypeDef  USBD_WIN;

typedef struct _USBD_WIN_Itf
{
  int8_t (* Init)(void);
  int8_t (* DeInit)(void);
  int8_t (* Control)(uint8_t cmd, uint8_t *pbuf, uint16_t length); // 根据cmd的不同,读取处理pbuf或者准备pbuf数据
  int8_t (* Receive)(uint8_t *Buf, uint32_t Len); // 处理bulk接收的数据(可能有截断)

} USBD_WIN_ItfTypeDef;

typedef struct
{
  uint32_t data[WIN_REQ_DATA_MAX_BUFF_SIZE / 4U];      /* Force 32bits alignment | EP0数据过程中使用的数据*/
  uint8_t  CmdOpCode;  // 自定义指令(请求的bRequest段)
  uint16_t  CmdLength;  // 自定义指令的数据长度(请求的wLength段,数据在data中)
  uint8_t  RxBuffer[WIN_DATA_OUT_MAX_PACKET_SIZE]; // 数据OUT端点的接收缓冲区
  uint8_t  *TxBuffer;
  uint32_t RxLength;
  uint32_t TxLength;

  __IO uint32_t TxState;
  __IO uint32_t RxState;
}
USBD_WIN_HandleTypeDef;

注意,定义了USBD_WIN_HandleTypeDef还需要修改conf.c的静态内存分配函数。

标准描述符

首先,设备描述符需要将设备类型设置为0。

__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
  0x12,                       /*bLength */
  USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
  0x00,                       /*bcdUSB */
  0x02,
  0x00,                       /*bDeviceClass*/
  0x00,                       /*bDeviceSubClass*/
  0x00,                       /*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*/
};

然后,需要实现配置描述符,设定接口类型为厂商类,有一个批量输入和一个批量输出端口。

/* USB WIN device OtherSpeedC Descriptor for HS,just simple with FS except Desc Type*/
__ALIGN_BEGIN uint8_t USBD_WIN_OtherSpeedCfgDesc[USB_WIN_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09,   /* bLength: Configuation Descriptor size */
  USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION,
  USB_WIN_CONFIG_DESC_SIZ,
  0x00,
  0x01,   /* bNumInterfaces: 1 interfaces */
  0x01,   /* bConfigurationValue: */
  0x04,   /* iConfiguration: */
  0xC0,   /* bmAttributes: */
  0x32,   /* MaxPower 100 mA */

  /*Data class interface descriptor*/
  0x09,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
  WIN_INTERFACE,   /* bInterfaceNumber: Number of Interface */
  0x00,   /* bAlternateSetting: Alternate setting */
  0x02,   /* bNumEndpoints: Two endpoints used */
  0xFF,   /* bInterfaceClass: Vendor for WinUSB */
  0x00,   /* bInterfaceSubClass: */
  0x00,   /* bInterfaceProtocol: */
  0x00,   /* iInterface: */

  /*Endpoint OUT Descriptor*/
  0x07,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
  WIN_OUT_EP,                        /* bEndpointAddress */
  0x02,                              /* bmAttributes: Bulk */
  0x40,                              /* wMaxPacketSize:64 */
  0x00,
  0x00,                              /* bInterval: ignore for Bulk transfer */

  /*Endpoint IN Descriptor*/
  0x07,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,     /* bDescriptorType: Endpoint */
  WIN_IN_EP,                        /* bEndpointAddress */
  0x02,                             /* bmAttributes: Bulk */
  0x40,                             /* wMaxPacketSize:64 */
  0x00,
  0x00                              /* bInterval */
};

实现OS字符串描述符

USBD_ClassTypeDef中定添加函数:

/* WinUSB interface class callbacks structure */
USBD_ClassTypeDef  USBD_WIN =
{
  USBD_WIN_Init,
  USBD_WIN_DeInit,
  USBD_WIN_Setup,
  NULL,                 /* EP0_TxSent, */
  USBD_WIN_EP0_RxReady,
  USBD_WIN_DataIn,
  USBD_WIN_DataOut,
  NULL,
  NULL,
  NULL,
  USBD_WIN_GetHSCfgDesc,
  USBD_WIN_GetFSCfgDesc,
  USBD_WIN_GetOtherSpeedCfgDesc,
  USBD_WIN_GetDeviceQualifierDescriptor,
#if WINUSB_VERSION == 1
  USBD_WIN_GetOSStrDesc, // 实现OS String
#endif
};

/* USB WIN device OS Descriptor */
extern uint8_t USBD_StrDesc[USBD_MAX_STR_DESC_SIZ];
uint8_t USBD_WIN_OSStr[]  = {'M','S','F','T','1','0','0',WIN_VENDOR_NUM}; //WIN_VENDOR_NUM 自定义
static uint8_t  *USBD_WIN_GetOSStrDesc(struct _USBD_HandleTypeDef *pdev, uint8_t index,  uint16_t *length)
{
  USBD_GetString((uint8_t *)USBD_WIN_OSStr, USBD_StrDesc, length); //该方法会自动将Ascii字符串转换为 unicode
  return USBD_StrDesc;
}

实现其他描述符

需要在Setup回调中处理其他两种描述符的请求。

由于WinUSB没有类请求,其发送的请求都是厂商请求,因此在Setup中需要如下处理:

tatic uint8_t  USBD_WIN_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
{
  USBD_WIN_HandleTypeDef   *hwin = (USBD_WIN_HandleTypeDef *) pdev->pClassData;
  uint8_t ifalt = 0U;
  uint16_t status_info = 0U;
  uint8_t ret = USBD_OK;

  switch (req->bmRequest & USB_REQ_TYPE_MASK)
  {
    case USB_REQ_TYPE_CLASS :
      /* There is No CLASS REQ in WinUSB */
      break;
    /* Handle Vendor Request */
    case USB_REQ_TYPE_VENDOR:
      if(req ->bRequest == WIN_VENDOR_NUM)
      {
#if WINUSB_VERSION == 1
        if(req ->wIndex == WIN_COMPAT_DESC) // Get Windows Extend Compat ID Descriptor
        {
          USBD_CtlSendData(pdev, WINUSB_WCIDDescriptor, MIN(sizeof( WINUSB_WCIDDescriptor), req->wLength) );
        }
        else if(req ->wIndex == WIN_EXTEND_DESC) // Get Windows Extend Properties Descriptor
        {
          USBD_CtlSendData(pdev, WINUSB_WCIDProperties, MIN(sizeof( WINUSB_WCIDProperties), req->wLength) );
        }
#endif
      }
      else // 其他自定义请求交给用户回调,在EP0_RxReady中处理
      {
        if (req->bmRequest & 0x80U) // Device -> Host (Read Data)
        {
          // 由用户注册的回调准备要发送给主机的数据
          ((USBD_WIN_ItfTypeDef *)pdev->pUserData)->Control(req->bRequest,
                                                            (uint8_t *)(void *)hwin->data,
                                                            req->wLength);
          
          /* 数据阶段发送数据,完成后在 EP0_TxReady 处理 */
          USBD_CtlSendData(pdev, (uint8_t *)(void *)hwin->data, MIN(req->wLength,WIN_REQ_DATA_MAX_BUFF_SIZE) );
        }
        else // Host -> Device (Write Data)
        {
          hwin->CmdOpCode = req->bRequest;
          hwin->CmdLength = req->wLength;
          
          if(hwin->CmdLength == 0)//没有数据阶段,直接处理
          {
            ((USBD_WIN_ItfTypeDef *)pdev->pUserData)->Control(req->bRequest,
                                                            (uint8_t *)(void *)hwin->data,
                                                            req->wLength);
            USBD_LL_Transmit(pdev, 0x00, NULL, 0U);//手动返回status包处理0数据过程
          }
          else
          {
            /* 接收数据阶段数据,完成后在 EP0_RxReady 里处理 */
            USBD_CtlPrepareRx(pdev, (uint8_t *)(void *)hwin->data, MIN(req->wLength,WIN_REQ_DATA_MAX_BUFF_SIZE) );
          }
        }
      }
      break;
    
    /* Standard Req Copied from CDC */
    case USB_REQ_TYPE_STANDARD:
      //....
    default:
      USBD_CtlError(pdev, req);
      ret = USBD_FAIL;
      break;
  }

  return ret;
}

其余部分实现基本和CDC模板工程一样,上面的用户回调是为了能实现和CDC一样的功能,主机通过控制端口与设备进行数据交互。

另外两个描述符定义如下:

/* USB WIN device Extend Compat ID Descriptor brequest=VendorNum wIndex=4*/
__ALIGN_BEGIN uint8_t WINUSB_WCIDDescriptor[40] __ALIGN_END ={
    0x28, 0x00, 0x00, 0x00,                       /* dwLength */
    0x00, 0x01,                                   /* bcdVersion 1.0*/
    0x04, 0x00,                                   /* wIndex */
    0x01,                                         /* bCount */
    0,0,0,0,0,0,0,                                /* Reserved */
    /* WCID Function 可以有多个接口功能 */
    WIN_INTERFACE,                                /* bFirstInterfaceNumber */
    0x01,                                         /* bReserved */
    /* CID 选择兼容ID-Win还有其他功能的设备 */
    'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, 
    /* sub CID */
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0,0,0,0,0,0,                                  /* Reserved */
};

/* USB WIN device Extend Properties Descriptor brequest=VendorNum wIndex=5*/
__ALIGN_BEGIN uint8_t WINUSB_WCIDProperties[142] __ALIGN_END ={
  ///////////////////////////////////////
  /// WCID property descriptor
  ///////////////////////////////////////
  0x8e, 0x00, 0x00, 0x00,                           /* dwLength */
  0x00, 0x01,                                       /* bcdVersion */
  0x05, 0x00,                                       /* wIndex */
  0x01, 0x00,                                       /* wCount */
  ///////////////////////////////////////
  /// registry propter descriptor
  ///////////////////////////////////////
  0x84, 0x00, 0x00, 0x00,                           /* dwSize */
  0x01, 0x00, 0x00, 0x00,                           /* dwPropertyDataType */
  0x28, 0x00,                                       /* wPropertyNameLength */
  /* DeviceInterfaceGUID */
  'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00,       /* wcName_20 */
  'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00,       /* wcName_20 */
  't', 0x00, 'e', 0x00, 'r', 0x00, 'f', 0x00,       /* wcName_20 */
  'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00,       /* wcName_20 */
  'U', 0x00, 'I', 0x00, 'D', 0x00, 0x00, 0x00,      /* wcName_20 */
  0x4e, 0x00, 0x00, 0x00,                           /* dwPropertyDataLength */
  /* {1D4B2365-4749-48EA-B38A-7C6FDDDD7E26} 系统通过该GUID枚举设备,可自定义*/
  '{', 0x00, '1', 0x00, 'D', 0x00, '4', 0x00,       /* wcData_39 */
  'B', 0x00, '2', 0x00, '3', 0x00, '6', 0x00,       /* wcData_39 */
  '5', 0x00, '-', 0x00, '4', 0x00, '7', 0x00,       /* wcData_39 */
  '4', 0x00, '9', 0x00, '-', 0x00, '4', 0x00,       /* wcData_39 */
  '8', 0x00, 'E', 0x00, 'A', 0x00, '-', 0x00,       /* wcData_39 */
  'B', 0x00, '3', 0x00, '8', 0x00, 'A', 0x00,       /* wcData_39 */
  '-', 0x00, '7', 0x00, 'C', 0x00, '6', 0x00,       /* wcData_39 */
  'F', 0x00, 'D', 0x00, 'D', 0x00, 'D', 0x00,       /* wcData_39 */
  'D', 0x00, '7', 0x00, 'E', 0x00, '2', 0x00,       /* wcData_39 */
  '6', 0x00, '}', 0x00, 0x00, 0x00,                 /* wcData_39 */
};

实现数据发送接收函数

这里同CDC一样,需要准备两个数据发送接收函数。

/**
  * @brief  USBD_WIN_TransmitPacket
  *         Transmit packet on IN endpoint
  * @param  pdev: device instance
  * @retval status
  */
uint8_t  USBD_WIN_TransmitPacket(USBD_HandleTypeDef *pdev, uint8_t* pbuf, uint16_t length)
{
  USBD_WIN_HandleTypeDef   *hwin = (USBD_WIN_HandleTypeDef *) pdev->pClassData;

  if (pdev->pClassData != NULL)
  {
    if (hwin->TxState == 0U)
    {
      /* Tx Transfer in progress */
      hwin->TxState = 1U;
      
      /* Set Tx Buffer */
      hwin->TxBuffer = pbuf;
      hwin->TxLength = length;

      /* Update the packet total length */
      pdev->ep_in[WIN_IN_EP & 0xFU].total_length = hwin->TxLength;

      /* Transmit next packet */
      USBD_LL_Transmit(pdev, WIN_IN_EP, hwin->TxBuffer,
                       (uint16_t)hwin->TxLength);

      return USBD_OK;
    }
    else
    {
      return USBD_BUSY;
    }
  }
  else
  {
    return USBD_FAIL;
  }
}


/**
  * @brief  USBD_WIN_ReceivePacket
  *         prepare OUT Endpoint for reception
  * @param  pdev: device instance
  * @retval status
  */
uint8_t  USBD_WIN_ReceivePacket(USBD_HandleTypeDef *pdev)
{
  USBD_WIN_HandleTypeDef   *hwin = (USBD_WIN_HandleTypeDef *) pdev->pClassData;

  /* Suspend or Resume USB Out process */
  if (pdev->pClassData != NULL)
  {
      /* Prepare Out endpoint to receive next packet */
      USBD_LL_PrepareReceive(pdev,
                             WIN_OUT_EP,
                             hwin->RxBuffer,
                             WIN_DATA_OUT_MAX_PACKET_SIZE);
    return USBD_OK;
  }
  else
  {
    return USBD_FAIL;
  }
}

值得注意的是,接收函数的数据依然在回调中处理,需要用户注册回调函数。

其余部分的实现基本和CDC模板工程一样,详细可见参考工程。

三,实现WinUSB 2.0

TODO

四,上位机编写测试

TODO

参考文章

WinUSB-USB中文网

STM32 WinUSB(WCID)免驱高速通信 20M/s

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