[
Advertise | Submit Code | About us | Contact us | Link us
]
Go!
Membership Services
Login
Register

Home
C# General

General

C# Language

Design & Architecture

Algorithms

Database

Security

Active Directory

COM Interop

Remoting
C# Windows Forms

General

Combo and List boxes

Miscellaneous Controls

Button Controls

Edit Controls
Cutting Edge

ASP.NET 2.0

Visual Studio 2005

Windows Longhorn

SQL Server 2005
C# Multimedia and GDI+

General

DirectX

GDI+

Audio
Internet & Web

General

Images and multimedia

Database

Utilities

Security

ASP.NET Controls

Design and Architecture

Webservices
.NET

General

Design & Architecture

Algorithms

Database

Security

Active Directory

COM Interop

Remoting

ADO.NET

XML.NET

Tools

Enterprise

IDE
Visual Basic .NET

VB.NET General

VB.NET Controls
General Reading

.NET Books Review

Product Showcase

Book Chapters

Business Design & Strategy
Community

Discuss

Job Board

Discussion

CodeXchange
DeveloperLand

Advertise

Submit Code

About us

Contact us

Link us
Miscellaneous

Favorite Links

Downloads

Programming Sites

Top Stories
Regular Expressions

E-Mail

Date/Time
Home > C# General > COM Interop
DeviceIoControl & USB using Managed C++ & C#
Posted by on Monday, September 27, 2004 (EST)

Low level I/O is not part of the .NET framework, so information on how its done is difficult to find.If you happen to have the Cypress EZ-USB FX development board, you can use the code given here as a starting point.

This article has been viewed: 11,183 times
Technology: COM Interop.

TestUsb.zip (29.95 KB)

Low level I/O is not part of the .NET framework, so information on how its done is difficult to find.  Since I am using some specialized hardware, which won't be available to most C# developers, my comments will focus on interoperability with unmanaged code.  If you happen to have the Cypress [^] EZ-USB FX development board, you can use the code given here as a starting point.  Otherwise you can use it as a source of ideas.

This code is my early attempt to produce a wrapper to allow .NET applications to use the device driver that comes with the Cypress development board, and is far from being complete.  As it turns out I may never complete this project.  I have purchased DriverX USB from Tetradyne [^].  The data acquisition system that I am working on uses a custom ISA card.  Originally I started with MSDOS, then moved to Win 95.  With MSDOS & Win 95, I could just use in & out instructions to talk directly to the hardware.  When I moved to NT 4, I purchased DriverX, which eliminated the need to write a device driver for communicating with the ISA card.  Now the data acquisition system is running on Win 2000 and XP.  The problem is that as we deploy more sites, it is getting difficult to purchase systems with ISA slots.  To eliminate the dependency on the ISA card we are moving to USB.  In the electronics crate for the data acquisition system a Cypress EZ-USB FX chip will be used.

My .NET reference books for this project are:

.NET Framework Solutions: In Search of the Lost Win32 API [^] by John Paul Mueller

and

Essential Guide to Managed Extensions for C++ [^] by Siva Challa, Artur Laksberg

I started writing the code in C# using Platform Invoke, but was having some problems creating all the data structures to be used with DeviceIoControl.  I decided to write this code in managed C++ instead.

When I first started using managed C++, I created .h and .cpp files, just like I did when using unmanaged C++.  I compiled dozens of unmanaged C++ classes and my new managed C++ wrapper code into a managed library dll.  To get it all to work properly I got rid of the Stdafx files and turned off precompiled headers.  When starting work on the USB code, I decided to eliminate the .h files in managed code.  This way the managed C++ code is structured much the same as the C# code.  Since this worked well here, I plan to try this technique on my legacy code wrapper as well.

At first I thought that I would need to have two versions of my data structures, one for the unmanaged code and one for the managed code.  As it turns out, it is easy to create structures in managed C++ and use them in both the C++ & C# code.  I am creating a C++ wrapper function for each USB function, which avoids the problems that I was having with passing different types of data structures to DeviceIoControl using Platform Invoke.

