通过 WinUSB Functions 访问 USB 设备

本文包含有关如何使用 WinUSB 函数 与使用 Winusb.sys 作为其功能驱动程序的 USB 设备进行通信的详细演练。

总结

  • 打开设备并获取 WinUSB 句柄。
  • 获取有关所有接口及其终结点的设备、配置和接口设置的信息。
  • 从批量终结点和中断终结点读取数据并将数据写入其中。

重要的 API

如果使用 Microsoft Visual Studio 2013,请使用 WinUSB 模板创建主干应用。 在这种情况下,请跳过步骤 1 到 3,然后从本文中的步骤 4 继续操作。 该模板将打开设备的文件句柄,并获取后续操作所需的 WinUSB 句柄。 该句柄存储在 device.h 中应用定义的DEVICE_DATA结构中。

有关模板的详细信息,请参阅“基于 WinUSB 模板编写 Windows 桌面应用”。

注意

WinUSB 函数需要 Windows XP 或更高版本。 可以在 C/C++应用程序中使用这些函数与 USB 设备通信。 Microsoft 不为 WinUSB 提供托管 API。

开始之前

以下各项适用于本演练:

  • 此信息适用于 Windows 8.1、Windows 8、Windows 7、Windows Server 2008、Windows Vista 版本的 Windows。
  • 已将 Winusb.sys 作为设备的函数驱动程序进行了安装。 有关此过程的详细信息,请参阅 WinUSB (Winusb.sys) 安装
  • 本文中的示例基于 OSR USB FX2 学习工具包设备。 可以使用这些示例将过程扩展到其他 USB 设备。

步骤 1:基于 WinUSB 模板创建主干应用

若要访问 USB 设备,请首先基于 Windows 驱动程序工具包(WDK)集成环境中随附的 WinUSB 模板创建主干应用(使用适用于 Windows 的调试工具),并Microsoft Visual Studio。 可以将模板用作起点。

有关模板代码与如何创建、生成、部署和调试主干应用的信息,请参阅基于 WinUSB 模板编写 Windows 桌面应用

该模板通过使用 SetupAPI 例程来枚举设备,打开设备的文件句柄,并创建后续任务所需的 WinUSB 接口句柄。 有关获取设备句柄并打开设备的示例代码,请参阅模板代码讨论

步骤 2:查询设备以获取 USB 描述符

接下来,向设备查询特定于 USB 的信息,例如设备速度、接口描述符、相关终结点及其管道。 此过程类似于 USB 设备驱动程序使用的过程。 但是,应用程序通过调用 WinUsb_GetDescriptor来完成设备查询。

以下列表显示了 WinUSB 函数,你可以调用这些函数来获取特定于 USB 的信息:

  • 更多设备信息。

    调用 WinUsb_QueryDeviceInformation 从设备描述符请求信息。 若要获取设备的速度,请设置 InformationType 参数中的DEVICE_SPEED(0x01)。 该函数返回 LowSpeed (0x01) 或 HighSpeed (0x03)。

  • 接口描述符

    调用 WinUsb_QueryInterfaceSettings 并传递设备的接口句柄以获取相应的接口描述符。 WinUSB 接口句柄对应于第一个接口。 某些 USB 设备(例如 OSR Fx2 设备)仅支持一个接口,没有任何备用设置。 因此,对于这些设备,AlternateSettingNumber 参数设置为零,并且该函数仅被调用一次。 WinUsb_QueryInterfaceSettings使用接口的相关信息填充调用方分配USB_INTERFACE_DESCRIPTOR结构(传入 UsbAltInterfaceDescriptor 参数)。 例如,接口中的终结点数在 USB_INTERFACE_DESCRIPTOR 的 bNumEndpoints 成员设置。

    对于支持多个接口的设备,调用WinUsb_GetAssociatedInterface通过在 AssociatedInterfaceIndex 参数中指定备用设置来获取关联接口的接口句柄。

  • 终结点

    调用 WinUsb_QueryPipe 以获取有关每个接口上每个终结点的信息。 WinUsb_QueryPipe使用有关指定终结点管道的信息填充调用方分配WINUSB_PIPE_INFORMATION结构。 终结点的管道由从零开始的索引标识,并且必须小于在上一次对 **WinUsb_QueryInterfaceSettings调用中检索的接口描述符的 bNumEndpoints 成员中的值。 OSR Fx2 设备有一个具有三个终结点的接口。 对于此设备,函数的 AlternateInterfaceNumber 参数设置为 0,PipeIndex 参数的值的值在 0 到 2 之间变动。

    若要确定管道类型,请检查 WINUSB_PIPE_INFORMATION 结构的 PipeInfo 成员。 此成员设置为USBD_PIPE_TYPE枚举值之一:UsbdPipeTypeControl、UsbdPipeTypeIsochronous、UsbdPipeTypeBulk 或 UsbdPipeTypeInterrupt。 OSR USB FX2 设备支持一个中断管道、一个批量传入管道和一个批量传出管道,因此 PipeInfo 设置为 UsbdPipeTypeInterrupt 或 UsbdPipeTypeBulk。 UsbdPipeTypeBulk 值标识批量管道,但不提供管道的方向。 方向信息在管道地址的高位编码,该地址存储在WINUSB_PIPE_INFORMATION结构的 PipeId 成员中 确定管道方向的最简单方法是将 PipeId 值传递到 Usb100.h 中的以下宏之一:

    • 如果方向为 in,则 USB_ENDPOINT_DIRECTION_IN (PipeId) 宏将返回 TRUE
    • 如果方向为 out,则 USB_ENDPOINT_DIRECTION_OUT(PipeId) 宏将返回 TRUE

    应用程序使用 PipeId 值来标识在调用 WinUSB 函数时用于数据传输的管道,例如 WinUsb_ReadPipe (本主题的“问题 I/O 请求”部分中所述),因此示例存储所有三 个 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;
}

