使用微软系统描述符1.0制作免驱动自定义USB设备
本文作者XTOOLBOX,本站得到了作者本人的转载授权。
本文介绍如何使用微软的操作系统描述符来实现自定义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 */
};
属性主要由三个部分组成
- 属性的数据类型,这个类型与注册表中的数据类型相同, 在”winnt.h”头文件中定义。其中比较常用的REG_SZ值为1, REG_MULTI_SZ值为7。
- 属性的名称,以NULL结束的unicode字符串。
- 属性的值,由属性的数据类型确定。
为了能让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,主要通过以下几个步骤访问设备。
- 通过扩展描述符中的GUID查看接口的路径
- 用接口的路径作为参数,调用CreateFile打开接口
- 使用WinUsb_Initialize得到WinUSB句柄
- 通过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设备端代码 https://github.com/xtoolbox/TeenyUSB/tree/master/sample/custom_bulk
- 完整的Windows端代码及VS2019工程 https://github.com/xtoolbox/TeenyUSB/tree/master/sample/win32app/test_winusb
- 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