The include files in the managed C++ module:

#include "stdafx.h" which contains:
  • #using <mscorlib.dll> which is the core of .NET
  • #include <windows.h> for the win32 API
  • #include <stdio.h> After all these years of using C++, I still like the I/O routines I used in C.

#using <UsbCs.dll> some C# classes which I use from the managed C++ code.

#include "ezusbsys.h" Comes with the Cypress development board

#include "usb100.h" Found in the Device Driver Kit, and contains the definitions for constants & structures used when working with the USB.

When using assemblies in managed C++ you need a #using <mydll.dll> statement in your code and need to tell the compiler where to find the assembly, using the Resolve #using References field in the properties for the project.

Then you have the using namespace statements:

using namespace System;
using namespace System::Text;
using namespace System::Runtime::InteropServices;

After programming in C# for a while, putting those double colons in there, takes some getting use to, even for an old C++ guy.

And here is one way you can create a managed data structure which you can easily marshal to the unmanaged world.

[StructLayout(LayoutKind::Explicit, Size=18, CharSet=CharSet::Auto)]
public __gc class UsbDeviceDescriptor 
{
  public:
    [FieldOffset(0)] System::Byte bLength;
    [FieldOffset(1)] System::Byte bDescriptorType;
    [FieldOffset(2)] System::UInt16 bcdUSB;
    [FieldOffset(4)] System::Byte bDeviceClass;
    [FieldOffset(5)] System::Byte bDeviceSubClass;
    [FieldOffset(6)] System::Byte bDeviceProtocol;
    [FieldOffset(7)] System::Byte bMaxPacketSize0;
    [FieldOffset(8)] System::UInt16 idVendor;
    [FieldOffset(10)] System::UInt16 idProduct;
    [FieldOffset(12)] System::UInt16 bcdDevice;
    [FieldOffset(14)] System::Byte iManufacturer;
    [FieldOffset(15)] System::Byte iProduct;
    [FieldOffset(16)] System::Byte iSerialNumber;
    [FieldOffset(17)] System::Byte bNumConfigurations;
};

Use LayoutKind::Explicit in the attribute and FieldOffset in front of each field to make sure the memory layout is the same as the unmanaged stuct that this one will be marshaled to.  Using CharSet=CharSet::Auto will let the compiler choose between Ansi or Unicode.  The CharSet probably doesn't matter on this structure since there are no strings involved.

Here is how you define a managed class in C++:

public __gc class Usb
Now I will show you some bits and pieces of the code which is contained in 
the Usb class.  If you want to see how it all fits together, take a look at the 
code in the download.

I wanted to report Win32 errors to my GUI layer, so I added an event.  In hindsight it would have been better to use exceptions for this.  Being able to trigger events in C++ is useful, so I will discuss this here instead of converting the code to use exceptions.  Here is the C++ syntax for your delegate & event, which goes in the public section of your class.

__delegate void ErrorMessage( Object* sender, ErrorEventArgs* e );
__event ErrorMessage* OnErrorMessage;

Here is the code to fire the event:

void ReportError( String* msg )
{
  if( OnErrorMessage )
  {
    OnErrorMessage( this, new ErrorEventArgs( msg ) );
  }
}
void CheckWin32Error()
{
  int errCode = GetLastError(); 
  if( errCode != 0 )
  {
    char msg[256];
    FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM, NULL, errCode, 0, msg, 256, NULL );
    ReportError( msg );
  }
}

ReportError & CheckWin32Error, are declared as private.

Note that the GetLastError & FormatMessage functions from the Win32 API is being used here.  The ability to mix managed and unmanaged code like this is one of the reasons for using managed C++ instead of C# for this code.

If you were writing this code in C# you would want to use:

int errCode = Marshal.GetLastWin32Error();

to obtain the Win32 last error code.

