The developer's resource for computer interfacing, especially USB, serial (COM) ports, mass storage, and embedded networking. (Formerly Lvr.com)

Home > Articles > Explore USB with WinUSB

Explore USB with WinUSB

This article originally appeared in Nuts & Volts.

If you’re developing a device that needs to talk to a PC, the chances are good that USB will be involved. For each USB device, the PC assigns a software driver. Windows provides drivers for devices that fit into defined USB classes such as human interface, printer, or mass storage. If your device doesn’t fit a defined class, Microsoft’s WinUSB driver is an option.

In this article, I'll show how to program and access WinUSB devices. The WinUSB driver requires a PC with Windows XP SP2 or later, including Windows Vista and Windows 7.

A Transfer Type for Every Purpose

Every USB data transfer is between a PC or other USB host computer and a device endpoint. A device endpoint is a buffer that stores received data or data to transmit. Every device must support endpoint zero, which is bidirectional. Additional, optional endpoint addresses each have a number (1-15) and a direction (IN or OUT).

Even though endpoints reside on devices, the USB specification defines endpoint direction from the view of the host PC. An IN endpoint sends data to the PC, and an OUT endpoint receives data from the PC. This naming convention can be confusing when writing code for the device side!

One reason why USB is so versatile is its support for four transfer types, each with different strengths. WinUSB supports control, bulk, and interrupt transfers. Control transfers use endpoint zero. The other transfer types can use endpoints one and higher.

Control transfers provide a structured way to send requests and data and receive responses. Control transfers are the only type that can pass information in both directions in a single transfer. After device attachment, in a process called enumeration, the host computer uses control transfers to learn about the device.

WinUSB devices can also use control transfers to send and receive data in vendor-defined requests. For example, you can define a request to set or read a switch, send data to configure device operation, or receive a sensor reading.

A control transfer has two or three stages. To learn about a newly attached device, the host computer uses control transfers to request data structures called descriptors from the device. In the Setup stage, the host sends the request. In the Data stage, the device sends the requested descriptor. In the Status stage, the host acknowledges receiving the descriptor. A host can also use control transfers to send information to a device in the Data stage, with the device acknowledging in the Status stage. Some requests have no Data stage.

A USB host reserves a portion of the bus bandwidth for control transfers: 10% for low- and full-speed endpoints and 20% for high-speed endpoints. If the bus isn’t busy, control transfers can use more than the reserved bandwidth. But all devices must share the bus, so on a busy bus, a control transfer may have to wait.

The other transfer types don’t have multiple stages and can transfer data for any purpose. On an otherwise idle bus, bulk transfers are the fastest. But bulk transfers have no guaranteed bandwidth, so on a busy bus, bulk transfers must wait. Common uses for bulk transfers are printers and scanners, where quick transfers are nice but not essential.

For interrupt transfers, the host guarantees a maximum interval between requests for data from IN endpoints or sending data to OUT endpoints. Common uses for interrupt transfers are mice and keyboards, which need to transfer user input quickly to the host computer.

Isochronous transfers have a guaranteed transfer rate but unlike the other transfer types, isochronous transfers don’t use acknowledgements, and the receiver has no defined way to request re-transmitting corrupted data. Common uses for isochronous transfers are streaming audio and video, where users won’t notice or will tolerate a few corrupted or missing packets. WinUSB doesn’t support isochronous transfers.

Using the USB Framework

My example code is for Microchip Technology’s PIC18F4550 microcontroller and MPLAB C18 compiler. I tested the code on Microchip’s PICDEM FS-USB development board. A complete WinUSB project for the PIC along with companion Visual Basic and Visual C# applications are available from my website.

My PIC code uses Microchip’s free USB Framework, which is a set of source-code modules that handle low-level USB communications. Using the Framework can save much time and trouble.

For each endpoint besides endpoint zero, the device provides an endpoint descriptor. This listing shows endpoint descriptors for bulk and interrupt endpoints in each direction:

// Endpoint descriptors
0x07,                    //  Descriptor size in bytes
USB_DESCRIPTOR_ENDPOINT, // Descriptor type
_EP01_OUT,               //  Endpoint number and direction
_BULK,                   //  Transfer type
0x40, 0x00,              //  Endpoint size in bytes
0x00,                    //  Ignored for bulk endpoint
0x07,                    //  Descriptor size in bytes
USB_DESCRIPTOR_ENDPOINT, // Descriptor type
_EP01_IN,                //  Endpoint number and direction
_BULK,                   //  Transfer type
0x40, 0x00,              //  Endpoint size in bytes
0x00,                    //  Ignored for bulk endpoint
0x07,                    //  Descriptor size in bytes
USB_DESCRIPTOR_ENDPOINT, // Descriptor type
_EP02_OUT,               //  Endpoint number and direction
_INT,                    //  Transfer type
0x08, 0x00,              //  Endpoint size in bytes
0x0A,                    //  Endpoint interval
0x07,                    //  Descriptor size in bytes
USB_DESCRIPTOR_ENDPOINT, // Descriptor type
_EP02_IN,                //  Endpoint number and direction
_INT,                    //  Transfer type
0x08, 0x00,              //  Endpoint size in bytes
0x0A                     //  Endpoint interval

