使用微软系统描述符1.0制作免驱动自定义USB设备


本文介绍如何使用微软的操作系统描述符来实现自定义USB设备在Windows系统上的免驱动使用。

前言

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

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

为了给设备安装WinUSB驱动,还需要使用包含设备VID和PID的inf文件,以及签名的cat文件(ST-Link就是使用WinUSB作为驱动的,它的驱动就是一个典型的WinUSB设备驱动)。

对于使用WinUSB驱动的设备来说,这些驱动文件做了两件事,1. 告诉系统使用这个设备使用了WinUSB驱动,2. 告诉系统我这个设备接口的GUID是什么,应用程序就能通过GUID来操作设备。

既然WinUSB的驱动程序只做了这两件事,能不能让这两件事更加自动化一些,这样不作编写驱动程序也可以安装。答案是肯定的,微软的操作系统描述符就是处理这个事情的。

操作系统字符串描述符

Windows在枚举设备时会先读取设备描述符和配置描述符,判断设备描述符中的bcdUSB字段,检查设备支持的USB版本号是否大于等于2.0,即bcdUSB大于等于0x0200。如果bcdUSB小于0x0200,则说明设备不支持,后续不会去请求 OS字符串描述符。

如果大于等于,说明设备有可能支持OS字符串描述符,会在下一步去获取index为0xee的OS字符串描述符。OS字符串描述符的格式固定,总长度为18,字符串前面的内容为unicode编码的”MSFT100″,最一个字符的低8位是vendor code,由厂商自己定义。

如果OS字符串描述符获取成功,会在注册表的这个位置[\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\usbflags]建立一个以设备VID,PID,版本号为索引的项。 并在此项中建立一个osvc字段,字段值为01,<上报的vendor code>。

例如设备描述符中的idVendor为0x0483,idProduct为0x0001,bcdDevice为0x0100,OS字符串描述符中vendor code为0x17。注册表的[\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\usbflags]中会建立一个名为048300010100的项,里面会建立一个osvc字段,字段值为01 17。如下图所示

如果注册表中已经有了这个字段,系统将不会再发起OS字符串描述符的获取请求。我们可以将上面的osvc中的值修改成00 00,表示不支持OS描述符。Windows获取到设备描述符后,发现对应的usbflags已经存在,并且不支持OS描述符。这个时候Windows不会再发送与OS描述符相关的请求。如果设备本来能够正常安装驱动,此时也会变得不正常。因此在开发USB设备的过程中,如果遇到驱动不正常,有时候并不是设备方面的问题,还有可能是因为用到了原来的一些信息,导致的错误。为了避免这种情况,一种办法是完全删除设备有关的旧内容。另一种办法是改设备的VID、PID或者设备版本号,让设备一直是最新的状态。

扩展兼容ID描述符

在获取到OS字符串描述,并且验证通过后,Windows会获取功能描述符。功能描述分类两种,一种是扩展兼容ID描述符(Extend Compat ID),wIndex值为4,一种是扩展属性(Extend Properties),wIndex为5。

Windows支持多种兼容描述符,如PTP、MTP、RNDIS,这里只说WinUSB的情况。兼容描述符由头和功能组成,头部内容固定为16字节,包含描述符总长度,版本号,接口数量等信息,如下

{
  ///////////////////////////////////////
  /// WCID descriptor
  ///////////////////////////////////////
  0x28, 0x00, 0x00, 0x00,                           /* dwLength */
  0x00, 0x01,                                       /* bcdVersion */
  0x04, 0x00,                                       /* wIndex */
  0x01,                                             /* bCount */
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,         /* bReserved_7 */
}

上面的头说明描述符总长度为40字节,共有一个接口。

然后接下来是功能部分,功能部分可以有多个,在这里说明接口号和接口的兼容ID,如下:

///////////////////////////////////////
/// WCID function descriptor
///////////////////////////////////////
0x00,                                             /* bFirstInterfaceNumber */
0x01,                                             /* bReserved */
/* WINUSB */
'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,         /* cCID_8 */
/*  */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   /* cSubCID_8 */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,               /* bReserved_6 */

上面的功能部分表示0号接口的兼容ID为”WINUSB”,没有子兼容ID。

完整的兼容描述符如下,这个描述符由TeenyDT在线生成

WEAK __ALIGN_BEGIN const uint8_t WINUSB_WCIDDescriptor [40] __ALIGN_END = {
  ///////////////////////////////////////
  /// WCID descriptor
  ///////////////////////////////////////
  0x28, 0x00, 0x00, 0x00,                           /* dwLength */
  0x00, 0x01,                                       /* bcdVersion */
  0x04, 0x00,                                       /* wIndex */
  0x01,                                             /* bCount */
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,         /* bReserved_7 */
  
  ///////////////////////////////////////
  /// WCID function descriptor
  ///////////////////////////////////////
  0x00,                                             /* bFirstInterfaceNumber */
  0x01,                                             /* bReserved */
  /* WINUSB */
  'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,         /* cCID_8 */
  /*  */
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,   /* cSubCID_8 */
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00,               /* bReserved_6 */
};

获取兼容描述符的请求内容如下图所示:

如上图所示,Windows会通过厂商请求来获取兼容ID,其中的bRequest即为OS字符串中上报的vendor code。上图中还可以看到,Windows先请求了头16个字节的数据,然后再根据实际长度请求了完整的数据。

扩展属性描述符

