USB バルク転送要求の送信方法

このトピックでは、USB バルク転送の概要を示しています。 また、クライアント ドライバーがデバイスからバルク データを送受信する方法について、ステップ バイ ステップで説明しています。

バルク エンドポイントについて

USB バルク エンドポイントは、大量のデータを転送できます。 バルク転送は、ハードウェア エラー検出を可能にする信頼性が高く、ハードウェアでの再試行回数が限られています。 バルク エンドポイントへの転送の場合、帯域幅はバスに予約されません。 異なる種類のエンドポイントを対象とする複数の転送要求がある場合、コントローラーは最初にアイソクロナス パケットや割り込みパケットなどのタイム クリティカルなデータの転送をスケジュールします。 バスで使用できる未使用の帯域幅がある場合にのみ、コントローラーはバルク転送をスケジュールします。 バス上に他の重要なトラフィックがない場合は、バルク転送が高速になる可能性があります。 ただし、バスが他の転送でビジー状態の場合、バルク データは無期限に待機できます。

バルク エンドポイントの主な機能を次に示します。

  • バルク エンドポイントは省略可能です。 大量のデータを転送する USB デバイスでサポートされています。 たとえば、フラッシュ ドライブにファイルを転送したり、プリンターやスキャナーからデータを転送したりします。
  • USB Full Speed、High Speed、および SuperSpeed デバイスは、バルク エンドポイントをサポートしています。 低速デバイスでは、バルク エンドポイントはサポートされません。
  • エンドポイントは一方向であり、データは IN 方向または OUT 方向で転送できます。 バルク IN エンドポイントはデバイスからホストにデータを読み取るために使用され、バルク OUT エンドポイントはホストからデバイスにデータを送信するために使用されます。
  • エンドポイントにはエラーをチェックするための CRC ビットがあり、データの整合性が提供されます。 CRC エラーの場合、データは自動的に再送信されます。
  • SuperSpeed バルク エンドポイントはストリームをサポートできます。 ストリームホストが個々のストリーム パイプに転送を送信できるようにします。
  • バルク エンドポイントの最大パケット サイズは、デバイスのバス速度によって異なります。 Full Speed、High Speed、および SuperSpeed 用。最大パケット サイズは、それぞれ 64 バイト、512 バイト、1024 バイトです。

バルク トランザクション

他のすべての USB 転送と同様に、ホストは常にバルク転送を開始します。 コミュニケーションは、ホストとターゲット エンドポイントの間で行われます。 USB プロトコルでは、バルク トランザクションで送信されるデータに対して形式は適用されません。

ホストとデバイスがバスでコミュニケーションする方法は、デバイスが接続されている速度によって異なります。 このセクションでは、ホストとデバイス間の通信を示す High Speed および SuperSpeed バルク転送の例をいくつか説明します。

トランザクションとパケットの構造は、Beagle、Ellisys、LeCroy USB プロトコル アナライザーなど、あらゆる USB アナライザーを使用して確認できます。 アナライザー デバイスには、通信回線経由で USB デバイスとの間でデータを送受信する方法が示されます。 この例では、LeCroy USB アナライザーによってキャプチャされたトレースをいくつか確認してみましょう。 この例は、情報提供目的のみで用意されています。 Microsoft がこの製品を推薦するものではありません。

バルク OUT トランザクションの例

このアナライザー トレースは、高速でのバルク OUT トランザクションの例を示しています。

Screenshot that shows a trace of an example bulk OUT analyzer transaction.

前のトレースでは、ホストは、PID が OUT (OUT トークン) に設定されたトークン パケットを送信することで、高速バルク エンドポイントへのバルク OUT 転送を開始します。 パケットには、デバイスとターゲット エンドポイントのアドレスが含まれています。 OUT パケットの後、ホストはバルク ペイロードを含むデータ パケットを送信します。 エンドポイントが受信データを受け入れると、ACK パケットが送信されます。 この例では、ホストがデバイス アドレス:1、エンドポイント アドレス:2 に 31 バイトを送信したことがわかります。

データ パケットの到着時にエンドポイントがビジーでデータを受信できない場合、デバイスは NAK パケットを送信できます。 その場合、ホストはデバイスへの PING パケットの送信を開始します。 デバイスがデータを受信する準備ができていない限り、デバイスは NAK パケットで応答します。 デバイスの準備ができたら、ACK パケットで応答します。 その後、ホストは OUT 転送を再開できます。

