Доступ к USB-устройству с помощью функций WinUSB

Эта статья содержит подробное пошаговое руководство по использованию функций WinUSB для взаимодействия с USB-устройством, которое использует Winusb.sys в качестве драйвера функций.

Сводка

  • Открытие устройства и получение дескриптора WinUSB.
  • Получение сведений об устройстве, конфигурации и параметрах интерфейса всех интерфейсов и их конечных точек.
  • Чтение и запись данных в конечные точки массовых операций и прерывания.

Важные API

Если вы используете Microsoft Visual Studio 2013, создайте приложение с помощью шаблона WinUSB. В этом случае пропустите шаги 1–3 и перейдите к шагу 4 в этой статье. Шаблон открывает дескриптор файла для устройства и получает дескриптор WinUSB, необходимый для последующих операций. Этот дескриптор хранится в структуре, определяемой приложением DEVICE_DATA в device.h.

Дополнительные сведения о шаблоне см. в статье Создание классического приложения для Windows на основе шаблона WinUSB.

Примечание

Для функций WinUSB требуется Windows XP или более поздней версии. Эти функции можно использовать в приложении C/C++ для взаимодействия с USB-устройством. Корпорация Майкрософт не предоставляет управляемый API для WinUSB.

Перед началом работы

К этому пошаговому руководству относятся следующие элементы:

  • Эти сведения относятся к Windows 8.1, Windows 8, Windows 7, Windows Server 2008, Windows Vista.
  • Вы установили Winusb.sys в качестве драйвера функции устройства. Дополнительные сведения об этом процессе см. в разделе Установка WinUSB (Winusb.sys).
  • Примеры в этой статье основаны на устройстве OSR USB FX2 Learning Kit. Эти примеры можно использовать для расширения процедур на другие USB-устройства.

Шаг 1. Создание приложения на основе шаблона WinUSB

Чтобы получить доступ к USB-устройству, начните с создания скелетного приложения на основе шаблона WinUSB, включенного в интегрированную среду пакета драйверов Windows (WDK) (с средствами отладки для Windows) и Microsoft Visual Studio. Шаблон можно использовать в качестве отправной точки.

Сведения о коде шаблона, создании, развертывании и отладке скелетного приложения см. в статье Создание классического приложения для Windows на основе шаблона WinUSB.

Шаблон перечисляет устройства с помощью подпрограмм SetupAPI , открывает дескриптор файла для устройства и создает дескриптор интерфейса WinUSB, необходимый для последующих задач. Пример кода, который получает дескриптор устройства и открывает устройство, см. в разделе Обсуждение кода шаблона.

Шаг 2. Запрос устройства для дескрипторов USB

Затем запросить у устройства сведения, относящиеся к USB, такие как скорость устройства, дескрипторы интерфейса, связанные конечные точки и их каналы. Процедура аналогична процедуре, используемой драйверами USB-устройств. Однако приложение выполняет запросы устройств, вызывая WinUsb_GetDescriptor.

В следующем списке показаны функции WinUSB, которые можно вызвать для получения сведений, относящихся к USB:

  • Дополнительные сведения об устройстве.

    Вызовите WinUsb_QueryDeviceInformation, чтобы запросить сведения из дескрипторов устройства. Чтобы получить скорость устройства, задайте DEVICE_SPEED (0x01) в параметре InformationType . Функция возвращает LowSpeed (0x01) или HighSpeed (0x03).

  • дескрипторы интерфейса;

    Вызовите WinUsb_QueryInterfaceSettings и передайте дескрипторы интерфейса устройства, чтобы получить соответствующие дескрипторы интерфейса. Дескриптор интерфейса WinUSB соответствует первому интерфейсу. Некоторые USB-устройства, например OSR Fx2, поддерживают только один интерфейс без каких-либо альтернативных параметров. Таким образом, для этих устройств параметру AlternateSettingNumber присваивается нулевое значение, а функция вызывается только один раз. WinUsb_QueryInterfaceSettings заполняет структуру, выделенную вызывающим объектом USB_INTERFACE_DESCRIPTOR (переданную в параметре UsbAltInterfaceDescriptor ) сведениями об интерфейсе. Например, количество конечных точек в интерфейсе задается в элементе bNumEndpointsUSB_INTERFACE_DESCRIPTOR.

    Для устройств, поддерживающих несколько интерфейсов, вызовите WinUsb_GetAssociatedInterface , чтобы получить дескриптора интерфейса для связанных интерфейсов, указав альтернативные параметры в параметре AssociatedInterfaceIndex .

  • Конечные точки

    Вызовите WinUsb_QueryPipe , чтобы получить сведения о каждой конечной точке в каждом интерфейсе. WinUsb_QueryPipe заполняет структуру WINUSB_PIPE_INFORMATION , выделенную вызывающим объектом, сведениями о канале указанной конечной точки. Каналы конечных точек идентифицируются с помощью отсчитываемого от нуля индекса и должны быть меньше значения в элементе bNumEndpoints дескриптора интерфейса, полученного при предыдущем вызове **WinUsb_QueryInterfaceSettings. Устройство OSR Fx2 имеет один интерфейс с тремя конечными точками. Для этого устройства параметр AlternateInterfaceNumber функции имеет значение 0, а значение параметра PipeIndex — от 0 до 2.

    Чтобы определить тип канала, изучите элемент PipeInfo структуры WINUSB_PIPE_INFORMATION. Этому элементу присваивается одно из значений перечисления USBD_PIPE_TYPE: UsbdPipeTypeControl, UsbdPipeTypeIsochronous, UsbdPipeTypeBulk или UsbdPipeTypeInterrupt. Устройство OSR USB FX2 поддерживает канал прерываний, канал массовых операций и канал массовой передачи, поэтому для PipeInfo задано значение UsbdPipeTypeInterrupt или UsbdPipeTypeBulk. Значение UsbdPipeTypeBulk определяет массовые каналы, но не указывает направление канала. Сведения о направлении кодируются в высоком бите адреса канала, который хранится в элементе PipeId структуры WINUSB_PIPE_INFORMATION. Самый простой способ определить направление канала — передать значение PipeId в один из следующих макросов из Usb100.h:

    • Макрос USB_ENDPOINT_DIRECTION_IN (PipeId) возвращает значение TRUE , если направление находится в направлении .
    • Если USB_ENDPOINT_DIRECTION_OUT(PipeId) направление выходит, макрос возвращает значение TRUE .

    Приложение использует значение PipeId , чтобы определить, какой канал следует использовать для передачи данных в вызовах функций WinUSB, таких как WinUsb_ReadPipe (описано в разделе "Запросы ввода-вывода проблем" этой статьи), поэтому в примере сохраняются все три значения PipeId для последующего использования.

В следующем примере кода возвращается скорость устройства, заданная дескриптором интерфейса WinUSB.

