2

I have programmed a microcontroller with USB and connected it a PC using WinUSB. I am able to communicate with my uC in c++ and in VB.NET using pinvoke with synchronous (blocking, overlapped = NULL) calls. I just got asynchronous calls working in c++ but I don't know how to get it coded in .NET, there is also another question I am not sure of:

Question 1: In c++ I initially tried asynchronous calls using IO Completion Callback Objects but when I called WinUSB to read it returned an error that asynchronous IO was already in progress on the file handle (HANDLE hDevice), CreateThreadpoolIo would not take the WinUSB handle (PWINUSB_INTERFACE_HANDLE hWinUsbHandle). My guess is WinUSB uses this file handle for IO and so I cannot. I eventually got it working using Wait Callback Objects. It is working now, I just want to clarify that what I am doing is correct usage.

The part that I was a little confused about is the CreateThreadpoolWait call. At first I thought this was creating a thread pool like other examples from MSDN, however I think now it is just an object that runs on a default thread pool. Also, objConext is the variable to use to sync calls with their callbacks, that is PVOID Context in the callback points to objConext.

Here is the c code snippet:

// Receive data asynchronously
void BeginReceiveData(UCHAR bytPipeId, UCHAR *bytData, ULONG intLength, PTP_WAIT_CALLBACK cbCallback)
{
    // Create the event
    HANDLE hEventCallback = CreateEvent(NULL, FALSE, FALSE, NULL);

    // Check for an error
    if (!hEventCallback)
    {
        // Set the error
        printf(strError, "Error creating callback event: %d.", GetLastError());
    }

    // Create the thread pool wait object
    tpWait = CreateThreadpoolWait(cbCallback, &objConext, NULL);

    // Check for an error
    if (!tpWait)
    {
        // Set the error
        printf(strError, "Error creating thread pool: %d.", GetLastError());
    }

    // Place the wait object in the thread pool
    SetThreadpoolWait(tpWait, hEventCallback, NULL);

    // Clear the callback
    ZeroMemory(&oCallback, sizeof(oCallback));

    // Set the event
    oCallback.hEvent = hEventCallback;

    // Read
    BOOL bResult = WinUsb_ReadPipe(*hWinUsbHandle, bytPipeId, bytData, intLength, NULL, &oCallback);

    // Check for an error
    if (!bResult)
    {
        if (GetLastError() != ERROR_IO_PENDING)
        {
            // Set the error
            printf(strError, "Error reading pipe: %d.", GetLastError());
        }
    }
}

// Receive data callback
void CALLBACK UsbCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult)
{
}

Question 2: How to convert the above to .NET? This is my guess so far:

    Dim wait As New WaitCallback(AddressOf UsbCallback)

    ' Create the event
    Dim eEventCallback As New AutoResetEvent(False)

    ThreadPool.QueueUserWorkItem(wait, Nothing)

    Dim oCallback As New Overlapped(0, 0, eEventCallback.SafeWaitHandle.DangerousGetHandle, Nothing)

    'Dim oCallback As New NativeOverlapped

    oCallback.EventHandleIntPtr = eEventCallback.SafeWaitHandle.DangerousGetHandle

    ' Read
    Dim bResult As Boolean = WinUsb_ReadPipe(hWinUsbHandle, bytPipeId, bytData, intLength, intLength, oCallback.EventHandleIntPtr)

