技术背景
我们的板子作为 USB Gadget 设备通过 USB 线接入 USB 主机使用,我们的板子被主机识别为一个 Compsite Device,这个 Compsite Device 是由我们板子根据 Host 口实际接的 USB 设备动态创建的,所以它包含哪些功能,由接在 Host 口的设备决定。假设我们的板的 USB Host 口接了一个键盘和一个鼠标,那个我们的板就会被主机识别为一个支持键盘和鼠标功能的 Compsite Device。我们板上 Host 口接的设备的数据会被转发给 USB 主机。
问题描述
某个鼠标接入我们板 USB Host 口后,我们板子使用此鼠标创建的 USB Gadget 设备无法被 Windows 系统枚举成功,异常现象为:
- 一个联想鼠标,接入我们板,我们板作 USB Gadget 接入 WIN10 系统后,系统无法识别,无法完成 USB 枚举过程
- 这个联想鼠标直接接在 WIN10 系统上,是能正常识别和工作的
- 这个联想鼠标接入 Ubuntu 系统,也是能正常识别和工作的
分析过程
从应用层程序入手,排除 USB 设备描述符与 HID 报文描述符设置等应用层问题,对比分析 Linux 与 Windows 系统 USB 枚举过程差异,跟踪 Linux 内核代码 configfs 驱动部分。
原因解析
Linux 系统在 USB 设枚举过程中获取到设备的实际 HID 报文描述符长度后,会使用此实际长度作为期望的长度,来获取 HID 报文描述符。而 WIN10 会使用实际长度另加上 64 作为期望长度来获取 HID 报文描述符。这是 Windows 系统和 Linux 系统的差异,这个差异会引起异常,而 WIN10 的这种处理机制也是符合 USB 规范的,所以此异常应该是 Linux 内核的一个 BUG。
主机端在获取 HID 报文描述符时如何判断设备端已经应答完毕是这个问题的关键。
控制传输中的最大包长:高速设备最大包长是 64 字节;低速设备是 8;全速设备可以是 8 或 16 或 32 或 64。最大包长表示一个端点单次接收/发送数据的能力,实际上就是该端点对应的缓冲区的大小。当一次传输的数据量超过该端点的最大包长时,需要将数据拆分成多个包传输,只有最后一个包可以小于最大包长,除最后一个包外的其他包都应等于最大包长。所以在一次控制传输中,如果一个端点收到/发送了一个长度小于最大包长的包,则表示此次数据传输结束。
在设备枚举阶段,传输类型是控制传输,最大包长由 MaxPacketSize 参数(对于 USB 2.0 设备此值是 64)指定。一次控制传输的数据会被切割成多个包,只有最后一个包的长度可以小于最大包长。当主机接收到的数据量已经等于期待数据长度时,直接就可以判定应答已经结束了。当主机接收的数据量小于期待数据长度时,若接收到的最新的应答数据包的长度小于最大包长,说明这个包是这次传输的最后一个包,表示应答已完毕;若最新收到的数据包等于最大包长,主机是无法判断这个包是不是本次传输的最后一个包的 ,USB 协议规定,针对这种情况,当设备端应答的数据量小于主机侧期待的数据长度,而应答数据量又刚好能被最大包长整除(即被切割成多个完整的包),就需要额外再发送一个 ZLP 包(Zero Length Packet)以指示本次传输完成。
异常的这个联想鼠标,它的 HID 报文描述符是 64 字节长(刚好等于 MaxPacketSize 参数),当 WIN10 系统以期望长度 128(64+64) 来获取 HID 报文描述符时,内核 Gadget 驱动只回复了实际的 64 个字节(它没有发 ZLP 包)的 HID 报文描述符,此时 Linux Gadget 认为自己已经应答完毕,而 WIN10 收到的回复不足 128 字节但最后一个包又是整包,它认为传输还没完成一直在等新数据,直到超时,总线进入异常状态,WIN10 复位 USB 总线,设备侧内核 USB 驱动模块进入错乱状态。原因就是,Linux Gadget 驱动在这种特殊情况下没发 ZLP 包。这就是问题描述中现象 1 的原因。
为什么这个鼠标接在 Ububtu 上又正常呢,因为 Ubuntu 期待的长度就是 64,收到 64 字节的应答,它就认为应答已经完毕。
解决方法
知道了原因就好解决了。修复内核 BUG,在 Gadget 的驱动代码里,判断当一次应答的数据量小于主机期待的长度且应答数据量又能被最大包长整除时,多发一个 ZLP 包。问题解决。