BOOL GetUSBDeviceSpeed(WINUSB_INTERFACE_HANDLE hDeviceHandle, UCHAR* pDeviceSpeed)
{
  if (!pDeviceSpeed || hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;
  ULONG length = sizeof(UCHAR);

  bResult = WinUsb_QueryDeviceInformation(hDeviceHandle, DEVICE_SPEED, &length, pDeviceSpeed);

  if(!bResult)
  {
    printf("Error getting device speed: %d.\n", GetLastError());
    goto done;
  }

  if(*pDeviceSpeed == LowSpeed)
  {
    printf("Device speed: %d (Low speed).\n", *pDeviceSpeed);
    goto done;
  }

  if(*pDeviceSpeed == FullSpeed)
  {
    printf("Device speed: %d (Full speed).\n", *pDeviceSpeed);
    goto done;
  }

  if(*pDeviceSpeed == HighSpeed)
  {
    printf("Device speed: %d (High speed).\n", *pDeviceSpeed);
    goto done;
  }

done:
  return bResult;
}

В следующем примере кода запрашивается различные дескрипторы для USB-устройства, указанного дескриптором интерфейса WinUSB. Пример функции извлекает типы поддерживаемых конечных точек и их идентификаторы каналов. В примере сохраняются все три значения PipeId для последующего использования.

struct PIPE_ID
{
  UCHAR  PipeInId;
  UCHAR  PipeOutId;
};

BOOL QueryDeviceEndpoints (WINUSB_INTERFACE_HANDLE hDeviceHandle, PIPE_ID* pipeid)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  USB_INTERFACE_DESCRIPTOR InterfaceDescriptor;
  ZeroMemory(&InterfaceDescriptor, sizeof(USB_INTERFACE_DESCRIPTOR));

  WINUSB_PIPE_INFORMATION  Pipe;
  ZeroMemory(&Pipe, sizeof(WINUSB_PIPE_INFORMATION));

  bResult = WinUsb_QueryInterfaceSettings(hDeviceHandle, 0, &InterfaceDescriptor);

  if (bResult)
  {
    for (int index = 0; index < InterfaceDescriptor.bNumEndpoints; index++)
    {
      bResult = WinUsb_QueryPipe(hDeviceHandle, 0, index, &Pipe);

      if (bResult)
      {
        if (Pipe.PipeType == UsbdPipeTypeControl)
        {
          printf("Endpoint index: %d Pipe type: Control Pipe ID: %d.\n", index, Pipe.PipeType, Pipe.PipeId);
        }

        if (Pipe.PipeType == UsbdPipeTypeIsochronous)
        {
          printf("Endpoint index: %d Pipe type: Isochronous Pipe ID: %d.\n", index, Pipe.PipeType, Pipe.PipeId);
        }

        if (Pipe.PipeType == UsbdPipeTypeBulk)
        {
          if (USB_ENDPOINT_DIRECTION_IN(Pipe.PipeId))
          {
            printf("Endpoint index: %d Pipe type: Bulk Pipe ID: %c.\n", index, Pipe.PipeType, Pipe.PipeId);
            pipeid->PipeInId = Pipe.PipeId;
          }

          if (USB_ENDPOINT_DIRECTION_OUT(Pipe.PipeId))
          {
            printf("Endpoint index: %d Pipe type: Bulk Pipe ID: %c.\n", index, Pipe.PipeType, Pipe.PipeId);
            pipeid->PipeOutId = Pipe.PipeId;
          }
        }

        if (Pipe.PipeType == UsbdPipeTypeInterrupt)
        {
          printf("Endpoint index: %d Pipe type: Interrupt Pipe ID: %d.\n", index, Pipe.PipeType, Pipe.PipeId);
        }
      }
      else
      {
        continue;
      }
    }
  }

done:
  return bResult;
}

Шаг 3. Отправка передачи управления в конечную точку по умолчанию

Затем общайтесь с устройством, отправив запрос на управление конечной точке по умолчанию.

Все USB-устройства имеют конечную точку по умолчанию в дополнение к конечным точкам, связанным с интерфейсами. Основная цель конечной точки по умолчанию — предоставить узлу сведения, которые он может использовать для настройки устройства. Однако устройства также могут использовать конечную точку по умолчанию для конкретных устройств. Например, устройство OSR USB FX2 использует конечную точку по умолчанию для управления индикатором и цифровым дисплеем с семью сегментами.

Команды управления состоят из 8-байтового пакета установки, который включает код запроса, указывающий конкретный запрос, и необязательный буфер данных. Коды запросов и форматы буфера определяются поставщиком. В этом примере приложение отправляет данные на устройство для управления индикатором. Код для задания индикатора 0xD8, который для удобства определяется как SET_BARGRAPH_DISPLAY. Для этого запроса устройству требуется 1-байтовый буфер данных, который указывает, какие элементы должны быть освещены, задав соответствующие биты.

Приложение может предоставить набор из восьми проверка элементов управления box, чтобы указать, какие элементы панели освещения должны быть освещены. Указанные элементы соответствуют соответствующим битам в буфере. Чтобы избежать кода пользовательского интерфейса, в примере кода в этом разделе задаются биты таким образом, чтобы загорелись альтернативные источники света.

Выдача запроса элемента управления

  1. Выделите 1-байтовый буфер данных и загрузите данные в буфер, который указывает элементы, которые должны быть освещены, задав соответствующие биты.

  2. Создайте пакет установки в структуре WINUSB_SETUP_PACKET, выделенной вызывающим объектом. Инициализируйте элементы для представления типа запроса и данных следующим образом:

    • Элемент RequestType указывает направление запроса. Для него задано значение 0, которое указывает на передачу данных между узлами и устройствами. Для передачи данных с устройства на узел задайте для параметра RequestType значение 1.
    • Элементу Запроса присваивается определенный поставщиком код для этого запроса, 0xD8. Для удобства он определяется как SET_BARGRAPH_DISPLAY.
    • Член Length имеет размер буфера данных.
    • Элементы Index и Value не требуются для этого запроса, поэтому для них задано значение 0.
  3. Вызовите WinUsb_ControlTransfer для передачи запроса в конечную точку по умолчанию, передав дескриптор интерфейса WinUSB устройства, пакет установки и буфер данных. Функция получает количество байтов, переданных на устройство в параметре LengthTransferred .

В следующем примере кода на указанное USB-устройство отправляется запрос на управление освещением на панели освещения.

BOOL SendDatatoDefaultEndpoint(WINUSB_INTERFACE_HANDLE hDeviceHandle)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  UCHAR bars = 0;

  WINUSB_SETUP_PACKET SetupPacket;
  ZeroMemory(&SetupPacket, sizeof(WINUSB_SETUP_PACKET));
  ULONG cbSent = 0;

  //Set bits to light alternate bars
  for (short i = 0; i < 7; i+= 2)
  {
    bars += 1 << i;
  }

  //Create the setup packet
  SetupPacket.RequestType = 0;
  SetupPacket.Request = 0xD8;
  SetupPacket.Value = 0;
  SetupPacket.Index = 0; 
  SetupPacket.Length = sizeof(UCHAR);

  bResult = WinUsb_ControlTransfer(hDeviceHandle, SetupPacket, &bars, sizeof(UCHAR), &cbSent, 0);

  if(!bResult)
  {
    goto done;
  }

  printf("Data sent: %d \nActual data transferred: %d.\n", sizeof(bars), cbSent);

done:
  return bResult;
}

Шаг 4. Выдача запросов ввода-вывода

Затем отправьте данные в конечные точки массовой и массовой обработки устройства, которые можно использовать для запросов на чтение и запись соответственно. На устройстве OSR USB FX2 эти две конечные точки настроены для замыкания на себя, поэтому устройство перемещает данные из конечной точки с массовым вводом в конечную точку массовой передачи. Он не изменяет значение данных и не добавляет новые данные. Для конфигурации замыкания на себя запрос на чтение считывает данные, отправленные последним запросом на запись. WinUSB предоставляет следующие функции для отправки запросов на запись и чтение:

Отправка запроса на запись

  1. Выделите буфер и заполните его данными, которые необходимо записать на устройство. Нет ограничений на размер буфера, если приложение не задает RAW_IO в качестве типа политики канала. При необходимости WinUSB разделяет буфер на блоки соответствующего размера. Если задано RAW_IO, размер буфера ограничивается максимальным размером передачи, поддерживаемым WinUSB.
  2. Вызовите WinUsb_WritePipe , чтобы записать буфер на устройство. Передайте дескриптор интерфейса WinUSB для устройства, идентификатор канала для канала массовой передачи (как описано в разделе Запрос устройства для дескрипторов USB этой статьи) и буфер. Функция возвращает число байтов, записанных на устройство в параметре bytesWritten . Параметр Overlapped имеет значение NULL для запроса синхронной операции. Чтобы выполнить асинхронный запрос на запись, задайте для свойства Overlapped указатель на структуру OVERLAPPED .

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

BOOL WriteToBulkEndpoint(WINUSB_INTERFACE_HANDLE hDeviceHandle, UCHAR* pID, ULONG* pcbWritten)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE || !pID || !pcbWritten)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  UCHAR szBuffer[] = "Hello World";
  ULONG cbSize = strlen(szBuffer);
  ULONG cbSent = 0;

  bResult = WinUsb_WritePipe(hDeviceHandle, *pID, szBuffer, cbSize, &cbSent, 0);

  if(!bResult)
  {
    goto done;
  }

  printf("Wrote to pipe %d: %s \nActual data transferred: %d.\n", *pID, szBuffer, cbSent);
  *pcbWritten = cbSent;

done:
  return bResult;
}

Отправка запроса на чтение

  • Вызовите WinUsb_ReadPipe для чтения данных из конечной точки массового входа устройства. Передайте дескриптор интерфейса WinUSB устройства, идентификатор канала для конечной точки массового ввода и пустой буфер соответствующего размера. При возврате функции буфер содержит данные, считанные с устройства. Число прочитанных байтов возвращается в параметре bytesRead функции. Для запросов на чтение буфер должен быть кратным максимальному размеру пакета.

Запросы на чтение нулевой длины немедленно завершаются успешно и не отправляются в стек. Если длина передачи больше максимальной длины передачи, WinUSB делит запрос на запросы меньшего размера максимальной длины передачи и отправляет их последовательно. Если длина передачи не кратна maxPacketSize конечной точки, WinUSB увеличивает размер передачи до следующего кратного MaxPacketSize. Если устройство возвращает больше данных, чем было запрошено, WinUSB сохраняет лишние данные. Если данные остаются из предыдущего запроса на чтение, WinUSB копирует их в начало следующего запроса на чтение и при необходимости завершает запрос. В следующем примере кода считываются данные из конечной точки массового входа устройства.

BOOL ReadFromBulkEndpoint(WINUSB_INTERFACE_HANDLE hDeviceHandle, UCHAR* pID, ULONG cbSize)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  UCHAR* szBuffer = (UCHAR*)LocalAlloc(LPTR, sizeof(UCHAR)*cbSize);
  ULONG cbRead = 0;

  bResult = WinUsb_ReadPipe(hDeviceHandle, *pID, szBuffer, cbSize, &cbRead, 0);

  if(!bResult)
  {
    goto done;
  }

  printf("Read from pipe %d: %s \nActual data read: %d.\n", *pID, szBuffer, cbRead);

done:
  LocalFree(szBuffer);
  return bResult;
}

Шаг 5. Освобождение дескрипторов устройства

Завершив все необходимые вызовы устройства, отпустите дескриптор файла и дескриптор интерфейса WinUSB для устройства, вызвав следующие функции:

  • CloseHandle , чтобы освободить дескриптор, созданный с помощью CreateFile, как описано на шаге 1.
  • WinUsb_Free освободить дескриптор интерфейса WinUSB для устройства, возвращаемый **WinUsb_Initialize.

Шаг 6. Реализация функции main

В следующем примере кода показана функция main консольного приложения.

Пример кода, который получает дескриптор устройства и открывает устройство (GetDeviceHandle и GetWinUSBHandle в этом примере), см. в разделе Обсуждение кода шаблона.

int _tmain(int argc, _TCHAR* argv[])
{

  GUID guidDeviceInterface = OSR_DEVICE_INTERFACE; //in the INF file
  BOOL bResult = TRUE;
  PIPE_ID PipeID;
  HANDLE hDeviceHandle = INVALID_HANDLE_VALUE;
  WINUSB_INTERFACE_HANDLE hWinUSBHandle = INVALID_HANDLE_VALUE;
  UCHAR DeviceSpeed;
  ULONG cbSize = 0;

  bResult = GetDeviceHandle(guidDeviceInterface, &hDeviceHandle);

  if(!bResult)
  {
    goto done;
  }

  bResult = GetWinUSBHandle(hDeviceHandle, &hWinUSBHandle);

  if(!bResult)
  {
    goto done;
  }

  bResult = GetUSBDeviceSpeed(hWinUSBHandle, &DeviceSpeed);

  if(!bResult)
  {
    goto done;
  }

  bResult = QueryDeviceEndpoints(hWinUSBHandle, &PipeID);

  if(!bResult)
  {
    goto done;
  }

  bResult = SendDatatoDefaultEndpoint(hWinUSBHandle);

  if(!bResult)
  {
    goto done;
  }

  bResult = WriteToBulkEndpoint(hWinUSBHandle, &PipeID.PipeOutId, &cbSize);

  if(!bResult)
  {
    goto done;
  }

  bResult = ReadFromBulkEndpoint(hWinUSBHandle, &PipeID.PipeInId, cbSize);

  if(!bResult)
  {
    goto done;
  }

  system("PAUSE");

done:
  CloseHandle(hDeviceHandle);
  WinUsb_Free(hWinUSBHandle);

  return 0;
}

Дальнейшие действия

Если устройство поддерживает изохронные конечные точки, вы можете использовать функции WinUSB для отправки передач. Эта функция поддерживается только в Windows 8.1. Дополнительные сведения см. в статье Отправка изохронных передач по USB из классического приложения WinUSB.

См. также раздел