使用 WinUSB 函数访问 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 版本:Windows 8.1、Windows 8、Windows 7、Windows Server 2008 和 Windows Vista。
  • 已将 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 (usbAltInterfaceDescriptor 参数) 中传递的调用方分配USB_INTERFACE_DESCRIPTOR结构填充有关接口的信息。 例如,接口中的终结点数在 USB_INTERFACE_DESCRIPTORbNumEndpoints 成员中设置。

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

  • 终结点

    调用 WinUsb_QueryPipe 以获取有关每个接口上每个终结点的信息。 WinUsb_QueryPipe 使用有关指定终结点管道的信息填充调用方分配 的WINUSB_PIPE_INFORMATION 结构。 终结点的管道由从零开始的索引标识,并且必须小于接口描述符的 bNumEndpoints 成员中的值,该成员在上一次调用 **WinUsb_QueryInterfaceSettings 中检索。 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 字节数据缓冲区,该缓冲区通过设置相应的位来指定应点亮哪些元素。

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

发出控制请求

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

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

    • RequestType 成员指定请求方向。 它设置为 0,表示主机到设备的数据传输。 对于设备到主机的传输,请将 RequestType 设置为 1。
    • Request 成员已针对此请求设置为供应商定义的代码 0xD8。 为方便起见,它定义为SET_BARGRAPH_DISPLAY。
    • Length 成员设置为数据缓冲区的大小。
    • 此请求不需要 IndexValue 成员,因此它们设置为零。
  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:发出 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 函数

下面的代码示例显示了控制台应用程序的 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 常时等量传输

另请参阅