このアナライザー トレースは、SuperSpeed バルク OUT トランザクションの例を示しています。

Screenshot that shows a trace of an example SuperSpeed bulk OUT data transaction.

前述のトレースでは、ホストはデータ パケットを送信することによって、SuperSpeed バルク エンドポイントへの OUT トランザクションを開始します。 データ パケットには、バルク ペイロード、デバイス、エンドポイント アドレスが含まれています。 この例では、ホストがデバイス アドレス:4、エンドポイント アドレス:2 に 31 バイトを送信したことがわかります。

デバイスは、データ パケットを受信して確認し、ACK パケットをホストに送信します。 データ パケットの到着時にエンドポイントがビジーでデータを受信できない場合、デバイスは NRDY パケットを送信できます。 High Speed とは異なり、NRDY パケットを受信した後、ホストはデバイスを繰り返しポーリングしません。 代わりに、ホストはデバイスからの ERDY を待機します。 デバイスの準備ができたら、ERDY パケットを送信し、ホストはエンドポイントにデータを送信できます。

バルク IN トランザクションの例

このアナライザー トレースは、高速でのバルク IN トランザクションの例を示しています。

Screenshot that shows a trace of an example bulk IN data transaction.

前のトレースでは、ホストは PID が IN (IN トークン) に設定されたトークン パケットを送信してトランザクションを開始します。 その後、デバイスはバルク ペイロードを含むデータ パケットを送信します。 エンドポイントに送信するデータがない場合、またはデータを送信する準備がまだできていない場合、デバイスは、NAK ハンドシェイク パケットを送信できます。 ホストは、デバイスから ACK パケットを受信するまで IN 転送を再試行します。 その ACK パケットは、デバイスがデータを受け入れたことを意味します。

このアナライザー トレースは、SuperSpeed バルク IN トランザクションの例を示しています。

trace of an example data transaction.

SuperSpeed エンドポイントからの一括 IN 転送を開始するために、ホストは ACK パケットを送信してバルク トランザクションを開始します。 USB 仕様バージョン 3.0 は、ACK パケットと IN パケットを 1 つの ACK パケットにマージすることで、転送のこの初期部分を最適化します。 IN トークンの代わりに、SuperSpeed の場合、ホストはバルク転送を開始する ACK トークンを送信します。 デバイスはデータ パケットで応答します。 その後、ホストは ACK パケットを送信してデータ パケットを確認します。 エンドポイントがビジー状態で、データを送信できなかった場合、デバイスは NRDY の状態を送信できます。 その場合、ホストはデバイスから ERDY パケットを取得するまで待機します。

バルク転送用の USB クライアント ドライバー タスク

ホスト上のアプリケーションまたはドライバーは、常にバルク転送を開始してデータを送受信します。 クライアント ドライバーは、USB ドライバー スタックに要求を送信します。 USB ドライバー スタックは、ホスト コントローラーへの要求をプログラムし、(前のセクションで説明したように) プロトコル パケットを有線経由でデバイスに送信します。

アプリケーションまたは別のドライバーの要求の結果として、クライアント ドライバーがどのようにバルク転送の要求を送信するかを見てみましょう。 または、ドライバーは、単独で転送を開始できます。 アプローチに関係なく、ドライバーはバルク転送を開始するために転送バッファーと要求を持っている必要があります。

KMDF ドライバーの場合、要求はフレームワーク要求オブジェクトで記述されます (WDF 要求オブジェクト参照を参照)。 クライアント ドライバーは、WDFREQUEST ハンドルを指定して要求オブジェクトのメソッドを呼び出し、要求を USB ドライバー スタックに送信します。 クライアント ドライバーがアプリケーションまたは別のドライバーからの要求に応答してバルク転送を送信している場合、フレームワークは要求オブジェクトを作成し、フレームワーク キュー オブジェクトを使用してその要求をクライアント ドライバーに配信します。 その場合、クライアント ドライバーは、バルク転送を送信するためにその要求を使用できます。 クライアント ドライバーが要求を開始した場合、ドライバーは独自の要求オブジェクトの割り当てを選択できます。

