通过 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 值供以后使用。
- 如果方向为 in,则
以下示例代码获取由 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 字节数据缓冲区,并将数据加载到通过设置相应位来指定应点亮的元素的缓冲区中。
在调用方分配 WINUSB_SETUP_PACKET 结构中构造设置数据包。 将成员初始化,以便表示请求类型和数据,如下所示:
- RequestType 成员指定请求方向。 RequestType 设置为 0,表示主机到设备的数据传输。 对于设备到主机的传输,请将 RequestType 设置为 1。
- Request 成员已针对此请求设置为供应商定义的代码 0xD8。 为了方便起见,将请求 定义为SET_BARGRAPH_DISPLAY。
- Length 成员设置为数据缓冲区的大小。
- 此 请求不需要 Index 和 Value 成员,因此它们设置为零。
通过传递设备的 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 提供了以下用于发送写入请求和读取请求的函数:
发送写入请求
- 分配一个缓冲区并使用要写入到设备的数据进行填充。 如果应用程序未将RAW_IO设置为管道的策略类型,则缓冲区大小没有限制。 如有必要,WinUSB 会将缓冲区划分为适当大小的区块。 如果设置了RAW_IO,则缓冲区的大小受 WinUSB 支持的最大传输大小的限制。
- 调用 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 常时等量传输。