マイクロコントローラーが USB 経由で PC と通信できるようにするコードを作成しようとしています。コードの大部分は適切に配置されていると思います。通信は通常、低速 (メッセージあたり > ~5ms) である限り正常に機能します。ただし、はるかに高速 (できれば、各メッセージ間で数十から数百マイクロ秒の速度) で 0% のドロップ率で動作させたいと考えています。通信はフルスピードの USB 2.0 プロトコルを使用するように構成されているため、転送速度は約 12 MHz です。
プロトコルがどのように機能するかという理由で、(たとえば) CAN に比べて USB の問題をデバッグするのは難しいと思いますが、問題はデバイスとのインターフェイス用に書いた PC 側のドライバーにあると思います。
デバイスとドライバーは、2 つの標準コントロール エンドポイント (物理エンドポイント 0 と 1) を使用し、さらに 2 つのバルク エンドポイントを使用します。
- BULK 2 Out (物理エンドポイント 4)
- BULK 2 In (物理的エンドポイント 5)
これらの両方の最大パケット サイズは 64 バイトです。
私が作成したドライバーは、Microsoft のサイトのサンプル コードに多かれ少なかれ基づいていました。私は関数をエクスポートしましたがreceiveMessage
、元々、これが呼び出されるたびに 64 バイトのバッファーが割り当てられ、 を使用して BULK 2 In パイプから 64 バイトのデータが要求されましたWinUsb_ReadPipe
。読み取るデータがない場合にアプリケーションがハングしないように、1 ミリ秒のタイムアウトがあります。
これは基本的に、パイプからデータが読み取られる速度が、アプリケーションがポーリングできる速度に制限されることを意味します。アプリケーションがパイプからデータを読み取るよりも、デバイスがパイプにデータを書き込む速度の方が速い場合、問題が発生することは間違いありません。この問題を解決するために、ドライバー内にキューとスレッドを作成して、パイプを継続的にポーリングし、受信したメッセージをキューに格納するだけで、アプリケーションは「receiveMessage」を使用して余暇にメッセージを読み取ることができます。しかし、これはあまりうまくいきませんでした。
私が望むのは、次のことを確実にする方法を示すコード サンプルです。
- データは、BULK In パイプからできるだけ速く読み取ることができます。
- メッセージを「バッファリング」する方法があるため、デバイスは、アプリケーションが短時間処理できるよりも速くパイプに書き込むことができますが、メッセージはドロップされません。
考えられるアプローチの 1 つは、デバイスとドライバーの間に別のバルク パイプラインをセットアップすることだと思います。次に、デバイスはすべての送信メッセージをバッファーに格納し、このバッファー内の対応するインデックスに各メッセージのバイト数を含む別の配列を保持する必要があります。次に、アプリケーションが一連のメッセージを読み取る必要があるときはいつでも、ドライバーはこれらのパイプラインのいずれかを使用して、デバイスからメッセージ バイト配列を要求します。この配列の値ごとに、ドライバーは を使用してそのバイト数の要求を発行します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 のようなプロトコルを使用すると、問題がどこにあるかを簡単に特定できます...
ドライバーに、受信したすべてのメッセージのログを生成させました。これには、スレッドが 2 つのメッセージを受信する間の時間差や、どのスレッドがメッセージを処理しているか (私は 2 つ持っています) などの追加の詳細が含まれます。次のような出力が得られます。
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 秒後、アプリケーションを使用して、50 ミリ秒間隔で送信される 4 つのメッセージのグループで構成されるデバイスからの安定したメッセージ ストリームを要求します。
- 0000000D...
- 0000010D...
- 0000020D...
- 0000030D...
予想されるポイントで数百マイクロ秒単位で一貫して値を報告していることがわかるため、ドライバーは実際に非常にうまく機能しているように見えます。でも:
- 30D... メッセージは 20D... メッセージの前に一貫して到着しているようです。
- 10D... メッセージが一貫してドロップされる
これは、マイクロコントローラ コードの問題である可能性があります。私はダブルバッファリングされたエンドポイントを使用しているので、メッセージが順不同で到着する理由が理解できます。
1 つのことは、後続のメッセージを送信する前に、マイクロコントローラー コードのバルク IN パイプで ACK を明示的に待機していないことです。それが問題かもしれませんが、以前に試してみましたが、あまり効果がないようでした.
以下は、USBLyzer 出力のスクリーンショットです (別の実行中に取得したため、データは完全に同一ではありませんが、非常に似ています)。ログ ファイルの形式が適切でないため、スクリーンショットになっています。
編集3
私のマイクロコントローラーは、各メッセージ間で約 30 マイクロ秒 (最大レート) でメッセージをドライバーに送信しているようですが、ログに基づくと、ドライバーが 1 つのメッセージを処理するのに 200 ~ 500 マイクロ秒かかるようです。それはかなりの食い違いです。本当の問題は、ドライバー ソフトウェアよりも低レベルの何かが、ドライバーが追いつけないにもかかわらず、メッセージを送信するのと同じ速度でマイクロコントローラーに ACK を送信していることです。そのため、それに基づいて調整することはできません。ドライバーがメッセージを受信するたびに、「別のメッセージを受信する準備ができています」というメッセージをドライバーから BULK Out パイプのマイクロコントローラーに明示的に送信する必要があるようですが、それは本当に遅くなるようです物事がダウンします。より良い代替手段はありますか?