<DllImport("winusb.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
        Public Function WinUsb_ReadPipe(InterfaceHandle As IntPtr, PipeID As Byte, Buffer() As Byte, BufferLength As UInteger, ByRef LengthTransferred As UInteger, Overlapped As IntPtr) As Boolean
        End Function

Edit: I was going to post as an answer but I don't have enough street cred

Well, I sort of have the answer to convert the code to .NET. I managed to convert all API functions to PInvoke and then replaced the ones that were built into .NET except a couple. The code works but care must be taken to where the variables are declared as GC will clean them up before the callback is made. Also, the context must be carefully allocated and destroyed. For example an array in the context structure may be cleaned if the right size of memory is not allocated, which is easy to understand.

For these reasons it may be more beneficial to keep handle references in an object for every pipe in USB and reuse them since multiple calls cannot be made to the same pipe before the previous one completes. If that is done then you can do the same with the context and avoid allocating and destroying memory for the context. for example, a dictionary can be used on the Wait pointer in the callback:

Dim dicHandlesAndContext As Dictionary(Of IntPtr, HandlesAndContext)

Although I haven't really found an answer to question 1: whether or not this is the 'right' way to do it, it seems to perform quite well. Here are the main code snippets:

' Receive data asynchronously
    Private Sub BeginReceiveData(bytPipeId As Byte, bytData() As Byte, intLength As UInteger)
        ' Allocate memory for the callback
        ptrContext = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(TestContext)))

        ' Copy the structure to memory
        Marshal.StructureToPtr(objConext, ptrContext, True)


        ' Create the callback
        cbCallback = New UsbCallbackDelegate(AddressOf UsbCallback)


        ' Create the event
        Dim areEvent As New AutoResetEvent(False)

        ' Set the event
        hEventCallback = areEvent.SafeWaitHandle.DangerousGetHandle

        ' Create the thread pool wait object
        tpWait = CreateThreadpoolWait(Marshal.GetFunctionPointerForDelegate(cbCallback), ptrContext, IntPtr.Zero)

        ' Place the wait object in the thread pool
        SetThreadpoolWait(tpWait, hEventCallback, IntPtr.Zero)

        ' Set the event
        oCallback.EventHandle = hEventCallback

        ' Read
        Dim bResult As Boolean = WinUsb_ReadPipe(hWinUsbHandle, bytPipeId, bytData, intLength, intLength, oCallback)

        If Not bResult Then
            If Not Marshal.GetLastWin32Error = 997 Then
                ' Get the error
                Dim intError As Integer = Marshal.GetLastWin32Error

                Throw New Win32Exception(intError, "Error getting device information.")
            End If
        End If
    End Sub

    ' Delegate for USB callbacks
    Private Delegate Sub UsbCallbackDelegate(Instance As IntPtr, Context As IntPtr, Wait As IntPtr, WaitResult As UInteger)

    ' Callback
    Private Sub UsbCallback(Instance As IntPtr, Context As IntPtr, Wait As IntPtr, WaitResult As UInteger)
        ' Number of bytes transferred
        Dim intTransferred As UInteger

        ' Get the number of bytes transferred
        WinUsb_GetOverlappedResult(hWinUsbHandle, oCallback, intTransferred, False)

        ' Get the context from memory
        Dim objConext As TestContext = Marshal.PtrToStructure(Context, GetType(TestContext))

        ' Free the memory
        Marshal.FreeHGlobal(Context)


        ' Do some work on the data


End Sub

The main API DLL imports

<DllImport("winusb.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
        Public Function WinUsb_GetOverlappedResult(InterfaceHandle As IntPtr, ByRef lpOverlapped As Threading.NativeOverlapped, ByRef lpNumberOfBytesTransferred As UInteger, bWait As Boolean) As IntPtr
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
        Public Function CreateThreadpoolWait(pfnwa As IntPtr, pv As IntPtr, pcbe As IntPtr) As IntPtr
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
        Public Function SetThreadpoolWait(pwa As IntPtr, h As IntPtr, pftTimeout As IntPtr) As IntPtr
        End Function

        <DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
        Public Function CloseThreadpoolWait(pwa As IntPtr) As IntPtr
        End Function

I tested these functions for GC protection by receiving data continuously and running GC at the same time, the program will crash if something was collected.

' Receive data asynchronously on endpoint 1
    BeginReceiveData(129, bytData, 8)

    While True
    ' Garbage collection
        GC.Collect()
        GC.WaitForFullGCComplete()
    End While


    ' Callback
    Private Sub UsbCallback(Instance As IntPtr, Context As IntPtr, Wait As IntPtr, WaitResult As UInteger)
        ' ...
        ' Once data is received, receive more
        BeginReceiveData(129, bytData, 8)
        '...
    End Sub

WARNING: A lot of cleanup code is not here, check MSDN for c code on how to do this properly. I think an object oriented approach is probably best to keep handles alive and implement IDIsposable to clean up when done.

Anyone interested in an Open Source DLL? :P

Manny
  • 426
  • 4
  • 11

0 Answers0