We need to obtain a HANDLE to the device driver using CreateFile.

The HANDLE is defined as private:

HANDLE _hEzUsb;

And the public function Open, which is just a wrapper around the API function CreateFile, with some error checking.

bool Open( String* driverName )
{
   char* lpFileName = new char[driverName->Length + 10];
   sprintf( lpFileName, "\\\\.\\%s", driverName );
   _hEzUsb = CreateFile( lpFileName,
      GENERIC_WRITE,
      FILE_SHARE_WRITE,
      NULL,
      OPEN_EXISTING,
      0,
      NULL);
   if( _hEzUsb == INVALID_HANDLE_VALUE )
   {
      CheckWin32Error();
   }
   delete lpFileName;
   return( _hEzUsb != INVALID_HANDLE_VALUE );
}

Make sure the HANDLE gets closed, when you are finished.  I have defined a Close function which can be called from the C# code:

void Close()
{
   CloseHandle( _hEzUsb );
}

If you forget to call Close, before calling Open again, strange things will happen.  I spent a lot of time trying to figure out why I the code on the EZ-USB FX board wouldn't run a second time.  After adding calls to Close in my test routines, the problem disappeared.

I have implement 3 of the many functions which are needed for communicating with a USB device.  These are GetDeviceDescriptor, BulkRead, & BulkWrite.  Here is the code:

Usb::UsbDeviceDescriptor* GetDeviceDescriptor()
{
   BOOLEAN success;
   ULONG nBytes;
   PUSB_DEVICE_DESCRIPTOR pDeviceDiscriptor = new USB_DEVICE_DESCRIPTOR();
   if( _hEzUsb == INVALID_HANDLE_VALUE )
   {
      ReportError( "Driver not open." );
   }
   else
   {
      success = DeviceIoControl( _hEzUsb,
         IOCTL_Ezusb_GET_DEVICE_DESCRIPTOR,
         NULL,
         0,
         pDeviceDiscriptor,
         sizeof(USB_DEVICE_DESCRIPTOR),
         &nBytes,
         NULL );
      if( success == FALSE )
      {
         CheckWin32Error();
      }
   }
   UsbDeviceDescriptor* DeviceDiscriptor = new UsbDeviceDescriptor();
   DeviceDiscriptor = (UsbDeviceDescriptor*)Marshal::PtrToStructure(
      pDeviceDiscriptor, DeviceDiscriptor->GetType() );
   delete pDeviceDiscriptor;
   return( DeviceDiscriptor );
}
System::Byte BulkRead( int pipe ) []
{
   if( _hEzUsb == INVALID_HANDLE_VALUE )
   {
      ReportError( "Driver not open." );
      return( 0 );
   }
   char lpOutBuffer[MaxBlkSize];
   BULK_TRANSFER_CONTROL btc;
   btc.pipeNum = pipe;
   BOOL success;
   int nBytes;
   success = DeviceIoControl( _hEzUsb,
      IOCTL_EZUSB_BULK_READ, 
      &btc,
      sizeof (BULK_TRANSFER_CONTROL),
      lpOutBuffer,
      MaxBlkSize,
      (unsigned long *)&nBytes,
      NULL);
   if( success == FALSE )
   {
      CheckWin32Error();
      return( 0 );
   }
   System::Byte buffer __gc[] = new System::Byte[nBytes];
   IntPtr ptr( lpOutBuffer );
   Marshal::Copy( ptr, buffer, 0, nBytes );
   return( buffer );
}
int BulkWrite( System::Byte buffer __gc[], int pipe )
{
   if( _hEzUsb == INVALID_HANDLE_VALUE )
   {
      ReportError( "Driver not open." );
      return( 0 );
   }
   char* lpOutBuffer = new char[buffer->Length];
   Marshal::Copy( buffer, 0, lpOutBuffer, buffer->Length );
   BULK_TRANSFER_CONTROL btc;
   BOOLEAN success;
   int nBytes;
   btc.pipeNum = pipe;
   success = DeviceIoControl( _hEzUsb,
      IOCTL_EZUSB_BULK_WRITE, 
      &btc,
      sizeof (BULK_TRANSFER_CONTROL),
      lpOutBuffer,
      buffer->Length,
      (unsigned long *)&nBytes,
      NULL);
   if( !success )
   {
      CheckWin32Error();
   }
   delete lpOutBuffer;
   return( nBytes );
}

