Я пытаюсь написать код, чтобы микроконтроллер мог обмениваться данными через USB с ПК. Я думаю, что большая часть кода на месте, и общение в целом работает нормально, пока оно медленное (> ~ 5 мс на сообщение). Тем не менее, я бы хотел, чтобы он работал с 0% -ной скоростью выпадения на гораздо более высоких скоростях (предпочтительно со скоростью от нескольких десятков до нескольких сотен микросекунд между каждым сообщением). Связь настроена на использование полноскоростного протокола USB 2.0, поэтому скорость передачи составляет около 12 МГц.
Мне труднее отлаживать проблемы с USB относительно (скажем) CAN из-за того, как работает протокол, но я считаю, что проблема связана с драйвером на стороне ПК, который я написал для взаимодействия с устройством.
Устройство и драйвер используют две стандартные конечные точки управления (физические конечные точки 0 и 1) и дополнительно используют две основные конечные точки:
Оба из них имеют максимальный размер пакета 64 байта.
Драйвер, который я создал, был более или менее основан на пример кода на сайте Microsoft. Я экспортировал receiveMessage
Функция и первоначально каждый раз, когда это вызывалось, он выделял 64-байтовый буфер и запрашивал 64 байта данных из BULK 2 In pipe, используя WinUsb_ReadPipe
, Время ожидания 1 мс для предотвращения зависания приложения, если нет данных для чтения.
По сути, это означает, что скорость чтения данных из канала ограничена скоростью, с которой приложение может их опрашивать. Если устройство записывает данные в канал быстрее, чем приложение может прочитать с него, я уверен, что это вызовет проблемы. Я попытался решить эту проблему, создав в драйвере очередь и поток, который ничего не делает, кроме как непрерывно опрашивает канал и сохраняет все принятые сообщения в очереди, которые затем приложение может прочитать на досуге, используя `receiveMessage ‘. Это не сработало очень хорошо, хотя.
Что я хотел бы, это пример кода, который демонстрирует способ обеспечить следующее:
Я думаю, что одним из возможных подходов может быть создание другого массового конвейера между устройством и драйвером. Затем устройство должно сохранить все исходящие сообщения в буфере, а также сохранить отдельный массив, содержащий количество байтов каждого из сообщений с соответствующим индексом в этом буфере. Затем, когда приложение хочет прочитать набор сообщений, драйвер использует один из этих конвейеров для запроса массива байтов сообщений от устройства. Для каждого значения в этом массиве драйвер выдает запрос на это количество байтов, используя 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;
}
}
РЕДАКТИРОВАТЬ
Основываясь на предложениях Хастуркуна, я внес следующие изменения в код.
В пределах инициализации:
...
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 мс:
Похоже, что драйвер действительно работает действительно хорошо, так как мы можем видеть, как он последовательно сообщает значения в несколько сотен микросекунд в ожидаемых точках. Тем не мение:
Это может быть проблемой с кодом микроконтроллера. Я использую конечные точки с двойной буферизацией, чтобы было понятно, почему сообщения приходят не по порядку.
Одна вещь заключается в том, что я не ожидаю явного подтверждения ACK на канале IN в моем коде микроконтроллера перед отправкой последующих сообщений. Это может быть проблемой, хотя я пытался сделать это ранее, и, похоже, это не имело большого эффекта.
Ниже приведены снимки экрана вывода USBLyzer, если они вообще помогают (сделанные во время другого прогона, поэтому данные не будут точно идентичными, но все же будут довольно похожими). Это скриншоты только потому, что файлы журнала плохо отформатированы.
РЕДАКТИРОВАТЬ 3
Похоже, мой микроконтроллер отправляет сообщения драйверу с интервалом около 30 микросекунд между каждым сообщением (с максимальной скоростью), тогда как на основе журналов кажется, что драйверу требуется около 200-500 микросекунд для обработки одного сообщения. Это довольно несоответствие. Реальная проблема заключается в том, что что-то более низкого уровня, чем мое программное обеспечение драйвера, отправляет ACK на микроконтроллер с той же скоростью, с которой он отправляет сообщения, даже если мой драйвер не успевает за ним, поэтому я не могу регулировать его на этом основании. Похоже, мне может понадобиться явно отправить сообщение от драйвера на микроконтроллер по каналу BULK Out, говорящее «Я готов получить другое сообщение» каждый раз, когда драйвер получает сообщение, но похоже, что это будет очень медленно вещи вниз Есть ли лучшая альтернатива?
Задача ещё не решена.