アプリケーションまたは別のドライバーがデータを送信または要求した場合、転送バッファーはフレームワークによってドライバーに渡されます。 あるいは、クライアント ドライバーが独自に転送を開始する場合は、クライアント ドライバーが転送バッファを割り当てて要求オブジェクトを作成することもできます。

クライアント ドライバーのメイン タスクを次に示します。

  1. 転送バッファーを取得します。
  2. フレームワーク要求オブジェクトを取得し、書式設定、および USB ドライバー スタックに送信します。
  3. USB ドライバー スタックが要求を完了したときに通知を受け取る完了ルーチンを実装します。

このトピックでは、アプリケーションのデータ送受信要求の結果としてドライバーが一括転送を開始する例を使用して、これらのタスクについて説明します。

デバイスからデータを読み取るために、クライアント ドライバーは、フレームワークが提供する連続リーダー オブジェクトを使用できます。 詳細については、「連続リーダーを使用して USB パイプからデータを読み取る方法」を参照してください。

バルク転送要求の例

アプリケーションがデバイスにデータを読み書きするシナリオの例を考えてみましょう。 アプリケーションは、このような要求を送信するために Windows API を呼び出します。 この例では、カーネル モードでドライバーによって発行されたデバイス インターフェイス GUID を使用して、アプリケーションがデバイスへのハンドルを開きます。 その後、アプリケーションは ReadFile または WriteFile を呼び出して、読み取り要求または書き込み要求を開始します。 その呼び出しでは、アプリケーションは、読み取りまたは書き込みするデータを含むバッファーとそのバッファーの長さも指定します。

I/O マネージャーは要求を受信し、I/O 要求パケット (IRP) を作成して、それをクライアント ドライバーに転送します。

フレームワークは要求をインターセプトし、フレームワーク要求オブジェクトを作成し、それをフレームワーク キュー オブジェクトに追加します。 次に、フレームワークは、新しい要求の処理を待機していることをクライアント ドライバーに通知します。 この通知は、EvtIoRead または EvtIoWrite のドライバーのキュー コールバック ルーチンを呼び出すことによって行われます。

フレームワークは、クライアント ドライバーに要求を配信すると、次のパラメーターを受け取ります。

  • 要求を含むフレームワーク キュー オブジェクトに対する WDFQUEUE ハンドル。
  • この要求に関する詳細を含むフレームワーク要求オブジェクトに対する WDFREQUEST ハンドル。
  • 転送の長さ (つまり、読み取りまたは書き込みするバイト数)。

クライアント ドライバーの EvtIoRead または EvtIoWrite の実装では、ドライバーは要求パラメーターを検査し、必要に応じて検証チェックを実行できます。

SuperSpeed バルク エンドポイントのストリームを使用している場合、KMDF はストリームを本質的にサポートしていないため、URB で要求を送信します。 一括エンドポイントのストリームへの転送要求の送信については、「USB バルク エンドポイントにおける静的ストリームのオープン/クローズ方法」を参照してください。

ストリームを使用していない場合は、次の手順で説明するように、KMDF 定義のメソッドを使用して要求を送信できます。

前提条件

始める前に、次の情報があることを確認してください。

  • クライアント ドライバーは、フレームワーク USB ターゲット デバイス オブジェクトを作成し、WdfUsbTargetDeviceCreateWithParameters メソッドを呼び出すことによって WDFUSBDEVICE ハンドルを取得する必要があります。

    Microsoft Visual Studio Professional 2012 に付属する USB テンプレートを使用している場合、テンプレート コードでこれらのタスクが実行されます。 テンプレート コードによりターゲット デバイス オブジェクトのハンドルが取得され、デバイス コンテキストに格納されます。 詳細については、「USB クライアント ドライバー コード構造について (KMDF)」の「デバイスのソース コード」を参照してください。

  • この要求に関する詳細を含むフレームワーク要求オブジェクトに対する WDFREQUEST ハンドル。

  • 読み取りまたは書き込みするバイト数。

  • ターゲット エンドポイントに関連付けられているフレームワーク パイプ オブジェクトに対する WDFUSBPIPE ハンドル。 パイプを列挙して、デバイスの構成中にパイプ ハンドルを取得する必要があります。 詳細については、「USB パイプを列挙する方法」をご参照ください。

    バルク エンドポイントがストリームをサポートしている場合は、ストリームへのパイプ ハンドルが必要です。 詳細については、「USB バルク エンドポイントにおける静的ストリームのオープン/クローズ方法」を参照してください。

