如何将发送 USB 大容量传输请求

本主题简要概述了 USB 大容量传输。 它还提供有关客户端驱动程序如何从设备发送和接收批量数据的分步说明。

关于批量终结点

USB 批量终结点可以传输大量数据。 批量传输很可靠,允许硬件错误检测,并且涉及硬件中的有限重试次数。 对于到批量终结点的传输,不会在总线上保留带宽。 当有多个针对不同类型的终结点的传输请求时,控制器首先计划时间关键数据的传输,如常时等和中断数据包。 仅当总线上存在未使用的带宽时,控制器才会计划批量传输。 如果总线上没有其他重要流量,则批量传输速度可能很快。 但是,当总线忙于其他传输时,批量数据可以无限期等待。

下面是批量终结点的主要功能:

  • 批量终结点是可选的。 要传输大量数据的 USB 设备支持它们。 例如,将文件传输到闪存驱动器、向打印机或扫描仪传输数据或从打印机或扫描仪传输数据。
  • USB 全速、高速和超高速设备支持批量终结点。 低速设备不支持批量终结点。
  • 终结点是单向的,可以在 IN 或 OUT 方向传输数据。 大容量 IN 终结点用于将数据从设备读取到主机,而大容量 OUT 终结点用于将数据从主机发送到设备。
  • 终结点具有用于检查错误的 CRC 位,因此可提供数据完整性。 对于 CRC 错误,数据会自动重新传输。
  • SuperSpeed 批量终结点可以支持流。 流允许主机将传输发送到单个流管道。
  • 批量终结点的最大数据包大小取决于设备的总线速度。 适用于全速、高速和超高速;最大数据包大小分别为 64、512 和 1024 字节。

批量事务

与所有其他 USB 传输一样,主机始终启动批量传输。 主机与目标终结点之间发生通信。 USB 协议不会对批量事务中发送的数据强制实施任何格式。

主机和设备在总线上的通信方式取决于设备连接的速度。 本部分介绍一些高速和超高速批量传输的示例,这些传输显示了主机和设备之间的通信。

可以使用任何 USB 分析器(例如 Beagle、Ellisys、LeCroy USB 协议分析器)来查看事务和数据包的结构。 分析器设备显示如何通过线路将数据发送到 USB 设备或从其接收数据。 在此示例中,让我们检查由 LeCroy USB 分析器捕获的某些跟踪。 此示例仅供参考, 不表示 Microsoft 的认可。

批量 OUT 事务示例

此分析器跟踪显示高速批量 OUT 事务的示例。

显示批量 OUT 分析器事务的示例跟踪的屏幕截图。

在前面的跟踪中,主机通过发送 PID 设置为 OUT 的令牌数据包, (OUT 令牌) 启动到高速批量批量终结点的批量 OUT 传输。 数据包包含设备和目标终结点的地址。 在 OUT 数据包之后,主机发送包含批量有效负载的数据包。 如果终结点接受传入数据,则会发送 ACK 数据包。 在此示例中,我们可以看到主机向设备地址发送了 31 个字节:1;终结点地址:2。

如果数据包到达时终结点正忙,并且无法接收数据,则设备可以发送 NAK 数据包。 在这种情况下,主机开始向设备发送 PING 数据包。 只要设备尚未准备好接收数据,设备就使用 NAK 数据包进行响应。 设备准备就绪后,它会使用 ACK 数据包进行响应。 然后,主机可以恢复 OUT 传输。

此分析器跟踪显示一个 SuperSpeed 批量 OUT 事务示例。

显示超级速度批量 OUT 数据事务示例的跟踪的屏幕截图。

在前面的跟踪中,主机通过发送数据包来启动到 SuperSpeed 批量终结点的 OUT 事务。 数据包包含批量有效负载、设备和终结点地址。 在此示例中,我们可以看到主机向设备地址发送了 31 个字节:4;终结点地址:2。

设备接收并确认数据包,并将 ACK 数据包发送回主机。 如果数据包到达时终结点正忙,并且无法接收数据,则设备可以发送 NRDY 数据包。 与高速不同,在收到 NRDY 数据包后,主机不会重复轮询设备。 相反,主机会等待来自设备的 ERDY。 设备准备就绪后,会发送 ERDY 数据包,然后主机可以将数据发送到终结点。