In the GetDeviceDescriptor function, an unmanaged version of the USB_DEVICE_DESCRIPTOR data structure is create.  A pointer to the data structure is used in the DeviceIoControl, which fills the data structure with data.  When using DeviceIoControl, you need to use macros to define what function you want to call in the device driver.  If you were calling DeviceIoControl, directly from C#, you would need a wrapper function for the macros written in C++, or duplicate their functionality it C#.  These techniques are documented in John Mueller's [^] book.  To pass the data structure back to the managed world, I create a managed structure, then use Marshal::PtrToStructure to copy the data over.

When calling DeviceIoControl to perform a bulk read, you need to pass it a pointer to a buffer where it can place the data.  I use a char array.  I then copy the data to a managed Byte array, using Marshal::Copy.

System::Byte buffer __gc[] = new System::Byte[nBytes];
IntPtr ptr( lpOutBuffer );
Marshal::Copy( ptr, buffer, 0, nBytes );

In the call to the bulk BulkWrite function, a System::Byte buffer __gc[], is passed in.  The data in this array is copied to an unmanaged buffer using Marshal::Copy.

char* lpOutBuffer = new char[buffer->Length];
Marshal::Copy( buffer, 0, lpOutBuffer, buffer->Length );

Now I will describe how I use this code from C#.

I have an error handler function which displays messages from the Usb class in the status bar.

private void ErrorMessageHandler( object sender, ErrorEventArgs e )
{
  statusBar.Panels[0].Text = e.Message;
}

The button1_Click function, shows how to open a connection to the driver and retrieve the Device Descriptor.

private void button1_Click(object sender, System.EventArgs e)
{
   Usb usb = new Usb();
   usb.OnErrorMessage += new Usb.ErrorMessage( this.ErrorMessageHandler );
   if( !usb.Open( "ezusb-0" ) )
   {
      return;
   }
   UsbDeviceDescriptor descriptor = usb.GetDeviceDescriptor();
   if( descriptor != null )
   {
      DisplayUsbDeviceDescriptor( descriptor );
   }
   usb.Close();
}

Included with the Cypress development kit is a simple 8051 assembly program which counts the number of Bulk reads and writes.  In the following code, directions are given relative to the host computer.  So an IN transfer, transfers data from the controller chip where this code runs, to the host computer.  Here is the code:

NAME ezbulk
;
      ISEG AT 60H ; stack (this app doesn't use it)
      stack: ds 20
;
      CSEG AT 0
      ljmp start
; -------------------------------------------------
      org 200h 
; -------------------------------------------------
start: mov SP,#STACK-1 ; set stack
      mov dptr,#IN2BUF
      mov r7,#64 ; fill EP2IN buffer with 64 bytes
fill: mov a,r7
      movx @dptr,a
      inc dptr
      djnz r7,fill ; load decrementing counter starting w. 64
;
      mov r1,#0 ; count the IN transfers
      mov r2,#0 ; count the OUT transfers
      mov dptr,#IN2BC ; load the IN2 Byte Count register
      mov a,#64 ; 64 bytes
      movx @dptr,a ; arm the IN transfer
;
loop: mov dptr,#IN2CS ; EP2IN Control & Status register
      movx a,@dptr
      jnb acc.1,service_IN2 ; check the busy bit, service if LOW
      mov dptr,#OUT2CS ; EP2OUT Control & Status reg
      movx a,@dptr
      jb acc.1,loop ; busy-keep checking
;
service_OUT2: inc r2 ; OUT packet counter
      mov dptr,#IN2BUF+1 ; load OUT packet count into second IN2BUF byte
      mov a,r2
      movx @dptr,a
      mov dptr,#OUT2BC ; load anything to byte count to re-arm
      mov a,#1 ; any value
      movx @dptr,a 
      sjmp loop
;
service_IN2: inc r1 ; IN packet countr
      mov dptr,#IN2BUF
      mov a,r1 ; load IN packet count into first IN2BUF byte
      movx @dptr,a
      mov dptr,#IN2BC ; load the EP2IN byte count to re-arm IN transfer
      mov a,#64 ; 64 byte payload
      movx @dptr,a
      sjmp loop 
;
      END

To continuously read data from this program, I have the following C# code:

private void button2_Click(object sender, System.EventArgs e)
{
   _stop = false;
   button4.Enabled = true;
   Usb usb = new Usb();
   usb.OnErrorMessage += new Usb.ErrorMessage( this.ErrorMessageHandler );
   if( !usb.Open( "ezusb-0" ) )
   {
      return;
   }
   byte[] byteBuf;
   while( !_stop )
   {
      byteBuf = usb.BulkRead( 0 );
      if( byteBuf != null )
      {
         StringBuilder msg = new StringBuilder( 256 );
         for( int j = 0 ; j < byteBuf.Length ; j++ )
         {
            msg.AppendFormat( "{0:X} ", byteBuf[j] );
         }
         msg.Append( "\r\n" );
         textBox1.AppendText( msg.ToString() );
      }
      System.Windows.Forms.Application.DoEvents();
      Thread.Sleep( 1000 );
   }
   usb.Close();
}

The code just loops until a stop button is clicked, performing bulk reads and displaying the data in hex.  The DoEvents is so that the UI can respond.  The Thread.Sleep, is to slow the loop down to make it easier to read what is displayed.  Using DoEvents & Sleep, doesn't work very well, but this is just test code.  If you needed to do something like this in production code, you would create a worker thread.

Another example program from Cypress, written in embedded C, continuously responds to bulk writes, and transfers the data to a bulk read buffer.  I won't show that code here, since it is long and has a lot of code not relevant to the discussion here.  For the host computer a program written using MFC is provide.  The following C# code was written to work the same way as the supplied MFC program.  The complete listing can be found in the FBulkXfer.cs file in the code download.

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Text;
using System.Runtime.InteropServices;
using EzUsb;
namespace TestUSB
{
   public class FBulkXfer : System.Windows.Forms.Form
   {
      private int _BlkSize = 1;
      // common Form designer code deleted from this listing
      private void FBulkXfer_Load(object sender, System.EventArgs e)
      {
         SetupDriverComboBox();
         textBoxBlkSize.Text = _BlkSize.ToString();
      }
      private void button1_Click(object sender, System.EventArgs e)
      {
        textBoxInData.Text = "";
      }
      private void button2_Click(object sender, System.EventArgs e)
      {
         textBoxOutData.Text = "";
      }
      private void textBoxBlkSize_TextChanged(object sender, System.EventArgs e)
      {
         _BlkSize = int.Parse( textBoxBlkSize.Text );
         if( _BlkSize > Usb.MaxBlkSize )
         {
            _BlkSize = Usb.MaxBlkSize;
            textBoxBlkSize.Text = _BlkSize.ToString();
         }
      }
      private void textBoxInData_TextChanged(object sender, System.EventArgs e)
      {
         if( textBoxInData.Text.Length >= _BlkSize )
         {
            string inText = textBoxInData.Text;
            int byteCount = inText.Length;
Create a Usb object, and set up the handler for error messages.

Usb usb = new Usb();
usb.OnErrorMessage += new Usb.ErrorMessage( this.ErrorMessageHandler );

Open the device driver.

if( usb.Open( (string)comboBox1.SelectedItem ) )
{

We have a char array but the call to BulkWrite requires a Byte array, so the data is copied from the char array to a byte array, using System.Buffer.  System.Buffer helps when moving data from one type to another type.  The char array contains Unicode.  If you need to make use of text messages on the microcontroller you likely would want to convert to Ansi before passing it to BulkWrite.  Since I am just reading it back in, I don't bother converting to Ansi.

char[] data = inText.ToCharArray();
int size = Buffer.ByteLength( data );
byte[] byteBuf = new byte[size];
Buffer.BlockCopy( data, 0, byteBuf, 0, size );
int count = usb.BulkWrite( byteBuf, 1 );
if( count != 0 )
{
 byteBuf = usb.BulkRead( 0 );
 if( byteBuf != null )
    {
The data returned by BulkRead is in a byte array, so System.Buffer is used to copy the data into a char array.

data = new char[byteBuf.Length];
Buffer.BlockCopy( byteBuf, 0, data, 0, data.Length );
string msg = new String( data );
textBoxOutData.Text = msg;
 }
 else
 {
 MessageBox.Show( "Bulk Read failed.",
    "EZ-USB Error", MessageBoxButtons.OK, MessageBoxIcon.Error );
 }
    else
 {
     MessageBox.Show( "Bulk Write failed.",
               "EZ-USB Error", MessageBoxButtons.OK, MessageBoxIcon.Error );
    }
       usb.Close();
    }
   }
  }
private const int MAX_USB_DEV_NUMBER = 32;

If you happen to have more then one Cypress development board plugged in the following code will load the combo box with the names of up to 32 device drivers.

  private void SetupDriverComboBox()
      {
         comboBox1.Items.Clear();
         Usb usb = new Usb();
         usb.OnErrorMessage += new Usb.ErrorMessage( this.ErrorMessageHandler );
         bool found = false;
         for( int j = 0 ; j < MAX_USB_DEV_NUMBER ; j++ )
         {
            StringBuilder driverName = new StringBuilder( "ezusb-" + j.ToString() );
            if( usb.Open( driverName.ToString() ) )
            {
               comboBox1.Items.Add( driverName.ToString() );
               found = true;
               usb.Close();
            }
         }
         if( !found )
         {
            MessageBox.Show( "No EZ-USB device drivers were found.",
               "EZ-USB Error", MessageBoxButtons.OK, MessageBoxIcon.Error );
         }
         else
         {
            statusBar.Panels[0].Text = "";
            comboBox1.SelectedIndex = 0;
            comboBox1.Select( 0, 0 );
         }
      }
      private void ErrorMessageHandler( object sender, ErrorEventArgs e )
      {
         statusBar.Panels[0].Text = e.Message;
      }
   }
}

When I started writing this page, I was planning on completing the wrapper code for the Cypress device driver.  Since I have now purchased DriverX USB, it is unlikely that I will complete this wrapper.  Instead I will be writing a wrapper for DriverX USB.  In the past I have used DriverX, as a device driver for a custom ISA board.  The code I wrote for this was written in C++, prior to the existence of .NET.  When .NET appeared I deleted all the MFC & ATL code from that project, wrapping what was left with managed C++.  The GUI was then re-written in C#.  Recently I have written an incomplete wrapper for DriverX, which I have use in C# test programs.

I will watch the statistics for this page and read your email requests, before deciding to write more about managed C++ and wrapper code.

Top Go to Table of Contents

About Bill Burris

3D Graphics, OpenGL AI Python, C#, C++

Click here if you want to know more about .

Other articles that may interest you

  • Write a Word Add-In – Part 0
  • Write a Word Add-In – Part I
  • Lengthy Operations on Single Thread in .NET Application
  • Learning Draughts
  • Exceptions and Performance
  • Average Rating :

    Discussion Forums
    Got a programming related question? Hopefully someone has the answer... Want to help out other developers? Visit our discussion forums.

    Sponsored by: