我正在尝试编写代码以使微控制器可以通过 USB 与 PC 进行通信。我认为大部分代码都已到位,只要速度较慢(每条消息> ~ 5ms),通信通常可以正常工作。但是,我想让它以更高的速度以 0% 的丢弃率工作(最好在每条消息之间以几十到几百微秒的速度)。通信配置为使用全速 USB 2.0 协议,因此我的传输速率约为 12 MHz。
由于协议的工作方式,我发现相对于(比如)CAN 的 USB 问题更难调试,但我相信问题出在我编写的与设备接口的 PC 端驱动程序上。
设备和驱动程序使用两个标准控制端点(物理端点 0 和 1)并另外使用两个批量端点:
- BULK 2 Out(物理端点 4)
- BULK 2 In(物理端点 5)
这两者的最大数据包大小为 64 字节。
我创建的驱动程序或多或少基于Microsoft 网站上的示例代码。我导出了一个receiveMessage
函数,最初,每次调用它时,它都会分配一个 64 字节的缓冲区,并使用WinUsb_ReadPipe
. 如果没有要读取的数据,则会有 1 毫秒的超时以防止应用程序挂起。
这实质上意味着从管道读取数据的速率受限于应用程序可以轮询它的速率。如果设备向管道写入数据的速度快于应用程序从中读取数据的速度,我相信这会导致问题。我试图通过在驱动程序中创建一个队列和一个线程来解决这个问题,该线程除了不断地轮询管道并将任何接收到的消息存储在队列中之外什么都不做,然后应用程序可以使用“receiveMessage”随意读取这些消息。但是,这并没有很好地工作。
我想要的是一个代码示例,它演示了一种确保以下内容的方法:
- 可以尽可能快地从 BULK In 管道中读取数据。
- 有一种“缓冲”消息的方法,以便设备可以比应用程序在短时间内更快地写入管道,但不会丢弃任何消息。
我认为一种可能的方法可能是在设备和驱动程序之间建立另一个批量管道。然后,设备应将所有传出消息存储到缓冲区中,并保留一个单独的数组,该数组包含该缓冲区中相应索引处的每个消息的字节数。然后每当应用程序想要读取一组消息时,驱动程序使用这些管道之一从设备请求消息字节数组。对于此数组中的每个值,驱动程序使用 发出对该字节数的请求,WinUsb_ReadPipe
设备通过以相同顺序通过 USB 总线发送每个消息来服务该请求。不过,我不确定这是否可行,或者是否过于复杂。
我当前的初始化代码:
_declspec(dllexport) int initialiseC()
{
HRESULT hr;
BOOL bResult;
BOOL noDevice;
ULONG lengthReceived;
ULONG cbSize;
cbSize = 0;
//OutputDebugString((LPCWSTR)"Beginning of initialisation\n");
//
// Find a device connected to the system that has WinUSB installed using our
// INF
//
hr = OpenDevice(&deviceData, &noDevice);
if (FAILED(hr)) {
if (noDevice) {
printf("Device not connected or driver not installed\n");
MessageBoxA(NULL, "Device not connected or driver not installed", "Debug", MB_OK);
}
else {
printf("Failed looking for device, HRESULT 0x%x\n", hr);
MessageBoxA(NULL, "Failed looking for device", "Debug", MB_OK);
}
getchar();
return 1;
}
//
// Get device descriptor
//
bResult = WinUsb_GetDescriptor(deviceData.WinusbHandle,
USB_DEVICE_DESCRIPTOR_TYPE,
0,
0,
(PBYTE)&deviceDesc,
sizeof(deviceDesc),
&lengthReceived);
if (FALSE == bResult || lengthReceived != sizeof(deviceDesc)) {
printf("Error among LastError %d or lengthReceived %d\n",
FALSE == bResult ? GetLastError() : 0,
lengthReceived);
MessageBoxA(NULL, "Initialisation error", "Debug", MB_OK);
CloseDevice(&deviceData);
getchar();
return 2;
}
//
// Print a few parts of the device descriptor
//
printf("Device found: VID_%04X&PID_%04X; bcdUsb %04X\n",
deviceDesc.idVendor,
deviceDesc.idProduct,
deviceDesc.bcdUSB);
// Retrieve pipe information.
bResult = QueryDeviceEndpoints(deviceData.WinusbHandle, &pipeID);
if (!bResult)
{
printf("Error querying device endpoints\n");
MessageBoxA(NULL, "Error querying device endpoints", "Debug", MB_OK);
CloseDevice(&deviceData);
getchar();
return 3;
}
// Set timeout for read requests.
ULONG timeout = 1; // 1 ms.
WinUsb_SetPipePolicy(deviceData.WinusbHandle, pipeID.PipeInId,
PIPE_TRANSFER_TIMEOUT, sizeof(timeout), &timeout);
// Create message polling thread.
messagePollerHandle = CreateThread(NULL, 0, messagePoller, NULL, 0, NULL);
if (messagePollerHandle == NULL)
{
printf("Error creating message poller thread\n");
MessageBoxA(NULL, "Error creating message poller thread", "Debug", MB_OK);
CloseDevice(&deviceData);
getchar();
return 4;
}
initStatus = 1;
return 0;
}
我使用以下代码在内部轮询驱动程序中的消息:
DWORD WINAPI messagePoller(LPVOID lpParam)
{
BOOL bResult;
ULONG cbReceived = 0;
USB_TYPE_T usbMessageIn;
while (initStatus)
{
UCHAR* receiveBuffer = (UCHAR*)LocalAlloc(LPTR, MAX_PACKET_SIZE*1000);
bResult = ReadFromBulkEndpoint(deviceData.WinusbHandle, &pipeID.PipeInId, MAX_PACKET_SIZE*1000, &cbReceived, receiveBuffer);
if (!bResult)
{
printf("Error reading data from endpoint\n");
//MessageBoxA(NULL, "Error reading data from endpoint", "Debug", MB_OK);
getchar();
CloseDevice(&deviceData);
usbMessageIn.len = 0;
return 1;
}
if (cbReceived == 0)
{
LocalFree(receiveBuffer);
continue;
}
const char* input = reinterpret_cast<const char*>(receiveBuffer);
strcpy_s(usbMessageIn.string, input);
usbMessageIn.len = strlen(input);
while (receiveMessageSema);
receiveMessageSema = TRUE;
while (receiveQueue.size() >= RECEIVE_QUEUE_MAX_SIZE) receiveQueue.pop_front();
receiveQueue.push_back(usbMessageIn);
receiveMessageSema = FALSE;
LocalFree(receiveBuffer);
}
return 0;
}
receiveMessage
此处给出了使用驱动程序的应用程序可以用来接收消息的导出代码:
_declspec(dllexport) void receiveUSBMessageC(USB_TYPE_T *usbMessageIn)
{
if (receiveMessageSema || receiveQueue.empty())
{
usbMessageIn->len = 0;
strcpy_s(usbMessageIn->string, "");
}
else
{
receiveMessageSema = TRUE;
*usbMessageIn = receiveQueue.front();
receiveQueue.pop_front();
receiveMessageSema = FALSE;
}
}
编辑
根据 Hasturkun 的建议,我对代码进行了以下更改。
在初始化中:
...
readEventHandle[0] = CreateEvent(NULL, FALSE, TRUE, TEXT("ReadEvent0"));
readEventHandle[1] = CreateEvent(NULL, FALSE, TRUE, TEXT("ReadEvent1"));
if (!readEventHandle[0] || !readEventHandle[1])
{
printf("readEvent creation error\n");
MessageBoxA(NULL, "readEvent creation error", "Debug", MB_OK);
CloseDevice(&deviceData);
getchar();
return 5;
}
readPipeHandle[0] = CreateThread(NULL, 0, readPipe, &readPipeParam0, 0, NULL);
readPipeHandle[1] = CreateThread(NULL, 0, readPipe, &readPipeParam1, 0, NULL);
if (!readPipeHandle[0] || !readPipeHandle[1])
{
printf("readPipe creation error\n");
MessageBoxA(NULL, "readPipe creation error", "Debug", MB_OK);
CloseDevice(&deviceData);
getchar();
return 6;
}
readOverlapped[0].hEvent = readEventHandle[0];
readOverlapped[1].hEvent = readEventHandle[1];
...
读取管螺纹:
DWORD WINAPI readPipe(LPVOID lpParam)
{
BOOL bResult;
ULONG cbReceived = 0;
USB_TYPE_T usbMessageIn;
DWORD waitResult;
uint8_t index = *static_cast<uint8_t*>(lpParam);
BOOLEAN init = FALSE;
UCHAR receiveBuffer[MAX_PACKET_SIZE] = { 0 };
LARGE_INTEGER start[2], end[2], freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start[index]);
uint32_t microDelta;
while (initStatus)
{
waitResult = WaitForSingleObject(readEventHandle[index], INFINITE);
switch (waitResult)
{
case WAIT_OBJECT_0:
if (init)
{
WinUsb_GetOverlappedResult(deviceData.WinusbHandle, &readOverlapped[index], &cbReceived, FALSE);
if (cbReceived > 0)
{
// Read data, set usbMessageIn and add to queue.
strcpy_s(usbMessageIn.string, reinterpret_cast<const char*>(receiveBuffer));
usbMessageIn.len = cbReceived;
mtx.lock();
while (receiveQueue.size() >= RECEIVE_QUEUE_MAX_SIZE)
{
MessageBoxA(NULL, "Message receive queue full", "Debug", MB_OK);
receiveQueue.pop_front();
}
receiveQueue.push_back(usbMessageIn);
#ifdef CREATE_DEBUG_LOG
QueryPerformanceCounter(&end[index]);
microDelta = (end[index].QuadPart - start[index].QuadPart) * 1000000 / freq.QuadPart;
std::string str = usbMessageIn.string;
while (str.length() < 24) str += " ";
fprintf(logFile, "Message %s (Len: %d) (Queue size: %d) (Delta: %6d us) (Thread: %d)\n",
str.c_str(), cbReceived, receiveQueue.size(), microDelta, index);
QueryPerformanceCounter(&start[index]);
#endif
mtx.unlock();
}
}
else
{
init = TRUE;
}
// Create another read request.
std::fill(receiveBuffer, receiveBuffer + sizeof(receiveBuffer), 0);
bResult = ReadFromBulkEndpoint(deviceData.WinusbHandle, &pipeID.PipeInId, MAX_PACKET_SIZE,
NULL, receiveBuffer, &readOverlapped[index]);
if (!bResult && GetLastError() != ERROR_IO_PENDING)
{
printf("Error reading data from endpoint\n");
MessageBoxA(NULL, "Error reading data from endpoint", "Debug", MB_OK);
getchar();
CloseDevice(&deviceData);
usbMessageIn.len = 0;
return 1;
}
break;
default:
MessageBoxA(NULL, "Error handling read event", "Debug", MB_OK);
break;
}
}
return 0;
}
我还注释掉了与 相关的所有内容messagePoller
,因为readPipe
现在处理所有这些。
但是,我仍然遇到性能问题。
编辑 2
更新readPipe
了上面的代码。
我开始认真地怀疑问题出在我的驱动程序还是微控制器上。使用像 CAN 这样的协议,可以更容易地判断问题出在哪里......
我让我的驱动程序生成它收到的所有消息的日志,包括其他详细信息,例如接收两条消息的线程之间的时间增量,以及哪个线程正在处理消息(我有两个)。我得到这样的输出:
Message 000000050FFF00640064 (Len: 20) (Queue size: 1) (Delta: 573120 us) (Thread: 0)
Message 000000070000323232323232 (Len: 24) (Queue size: 1) (Delta: 593050 us) (Thread: 1)
Message 000000070100323232323232 (Len: 24) (Queue size: 1) (Delta: 39917 us) (Thread: 0)
Message 000000090000 (Len: 12) (Queue size: 1) (Delta: 39950 us) (Thread: 1)
Message 0000000B0000 (Len: 12) (Queue size: 1) (Delta: 59842 us) (Thread: 0)
Message 0000000D0FFF001B003A001B (Len: 24) (Queue size: 1) (Delta: 59979 us) (Thread: 1)
Message 0000030D001F (Len: 12) (Queue size: 2) (Delta: 20207 us) (Thread: 0)
Message 0000020D00280024001B002D (Len: 24) (Queue size: 3) (Delta: 227 us) (Thread: 1)
Message 0000000F000F000000000000 (Len: 24) (Queue size: 1) (Delta: 39890 us) (Thread: 0)
Message 0000010F0000 (Len: 12) (Queue size: 2) (Delta: 39902 us) (Thread: 1)
Message 0000001100FF001D001D0020 (Len: 24) (Queue size: 1) (Delta: 19827 us) (Thread: 0)
Message 000001110FFF0020001E001E (Len: 24) (Queue size: 2) (Delta: 19824 us) (Thread: 1)
Message 00000211001E (Len: 12) (Queue size: 3) (Delta: 224 us) (Thread: 0)
Message 0000001300 (Len: 10) (Queue size: 1) (Delta: 19996 us) (Thread: 1)
Message 0000001D4000 (Len: 12) (Queue size: 1) (Delta: 63864 us) (Thread: 0)
Message 0000000D0FFF001600310016 (Len: 24) (Queue size: 1) (Delta: 4025107 us) (Thread: 1)
Message 0000030D001F (Len: 12) (Queue size: 2) (Delta: 3981220 us) (Thread: 0)
Message 0000020D002800240016002D (Len: 24) (Queue size: 3) (Delta: 326 us) (Thread: 1)
Message 0000000D0FFF001600310016 (Len: 24) (Queue size: 1) (Delta: 58885 us) (Thread: 0)
Message 0000030D0024 (Len: 12) (Queue size: 2) (Delta: 58852 us) (Thread: 1)
Message 0000020D0024001F001F0031 (Len: 24) (Queue size: 3) (Delta: 310 us) (Thread: 0)
Message 0000000D0FFF001B0036001B (Len: 24) (Queue size: 1) (Delta: 49755 us) (Thread: 1)
Message 0000030D0024 (Len: 12) (Queue size: 2) (Delta: 49886 us) (Thread: 0)
Message 0000020D00240024001B0036 (Len: 24) (Queue size: 3) (Delta: 447 us) (Thread: 1)
Message 0000000D0FFF001600360016 (Len: 24) (Queue size: 1) (Delta: 49703 us) (Thread: 0)
Message 0000030D001F (Len: 12) (Queue size: 2) (Delta: 49589 us) (Thread: 1)
Message 0000020D001F0024001B002D (Len: 24) (Queue size: 3) (Delta: 357 us) (Thread: 0)
Message 0000000D0FFF001600310016 (Len: 24) (Queue size: 1) (Delta: 49896 us) (Thread: 1)
Message 0000030D001F (Len: 12) (Queue size: 2) (Delta: 49860 us) (Thread: 0)
Message 0000020D0024001F001B002D (Len: 24) (Queue size: 3) (Delta: 315 us) (Thread: 1)
Message 0000000D0FFF00160036001B (Len: 24) (Queue size: 1) (Delta: 49724 us) (Thread: 0)
Message 0000030D001F (Len: 12) (Queue size: 2) (Delta: 49891 us) (Thread: 1)
Message 0000020D00240024001B0031 (Len: 24) (Queue size: 3) (Delta: 452 us) (Thread: 0)
Message 0000000D0FFF001B00360016 (Len: 24) (Queue size: 1) (Delta: 49742 us) (Thread: 1)
本质上,当应用程序首次启动时,会在应用程序和设备之间发送许多“启动”消息。大约 4 秒后,我使用应用程序从设备请求稳定的消息流,其中包含一组 4 条消息,这些消息以 50 毫秒的间隔发送:
- 0000000D...
- 0000010D...
- 0000020D...
- 0000030D...
看起来驱动程序实际上表现得非常好,因为我们可以看到它在几百微秒内始终如一地在我们预期的点上报告值。然而:
- 30D... 消息似乎始终在 20D... 消息之前到达
- 10D... 消息不断被丢弃
这可能是微控制器代码的问题。我正在使用双缓冲端点,因此可以理解为什么消息无序到达。
一件事是,在发送后续消息之前,我没有明确等待微控制器代码中批量 IN 管道上的 ACK。这可能是问题所在,尽管我之前尝试过这样做,但似乎没有太大影响。
下面是 USBLyzer 输出的屏幕截图,如果它们有帮助的话(在不同的运行期间拍摄,因此数据不会完全相同但仍然非常相似)。它们是屏幕截图,只是因为日志文件格式不正确。
编辑 3
似乎我的微控制器正在向驱动程序发送消息,每条消息之间只有大约 30 微秒(以最大速率),而根据日志,驱动程序处理一条消息似乎需要 200-500 微秒。这是一个很大的差异。真正的问题是,即使我的驱动程序跟不上,比我的驱动程序软件更低级别的东西正在以与发送消息相同的速率向微控制器发送 ACK,因此我不能基于此限制它。似乎每次驱动程序收到消息时,我可能需要从驱动程序显式向 BULK Out 管道上的微控制器发送一条消息,说“我已准备好接收另一条消息”,但这似乎真的很慢事情下来了。有没有更好的选择?