手順 1: 転送バッファーを取得する

転送バッファーまたは転送バッファー MDL には、送受信するデータが含まれています。 このトピックでは、転送バッファー内のデータを送受信することを前提としています。 転送バッファーは、WDF メモリ オブジェクトで説明されています (WDF メモリ オブジェクト参照を参照)。 転送バッファーに関連付けられているメモリ オブジェクトを取得するには、次のいずれかのメソッドを呼び出します。

クライアント ドライバーは、このメモリを解放する必要はありません。 メモリは親要求オブジェクトに関連付けられており、親が解放されると解放されます。

手順 2: フレームワーク要求オブジェクトを書式設定して USB ドライバー スタックに送信する

転送要求は、非同期または同期的に送信できます。

非同期メソッドは次のとおりです。

この一覧のメソッドは、要求の書式を設定します。 要求を非同期的に送信する場合は、WdfRequestSetCompletionRoutine メソッドを呼び出して、ドライバー実装完了ルーチンへのポインターを設定します (次の手順で説明します)。 要求を送信するには、WdfRequestSend メソッドを呼び出します。

要求を同期的に送信する場合は、次のメソッドを呼び出します。

コード例については、これらのメソッドのリファレンス トピックの「例」セクションを参照してください。

手順 3: 要求の完了ルーチンを実装する

要求が非同期で送信される場合は、USB ドライバー スタックが要求を完了したときに通知を受け取る完了ルーチンを実装する必要があります。 完了すると、フレームワークはドライバーの完了ルーチンを呼び出します。 フレームワークは、次のパラメーターを渡します。

  • 要求オブジェクトに対する WDFREQUEST ハンドル。
  • 要求の I/O ターゲット オブジェクトに対する WDFIOTARGET ハンドル。
  • 完了情報を含む WDF_REQUEST_COMPLETION_PARAMS 構造体へのポインター。 USB 固有の情報は、CompletionParams->Parameters.Usb メンバーに含まれています。
  • WdfRequestSetCompletionRoutine の呼び出しでドライバーが指定したコンテキストへの WDFCONTEXT ハンドル。

完了ルーチンで、次のタスクを実行します。

  • CompletionParams->IoStatus.Status 値を取得して、要求の状態を確認します。

  • USB ドライバー スタックによって設定された USBD 状態を確認します。

  • パイプ エラーが発生した場合は、エラー回復操作を実行します。 関連情報については、「USB パイプ エラーから回復する方法」を参照してください。

  • 転送されたバイト数を確認します。

    要求されたバイト数がデバイス間で転送されると、バルク転送が完了します。 KMDF メソッドを呼び出して要求バッファーを送信する場合は、CompletionParams->Parameters.Usb.Completion->Parameters.PipeWrite.Length メンバーまたは CompletionParams->Parameters.Usb.Completion->Parameters.PipeRead.Length メンバーで受信した値をチェックします。

    USB ドライバー スタックが要求されたすべてのバイトを 1 つのデータ パケットで送信する単純な転送では、Length 値を要求されたバイト数と比較して確認できます。 USB ドライバー スタックが複数のデータ パケットで要求を転送する場合は、転送されたバイト数と残りのバイト数を追跡する必要があります。

  • 合計バイト数が転送された場合は、要求を完了します。 エラー状態が発生した場合は、返されたエラー コードを使用して要求を完了します。 WdfRequestComplete メソッドを呼び出して要求を完了します。 転送されたバイト数などの情報を設定する場合は、WdfRequestCompleteWithInformation を呼び出します。

  • 情報を使用して要求を完了するときは、バイト数が要求されたバイト数以下である必要があります。 フレームワークは、これらの値を検証します。 完了した要求で設定された長さが元の要求の長さを超える場合は、バグチェックが発生する可能性があります。

このコード例は、クライアント ドライバーがバルク転送要求を送信する方法を示しています。 ドライバーは、完了ルーチンを設定します。 このルーチンは、次のコード ブロックに示されています。

/*++

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;
}