Как посылать bulk-запросы передач USB Печать
Добавил(а) microsin   

В этой статье (перевод документации MSDN [1]) дается обзор обмену данными пакетами USB bulk [2]. Также приведены пошаговые инструкции, как драйвер клиента может отправлять данные в устройство USB и принимать его данные пакетами bulk.

[Конечные точки bulk]

Конечная точка USB типа bulk (bulk endpoint) способна передавать большие объемы данных. Bulk-передачи надежны, потому что в них встроена аппаратное обнаружение ошибок, включающее ограниченное количество попыток повтора, реализованных аппаратно. Для передач в конечные точки bulk полоса шины USB не резервируется. Когда имеется несколько запросов передачи, которые предназначаются для различных типов конечных точек, контроллер USB сначала планирует передачи для тех данных, которые критичны по времени обработки (см. [2]), такие как изохронные пакеты и interrupt-пакеты. Только в том случае, когда на шине есть свободная полоса во фрейме, контроллер планирует обработку bulk-передач. Когда на шине нет другого важного трафика (обладающего повышенным приоритетом по доставке), bulk-передача способна быстро перемещать данные. Однако когда шина занята передачами других типов, bulk-данные могут бесконечно ожидать, пока на шине не появится во фрейме свободный слот времени для передачи.

Ключевые особенности конечной точки bulk:

• Конечные точки типа bulk опциональны, т. е. их тип bulk выбирается разработчиком устройства USB. Они реализуются на устройстве USB в том случае, когда необходимо передать большие объемы данных. Например, передача файлов на USB-флешку и с неё, данные для принтера или данные от сканера фотографий.

• Конечные точки типа bulk поддерживают устройства USB, работающие со скоростями Full Speed, High Speed и SuperSpeed. Устройства Low Speed не поддерживают конечные точки типа bulk.

• Конечная точка однонаправленная, и данные через неё могут передаваться либо в направлении IN (направление от устройства к хосту USB), либо в направлении OUT (от хоста USB к устройству). Конечная точка bulk IN используется для чтения хостом данных из устройства USB, и конечная точка bulk OUT используется для отправки данных от хоста в устройство USB.

• Конечная точка снабжена битами CRC для проверки на наличие ошибок в передаваемых данных, это обеспечивает целостность данных в процессе передачи. Когда детектируются ошибки CRC, данные автоматически ретранслируются (передаются повторно).

• Конечная точка SuperSpeed bulk может поддерживать потоки (stream). Потоки позволяют хосту посылать данные по индивидуальным каналам потоков.

• Максимальный размер пакета конечной точки зависит от скорости на шине, поддерживаемой устройством. Для скоростей Full Speed, High Speed и SuperSpeed максимальные размеры пакетов составляют 64, 512 и 1024 байт соответственно [2].

[Bulk-транзакции]

Как и все другие передачи USB, передачу bulk также инициирует хост USB (компьютер). Коммуникация между хостом и устройством происходит через конечную точку bulk. Протокол USB не накладывает никаких ограничений на формат полезной нагрузки, которая передается в пакете bulk-транзакции.

Обмен между хостом USB и устройством USB по шине зависит от скорости, на которой было подключено устройство к порту USB. В этой секции описаны некоторые примеры bulk-передач High Speed и SuperSpeed, которые показывают коммуникацию между хостом и устройством.

Структуру транзакций и пакетов можно увидеть с помощью анализатора (сниффера) протоколов шины USB, такого как Beagle, Ellisys, LeCroy. Анализатор покажет, как данные отправляются или принимаются в процессе обмена между устройством и хостом. В этом примере показываются трассировки, захваченные анализатором LeCroy USB.

Пример транзакции Bulk OUT. На рисунке ниже показана трассировка bulk OUT транзакции на высокой скорости (High Speed).

USB bulk OUT High Speed example

