发送来自 UWP 桌面应用的 USB 常时等量传输
从 Windows 8.1 开始,WinUSB 函数集具有 API,这些 API 允许桌面应用程序将数据传入和传出 USB 设备的常时等量终结点。 对于此类应用程序,Microsoft 提供的 Winusb.sys 必须是设备驱动程序。
本文提供了以下信息:
- 常时等量传输的简要概述。
- 基于终结点间隔值的传输缓冲区计算。
- 使用 WinUSB 函数发送读取和写入常时等量数据的传输。
重要的 API
- WinUsb_QueryPipeEx
- WinUsb_WriteIsochPipeAsap
- WinUsb_ReadIsochPipeAsap
从 Windows 8.1 开始,WinUSB 函数集具有 API,这些 API 允许桌面应用程序与 USB 设备的常时等量终结点传输数据。 对于此类应用程序,Microsoft 提供的 Winusb.sys 必须是设备驱动程序。
USB 设备可以支持常时等量终结点,以稳定速率传输依赖于时间的数据,例如音频/视频流。 无法保证交付。 良好的连接不应丢弃任何数据包,这不是正常或预期会丢失数据包,但常时等量协议可以容忍此类丢失。
主机控制器在总线上的保留时间段内发送或接收数据,称为 总线间隔。 总线间隔的单位取决于总线速度。 对于全速,它是 1 毫秒帧,对于高速和超高速,它是 250 微秒微帧。
主机控制器定期轮询设备。 对于读取操作,当终结点准备好发送数据时,设备将通过在总线间隔内发送数据来响应。 若要写入设备,主机控制器会发送数据。
应用在一个服务间隔内可以发送的数据量
本主题中的 常时等量数据包 是指在一个服务间隔内传输的数据量。 该值由 USB 驱动程序堆栈计算,应用可以在查询管道属性时获取该值。
常时等量数据包的大小决定了应用分配的传输缓冲区的大小。 缓冲区必须在帧边界处结束。 传输的总大小取决于应用想要发送或接收的数据量。 在应用启动传输后,主机会将传输缓冲区数据包化,以便在每个时间间隔内,主机可以发送或接收每个间隔允许的最大字节数。
对于数据传输,不会使用所有总线间隔。 在本主题中,使用的总线间隔称为 服务间隔。
如何计算传输数据的帧
应用可以选择使用以下两种方式之一指定帧:
- 自动启动。 在此模式下,应用指示 USB 驱动程序堆栈在下一个适当的帧中发送传输。 应用还必须指定缓冲区是否为连续流,以便驱动程序堆栈可以计算开始帧。
- 指定晚于当前帧的起始帧。 应用应考虑应用启动传输的时间与 USB 驱动程序堆栈处理传输之间的延迟。
代码示例讨论
本主题中的示例演示了以下 WinUSB 函数的用法:
- WinUsb_QueryPipeEx
- WinUsb_RegisterIsochBuffer
- WinUsb_UnregisterIsochBuffer
- WinUsb_WriteIsochPipeAsap
- WinUsb_ReadIsochPipeAsap
- WinUsb_WriteIsochPipe
- WinUsb_ReadIsochPipe
- WinUsb_GetCurrentFrameNumber
- WinUsb_GetAdjustedFrameNumber
在本主题中,我们将在三次传输到高速设备时读取和写入 30 毫秒的数据。 管道能够在每个服务间隔内传输 1024 个字节。 由于轮询间隔为 1,因此数据在帧的每个微帧中传输。 总共 30 帧将携带 30*8*1024 字节。
用于发送读取和写入传输的函数是类似的。 应用分配的传输缓冲区足够大,可以容纳所有三个传输。 应用通过调用 WinUsb_RegisterIsochBuffer 注册特定管道的缓冲区。 调用返回用于发送转移的注册句柄。 缓冲区将重复用于后续传输,缓冲区中的偏移量调整为发送或接收下一组数据。
示例中的所有传输都是异步发送的。 为此,应用分配一个 OVERLAPPED 结构数组,其中包含三个元素,每个传输一个元素。 应用提供事件,以便它可以在传输完成时收到通知,并检索操作的结果。 为此,在数组中的每个 OVERLAPPED 结构中,应用分配一个事件并在 hEvent 成员中设置句柄。
此图显示了使用 WinUsb_ReadIsochPipeAsap 函数进行的三 次 读取传输。 调用指定每次传输的偏移量和长度。 ContinueStream 参数值为 FALSE,表示新流。 之后,应用请求在上一个请求的最后一帧之后立即计划后续传输,以便连续流式传输数据。 常时等量数据包数按每帧数据包数 * 帧数计算;8*10. 对于此调用,应用无需担心开始帧数的计算。
此图显示了使用 WinUsb_WriteIsochPipe 函数进行的三次写入传输。 调用指定每次传输的偏移量和长度。 在这种情况下,应用必须计算主机控制器可以开始发送数据的帧数。 在输出时, 函数接收上一个传输中使用的最后一帧之后的帧的帧编号。 为了获取当前帧,应用调用 WinUsb_GetCurrentFrameNumber。 此时,应用必须确保下一次传输的起始帧晚于当前帧,以便 USB 驱动程序堆栈不会丢弃延迟的数据包。 为此,应用调用 WinUsb_GetAdjustedFrameNumber 以获取真实的当前帧编号, (此编号晚于接收的当前帧编号) 。 为了安全起向,应用再添加五个帧,然后发送传输。
每次传输完成后,应用通过调用 WinUsb_GetOverlappedResult 获取传输结果。 bWait 参数设置为 TRUE,以便调用在操作完成之前不会返回。 对于读取和写入传输, lpNumberOfBytesTransferred 参数始终为 0。 对于写入传输,应用假定如果操作成功完成,则传输所有字节。 对于读取传输,每个常时等量数据包的 Length 成员 (USBD_ISO_PACKET_DESCRIPTOR) 包含每个间隔在该数据包中传输的字节数。 为了获取总长度,应用会添加所有 Length 值。
完成后,应用通过调用 WinUsb_UnregisterIsochBuffer 释放常时等量缓冲区句柄。
准备工作
确保,
设备驱动程序是 Microsoft 提供的驱动程序:WinUSB (Winusb.sys) 。 该驱动程序包含在 \Windows\System32\ 文件夹中。 有关详细信息,请参阅 WinUSB (Winusb.sys) 安装。
你之前已通过调用 WinUsb_Initialize 获取了设备的 WinUSB 接口句柄。 所有操作都使用该句柄执行。 阅读 如何使用 WinUSB 函数访问 USB 设备。
活动接口设置具有常时等量终结点。 否则,无法访问目标终结点的管道。
步骤 1:在活动设置中查找常时常量管道
- 通过调用 WinUsb_QueryInterfaceSettings 获取具有常时等量终结点的 USB 接口。
- 枚举定义终结点的接口设置的管道。
- 对于每个终结点,通过调用WinUsb_QueryPipeEx 获取WINUSB_PIPE_INFORMATION_EX 结构中的关联管道属性。 检索 到WINUSB_PIPE_INFORMATION_EX 结构,其中包含有关常量管道的信息。 结构包含有关管道、其类型、ID 等的信息。
- 检查结构成员以确定它是否是必须用于传输的管道。 如果是,请存储 PipeId 值。 在模板代码中,将成员添加到 device.h 中定义的 DEVICE_DATA 结构。
此示例演示如何确定活动设置是否具有常时等量终结点并获取有关它们的信息。 在此示例中,设备是 SuperMUTT 设备。 设备在默认接口中具有两个常时等量终结点,备用设置 1。
typedef struct _DEVICE_DATA {
BOOL HandlesOpen;
WINUSB_INTERFACE_HANDLE WinusbHandle;
HANDLE DeviceHandle;
TCHAR DevicePath[MAX_PATH];
UCHAR IsochOutPipe;
UCHAR IsochInPipe;
} DEVICE_DATA, *PDEVICE_DATA;
HRESULT
GetIsochPipes(
_Inout_ PDEVICE_DATA DeviceData
)
{
BOOL result;
USB_INTERFACE_DESCRIPTOR usbInterface;
WINUSB_PIPE_INFORMATION_EX pipe;
HRESULT hr = S_OK;
UCHAR i;
result = WinUsb_QueryInterfaceSettings(DeviceData->WinusbHandle,
0,
&usbInterface);
if (result == FALSE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
printf(_T("WinUsb_QueryInterfaceSettings failed to get USB interface.\n"));
CloseHandle(DeviceData->DeviceHandle);
return hr;
}
for (i = 0; i < usbInterface.bNumEndpoints; i++)
{
result = WinUsb_QueryPipeEx(
DeviceData->WinusbHandle,
1,
(UCHAR) i,
&pipe);
if (result == FALSE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
printf(_T("WinUsb_QueryPipeEx failed to get USB pipe.\n"));
CloseHandle(DeviceData->DeviceHandle);
return hr;
}
if ((pipe.PipeType == UsbdPipeTypeIsochronous) && (!(pipe.PipeId == 0x80)))
{
DeviceData->IsochOutPipe = pipe.PipeId;
}
else if (pipe.PipeType == UsbdPipeTypeIsochronous)
{
DeviceData->IsochInPipe = pipe.PipeId;
}
}
return hr;
}
SuperMUTT 设备在设置 1 时在默认接口中定义其常时等量终结点。 前面的代码获取 PipeId 值,并将其存储在 DEVICE_DATA 结构中。
步骤 2:获取有关常时等量管道的间隔信息
接下来,获取有关在调用 WinUsb_QueryPipeEx 时获取的管道的详细信息。
传输大小
从检索到 的 WINUSB_PIPE_INFORMATION_EX 结构中,获取 MaximumBytesPerInterval 和 Interval 值。
根据要发送或接收的常时等量数据量,计算传输大小。 例如,请考虑以下计算:
TransferSize = ISOCH_DATA_SIZE_MS * pipeInfoEx.MaximumBytesPerInterval * (8 / pipeInfoEx.Interval);
在本示例中,传输大小针对 10 毫秒的常时等量数据进行计算。
常时常量数据包数例如,请考虑以下计算:
计算保存整个传输所需的常时等量数据包总数。 此信息是读取传输所必需的,
>IsochInTransferSize / pipe.MaximumBytesPerInterval;
计算方式为 。
此示例演示如何将代码添加到步骤 1 示例,并获取常量管道的间隔值。
#define ISOCH_DATA_SIZE_MS 10
typedef struct _DEVICE_DATA {
BOOL HandlesOpen;
WINUSB_INTERFACE_HANDLE WinusbHandle;
HANDLE DeviceHandle;
TCHAR DevicePath[MAX_PATH];
UCHAR IsochOutPipe;
UCHAR IsochInPipe;
ULONG IsochInTransferSize;
ULONG IsochOutTransferSize;
ULONG IsochInPacketCount;
} DEVICE_DATA, *PDEVICE_DATA;
...
if ((pipe.PipeType == UsbdPipeTypeIsochronous) && (!(pipe.PipeId == 0x80)))
{
DeviceData->IsochOutPipe = pipe.PipeId;
if ((pipe.MaximumBytesPerInterval == 0) || (pipe.Interval == 0))
{
hr = E_INVALIDARG;
printf("Isoch Out: MaximumBytesPerInterval or Interval value is 0.\n");
CloseHandle(DeviceData->DeviceHandle);
return hr;
}
else
{
DeviceData->IsochOutTransferSize =
ISOCH_DATA_SIZE_MS *
pipe.MaximumBytesPerInterval *
(8 / pipe.Interval);
}
}
else if (pipe.PipeType == UsbdPipeTypeIsochronous)
{
DeviceData->IsochInPipe = pipe.PipeId;
if (pipe.MaximumBytesPerInterval == 0 || (pipe.Interval == 0))
{
hr = E_INVALIDARG;
printf("Isoch Out: MaximumBytesPerInterval or Interval value is 0.\n");
CloseHandle(DeviceData->DeviceHandle);
return hr;
}
else
{
DeviceData->IsochInTransferSize =
ISOCH_DATA_SIZE_MS *
pipe.MaximumBytesPerInterval *
(8 / pipe.Interval);
DeviceData->IsochInPacketCount =
DeviceData->IsochInTransferSize / pipe.MaximumBytesPerInterval;
}
}
...
在前面的代码中,应用从WINUSB_PIPE_INFORMATION_EX获取 Interval 和 MaximumBytesPerInterval,以计算读取传输所需的常时等量数据包的传输大小和数量。 对于两个常时等量终结点, 间隔 为 1。 该值指示帧的所有微帧都携带数据。 基于此,要发送 10 毫秒的数据,需要 10 帧,总传输大小为 10*1024*8 字节和 80 个常时等量数据包,每个数据包长度为 1024 字节。
步骤 3:发送写入传输以将数据发送到常时等量 OUT 终结点
此过程总结了将数据写入常量终结点的步骤。
- 分配包含要发送的数据的缓冲区。
- 如果要异步发送数据,请分配并初始化包含调用方分配的事件对象的句柄的 OVERLAPPED 结构。 结构必须初始化为零,否则调用将失败。
- 通过调用 WinUsb_RegisterIsochBuffer 注册缓冲区。
- 通过调用 WinUsb_WriteIsochPipeAsap 开始传输。 如果要手动指定传输数据的帧,请改为调用 WinUsb_WriteIsochPipe 。
- 通过调用 WinUsb_GetOverlappedResult 获取传输结果。
- 完成后,通过调用 WinUsb_UnregisterIsochBuffer、重叠的事件句柄和传输缓冲区来释放缓冲区句柄。
以下示例演示如何发送写入传输。
#define ISOCH_TRANSFER_COUNT 3
VOID
SendIsochOutTransfer(
_Inout_ PDEVICE_DATA DeviceData,
_In_ BOOL AsapTransfer
)
{
PUCHAR writeBuffer;
LPOVERLAPPED overlapped;
ULONG numBytes;
BOOL result;
DWORD lastError;
WINUSB_ISOCH_BUFFER_HANDLE isochWriteBufferHandle;
ULONG frameNumber;
ULONG startFrame;
LARGE_INTEGER timeStamp;
ULONG i;
ULONG totalTransferSize;
isochWriteBufferHandle = INVALID_HANDLE_VALUE;
writeBuffer = NULL;
overlapped = NULL;
printf(_T("\n\nWrite transfer.\n"));
totalTransferSize = DeviceData->IsochOutTransferSize * ISOCH_TRANSFER_COUNT;
if (totalTransferSize % DeviceData->IsochOutBytesPerFrame != 0)
{
printf(_T("Transfer size must end at a frame boundary.\n"));
goto Error;
}
writeBuffer = new UCHAR[totalTransferSize];
if (writeBuffer == NULL)
{
printf(_T("Unable to allocate memory.\n"));
goto Error;
}
ZeroMemory(writeBuffer, totalTransferSize);
overlapped = new OVERLAPPED[ISOCH_TRANSFER_COUNT];
if (overlapped == NULL)
{
printf("Unable to allocate memory.\n");
goto Error;
}
ZeroMemory(overlapped, (sizeof(OVERLAPPED) * ISOCH_TRANSFER_COUNT));
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
overlapped[i].hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (overlapped[i].hEvent == NULL)
{
printf("Unable to set event for overlapped operation.\n");
goto Error;
}
}
result = WinUsb_RegisterIsochBuffer(
DeviceData->WinusbHandle,
DeviceData->IsochOutPipe,
writeBuffer,
totalTransferSize,
&isochWriteBufferHandle);
if (!result)
{
printf(_T("Isoch buffer registration failed.\n"));
goto Error;
}
result = WinUsb_GetCurrentFrameNumber(
DeviceData->WinusbHandle,
&frameNumber,
&timeStamp);
if (!result)
{
printf(_T("WinUsb_GetCurrentFrameNumber failed.\n"));
goto Error;
}
startFrame = frameNumber + 5;
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
if (AsapTransfer)
{
result = WinUsb_WriteIsochPipeAsap(
isochWriteBufferHandle,
DeviceData->IsochOutTransferSize * i,
DeviceData->IsochOutTransferSize,
(i == 0) ? FALSE : TRUE,
&overlapped[i]);
printf(_T("Write transfer sent by using ASAP flag.\n"));
}
else
{
printf("Transfer starting at frame %d.\n", startFrame);
result = WinUsb_WriteIsochPipe(
isochWriteBufferHandle,
i * DeviceData->IsochOutTransferSize,
DeviceData->IsochOutTransferSize,
&startFrame,
&overlapped[i]);
printf("Next transfer frame %d.\n", startFrame);
}
if (!result)
{
lastError = GetLastError();
if (lastError != ERROR_IO_PENDING)
{
printf("Failed to send write transfer with error %x\n", lastError);
}
}
}
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
result = WinUsb_GetOverlappedResult(
DeviceData->WinusbHandle,
&overlapped[i],
&numBytes,
TRUE);
if (!result)
{
lastError = GetLastError();
printf("Write transfer %d with error %x\n", i, lastError);
}
else
{
printf("Write transfer %d completed. \n", i);
}
}
Error:
if (isochWriteBufferHandle != INVALID_HANDLE_VALUE)
{
result = WinUsb_UnregisterIsochBuffer(isochWriteBufferHandle);
if (!result)
{
printf(_T("Failed to unregister isoch write buffer. \n"));
}
}
if (writeBuffer != NULL)
{
delete [] writeBuffer;
}
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
if (overlapped[i].hEvent != NULL)
{
CloseHandle(overlapped[i].hEvent);
}
}
if (overlapped != NULL)
{
delete [] overlapped;
}
return;
}
步骤 4:发送读取传输以从常时等量 IN 终结点接收数据
此过程总结了从常时等量终结点读取数据的步骤。
- 分配将在传输结束时接收数据的传输缓冲区。 缓冲区的大小必须基于步骤 1 中的传输大小计算。 传输缓冲区必须在帧边界处结束。
- 如果要异步发送数据,请分配包含调用方分配的事件对象的句柄的 OVERLAPPED 结构。 结构必须初始化为零,否则调用将失败。
- 通过调用 WinUsb_RegisterIsochBuffer 注册缓冲区。
- 根据步骤 2 中计算的常时等量数据包数, (USBD_ISO_PACKET_DESCRIPTOR) 分配 常量数据包数组。
- 通过调用 WinUsb_ReadIsochPipeAsap 开始传输。 如果要手动指定传输数据的起始帧,请改为调用 WinUsb_ReadIsochPipe 。
- 通过调用 WinUsb_GetOverlappedResult 获取传输结果。
- 完成后,通过调用 WinUsb_UnregisterIsochBuffer、重叠的事件句柄、常时等量数据包数组和传输缓冲区来释放缓冲区句柄。
以下示例演示如何通过调用 WinUsb_ReadIsochPipeAsap 和 WinUsb_ReadIsochPipe 发送读取传输。
#define ISOCH_TRANSFER_COUNT 3
VOID
SendIsochInTransfer(
_Inout_ PDEVICE_DATA DeviceData,
_In_ BOOL AsapTransfer
)
{
PUCHAR readBuffer;
LPOVERLAPPED overlapped;
ULONG numBytes;
BOOL result;
DWORD lastError;
WINUSB_ISOCH_BUFFER_HANDLE isochReadBufferHandle;
PUSBD_ISO_PACKET_DESCRIPTOR isochPackets;
ULONG i;
ULONG j;
ULONG frameNumber;
ULONG startFrame;
LARGE_INTEGER timeStamp;
ULONG totalTransferSize;
readBuffer = NULL;
isochPackets = NULL;
overlapped = NULL;
isochReadBufferHandle = INVALID_HANDLE_VALUE;
printf(_T("\n\nRead transfer.\n"));
totalTransferSize = DeviceData->IsochOutTransferSize * ISOCH_TRANSFER_COUNT;
if (totalTransferSize % DeviceData->IsochOutBytesPerFrame != 0)
{
printf(_T("Transfer size must end at a frame boundary.\n"));
goto Error;
}
readBuffer = new UCHAR[totalTransferSize];
if (readBuffer == NULL)
{
printf(_T("Unable to allocate memory.\n"));
goto Error;
}
ZeroMemory(readBuffer, totalTransferSize);
overlapped = new OVERLAPPED[ISOCH_TRANSFER_COUNT];
ZeroMemory(overlapped, (sizeof(OVERLAPPED) * ISOCH_TRANSFER_COUNT));
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
overlapped[i].hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (overlapped[i].hEvent == NULL)
{
printf("Unable to set event for overlapped operation.\n");
goto Error;
}
}
isochPackets = new USBD_ISO_PACKET_DESCRIPTOR[DeviceData->IsochInPacketCount * ISOCH_TRANSFER_COUNT];
ZeroMemory(isochPackets, DeviceData->IsochInPacketCount * ISOCH_TRANSFER_COUNT);
result = WinUsb_RegisterIsochBuffer(
DeviceData->WinusbHandle,
DeviceData->IsochInPipe,
readBuffer,
DeviceData->IsochInTransferSize * ISOCH_TRANSFER_COUNT,
&isochReadBufferHandle);
if (!result)
{
printf(_T("Isoch buffer registration failed.\n"));
goto Error;
}
result = WinUsb_GetCurrentFrameNumber(
DeviceData->WinusbHandle,
&frameNumber,
&timeStamp);
if (!result)
{
printf(_T("WinUsb_GetCurrentFrameNumber failed.\n"));
goto Error;
}
startFrame = frameNumber + 5;
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
if (AsapTransfer)
{
result = WinUsb_ReadIsochPipeAsap(
isochReadBufferHandle,
DeviceData->IsochInTransferSize * i,
DeviceData->IsochInTransferSize,
(i == 0) ? FALSE : TRUE,
DeviceData->IsochInPacketCount,
&isochPackets[i * DeviceData->IsochInPacketCount],
&overlapped[i]);
printf(_T("Read transfer sent by using ASAP flag.\n"));
}
else
{
printf("Transfer starting at frame %d.\n", startFrame);
result = WinUsb_ReadIsochPipe(
isochReadBufferHandle,
DeviceData->IsochInTransferSize * i,
DeviceData->IsochInTransferSize,
&startFrame,
DeviceData->IsochInPacketCount,
&isochPackets[i * DeviceData->IsochInPacketCount],
&overlapped[i]);
printf("Next transfer frame %d.\n", startFrame);
}
if (!result)
{
lastError = GetLastError();
if (lastError != ERROR_IO_PENDING)
{
printf("Failed to start a read operation with error %x\n", lastError);
}
}
}
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
result = WinUsb_GetOverlappedResult(
DeviceData->WinusbHandle,
&overlapped[i],
&numBytes,
TRUE);
if (!result)
{
lastError = GetLastError();
printf("Failed to read with error %x\n", lastError);
}
else
{
numBytes = 0;
for (j = 0; j < DeviceData->IsochInPacketCount; j++)
{
numBytes += isochPackets[j].Length;
}
printf("Requested %d bytes in %d packets per transfer.\n", DeviceData->IsochInTransferSize, DeviceData->IsochInPacketCount);
}
printf("Transfer %d completed. Read %d bytes. \n\n", i+1, numBytes);
}
Error:
if (isochReadBufferHandle != INVALID_HANDLE_VALUE)
{
result = WinUsb_UnregisterIsochBuffer(isochReadBufferHandle);
if (!result)
{
printf(_T("Failed to unregister isoch read buffer. \n"));
}
}
if (readBuffer != NULL)
{
delete [] readBuffer;
}
if (isochPackets != NULL)
{
delete [] isochPackets;
}
for (i = 0; i < ISOCH_TRANSFER_COUNT; i++)
{
if (overlapped[i].hEvent != NULL)
{
CloseHandle(overlapped[i].hEvent);
}
}
if (overlapped != NULL)
{
delete [] overlapped;
}
return;
}