下面的示例代码查询 WinUSB 接口句柄指定的 USB 设备的各种描述符。 该示例函数检索受支持终结点的类型及其管道标识符。 该示例存储所有三个 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 设备使用默认终结点来控制灯条和 7 段数字显示器。

控制命令包含一个 8 字节设置数据包,其中包括指定特定请求的请求代码和可选的数据缓冲区。 请求代码和缓冲区格式是供应商定义的。 在此示例中,应用程序将数据发送到设备来控制灯条。 设置浅色条的代码是0xD8,这是为方便起见而定义的SET_BARGRAPH_DISPLAY。 对于此请求,设备需要一个 1 字节数据缓冲区,该缓冲区通过设置相应的位来指定应点亮哪些元素。

应用程序可以提供一组八个复选框控件来指定应点亮的光条元素。 指定的元素对应于缓冲区中的相应位。 为避免编写 UI 代码,此部分中的示例代码将设置位以使备用灯亮起。

发出控制请求

  1. 分配一个 1 字节数据缓冲区,并将数据加载到通过设置相应位来指定应点亮的元素的缓冲区中。

  2. 在调用方分配 WINUSB_SETUP_PACKET 结构中构造设置数据包。 将成员初始化,以便表示请求类型和数据,如下所示:

    • RequestType 成员指定请求方向。 它设置为 0,表示主机到设备的数据传输。 对于设备到主机的传输,请将 RequestType 设置为 1。
    • Request 成员已针对此请求设置为供应商定义的代码 0xD8。 它定义为方便SET_BARGRAPH_DISPLAY。
    • Length 成员设置为数据缓冲区的大小。
    • 请求不需要 IndexValue 成员,因此它们设置为零。
  3. 通过传递设备的 WinUSB 接口句柄、设置数据包和数据缓冲区,调用 WinUsb_ControlTransfer 将请求传输到默认终结点。 该函数在 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:发出 I/O 请求

接下来,将数据发送到设备的批量传入终结点和批量传出终结点,这两种终结点可分别用于读取请求和写入请求。 在 OSR USB FX2 设备上,已为环回功能配置了这两个终结点,因此设备会将数据从批量传入终结点移动到批量传出终结点。 它不会更改数据的值或添加任何新数据。 对于环回配置,读取请求将读取由最新写入请求发送的数据。 WinUSB 提供了以下用于发送写入请求和读取请求的函数:

  • WinUsb_ReadPipe
  • WinUsb_ReadPipe

发送写入请求

  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 函数。

有关获取设备句柄和打开设备的示例代码(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受支持。 有关详细信息,请参阅从 WinUSB 桌面应用发送 USB 常时等量传输

另请参阅