Bootstrap

8.USB:WINUSB设备(通用串行总线设备)

目录

前言

配置 

设备解析 

 描述符配置

Winusb OS 字符串描述符

Winusb 兼容ID特征描述符

Winusb 扩展属性描述符

通信框架设计

验证

补充 

总结 


前言

经过前面的学习,从USB理论到实践,从工程实例一步步的证实,应该对于USB来说有一定的认识了,到了这里wimusb并没有想象中的神秘,它也只是一个设备类。为了能够在windows下快速建立一个唯一寻址的免驱串行通信设备,微软制定了一个特别厂商类设备,为winusb设备,它的类代码为0xFF(特定厂商类)

配置 

打开usbd_conf.h文件,找到总配置开关,USE_USBD_WINUSB配置为1。其他的为0,注意:USBD_INT_DEBUG 这个配置是在中断调试的配置,如果要运行这个工程,这个配置一定要配置为0。

设备解析 

打开standard_winusb_core.c文件,对应的头文件是standard_winusb_core.h。在打开对应的配置的后,只会有一个设备类的文件生效

 描述符配置

在经历了CDC_ACMHID练手后,可以发现USB设备无非就是这么一些流程,所以先正常配置标准设备描述符

usb_desc_dev winusb_dev_desc =
{
    .header = 
     {
         .bLength          = USB_DEV_DESC_LEN, 
         .bDescriptorType  = USB_DESCTYPE_DEV,
     },
    .bcdUSB                = 0x0200U,
    .bDeviceClass          = 0x00U,
    .bDeviceSubClass       = 0x00U,
    .bDeviceProtocol       = 0x00U,
    .bMaxPacketSize0       = USBD_EP0_MAX_SIZE,
    .idVendor              = USBD_VID,
    .idProduct             = USBD_PID,
    .bcdDevice             = 0x0200U,
    .iManufacturer         = STR_IDX_MFC,
    .iProduct              = STR_IDX_PRODUCT,
    .iSerialNumber         = STR_IDX_SERIAL,
    .bNumberConfigurations = USBD_CFG_MAX_NUM,
};

这里的话我们先不在设备描述符写类代码,全部为0,这样的话主机要在接口中才检索。

对于配置描述符的设计,我们就设计一个单一的数据接口就行了,进行数据的收发。

在接口描述符中 .bInterfaceClass      =  USB_CLASS_WINUSB   

USB_CLASS_WINUSB 的值为 0xFFU

然后设计一个IN批量端点一个OUT批量端点。

到这都是常规的设计,因为0xFF为特定厂商类,所以这个类代码是不被定义的,想要让设备被识别为WINUSB设备,就需要准备一些相关的东西。

1.Winusb OS 字符串描述符

2.Winusb 兼容ID特征描述符

3.Winusb 扩展属性描述符

Winusb OS 字符串描述符

数据结构

定义

上述字段都是固定的,除了vendor字段。

vendor:厂商请求代码,用户自定义,在后续请求中会用到。

这个描述符的请求条件为:当有USB设备插入,系统会向设备索取设备描述符,根据设备描述符中的bcdUSB版本号判断是否检索0xEE位置的OS设备字串描述符,如果bcdUSB版本号大于等于2.0,则系统会检索0xEE位置的OS设备字串描述符。

所以这个描述符的请求还是标准的设备请求,获取描述符,不过索引为0xEE,这个时候我们需要看一下框架中的请求处理器,看一下是否预想了这个情况。