The USB Framework defines constants that help make the code more readable and easier to maintain. For example, in Listing 1, USB_DESCRIPTOR_ENDPOINT is the constant 0x05, which the USB specification defines as the value that identifies an endpoint descriptor.

Other descriptors include the device descriptor, which contains the device’s Vendor ID (VID) and Product ID (PID) and one or more interface descriptors that specify an interface number and how many endpoints belong to the interface. The USB 2.0 specification defines the fields in the descriptors.

Bulk and Interrupt Transfers

To read and write to endpoints, program code accesses an endpoint’s buffer descriptor (BD).  To program USB Framework communications on PICs, you need to understand BDs.

A BD consists of four byte-wide registers that hold information about an endpoint’s most recent data transfer or the next data transfer. The microcontroller core and the USB module share ownership of the BD. The microcontroller core is the CPU that executes the code, or firmware, that you program into the device. The USB module, also called the serial interface engine (SIE), provides hardware support for USB communications. A USB_HANDLE is a pointer to an endpoint’s BD.

The key to accessing a BD is its UOWN bit. When UOWN = 0, the microcontroller core owns the buffer, and firmware can read and write to the BD. When UOWN = 1, the USB module owns the BD, and firmware can read UOWN but should not read or write to other locations in the BD.

This listing shows code for reading received data on a bulk OUT endpoint.

#define WINUSB_BULK_EP 1
#define WINUSB_BULK_IN_EP_SIZE 64
#define WINUSB_BULK_OUT_EP_SIZE 64
WORD bulk_bytes = 0; 
USB_HANDLE USBWinUsbBulkOutHandle;
unsigned char winusb_bulk_in_buffer[WINUSB_BULK_IN_EP_SIZE];
unsigned char winusb_bulk_out_buffer[WINUSB_BULK_OUT_EP_SIZE];
// Set up the endpoint to enable receiving data.
USBWinUsbBulkOutHandle = USBGenRead(WINUSB_BULK_EP,
     (BYTE*)&winusb_bulk_out_buffer, WINUSB_BULK_OUT_EP_SIZE);
if(!USBHandleBusy(USBWinUsbBulkOutHandle))
{
   // The microcontroller  core owns the endpoint. 
   // Check for received  data.
   bulk_bytes =  USBHandleGetLength(USBWinUsbBulkOutHandle); 
   if (bulk_bytes > 0)
   {
      // Data was received. 
      // Copy it to for  sending back to the host.
      for (count; count <=  bulk_bytes - 1; count++)
      {
         winusb_bulk_in_buffer[count] = 
         winusb_bulk_out_buffer[count];
      }
   }
}

(Remember that an OUT endpoint receives data from the host.) The USB Framework’s USBGenRead function handles many details of preparing the endpoint to receive data. The function accepts an endpoint number, a pointer to a buffer to hold received data, and the maximum number of bytes to receive. The function sets up the transfer, sets UOWN = 1 to transfer BD ownership to the USB module, and returns a pointer to the BD.

The USB module then manages the data transfer without further intervention by firmware. When the endpoint receives an OUT token packet followed by data, the USB module stores the data in the passed buffer and sets UOWN = 0 to transfer BD ownership back to the microcontroller core.

To check for received data, the Framework’s USBHandleBusy macro first checks to see if UOWN = 0. If so, the USBHandleGetLength macro returns the number of bytes received. Firmware can retrieve and use the received data in any way. Listing 2 copies the data into winusb_bulk_in_buffer for sending back to the host in a basic loopback test. After retrieving the data, firmware can call USBGenRead again to prepare the endpoint to receive new data.

This listing shows code for sending data to the host from a bulk IN endpoint:

USB_HANDLE USBWinUsbBulkInHandle;
if (!USBHandleBusy(USBWinUsbBulkInHandle)) 
{
  // The microcontroller core owns the endpoint. 
  // Prepare to send data to the host.
  USBWinUsbBulkInHandle = USBGenWrite(WINUSB_BULK_EP, 
    (BYTE*)&winusb_bulk_in_buffer, bulk_bytes);

To send data, USBHandleBusy first checks to see if UOWN = 0. If so, a call to USBGenWrite prepares to send the data.

The function accepts an endpoint number, a pointer to a buffer that holds the data to send, and the number of bytes to send. The function sets up the transfer, sets UOWN = 1 to transfer BD ownership to the USB module, and returns a pointer to the BD.

The USB module then manages the data transfer without further intervention by firmware. On receiving an IN token packet at the endpoint, the USB module sends the data and sets UOWN = 0 to pass ownership back to the microcontroller core. Firmware can then prepare for another transfer.

At the device, bulk and interrupt transfers are identical except for the endpoint type. The only difference is in scheduling by the host. So to convert listings 2 and 3 for use with interrupt transfers, just replace every instance of bulk with interrupt and set WINUSB_INTERRUPT_EP = 2 (or whatever endpoint number the interrupt endpoint addresses are using) and set WINUSB_INTERRUPT_IN_EP_SIZE and WINUSB_INTERRUPT_OUT_EP_SIZE to match the endpoint sizes in the endpoint descriptors.

Control Transfers

Because of their multiple stages, control transfers are more complicated to program than bulk and interrupt transfers. The first step in responding to a control transfer is to detect the received request. From information received in the Setup stage, firmware can learn whether the request is directed to the whole device or to a specific interface in the device.

This listing checks values received in the Setup stage to find out if the request is directed to the WinUSB interface and if the firmware has defined the request. If so, the function examines the Setup data to determine whether the host or device sends data in the Data stage and calls a function to handle the request:

// Check the Setup packet to find out if the request is  
// directed to an interface, names the WinUSB interface ID,
// and is a Vendor request.
if(SetupPkt.Recipient != RCPT_INTF) return;
if(SetupPkt.bIntfID != WINUSB_INTF_ID) return;
if(SetupPkt.RequestType != VENDOR) return;
// It’s a vendor-specific request to the WinUSB interface.
// Decode the request and call a routine to handle it.
switch(SetupPkt.bRequest)
{
  case WINUSB_REQUEST_1:
    // The Data stage is  host-to-device.
    WinusbControlWriteTransferHandler();
    break;
  case WINUSB_REQUEST_2:
    // The Data stage is  device-to-host.
    WinusbControlReadTransferHandler();            
    break;    
}   

The example handles two requests. Request 1 has a host-to-device Data stage, and request 2 has a device-to-host Data stage.

I patterned my code to handle the control-transfer requests after similar code in the USB Framework. For requests where the device sends data to the host, I used the Get_Descriptor request as a model. Code for requests where the host sends data to the device is less common, but I found an example in the Framework’s virtual COM port example in the SET_LINE_CODING request.

Installing a Device

The other side of WinUSB communications is the PC software that detects the device, assigns a driver, and exchanges data with the device.

An INF file is a text file that Windows uses to match a driver to a device. The INF file for a WinUSB device includes the VID and PID from the device descriptor and a 128-bit value called a GUID, which applications use to identify a specific WinUSB device. The GUID’s length and the method used to generate the GUID make it highly unlikely that multiple devices will have the same GUID.

You can generate a GUID in several ways. In Microsoft’s Visual Studio Standard edition and higher, select Tools > Create GUID. Other options are Microsoft’s GUID generator, guidgen.exe, or an online GUID generator, both easily found via a web search.

To customize my project’s WinUSB INF file for your device, replace the GUID and the VID and PID with your values. The GUID is in the [Version] section’s ClassGUID item:

   ClassGUID = {36FC9E60-C465-11CF-8056-444553540000}

Replace the value between the curly brackets with your GUID.

The device’s VID and PID are in the INF file’s [Manufacturer] section in this item:

   %USB\MyDevice.DeviceDesc% = USB_Install, USB\VID_0925&PID_1456

Replace the VID (0925h) and PID (1456h) with the idVendor and idProduct values in the device descriptor for your device.

To install a WinUSB device on Windows XP, the PC must have three co-installer DLLs. Microsoft’s free Windows Driver Kit (WDK) contains the files, which you can distribute with your software. You don’t need to provide the files for Windows Vista systems.

On first attachment, Windows searches for an INF file with a matching VID and PID. If needed, point the Found New Hardware Wizard to the location of the INF file and the co-installer files.

When the device is installed and ready for use, Windows Device Manager shows the device under Universal Serial Bus Controllers. To view the Device Manager, right-click My Computer and select Manage, then Device Manager.

Writing Applications

You can access WinUSB devices with Visual Basic or Visual C#, including the free Express editions. But Microsoft’s .NET Framework doesn’t provide a class for accessing WinUSB devices. Instead, applications use Windows API functions and the WinUSB API to detect and communicate with devices.

For each API function used, Visual Basic and Visual C# applications must provide a declaration. Writing a declaration requires translating Microsoft’s declaration, written in C, to the syntax and data types supported by Visual Basic or Visual C#. To call a function, you provide parameters whose data types match those in the declaration.

API functions can find a specific device by GUID value, obtain a handle for accessing the device, learn the number and type of endpoints, configure timeouts and other behavior, and exchange data using bulk, interrupt, and control transfers. If you’re not familiar with calling API functions, the programming can seem obscure, but my example applications show the way.

With this introduction to firmware and applications, you’re ready to start experimenting with USB transfers for use in your projects.