批量 IN 事务示例

此分析器跟踪显示高速批量 IN 事务的示例。

显示批量 IN 数据事务的示例跟踪的屏幕截图。

在前面的跟踪中,主机通过发送 PID 设置为 IN 的令牌数据包来启动事务 (IN 令牌) 。 然后,设备发送包含批量有效负载的数据包。 如果终结点没有要发送的数据或尚未准备好发送数据,设备可以发送 NAK 握手数据包。 主机会重试 IN 传输,直到收到来自设备的 ACK 数据包。 该 ACK 数据包意味着设备已接受数据。

此分析器跟踪显示一个 SuperSpeed 批量 IN 事务示例。

示例数据事务的跟踪。

若要从 SuperSpeed 终结点启动批量 IN 传输,主机通过发送 ACK 数据包启动批量事务。 USB 规范版本 3.0 通过将 ACK 和 IN 数据包合并为一个 ACK 数据包来优化传输的初始部分。 对于 SuperSpeed,主机发送 ACK 令牌来启动批量传输,而不是 IN 令牌。 设备使用数据包进行响应。 然后,主机通过发送 ACK 数据包来确认数据包。 如果终结点正忙且无法发送数据,则设备可以发送 NRDY 状态。 在这种情况下,主机将等待,直到它从设备获取 ERDY 数据包。

大容量传输的 USB 客户端驱动程序任务

主机上的应用程序或驱动程序始终会启动大容量传输以发送或接收数据。 客户端驱动程序将请求提交到 USB 驱动程序堆栈。 USB 驱动程序将请求堆栈到主机控制器中,然后将协议数据包 (发送,如上一部分所述,) 通过线路发送到设备。

让我们看看客户端驱动程序如何因应用程序或其他驱动程序的请求而提交批量传输请求。 或者,驱动程序可以自行启动传输。 无论采用哪种方法,驱动程序都必须具有传输缓冲区和请求才能启动批量传输。

对于 KMDF 驱动程序,请求在框架请求对象中描述 (请参阅 WDF 请求对象引用) 。 客户端驱动程序通过指定 WDFREQUEST 句柄来调用请求对象的方法,以将请求发送到 USB 驱动程序堆栈。 如果客户端驱动程序发送大容量传输以响应来自应用程序或其他驱动程序的请求,框架会创建一个请求对象,并使用框架队列对象将请求传递到客户端驱动程序。 在这种情况下,客户端驱动程序可能会使用该请求发送批量传输。 如果客户端驱动程序发起了请求,驱动程序可以选择分配自己的请求对象。

如果应用程序或其他驱动程序发送或请求了数据,则传输缓冲区由框架传递给驱动程序。 或者,如果驱动程序自行启动传输,则客户端驱动程序可以分配传输缓冲区并创建请求对象。

下面是客户端驱动程序的main任务:

  1. 获取传输缓冲区。
  2. 获取框架请求对象、设置框架请求对象的格式并将其发送到 USB 驱动程序堆栈。
  3. 实现完成例程,以在 USB 驱动程序堆栈完成请求时收到通知。

本主题通过使用一个示例来描述这些任务,其中驱动程序由于应用程序发送或接收数据的请求而启动批量传输。

若要从设备读取数据,客户端驱动程序可以使用框架提供的连续读取器对象。 有关详细信息,请参阅 如何使用连续读取器从 USB 管道读取数据

批量传输请求示例

请考虑一个示例方案,其中应用程序想要读取数据或将数据写入设备。 应用程序调用 Windows API 来发送此类请求。 在此示例中,应用程序使用驱动程序在内核模式下发布的设备接口 GUID 打开设备的句柄。 然后,应用程序调用 ReadFileWriteFile 来启动读取或写入请求。 在该调用中,应用程序还指定包含要读取或写入的数据的缓冲区以及该缓冲区的长度。

I/O 管理器接收请求,创建 I/O 请求数据包 (IRP) ,并将其转发到客户端驱动程序。