На показанной трассировке хост инициирует транзакцию bulk OUT в конечную точку high-speed bulk путем отправки пакета токена, где PID установлен в OUT (OUT token). Пакет содержит адрес устройства и его целевую конечную точку. После пакета OUT хост отправляет пакет данных, который содержит полезную нагрузку bulk-транзакции. Если конечная точка приняла пришедшие данные, то она отправляет пакет ACK (положительное подтверждение приема). В этом примере мы можем увидеть, что хост передал 31 байт в конечную точку 2 устройства 1 (device address:1; endpoint address: 2).

Если конечная точка занята в тот момент, когда поступил новый пакет данных, и она не может принять данные (это происходит в том случае, когда управляющий микроконтроллер устройства еще не успел обработать данные предыдущей bulk-транзакции), устройство может отправить пакет NAK (отрицательное подтверждение приема). В этом случае хост начинает отправлять в устройство пакеты PING. В ответ на эти пакеты устройство посылает пакеты NAK до тех пор, пока не будет снова готово принять данные. Когда устройство станет готовым, оно ответит пакетом ACK. После этого хост может возобновить передачу OUT.

Ниже показана трассировка транзакции SuperSpeed bulk OUT.

USB bulk OUT SuperSpeed example

В этом примере трассировки хост инициирует OUT-транзакцию на конечную точку SuperSpeed bulk путем отправки пакета данных (data). Пакет данных содержит полезную нагрузку (bulk payload), а также адреса устройства и конечной точки. На трассировке видно, что хост отправил 31 байт в конечную точку 2 устройства 4 (device address:4; endpoint address: 2).

Устройство принимает и подтверждает пакет данных, отправив хосту обратно пакет ACK. Если конечная точка занята в тот момент, как поступил новый пакет данных, и она не может принять эти данные, устройство может вместо ACK отправить пакет NRDY. В отличие от обмена High Speed, хост после приема пакета NRDY не будет опрашивать готовность устройства пакетами PING. Вместо этого хост ждет от устройства пакет ERDY. Когда устройство готово, оно посылает пакет ERDY, и теперь хост может снова посылать данные в конечную точку.

Пример транзакции Bulk IN. На рисунке ниже показана трассировка bulk IN транзакции на высокой скорости (High Speed).

USB bulk IN High Speed example

На показанной трассировке хост инициирует транзакцию отправкой пакета токена, у которого PID установлен в IN (IN token). Затем устройство отправит bulk-пакет данных с полезной нагрузкой. Если в конечной точке нет данных для отправки, или она еще не готова отправить данные, то устройство может отправить пакет рукопожатия NAK. Хост будет повторять попытки IN-передачи, пока не получит пакет ACK от устройства. Пакет ACK подразумевает, что устройство успешно передало данные.

Ниже показана трассировка транзакции SuperSpeed bulk IN.

USB bulk IN SuperSpeed example

Для инициации транзакции bulk IN от конечной точки SuperSpeed хост начинает bulk-транзакцию отправкой пакета ACK. Спецификация USB версии 3.0 оптимизирует эту начальную порцию передачи путем слияния пакетов ACK и IN в один ACK-пакет. Итак, вместо токена IN в режиме SuperSpeed хост отправляет токен ACK для инициации bulk-передачи. Устройство ответит пакетом данных. Затем хост подтвердит пакет данных отправкой пакета ACK. Если конечная точка занята и не может отправить данные, то устройство может отправить статус NRDY. В этом случае хост ждет, пока не получит пакет ERDY от устройства.

[Действия драйвера клиента USB для bulk-передачи]

Приложение или драйвер на хосте всегда инициирует bulk-передачу для отправки или приема данных. Драйвер клиента отправляет соответствующий запрос стеку драйверов USB. Драйвер стека USB программирует запрос в контроллер USB хоста, и затем отправляет пакеты протокола (как это было описано в предыдущей секции) по линку порта USB в устройство.

Давайте посмотрим, как драйвер клиента отправляет запрос для bulk-передачи как результат запроса от приложения или от другого драйвера. Альтернативно драйвер сам может инициировать эту передачу. Независимо от того, как все происходит, у драйвера должен быть буфер передачи и запрос, чтобы инициировать bulk-передачу.

Для драйвера Kernel-Mode Driver Framework (KMDF) запрос описан в объекте запроса фреймворка (см. Framework Request Object Reference [3]). Драйвер клиента вызывает методы объекта запроса путем указания дескриптора WDFREQUEST для отправки запроса стеку драйверов USB. Если драйвер клиента отправляет bulk-транзакцию в ответ на запрос приложения или другого драйвера, то фреймворк создает объект запроса и поставляет запрос драйверу клиента с помощью объекта очереди фреймворка (framework queue object). Тогда драйвер клиента может использовать этот запрос для целей отправки bulk-транзакции. Если драйвер клиента инициировал запрос, то драйвер может принять решение выделить свой собственный объект запроса.

Если приложение или другой драйвер отправил или запросил данные, драйверу передается буфер транзакции, предоставленный фреймворком. Альтернативно драйвер клиента может выделить буфер транзакции и создать объект запроса, если драйвер сам инициирует передачу.

Вот основные задачи драйвера клиента, которые он должен выполнить:

1. Получить буфер передачи.

2. Получить, отформатировать и послать framework request object в стек драйверов USB.

3. Реализовать стандартную подпрограмму завершения, чтобы получить оповещение, когда стек драйверов USB выполнит запрос.

Здесь эти задачи описываются на примере, в котором драйвер инициирует bulk-передачу в ответ на запрос от приложения для отправки или приема данных.

Для чтения данных из устройства драйвер клиента может использовать предоставленный фреймворком объект continuous reader. Для дополнительной информации см. статью [4].

[Пример запроса bulk-передачи]

В качестве примера рассмотрим сценарий, когда программа хочет прочитать данные из устройства USB или записать данные в него. Для отправки таких запросов приложение вызывает Windows API. В этом примере приложение открывает дескриптор для устройства путем использования идентификатора интерфейса устройства (device interface GUID), опубликованного Вашим драйвером в режиме ядра (kernel mode). Затем приложение вызывает ReadFile или WriteFile для инициирования запроса чтения или записи. В этом вызове приложение также указывает буфер, содержащий данные для читаемых или записываемых данных, и длину этого буфера.

Менеджер ввода/вывода (I/O Manager) принимает запрос, создает пакет запроса ввода/вывода (I/O Request Packet, IRP), и перенаправляет его драйверу клиента.

Фреймворк перехватывает этот запрос, создает объект запроса (framework request object), и добавляет его к объекту очереди (framework queue object). Затем фреймворк оповещает драйвер клиента, что новый запрос ожидает обработки. Оповещения реализуются путем запуска подпрограмм обратного вызова драйвера очереди (driver’s queue callback) для EvtIoRead или EvtIoWrite (обработка событий чтения или записи ввода/вывода).

Когда платформа поставляет запрос драйверу клиента, он получает следующие параметры:

• Дескриптор WDFQUEUE для объекта очереди (framework queue object) который содержит запрос.

• Дескриптор WDFREQUEST для объекта запроса (framework request object), в котором содержится информация об этом запроса.

• Длина передачи, т. е. количество байт, которые нужно прочитать или записать.

В реализации драйвера клиента обработчиков EvtIoRead или EvtIoWrite драйвер инспектирует параметры запроса и может опционально выполнять проверки на корректность.

Если Вы используете потоки на конечной точке SuperSpeed bulk, то будете отправлять запрос в URB, потому что KMDF не поддерживает потоки внутри себя. Для информации по поводу предоставления запроса для передачи потокам конечной точки bulk см. [7].

Если Вы не используете потоки, то можете использовать методы, определенные KMDF, для отправки запроса, следуя описанной ниже пошаговой процедуре.

Предварительные требования. Перед тем как начать, убедитесь, что у Вас есть следующая информация:

• У драйвера клиента должен быть созданный объект целевого устройства USB фреймворка, и получен дескриптор WDFUSBDEVICE путем вызова метода WdfUsbTargetDeviceCreateWithParameters.

Если Вы используете шаблоны USB, которые предоставляет Microsoft Visual Studio Professional 2012, то эти задачи выполняет код шаблона. Код шаблона получает дескриптор для объекта целевого устройства и сохраняет его в контексте устройства. Для дополнительной информации см. "Device source code" в статье [5].

• Дескриптор WDFREQUEST для объекта запроса фреймворка, который содержит информацию по этому запросу.

• Количество читаемых или записываемых байт.

• Дескриптор WDFUSBPIPE для объекта канала фреймворка (framework pipe object), связанного с целевой конечной точкой. Вы для этого должны получить дескрипторы каналов во время конфигурации устройства путем перечисления каналов USB (см. [6]).

Если конечная точка поддерживает потоки (stream), то Вы должны иметь дескриптор канала (pipe handle) для потока (см. [7]).

Шаг 1: получение буфера передачи. Буфер передачи, или transfer buffer MDL (MDL расшифровывается как memory descriptor list), содержит данные для передачи или приема. Эта подразумевает, что Вы передаете или принимаете данные в буфере передачи. Буфер передачи описан в объекте памяти WDF (WDF расшифровывается как Windows Driver Framework, см. [8]). Чтобы получить объект памяти, связанный с буфером передачи, вызовите один из этих методов:

• Для запроса передачи bulk IN вызовите метод WdfRequestRetrieveOutputMemory.

• Для запроса передачи bulk OUT вызовите метод WdfRequestRetrieveInputMemory.

Драйверу клиента не нужно освобождать эту память. Эта память связанна с родительским объектом запроса, и она будет освобождена, когда будет освобожден родитель.

Шаг 2: форматирование и отправка объекта запроса драйверам стека USB. Вы можете отправить запрос асинхронно (без блокировки хода вычислений на ожидании результата) или синхронно (подразумевает бесконечную блокировку, пока не будет выполнен запрос).

Асинхронные методы:

WdfUsbTargetPipeFormatRequestForRead
WdfUsbTargetPipeFormatRequestForWrite

Эти два метода форматируют запрос. Если Вы отправляете запрос асинхронно, то установите указатель на реализованную драйвером подпрограмму завершения, путем вызова метода WdfRequestSetCompletionRoutine (описано на следующем шаге). Для отправки запроса вызовите WdfRequestSend.

Для синхронной отправки запроса вызовите методы:

WdfUsbTargetPipeReadSynchronously
• WdfUsbTargetPipeWriteSynchronously

Шаг 3: реализуйте для запроса подпрограмму завершения. Если запрос отправляется асинхронно, то Вы должны реализовать обработчик события завершения (completion routine), чтобы получить оповещение о том, что драйвера стека USB завершили обработку запроса. По завершении фреймворк вызовет этот обработчик завершения с передачей следующих параметров:

• Дескриптор WDFREQUEST для объекта запроса.

• Дескриптор WDFIOTARGET для целевого объекта ввода/вывода (I/O target object) запроса.

• Указатель на структуру WDF_REQUEST_COMPLETION_PARAMS, которая содержит информацию о завершении. Информация, специфичная для USB, содержится в поле CompletionParams->Parameters.Usb этой структуры.

• Дескриптор WDFCONTEXT, указывающий на контекст, который указал драйвер в своем вызове WdfRequestSetCompletionRoutine.

В обработчике завершения (completion routine), выполните следующие действия:

• Проверьте статус запроса в значении CompletionParams->IoStatus.Status.

• Проверьте USBD статус, установленный стеком драйверов USB.

• В случае ошибок на канале (pipe errors) выполните операции по восстановлению из состояния ошибки (см. [9]).

• Проверьте количество переданных (принятых или отправленных) байт.

Передача bulk завершена, когда запрошенное количество байт передано в устройство или из него. Если Вы отправили буфер запроса вызовом метода KMDF, то проверьте значение, принятое в поле CompletionParams->Parameters.Usb.Completion->Parameters.PipeWrite.Length или CompletionParams->Parameters.Usb.Completion->Parameters.PipeRead.Length.

В простой передаче, где стек драйверов USB передал все запрошенные данные в одном пакете, Вы можете сравнить длину Length с количеством запрошенных байт. Если драйверы стека USB обрабатывают запрос в нескольких пакетах данных, Вы должны отслеживать количество переданных данных и оставшееся количество байт.

• Если было передано все запрошенное количество байт, то запрос завершен. Если произошла ошибка (error condition), запрос завершится с возвратом кода ошибки. Завершите запрос вызовом WdfRequestComplete. Если Вы хотите установить информацию, такую как количество переданных байт, вызовите WdfRequestCompleteWithInformation.

• Удостоверьтесь, когда завершаете запрос, что количество байт должны быть равно или меньше количество запрошенных байт. Фреймворк проверяет эти значения. Если количество, установленное в завершенном запросе, больше количества в оригинальном запросе, то может произойти bugcheck (критическая ошибка Windows).

Листинг примера кода ниже показывает, как драйвер клиента может отправить запрос bulk-передачи. Драйвер также устанавливает подпрограмму завершения, которая показана во втором блоке кода.

/*
Эта подпрограмма посылает запрос bulk write в стек драйверов USB.
Запрос отправляется асинхронно, и драйвер оповещается подпрограммой
завершения.
 
Аргументы:
   Queue - дескриптор объекта очереди (framework queue object).
   Request - дескриптор объекта запроса (framework request object).
   Length - количество байт для передачи.
 
Подпрограмма не возвращает никакого значения.
*/
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;
}

Ниже показан пример кода подпрограммы завершения для bulk-передачи. Драйвер клиента завершает запрос в подпрограмме завершения и устанавливает следующую информацию запроса: статус и количество переданных байт.

/*
Это подпрограмма завершения (completion routine), которую вызовет фреймворк,
когда стек драйверов USB завершит ранее отправленный запрос bulk write.
Драйвер клиента завершает запрос, если в устройство было передано полное
количество запрошенных байт.
 
В случае отказа это ставит рабочий элемент очереди на начало процедуры
восстановления ошибки путем сброса целевого канала (target pipe).
 
Аргументы:
   Queue - дескриптор объекта очереди (framework queue object).
   Request - дескриптор объекта запроса (framework request object).
   Length - количество байт для передачи.
   Pipe - дескриптор канала, который является целью этого запроса.
 
Подпрограмма не возвращает никакого значения.
*/
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"));
 
   // Получение контекста устройства. Это структура контекста, которую драйвер
   // клиента предоставил в момент отправки запроса.
   deviceContext = (PDEVICE_CONTEXT)Context;
 
   // Получение статуса запроса.
   status = CompletionParams->IoStatus.Status;
   if (!NT_SUCCESS (status))
   {
      // Получение кода статуса USBD для дополнительной информации
      // о состоянии ошибки.
      status = CompletionParams->Parameters.Usb.Completion->UsbdStatus;
      KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
                "Bulk transfer failed. 0x%x\n",
                status));
      // Постановка рабочего элемента в очередь, чтобы начать операцию
      // сброса на канале (не показано). 
      goto Exit; 
   } 
 
   // Получение реального количества переданных байт.
   bytesTransferred =
      CompletionParams->Parameters.Usb.Completion->Parameters.PipeWrite.Length; 
   KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
             "Bulk transfer completed. Transferred %d bytes. \n",
             bytesTransferred));
 
Exit:
   // Завершение запроса и обновление запроса информацией кода статуса
   // и количеством переданных байт.
   WdfRequestCompleteWithInformation(Request, status, bytesTransferred);
   return;
}

[Ссылки]

1. How to send USB bulk transfer requests site:msdn.microsoft.com.
2. Фреймы и типы передач USB.
3. Framework Request Object Reference site:msdn.microsoft.com.
4. How to use the continuous reader for reading data from a USB pipe site:msdn.microsoft.com.
5. Understanding the USB client driver code structure (KMDF) site:msdn.microsoft.com.
6. How to enumerate USB pipes site:msdn.microsoft.com.
7. How to open and close static streams in a USB bulk endpoint site:msdn.microsoft.com.
8. Framework Memory Object Reference site:msdn.microsoft.com.
9. How to recover from USB pipe errors site:msdn.microsoft.com.