switch (desc_type) {
        case USB_DESCTYPE_DEV:
            transc->xfer_buf = std_desc_get[desc_type - 1U](udev, desc_index, &transc->xfer_len);
            if (64U == req->wLength) {
                transc->xfer_len = 8U;
            }
            break;

        case USB_DESCTYPE_CONFIG:
            transc->xfer_buf = std_desc_get[desc_type - 1U](udev, desc_index, &transc->xfer_len);
            break;

        case USB_DESCTYPE_STR:
            if (desc_index < STR_IDX_MAX) {
                transc->xfer_buf = std_desc_get[desc_type - 1U](udev, desc_index, &transc->xfer_len);
            }
            else/*修改扩展标记,当序列超过最大标准时,进入设备类的请求*/
            {
                udev->class_core->req_process(udev, req);
            }
            break;

描述符类型字符串描述符时,可以发现索引的值要小于定义的最大字符串描述符数量时才进入获取,否则是没有的,上述代码是我修改后的代码,原版的话是如果索引超过就直接错误处理,我们修改一下,如果超过就进入设备类请求处理

然后在设备类请求中判断为获取描述符,索引为0xEE,0x03为获取描述符这个类型请求的值。返回我们定义的Winusb OS 字符串描述符。设置状态为支持的请求,毕竟函数是一层层调用,如果有状态的返回,那么一定要给正确状态,否则会造成硬件不响应总线。

注意:如果调试没有发现主机会获取索引为0xEE的字符串描述符,是因为主机在获取设备描述符的时候会记录设备的 VID 和 PID,在第一次记录的时候才会尝试获取索引为0xEE的字符串描述符,它会记录下来这个设备是不是WINUSB设备。在这之后不管是枚举成功还是失败都不会再获取,因为WINUSB调试难免多次枚举,所以每次枚举失败后,在PID加1就行了,就是每次枚举都使用不同的 PID,让主机认为是新的设备,再次索取,直到真正枚举成功后才固定。

Winusb 兼容ID特征描述符

这个描述符的获取只有在成功获取到  Winusb OS 字符串描述符 为有效之后才会继续获取这个描述符,所以每一步都是循序渐进的,只要有任何一步错误就直接失败。

描述符定义

/*Winusb 兼容ID特征描述符*/
const uint8_t bosfeature_desc[USB_LEN_OS_FEATURE_DESC] = 
{
   0x28, 0, 0, 0, // length
   0, 1,          // bcd version 1.0
   4, 0,          // windex: extended compat ID descritor
   1,             // no of function
   0, 0, 0, 0, 0, 0, 0, // reserve 7 bytes
   // function
   0,             // interface no
   0,             // reserved
   'W', 'I', 'N', 'U', 'S', 'B', 0, 0, //  first ID
   0,   0,   0,   0,   0,   0, 0, 0,  // second ID
   0,   0,   0,   0,   0,   0 // reserved 6 bytes    
};

USB_LEN_OS_FEATURE_DESC 为 0x28

描述符具体的定义可以查看对应规范手册,在文件资源链接处获取。

这里的话  first ID 是固定的  second ID 是自定义的。

描述符获取代码

在成功获取  Winusb OS 字符串描述符 后,主机会记录  vendor 字段,这个字段是用户自定义的,然后主机发出请求,请求代码中bmRequestType的值就为这个 vendor 字段,所以需要留意一下请求处理器中这个字段请求会进入哪里。

这是最开始的请求处理器,一个有三个大类请求,分别是标准设备请求、设备类请求、供应商请求

1.USB_REQTYPE_STRD:当bmRequestType & 0x60 等于 0x00时进入。

2.USB_REQTYPE_CLASS:当bmRequestType & 0x60 等于 0x20时进入。

3.USB_REQTYPE_VENDOR:当bmRequestType & 0x60 等于 0x40时进入。

这个框架是标准设计,基本所有的USB库和协议栈都是这个设计。

所以我们在定义vendor 字段的时候不能随意定义,不能让它进入标准设备请求,不然就不好处理,我们统一在设备类请求中处理,所以它进入设备类请求或供应商请求都可以,所以工程这里定义的是0xA0,刚好进入供应商请求。

框架中的供应商请求是一个空函数,是由用户字节对接的。所以我直接对接到了设备类请求处理了。

最终还是回到了设备类请求处理,统一在这里处理好。

当请求代码符合时进入,判断序列值为 0x04时返回这个描述符。

注意:这个描述符的获取是每次枚举都会获取的,当主机成功为设备加载WINUSB的驱动并记录了 vendor 字段,此后每次上电枚举都只会获取这个描述符。

Winusb 扩展属性描述符

这是最后一步,也是最关键的一步,winusb有一种非完全枚举状态,如果前面两步都获取成功,但是这个描述符获取失败,可以在设备管理器上看到设备枚举成功,但是在通信时无法识别到设备,这个状态可以说是非完全枚举状态,这样是不行的。

描述符定义。

/*Winusb 扩展属性描述符*/
const uint8_t bosproperty_desc[USB_LEN_OS_PROPERTY_DESC] = 
{
  0x8E, 0, 0, 0,  // length 246 byte
  0x00, 0x01,   // BCD version 1.0
  0x05, 0x00,   // Extended Property Descriptor Index(5)
  0x01, 0x00,   // number of section (1)
  //; property section        
  0x84, 0x00, 0x00, 0x00,// size of property section
  0x1, 0, 0, 0,   //; property data type (1)
  0x28, 0,        //; property name length (42)
  'D', 0,
  'e', 0,
  'v', 0,
  'i', 0,
  'c', 0,
  'e', 0,
  'I', 0,
  'n', 0,
  't', 0,
  'e', 0,
  'r', 0,
  'f', 0,
  'a', 0,
  'c', 0,
  'e', 0,
  'G', 0,
  'U', 0,
  'I', 0,
  'D', 0,
  0, 0,
  /* {13eb360b-bc1e-46cb-ac8b-ef3da47b4062} */  /*LJY:{12DE6357-6828-F819-6B8F-A334024A741E}*/
  0x4E, 0, 0, 0,  // ; property data length
  '{', 0,
  '1', 0,
  '3', 0,
  'E', 0,
  'B', 0,
  '3', 0,
  '6', 0,
  '0', 0,
  'B', 0,
  '-', 0,
  'B', 0,
  'C', 0,
  '1', 0,
  'E', 0,
  '-', 0,
  '4', 0,
  '6', 0,
  'C', 0,
  'B', 0,
  '-', 0,
  'A', 0,
  'C', 0,
  '8', 0,
  'B', 0,
  '-', 0,
  'E', 0,
  'F', 0,
  '3', 0,
  'D', 0,
  'A', 0,
  '4', 0,
  '7', 0,
  'B', 0,
  '4', 0,
  '0', 0,
  '6', 0,
  '2', 0,
  '}', 0,
    0, 0,
};

具体的定义可以看注释或参考规范文档,前面基本都是固定的,只有设备GUID字段。

设备GUID概念:

在 USB 设备中,每个设备都有一个唯一的 Vendor ID(供应商标识符)和 Product ID(产品标识符)。这些 ID 是由 USB-IF(USB Implementers Forum)颁发的,用于唯一标识每个 USB 设备的制造商和型号。

除了 Vendor ID 和 Product ID 外,某些 USB 设备还可能包含一个设备特定的 GUID(全局唯一标识符)。这个 GUID 通常是由设备制造商为了在系统中唯一标识设备而分配的。GUID 是一个全局唯一的标识符,它在整个系统中都应该是唯一的,即使是不同的设备也应该有不同的 GUID。

所以,USB 设备的 GUID 在理论上是唯一的,但在实际中,是否每个设备都使用 GUID,以及设备制造商如何为设备分配 GUID,这取决于具体的设备设计和制造商的实践。

GUID是设备的唯一标识ID,可以说是一个设备的身份。所以我们这里的设备GUID为 {13eb360b-bc1e-46cb-ac8b-ef3da47b4062}  这里是可改的,可以直接用我这个,也可以自己去网站生成,直接百度搜设备GUID生成就可以了。

描述符的获取

这个描述符的获取和 兼容ID描述符获取类似,请求代码也是vendor 字段,序列是0x05,这里的话最终是进入设备类请求处理。

注意:这个描述符的获取条件和 Winusb OS字符串描述符的条件是一样的,只有在设备的VID PID是第一次记录时获取,所以主机只有获取了Winusb OS字符串描述符且前面两步都成功后就一定会获取这个描述符,成功获取后,设备的GUID被注册记录。后续对这个设备就不会再获取了。

通信框架设计

通信框架可以参考CDC_ACM的,反正都做好分包处理了,缓冲区大小拉满就行了。

工程中的通信框架设计是只能做回显需求,可以根据自己的场景修改,本质就是调用端点发送接收数据,再CDC_ACM时详细讲过,这里就不过多叙述了。

验证

使用winusb数据上位机进行通信测试,在主页资源可以下载。

首先查看设备管理器,是否有如下设备。

如果有的话可以继续测试,这里不代表完全枚举成功。可以发现这个设备类的驱动可以显示描述符中自定义的信息,就比较舒服,可以自由修改设备名字。

打开winusb数据上位机。

1.把设备的GUID输入上去,然后点Search device,搜索设备。

2.如果看到设备的实例路径出来后,表示设备完全枚举成功。

3.打开设备,然后会显示设备的接口和端点信息。

这里可以看到端点1是OUT,端点2是IN,将端点2的Start打开才能建立通信,这个的本质就和你用串口助手没打开串口是一个道理,只有在打开Start后才开始建立联系,主机才会对这个端点发IN令牌,数据才能正常收发。

数据正常回显,我这里的话是做了一个数据按位取反再回显,所以是没问题的。到这里,我们的WINUSB设备就实现了。

补充 

对于设备而言,每个设备应该都只有唯一的一个 VID(制造商ID)  PID(产品ID),所以VID和PID组合起来就是设备的实例路径

所以可以发现工程中虽然有3个设备类的实现,但是每个设备的VID和PID至少有一个不相同,这是有意为之。

但是这个 VID PID 并不是随便取的,现在还在维护并更新USB协议规范的有一个国际官方组织,叫 USB-IF。官网:https://www.usb.org/

VID需要向官方申请,需要支付一定的费用。所以VID表示就是供应商的身份,然后PID就是它的产品的身份,这个PID可以随便取。

我们在开发的时候,VID和PID可以随便用,但是产品要上市的话需要使用正规合法的VID,当然这个不是我们该关心的事,这个是公司去申请的,所以我们在开发测试的时候随便用就可以了。

付费的好处就是,当你注册了一个VID并通过后,可以在你的产品中使用,别人在使用你的产品时可以看到制造商信息等等是你注册的信息,不然就是默认的。

总结 

到这里我们的WINUSB就结束了,值得注意的是除了枚举设时实例路径要变化外等相关注意外,还需要知道VID和PID对于设备来说的意义,以及设备的GUID。

学到这里,基本上对USB体系以及设备固件开发都有自己的感悟和一定的熟悉了。具体的情况可以根据实践去修改,最重要的是理解,理解了USB就会发现其实很多东西都有规律,而且也很有趣,并不会太难,接下来的话就是实际项目中我在开发更复杂的嵌入式硬件USB外设USB OTG中的历程和总结,并不会像之前解析工程这么细致,因为都学习到这里了,很多东西不说应该也懂了,对于USB OTG会提一下不同的点以及注意事项等。

;