框架截获请求,创建框架请求对象,并将其添加到框架队列对象。 然后,框架会通知客户端驱动程序正在等待处理新请求。 该通知是通过为 EvtIoReadEvtIoWrite 调用驱动程序的队列回调例程来完成的。

当框架将请求传送到客户端驱动程序时,它会收到以下参数:

  • 包含请求的框架队列对象的 WDFQUEUE 句柄。
  • 包含有关此请求的详细信息的框架请求对象的 WDFREQUEST 句柄。
  • 传输长度,即要读取或写入的字节数。

在客户端驱动程序的 EvtIoReadEvtIoWrite 实现中,驱动程序检查请求参数,并可以选择执行验证检查。

如果使用 SuperSpeed 批量终结点的流,则会在 URB 中发送请求,因为 KMDF 本身不支持流。 有关提交请求以传输到批量终结点的流的信息,请参阅 如何在 USB 批量终结点中打开和关闭静态流

如果不使用流,则可以使用 KMDF 定义的方法发送请求,如以下过程所述:

先决条件

在开始之前,请确保你拥有以下信息:

步骤 1:获取传输缓冲区

传输缓冲区或传输缓冲区 MDL 包含要发送或接收的数据。 本主题假定你在传输缓冲区中发送或接收数据。 传输缓冲区在 WDF 内存对象中描述 (请参阅 WDF 内存对象引用) 。 若要获取与传输缓冲区关联的内存对象,请调用以下方法之一:

客户端驱动程序不需要释放此内存。 内存与父请求对象相关联,并在释放父对象时释放。

步骤 2:格式化框架请求对象并将其发送到 USB 驱动程序堆栈

可以异步或同步发送传输请求。

以下是异步方法:

此列表中的方法设置请求的格式。 如果异步发送请求,请通过调用 WdfRequestSetCompletionRoutine 方法 (下一步) 中所述,设置指向驱动程序实现的完成例程的指针。 若要发送请求,请调用 WdfRequestSend 方法。

如果以同步方式发送请求,请调用以下方法:

有关代码示例,请参阅这些方法的参考主题的“示例”部分。

步骤 3:实现请求的完成例程

如果以异步方式发送请求,则必须实现完成例程,以在 USB 驱动程序堆栈完成请求时收到通知。 完成后,框架将调用驱动程序的完成例程。 框架传递以下参数:

  • 请求对象的 WDFREQUEST 句柄。
  • 请求的 I/O 目标对象的 WDFIOTARGET 句柄。
  • 指向包含完成信息的 WDF_REQUEST_COMPLETION_PARAMS 结构的指针。 特定于 USB 的信息包含在 CompletionParams-Parameters.Usb> 成员中。
  • WDFCONTEXT 句柄指向驱动程序在其对 WdfRequestSetCompletionRoutine 的调用中指定的上下文。

在完成例程中,执行以下任务:

  • 通过获取 CompletionParams-IoStatus.Status> 值检查请求的状态。

  • 检查 USB 驱动程序堆栈设置的 USBD 状态。

  • 如果发生管道错误,请执行错误恢复操作。 有关详细信息,请参阅 如何从 USB 管道错误中恢复

  • 检查传输的字节数。

    当请求的字节数已传输到设备或从设备传出时,批量传输完成。 如果通过调用 KMDF 方法发送请求缓冲区,则检查 CompletionParams-Parameters.Usb.Completion-Parameters.PipeWrite.Length>>CompletionParams-Parameters.Usb.Completion-Parameters.PipeRead.Length>> 成员中收到的值。

    在 USB 驱动程序堆栈在一个数据包中发送所有请求的字节的简单传输中,可以检查将 Length 值与请求的字节数进行比较。 如果 USB 驱动程序堆栈以多个数据包传输请求,则必须跟踪传输的字节数和剩余的字节数。

  • 如果传输的总字节数,请完成请求。 如果出现错误情况,请使用返回的错误代码完成请求。 通过调用 WdfRequestComplete 方法完成请求。 如果要设置信息(例如传输的字节数),请调用 WdfRequestCompleteWithInformation

  • 确保在完成包含信息的请求时,字节数必须等于或小于请求的字节数。 框架验证这些值。 如果在已完成的请求中设置的长度大于原始请求长度,则可能发生 bug 检查。

此示例代码演示客户端驱动程序如何提交批量传输请求。 驱动程序设置完成例程。 该例程显示在下一个代码块中。

/*++

Routine Description:

This routine sends a bulk write request to the
USB driver stack. The request is sent asynchronously and
the driver gets notified through a completion routine.

Arguments:

Queue - Handle to a framework queue object.
Request - Handle to the framework request object.
Length - Number of bytes to transfer.


Return Value:

VOID

--*/


VOID Fx3EvtIoWrite(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request,
    IN size_t  Length
    )
{
    NTSTATUS  status;
    WDFUSBPIPE  pipe;
    WDFMEMORY  reqMemory;
    PDEVICE_CONTEXT  pDeviceContext;

    pDeviceContext = GetDeviceContext(WdfIoQueueGetDevice(Queue));

    pipe = pDeviceContext->BulkWritePipe;

    status = WdfRequestRetrieveInputMemory(
                                           Request,
                                           &reqMemory
                                           );
    if (!NT_SUCCESS(status))
    {
        goto Exit;
    }

    status = WdfUsbTargetPipeFormatRequestForWrite(
                                                   pipe,
                                                   Request,
                                                   reqMemory,
                                                   NULL
                                                   );
    if (!NT_SUCCESS(status))
       {
        goto Exit;
    }

    WdfRequestSetCompletionRoutine(
                                   Request,
                                   BulkWriteComplete,
                                   pipe
                                   );

    if (WdfRequestSend( Request,
                        WdfUsbTargetPipeGetIoTarget(pipe),
                        WDF_NO_SEND_OPTIONS) == FALSE)
       {
        status = WdfRequestGetStatus(Request);
        goto Exit;
    }

Exit:
    if (!NT_SUCCESS(status)) {
        WdfRequestCompleteWithInformation(
                                          Request,
                                          status,
                                          0
                                          );
    }
    return;
}

此示例代码演示批量传输的完成例程实现。 客户端驱动程序在完成例程中完成请求并设置此请求信息:状态和传输的字节数。

/*++

Routine Description:

This completion routine is invoked by the framework when
the USB drive stack completes the previously sent
bulk write request. The client driver completes the
the request if the total number of bytes were transferred
to the device.
In case of failure it queues a work item to start the
error recovery by resetting the target pipe.

Arguments:

Queue - Handle to a framework queue object.
Request - Handle to the framework request object.
Length - Number of bytes to transfer.
Pipe - Handle to the pipe that is the target for this request.

Return Value:

VOID

--*/

VOID BulkWriteComplete(
    _In_ WDFREQUEST                  Request,
    _In_ WDFIOTARGET                 Target,
    PWDF_REQUEST_COMPLETION_PARAMS   CompletionParams,
    _In_ WDFCONTEXT                  Context
    )
{

    PDEVICE_CONTEXT deviceContext;

    size_t          bytesTransferred=0;

    NTSTATUS        status;


    UNREFERENCED_PARAMETER (Target);
    UNREFERENCED_PARAMETER (Context);


    KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
        "In completion routine for Bulk transfer.\n"));

    // Get the device context. This is the context structure that
    // the client driver provided when it sent the request.

    deviceContext = (PDEVICE_CONTEXT)Context;

    // Get the status of the request
    status = CompletionParams->IoStatus.Status;
    if (!NT_SUCCESS (status))
    {
        // Get the USBD status code for more information about the error condition.
        status = CompletionParams->Parameters.Usb.Completion->UsbdStatus;

        KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
            "Bulk transfer failed. 0x%x\n",
            status));

        // Queue a work item to start the reset-operation on the pipe
        // Not shown.

        goto Exit;
    }

    // Get the actual number of bytes transferred.
    bytesTransferred =
            CompletionParams->Parameters.Usb.Completion->Parameters.PipeWrite.Length;

    KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
            "Bulk transfer completed. Transferred %d bytes. \n",
            bytesTransferred));

Exit:

    // Complete the request and update the request with
    // information about the status code and number of bytes transferred.

    WdfRequestCompleteWithInformation(Request, status, bytesTransferred);

    return;
}