为MCU的全速USB添加WinUSB免驱支持 CH32X035系列WinUSB免驱通信配置

为MCU的全速USB添加WinUSB免驱支持 CH32X035系列WinUSB免驱通信配置

接触过USB开发的用户或许都知道,编写Windows下的USB驱动程序较为繁琐,Win8.1+还有强制数字签名的问题(虽然可以让最终用户使用Zadig等自签名工具绕过);而免驱通信常用的HID自定义设备又面临HID通信速率较低的问题。是否有一种可以不必编写驱动程序,又能使用批量端点快速传输的方法呢?答案是有的,微软在Win7及更高版本操作系统上提供了WinUSB驱动程序,并在Win8.1+或更高版本的操作系统上不必提供额外的inf文件,实现真正意义上的免安装驱动。(较早版本的Win8可能需要安装几个特定的系统更新才能使用)

但这并不意味着WinUSB完全不需要额外的操作,实际上这需要在USB枚举阶段响应特定的请求,并提供特定的信息供操作系统初始化WinUSB。本文将帮助你通过几个步骤为全速USB设备添加WinUSB支持。本文以CH32X035系列芯片为例,使用USBFS外设。CH32系的USBD外设可按同样原理操作。

WinUSB的配置过程

WinUSB的自动配置依赖于操作系统在枚举USB设备时的一些描述符,下面将简单介绍WinUSB的配置过程和相关的描述符。

由于测试时MSOS1.0描述符在笔者的Win11计算机上未按预期作用,故只介绍通过MSOS2.0描述符配置WinUSB。该方式下,描述符的获取过程是通过以下方式进行的:

1
2
3
4
5
6
7
8
9
10
11
// 以伪代码方式描述
获取设备描述符();
if(bcdUSB >= 2.01){
获取BOS描述符();
if(BOS描述符中含有WCID信息){
发起厂商请求获取MSOS2.0描述符();
if(MSOS2.0描述符含有有效的WinUSB信息){
初始化WinUSB();
}
}
}

不难发现,上面的过程涉及3个描述符,分别是设备描述符BOS描述符MSOS2.0描述符。每个描述符为操作系统请求下一个描述符提供引导指示。我们可以列出每个描述符被请求到的条件和被请求失败的后果:

  • 设备描述符
    • 条件:无条件
    • 请求失败后果:设备无法枚举,报“代码43,请求 USB 设备描述符失败。”
  • BOS描述符
    • 条件:设备描述符中bcdUSB字段大于等于2.01(也有资料写大于等于2.10)
    • 请求失败后果:设备无法枚举,报“代码43,请求 USB BOS 描述符失败。”或“代码10,指定不存在的设备。”
  • MSOS2.0描述符
    • 条件:BOS描述符中有有效的WCID信息
    • 请求失败后果:报“代码43,USB 设备返回的 Microsoft 操作系统 2.0 描述符集无效。”或“代码10,指定不存在的设备。”

编写描述符

那么,为了使得WinUSB可以正确初始化,我们需要编写相应的描述符。

设备描述符的编写

设备描述符绝大多数USB应用中已经存在,只需要修改bcdUSB为2.01或更高(①处)即可。以下是一个样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const uint8_t deviceDesc[] =
{
0x12, // bLength
0x01, // bDescriptorType (Device)
0x10, 0x02, // bcdUSB 2.10 【①】
0x00, // bDeviceClass
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
64U, // bMaxPacketSize0 64
0x11, 0x45, // idVendor
0x19, 0x19, // idProduct
0x00, 0x01, // bcdDevice
0x01, // iManufacturer (String Index)
0x02, // iProduct (String Index)
0x03, // iSerialNumber (String Index)
0x01 // bNumConfigurations 1
};

BOS描述符的编写

BOS描述符的编写格式相对固定,只有2处需要按实际情况修改。分别是MSOS2.0描述符的总长度(①处)和厂商码(②处)。厂商码是自定义的一个byte数值,用于和设备可能使用的其他厂商请求区分,本例中为0x01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const uint8_t bosDesc[] =
{
// BOS Base Descriptor
0x05, //bLength
0x0f, //bDescriptorType
0x21, 0x00, // wTotalLength
0x01, // bNumDeviceCaps

// BOS Device Capability Descriptor
0x1c, // bLength
0x10, // bDescriptorType
0x05, // bDevCapabilityType
0x00, // bReserved
0xdf, 0x60, 0xdd, 0xd8, 0x89, 0x45, 0xc7, 0x4c, // bPlatformCapabilityUUID_16
0x9c, 0xd2, 0x65, 0x9d, 0x9e, 0x64, 0x8a, 0x9f, // bPlatformCapabilityUUID_16
0x00, 0x00, 0x03, 0x06, // dwWindowsVersion Win8.1+
0xa2, 0x00, // wDescriptorSetTotalLength 【①】
0x01, // bVendorCode 【②】
0x00, // bAltEnumCode
};

