如何从 USB 管道错误中恢复
注意
本文适用于设备驱动程序开发人员。 如果你在使用 USB 设备时遇到困难,请参阅修复 Windows 中的 USB-C 问题
本文提供了有关向 USB 管道传输数据失败时可以尝试的步骤的信息。 本文中所述的机制涵盖批量、中断和等时管道上的中止、重置和循环端口操作。
USB 客户端驱动程序通过向默认终结点发送控制传输来与其设备通信;数据传输到设备的批量、中断和等时终结点。 有时,由于各种原因(例如端点中的停滞条件),这些传输可能会失败。 如果传输失败,则相关管道在清除错误条件之前无法处理请求。
对于控制传输,USB 驱动程序堆栈会自动清除错误条件。 对于数据传输,客户端必须采取适当的步骤才能从错误状态中恢复。 数据传输失败时,USB 驱动程序堆栈会通过失败的 USBD 状态代码向客户端驱动程序报告错误。 根据状态代码,驱动程序可以提供错误恢复机制。
本文提供了通过这些操作进行错误恢复的指南。
- 重置 USB 管道
- 重置设备连接到的 USB 端口
- 循环 USB 端口以重新枚举客户端驱动程序的设备堆栈
若要清除错误条件,请从重置管道操作开始,并仅在必要时执行更复杂的操作,如重置端口和循环端口。
关于协调各种恢复机制:
客户端驱动程序必须协调不同的恢复操作,并确保在给定时间只使用一种方法。 例如,考虑具有两个终结点的设备:批量和中断。 在向设备发送了一些数据传输请求后,驱动程序注意到批量管道上的请求失败。 为了从这些错误中恢复,驱动程序重置了批量管道。 但是,该操作并不能解决传输错误,批量传输继续失败。 因此,驱动程序发出重置 USB 端口的请求。 同时,中断管道上的传输开始失败,然后发出重置设备请求。 为了从中断传输失败中恢复,驱动程序在中断管道上发出重置管道请求。 如果这两个操作不协调,由于两个管道上的故障,驱动程序可以同时启动两个重置设备操作。 这些同时进行的操作可能会产生问题。
客户端驱动程序必须确保在给定时间,驱动程序只执行一个重置端口或循环端口操作。 在这些操作期间,任何管道上都不应进行重置管道操作,并且驱动程序不得发出新的重置管道请求。
需要了解的事项
本文使用内核模式驱动程序框架 (KMDF)。
先决条件
客户端驱动程序必须已创建框架 USB 目标设备对象。
如果您使用的是 Microsoft Visual Studio Professional 2012 附带的 USB 模板,则模板代码将执行这些任务。 模板代码会获取目标设备对象的句柄并将其存储在设备上下文中。
KMDF 客户端驱动程序必须调用 WdfUsbTargetDeviceCreateWithParameters 方法来获取 WDFUSBDEVICE 句柄。 有关详细信息,请参阅了解 USB 客户端驱动程序代码结构 (KMDF) 中的“设备源代码”。
客户端驱动程序必须具有框架目标管道对象的句柄。 有关详细信息,请参阅如何枚举 USB 管道。
步骤 1:确定错误情况的原因
客户端驱动程序通过使用 USB 请求块 (URB) 启动数据传输。 请求完成后,USB 驱动程序堆栈将返回一个 USBD 状态代码,该代码指示传输是成功还是失败。 如果失败,USBD 代码将指示失败的原因。
- 如果通过调用 WdfUsbTargetDeviceSendUrbSynchronously 方法提交 URB,请在方法返回后检查 URB 结构的 Hdr.Status 成员。
- 如果通过调用 WdfRequestSend 方法异步提交 URB,请检查 EVT_WDF_REQUEST_COMPLETION_ROUTINE 中的 URB 状态。 Params 参数指向 WDF_REQUEST_COMPLETION_PARAMS 结构。 若要检查 USBD 状态代码,请检查 Usb-UsbdStatus> 成员。 有关此代码的信息,请参阅 USBD_STATUS。
传输失败可能是由设备错误引起的,例如 USBD_STATUS_STALL_PID 或 USBD_STATUS_BABBLE_DETECTED。 它们也可能是由于主机控制器报告的错误造成的,例如 USBD_STATUS_XACT_ERROR。
步骤 2:确定设备是否已连接到端口
在发出重置管道或设备的任何请求之前,请确保设备已连接。 可以通过调用 WdfUsbTargetDeviceIsConnectedSynchronous 方法来确定设备的连接状态。
步骤 3:取消所有挂起的管道传输
在发送任何重置管道或端口的请求之前,请取消向管道的所有挂起传输请求,USB 驱动程序堆栈尚未完成。 可以通过以下方式之一取消请求:
通过调用 WdfIoTargetStop 方法停止 I/O 目标。
若要停止 I/O 目标,请先通过调用 WdfUsbTargetPipeGetIoTarget 方法获取与框架管道对象关联的 WDFIOTARGET 句柄。 通过使用句柄,调用 WdfIoTargetStop。 在调用中,将操作设置为 WdfIoTargetCancelSentIo(请参阅 WDF_IO_TARGET_SENT_IO_ACTION)**,以指示框架取消 USB 驱动程序堆栈尚未完成的所有请求。 对于已完成的请求,客户端驱动程序必须等待其完成回调才能被框架调用。
发送中止管道请求。 可以通过调用以下方法之一来发送请求:
调用 WdfUsbTargetPipeAbortSynchronously 方法。
该调用是同步的,只有在取消所有挂起的请求后才会返回。 WdfUsbTargetPipeAbortSynchronously 接受可选的 Request 参数。 建议将 WDFREQUEST 句柄传递给预分配的框架请求对象。 该参数使框架能够使用指定的请求对象,而不是驱动程序无法访问的内部请求对象。 此参数值可确保 WdfUsbTargetPipeAbortSynchronously 不会因内存不足而失败。
调用 WdfUsbTargetPipeFormatRequestForAbort 方法,为中止管道请求格式化请求对象,然后通过调用 WdfRequestSend 方法发送请求。
如果驱动程序以异步方式发送请求,则必须指定一个指针指向驱动程序实现的驱动程序的 EVT_WDF_REQUEST_COMPLETION_ROUTINE。 若要指定指针,请调用 WdfRequestSetCompletionRoutine 方法。
驱动程序可以通过指定 WDF_REQUEST_SEND_OPTION_SYNCHRONOUS 作为 WdfRequestSend 中的请求选项之一来同步发送请求。 如果以同步方式发送请求,则改为调用 WdfUsbTargetPipeAbortSynchronously。
步骤 4:重置 USB 管道
通过重置管道启动错误恢复。 您可以通过调用以下方法之一发送重置管道请求:
调用 WdfUsbTargetPipeResetSynchronously 以同步发送重置管道请求。
调用 WdfUsbTargetPipeFormatRequestForReset 方法,为重置管道请求格式化请求对象,然后通过调用 WdfRequestSend 方法来发送请求。 这些调用与步骤 3 中所述的中止管道请求的调用类似。
注意
在重置管道操作完成之前,不要发送任何新的传输请求。
重置管道请求清除设备和主机控制器硬件中的错误条件。 为了清除设备错误,USB 驱动程序堆栈使用 ENDPOINT_HALT 功能选择器向设备发送 CLEAR_FEATURE 控制请求。 请求的接收方是与管道关联的终结点。 如果错误情况发生在等时管道上,则驱动程序堆栈不会采取任何操作来清除设备,因为在发生错误的情况下,等时终结点会自动清除。
为了清除主机控制器错误,驱动程序堆栈将清除管道的 HALT 状态,并将管道的数据切换重置为 0。
步骤 5:重置 USB 端口
如果重置管道操作未清除错误条件,并且数据传输继续失败,请发送重置端口请求。
取消到设备的所有传输。 为此,请枚举当前配置中的所有管道,并取消为每个管道计划挂起的请求。
停止设备的 I/O 目标。
调用 WdfUsbTargetDeviceGetIoTarget 方法以获取与框架目标设备对象关联的 WDFIOTARGET 句柄。 然后,调用 WdfIoTargetStop,并指定 WDFIOTARGET 句柄。 在调用中,将操作设置为 WdfIoTargetCancelSentIo (WDF_IO_TARGET_SENT_IO_ACTION)。
通过调用 WdfUsbTargetDeviceResetPortSynchronously 方法发送重置端口请求。
重置端口操作会导致设备在 USB 总线上重新枚举。 USB 驱动程序堆栈在枚举后保留设备配置。 客户端驱动程序可以使用以前获取的管道句柄,因为驱动程序堆栈可确保现有管道句柄保持有效。
无法重置复合设备的单个功能。 对于复合设备,当特定功能的客户端驱动程序发送重置端口请求时,整个设备都会被重置。 如果 USB 设备保持状态,则重置端口请求可能会影响其他功能的客户端驱动程序。 因此,客户端驱动程序在重置端口之前尝试重置管道非常重要。
步骤 6:循环 USB 端口
循环端口操作类似于拔下并插回端口的设备,除了设备没有断开电气连接。 设备断开连接,在软件中重新连接。 此操作会导致设备重置和枚举。 因此,PnP 管理器将重新生成设备节点。
如果重置端口操作未清除错误条件,并且数据传输继续失败,请发送循环端口请求。
取消到设备的所有传输。 请确保取消当前配置中为每个管道计划挂起的请求(请参阅步骤 3)。
停止设备的 I/O 目标。
调用 WdfUsbTargetDeviceGetIoTarget 方法以获取与框架目标设备对象关联的 WDFIOTARGET 句柄。 然后,调用 WdfIoTargetStop,并指定 WDFIOTARGET 句柄。 在调用中,将操作设置为 WdfIoTargetCancelSentIo (WDF_IO_TARGET_SENT_IO_ACTION)。
通过调用以下方法之一发送循环端口请求:
- 调用 WdfUsbTargetDeviceCyclePortSynchronously 以同步发送循环端口请求。
- 调用 WdfUsbTargetDeviceFormatRequestForCyclePort 方法,为循环端口请求格式化请求对象,然后通过调用 WdfRequestSend 方法来发送请求。 这些调用与步骤 3 中所述的中止管道请求的调用类似。
客户端驱动程序只有在循环端口请求完成后才能向设备发送传输请求。 这是因为在 USB 驱动程序堆栈处理循环端口请求时,设备节点会被删除。
循环端口请求会导致设备重新枚举。 USB 驱动程序堆栈通知 PnP 管理器设备已断开连接。 PnP 管理器会删除与客户端驱动程序关联的设备堆栈。 驱动程序堆栈重置设备,在 USB 总线上重新枚举该设备,并通知 PnP 管理器设备已连接。 然后,PnP 管理器为 USB 设备重新生成设备堆栈。
作为循环端口操作的结果,任何对设备打开句柄的应用程序都会收到设备删除通知(如果应用程序已注册此类通知)。 作为响应,应用程序可能会向用户报告设备断开连接的消息。 由于它会影响用户体验,因此只有在其他恢复机制无法解决错误情况时,客户端驱动程序才应选择循环端口请求。
与重置端口操作(步骤 6 中所述)类似,对于复合设备,循环端口操作会影响整个设备,而不是设备的各个功能。