获取到兼容ID后,系统会根据兼容ID的情况请求属性描述符。对于WinUSB设备,前文中提到,需要做两件事,1. 告诉系统使用什么驱动,2. 告诉系统接口的Guid。通过前面的兼容ID描述符,系统已经知道了设备需要WinUSB驱动。通过扩展属性,告诉系统我们设备的GUID是什么。扩展属性描述符也是由头和属性部分组成的。头部告诉系统描述符的总长度以及属性的数量,然后后面跟着各个属性。属性描述符内容如下:

WEAK __ALIGN_BEGIN const uint8_t COMP_IF3_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} */
  '{', 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 */
};

属性主要由三个部分组成

  1. 属性的数据类型,这个类型与注册表中的数据类型相同, 在”winnt.h”头文件中定义。其中比较常用的REG_SZ值为1,REG_MULTI_SZ值为7。
  2. 属性的名称,以NULL结束的unicode字符串。
  3. 属性的值,由属性的数据类型确定。

为了能让WinUSB设备被应用层软件访问,需要设置设备各个接口的DeviceInterfaceGUIDs,上述的描述符请求成功后,会在注册表的[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\VID_0483&PID_0001\TUSB123456\Device Parameters]项中建立一个名为DeviceInterfaceGUIDs的键,内容为我们上报的Guid。其中VID和PID为我们设备的VID和PID,TUSB123456为设备的序列号。根据这个DeviceInterfaceGUIDs中的GUID,就可以通过Windows的API发现并打开设备接口。

仔细看上图会发现一个问题,我们上报的数据类型是1,应该是REG_SZ,而且属性名字是DeviceInterfaceGUID,在这里类型被替换成了REG_MULTI_SZ,名称也变成了DeviceInterfaceGUIDs。这个地方可能是兼容性的原因,Windows系统帮我们处理了。如果我们直接上报成类型为REG_MULTI_SZ的DeviceInterfaceGUIDs也是可以的。

获取设备扩展属性描述符的请求如下图所示

和前面的兼容ID一样,Windows系统也是先获取了头部内容,得到实际长度后,再获取完整的描述符内容。

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

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

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

多接口的情况

对于多接口的情况,兼容ID描述符中会包含多个设备的兼容ID。而扩展属性描述符会通过多次请求,指定不同的接口号来获取。每个接口的GUID可以相同,也可以不同。完成的多接口描述符可以在TeenyDT的在线工具中看到,这里不再列出。在线工具地址为http://dt1.tusb.org,进入在线工具后,选择WinUSB,点击【==> TeenyUSB .c】按钮,就可以在生成的C语言格式描述符中看到兼容描述符和扩展描述符。

如何使用WinUSB设备

前面介绍了如何通过一些特殊描述符,来实现Windows上WinUSB驱动的自动安装。接下来介绍在Windows上如何使用WinUSB设备。

Windows为WinUSB设备提供了API,主要通过以下几个步骤访问设备。

  1. 通过扩展描述符中的GUID查看接口的路径
  2. 用接口的路径作为参数,调用CreateFile打开接口
  3. 使用WinUsb_Initialize得到WinUSB句柄
  4. 通过WinUsb_WritePipe和WinUsb_ReadPipe对接口进行读写操作

下面代码完成了根据GUID查找设备路径,然后打开设备,向设备发送一些数据,再回读一些数据的功能。

// ensure data size is not multiple of endpoint Max Packet size or we will send a zero length packet
UCHAR test_data_out[256-1];

UCHAR test_data_in[1024];

int main()
{
  const TCHAR* path = get_first_interface_device_path(_T("{1D4B2365-4749-48EA-B38A-7C6FDDDD7E23}"));
  if (path == NULL) {
    printf("device not found\n");
    return 0;
  }
  _tprintf(_T("Got device interface %s\n"), path);


  HANDLE hDev = CreateFile(path,
    GENERIC_WRITE | GENERIC_READ,
    FILE_SHARE_WRITE | FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
    NULL);

  if (INVALID_HANDLE_VALUE == hDev) {

    printf("fail to open device, last error %d\n", GetLastError());
    return 0;
  }

  WINUSB_INTERFACE_HANDLE hWinusb;

  BOOL bResult = WinUsb_Initialize(hDev, &hWinusb);

  if (!bResult) {
    CloseHandle(hDev);
    printf("Fail to invoke WinUsb_Initialize, last error %d\n", GetLastError());
    return 0;
  }

  ULONG transferred;
  for (int i = 0; i<sizeof(test_data_out); i++) {
    test_data_out[i] = i;
  }
  bResult = WinUsb_WritePipe(hWinusb, 0x01, test_data_out, sizeof(test_data_out), &transferred, NULL);
  if (!bResult) {
    printf("Fail to invoke WinUsb_WritePipe, last error %d\n", GetLastError());
    goto error;
  }
  printf("Write %d bytes data\n", transferred);

  bResult = WinUsb_ReadPipe(hWinusb, 0x81, test_data_in, sizeof(test_data_in), &transferred, NULL);
  if (!bResult) {
    printf("Fail to invoke WinUsb_WritePipe, last error %d\n", GetLastError());
    goto error;
  }
  printf("Read back %d bytes data\n", transferred);

error:
  
  WinUsb_Free(hWinusb);
  CloseHandle(hDev);

  exit(0);
}

 

附录

完整的USB设备端代码

完整的Windows端代码及VS2019工程

单接口WinUSB设备枚举过程抓包使用USB Packet Viewer查看

多接口WinUSB设备枚举过程抓包使用USB Packet Viewer查看

参考文档

https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon

https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/winusb

https://msdn.microsoft.com/zh-cn/windows/hardware/gg463179

https://github.com/pbatard/libwdi/wiki/WCID-Devices

Microsoft OS 1.0 Descriptor Specification