MSOS2.0描述符的编写

该部分是最为复杂的部分。编写时,需要根据该设备具有多少个接口(不论其中是否有不被WinUSB管理的接口)决定其格式。

若设备只有一个接口,则MSOS2.0描述符应有以下条目:

  • Descriptor Set Descriptor(描述符集描述符这什么鬼名字
  • Compatible ID Descriptor(兼容ID描述符)
  • Registry Property Descriptor(注册表属性描述符)

若设备有多于一个接口,则MSOS2.0描述符应有以下条目:

  • Descriptor Set Descriptor(描述符集描述符这什么鬼名字
  • 对于每个需要被WinUSB管理的接口:
    • Function Subset Descriptor(功能子集描述符)
    • Compatible ID Descriptor(兼容ID描述符)
    • Registry Property Descriptor(注册表属性描述符)

开发时需要注意修改的主要有MSOS2.0描述符的总长度(①处)和设备的接口ID(③处)。设备的接口ID可用于多个VID/PID不同的设备通过同一接口ID适配于同一应用程序,例子中为CMSIS-DAP的ID,请在实际开发中替换为自己的ID。该ID遵循UTF-16 LE格式,使用双NULL结束字符串。若设备有多于一个接口,还需要注意修改每个功能子集描述符所描述接口的接口序号(②处)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const uint8_t msos2Desc[] =
{
// MSOS2.0 Descriptor Set Descriptor
0x0a, 0x00, // wLength
0x00, 0x00, // wDescriptorType
0x00, 0x00, 0x03, 0x06, // dwWindowsVersion
0xa2, 0x00, // wDescriptorSetTotalLength 【①】

/*
// MSOS2.0 Function Subset Descriptor
0x08, 0x00, // wLength
0x02, 0x00, // wDescriptorType
0x00, // bFirstInterface 【②】
0x00, // bReserved
0xa0, 0x00, // wSubsetLength
*/

// MSOS2.0 Compatible ID Descriptor
0x14, 0x00, // wLength
0x03, 0x00, // wDescriptorType
'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00, // cCID_8
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // cSubCID_8

// MSOS2.0 Registry Property Descriptor
0x84, 0x00, // wLength
0x04, 0x00, // wDescriptorType
0x07, 0x00, // wPropertyDataType

0x2a, 0x00, // wPropertyNameLength
// String "DeviceInterfaceGUIDs"
'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00,
'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00,
't', 0x00, 'e', 0x00, 'r', 0x00, 'f', 0x00,
'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00,
'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00,
0x00, 0x00,

0x50, 0x00, // wPropertyDataLength
// String "{CDB3B5AD-293B-4663-AA36-1AAE46463776}" 【③】
'{', 0x00, 'C', 0x00, 'D', 0x00, 'B', 0x00,
'3', 0x00, 'B', 0x00, '5', 0x00, 'A', 0x00,
'D', 0x00, '-', 0x00, '2', 0x00, '9', 0x00,
'3', 0x00, 'B', 0x00, '-', 0x00, '4', 0x00,
'6', 0x00, '6', 0x00, '3', 0x00, '-', 0x00,
'A', 0x00, 'A', 0x00, '3', 0x00, '6', 0x00,
'-', 0x00, '1', 0x00, 'A', 0x00, 'A', 0x00,
'E', 0x00, '4', 0x00, '6', 0x00, '4', 0x00,
'6', 0x00, '3', 0x00, '7', 0x00, '7', 0x00,
'6', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00,
};

向主机提供描述符

前面提到,主机需要在特定的条件下才会获取BOS描述符和MSOS2.0描述符。我们需要让USB框架响应特定的请求,并回复主机正确的描述符。

具体来说,需要额外添加2条逻辑。添加逻辑时请务必注意协议栈中的逻辑层级关系,部分例程的USB协议栈代码可能与文中不同,请按正确的逻辑层级适当补充所需要的代码。

首先添加响应请求BOS描述符的逻辑。主机请求BOS描述符时,发出的请求是标准请求-获取描述符-获取BOS描述符(请求码0x0f)。在CH32X035的USB协议栈实现中处理该请求位于该处:(部分例程可能缺失这一个case,手动添加即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void USBFS_IRQHandler(void)
{
// 省略无关代码
if (intflag & USBFS_UIF_TRANSFER) // 传输中断
{
switch (intst & USBFS_UIS_TOKEN_MASK) // 判断传输令牌
{
// 省略无关代码
case USBFS_UIS_TOKEN_SETUP: // SETUP令牌
// 省略无关代码
if ((USBFS_SetupReqType & USB_REQ_TYP_MASK) != USB_REQ_TYP_STANDARD) // 处理非标准请求
{
// 省略无关代码
} else { // 处理标准请求
switch (USBFS_SetupReqCode)
{
case USB_GET_DESCRIPTOR: // 获取描述符
switch ((uint8_t)(USBFS_SetupReqValue >> 8)) // 判断获取的描述符类型
{
// 省略无关代码
/* ####【编写的逻辑开始】#### */
case USB_DESCR_TYP_BOS: // 获取BOS描述符
pUSBFS_Descr = bosDesc; // 返回BOS描述符
len = USBDESC_BOS_LEN; // 指定发送长度
break;
/* ####【编写的逻辑结束】#### */
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}

然后添加响应MSOS2.0描述符的逻辑。这里需要用到编写BOS描述符时指定的厂商码。本文例子中厂商码为0x01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void USBFS_IRQHandler(void)
{
// 省略无关代码
if (intflag & USBFS_UIF_TRANSFER) // 传输中断
{
switch (intst & USBFS_UIS_TOKEN_MASK) // 判断传输令牌
{
// 省略无关代码
case USBFS_UIS_TOKEN_SETUP: // SETUP令牌
// 省略无关代码
if ((USBFS_SetupReqType & USB_REQ_TYP_MASK) != USB_REQ_TYP_STANDARD) // 处理非标准请求
{
// 省略无关代码
/* ####【编写的逻辑开始】#### */
if (USBFS_SetupReqType & USB_REQ_TYP_VENDOR) // 处理厂商请求
{
if (USBFS_SetupReqCode == 0x01) // 判断厂商码
{
if (USBFS_SetupReqIndex == 0x0007)
{
pUSBFS_Descr = (uint8_t *)msos2Desc; // 返回MSOS2.0描述符
len = USBDESC_MSOS2_LEN; // 指定发送长度
}
}
}
/* ####【编写的逻辑结束】#### */
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}

CH32X035的部分例程可能未处理非标准请求上行数据,可参考以下代码补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void USBFS_IRQHandler(void)
{
// 省略无关代码
if (intflag & USBFS_UIF_TRANSFER) // 传输中断
{
switch (intst & USBFS_UIS_TOKEN_MASK) // 判断传输令牌
{
// 省略无关代码
case USBFS_UIS_TOKEN_IN:
switch (intst & (USBFS_UIS_TOKEN_MASK | USBFS_UIS_ENDP_MASK))
{

case USBFS_UIS_TOKEN_IN | DEF_UEP0: // 端点0上行数据
// 省略无关代码
if ((USBFS_SetupReqType & USB_REQ_TYP_MASK) != USB_REQ_TYP_STANDARD) // 非标准请求的上行数据
{
/* ####【从这里开始是需要补充的代码】#### */
if (USBFS_SetupReqType & USB_REQ_TYP_VENDOR) // 非标准请求-厂商请求
{
len = USBFS_SetupReqLen >= DEF_USBD_UEP0_SIZE ? DEF_USBD_UEP0_SIZE : USBFS_SetupReqLen;
memcpy(USBFS_EP0_4Buf, pUSBFS_Descr, len);
USBFS_SetupReqLen -= len;
pUSBFS_Descr += len;
USBFSD->UEP0_TX_LEN = len;
USBFSD->UEP0_CTRL_H ^= USBFS_UEP_T_TOG;
}
/* ####【需要补充的代码到此为止】#### */
}
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}
// 省略无关代码
}

Done!

至此,如果描述符编写没有错误,响应描述符请求的逻辑没有问题,编译上传复位一把梭后,计算机将会识别到该设备并自动配置WinUSB驱动。

如果没有,重点检查以下几个问题:

  • 各个描述符中涉及长度的字段是否正确
  • 添加的两条逻辑是否被正确调用到
  • 设备是否在上行阶段发送了正确长度的数据

另外需要补充的一点是,调试枚举阶段问题时,Bus Hound等纯软件抓包方法可能起不到很大作用。如果设备无法正常枚举完成,Bus Hound是无法捕获数据的。

为MCU的全速USB添加WinUSB免驱支持 CH32X035系列WinUSB免驱通信配置

https://wuxiproj.mzy7.cn/posts/3c2970c9.html

作者

MZY7

发布于

2024-08-31

更新于

2024-12-07

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×