Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Developing a WDF USB Kernel Mode Driver for the OSR USB FX2

0.00/5 (No votes)
30 Mar 2006 4  
This article describes the process of developing a USB Kernel mode device driver using the WDF Kernel Mode Driver Foundation.

Sample Image - screenshot.gif

Introduction

This article explains and demonstrates the steps involved in developing a kernel mode device driver using the WDF Kernel Mode Driver Foundation (KMDF).

The specific USB device that is used in this article is the OSR USB-FX2 learning kit that is available at OSR Online. Of course, the things that are discussed are valid for other USB devices, but the sample code will only work for the FX2 kit.

The following items are discussed or touched in this article:

  • General USB driver issues.
  • USB Interrupt handling.
  • Read, write, and IO control operations.
  • General power management issues.
  • Device suspend and wakeup.

The basic things like how to compile a driver, how to deploy it using an INF file, and other basic things are not explained in this article. These things are all explained in my previous article: Building and deploying a basic WDF Kernel Mode Driver.

Background

Once upon a time, I became interested in driver development. I have always been intrigued by making hardware do things via software. I started reading about driver development, bought Oney's book, and finally bought the OSR USB FX2 learning kit.

As I already mentioned in my previous article, learning WDM is hard. You have to spend a gigantic amount of time on it. I decided to drop WDM after a while, and my FX2 kit disappeared into the closet.

Then, in December 2005, the KMDF was released. I learned USB device driver programming with the KMDF, and decided to write an article about it. During the time I was writing this article, I had to explain so many things that I decided to first write an article about KMDF driver development basics.

This second article describes the USB specific topics and driver functionality.

Prerequisites

The list of prerequisites for using the code is very short:

  • The latest version of the WDF DDK. You can download it here.
  • Windows XP or higher for testing the driver. Version 1.1 of the KMDF will also support Windows 2000, but that has not yet been released at this time.
  • The OSR USB-FX2 learning kit if you want to actually run the code in this article.
  • The DebugView utility for viewing KdPrint messages. This is available at sysinternals.

New concepts used in this driver

Before I can show the implementation of the USB driver, there are some KMDF concepts that need a bit of explaining before the code makes sense.

WDF memory management

To facilitate safe memory handling, the WDF uses WDFMEMORY objects. These objects are opaque to the programmer. You can only use them through their handles.

WDF memory objects contain both the buffer pointer and the size descriptor of a block of memory. This means that when you pass a memory object handle to another function, it always carries the means for using it safely with it.

Likewise, if you get a memory handle from the framework, you can always verify that the data buffer is large enough for what you want to do with it.

Like all other framework objects, WDFMEMORY objects have a parent and are reference counted. This means that you never need to explicitly delete the memory objects that you create, assuming that they are allowed to live as long as your device object.

The fact that they are reference counted also allows you to hang on to a memory object after its normal lifetime. Suppose you want to use an input buffer of a write request after you already completed the write operation. To do this, you simply increment its reference count. This guarantees that you can safely continue to use it until you decrement its reference count, even though that object would have already been deleted when its normal lifetime ended.

In order to combine safety with ease of use, the WDF DDK contains the WdfMemoryCopyToBuffer and WdfMemoryCopyFromBuffer functions that you can use for safely copying data to and from WDFMEMORY objects.

There are different functions for creating WDF memory objects, but one that is particularly interesting is WdfMemoryCreatePreallocated. This function can be used for wrapping a WDFMEMORY object around an existing raw data buffer. This is a technique that I use later on.

USB basics

One of the reasons USB was developed was to have a modern replacement for legacy serial interfaces and a low-cost alternative to Firewire. If you look at the hardware and protocol specification, you'll notice that USB - even though it has some bells and whistles- is really nothing more than a glorified serial interface that supports multiple hot pluggable devices on the same bus.

One of the most important principles behind USB is that there is one bus controller (the PC) and multiple possible slaves. All data transfer is initialized by the master. If the master doesn't request data, the slave cannot send it.

This is even true for USB interrupts. A device cannot send an interrupt to the master. The master has to poll the interrupt status periodically. If an interrupt transfer succeeds, the USB host controller then interrupts the USB driver as if the transfer was a 'real' interrupt.

Configurations, interfaces, and endpoints

The USB protocol allows for a very flexible use of devices. This also means that there are a lot of things you should know before you can do anything. On the other hand, the framework does most things for you so you don't have to know the low level details.

The first thing you have to think of is the configuration. The USB configuration of a device can be thought of as a classification of physical functionality. Most devices out there have just one configuration, i.e., it has one physical representation. It is possible for devices to allow multiple configurations.

I know of only one such device: a USB chip that can act as a USB to RS-232 converter and as an 8 bit digital IO device. Since those are two completely different types of device that cannot ever be used at the same time, it makes sense to let it have two different configurations.

99% of the time, however, you'll just have one configuration per physical device.

Once a device is given a configuration, it can export multiple interfaces. An interface can be thought of as an independent part of the device functionality. For example, you could have a data acquisition device that has both analog and digital IO capabilities. If those parts of the device could be operated independently, it would make sense to provide two interfaces: one for the digital IO, and one for the analog IO.

Finally, each interface can have one or more endpoints. An endpoint is a target for actual data transfers. Each time you want to send data to the device, you have to specify which endpoint it should go to. Each endpoint has a specific data transfer type associated with it.

Types of data transfer

There are four types of data transfer in the USB protocol. Each has its own purpose:

  • Interrupt: data is sent to the driver as a result of an event on the USB board. This transfer type is typically used for event notifications.
  • Isochronous: data is sent periodically with timing guarantees. This transfer type is mostly used for real-time streaming of data like sound.
  • Bulk: data is sent in potentially large quantities, but without real-time guarantees. This type of transfer is used by data acquisition devices, portable storage etc...
  • Control: data is sent to the board to control its behavior or change settings. A good example of control transfers is USB to serial converters. You would use control transfers to change baud rate settings and things like that, while ordinary read and write operations use bulk transfers.

Each endpoint is represented in the software by a so-called pipe. The principle behind a pipe is very simple: you push something in it at one end, and it comes out at the other end.

USB interrupts

USB interrupts are not real interrupts like PCI interrupts. They can't interrupt the system. Rather, if a USB interrupt is enabled for a device, the USB bus driver will poll the interrupt endpoint at a configurable periodic interval.

If an interrupt packet is received, the framework will execute the callback function that was registered earlier. The interrupt data itself will be packaged in a WDFMEMORY object, and supplied as a parameter to the callback function.

This process looks so deceptively simple that you are not sufficiently awed by it unless you know the WDM magic that is going on in the background. In order to receive a USB interrupt, a driver has to have an outstanding read request queued for the interrupt endpoint.

As soon as the read request succeeds, the USB bus driver can complete the read request. Of course, a new interrupt event could happen on the USB board while the previous interrupt is still being handled.

To prevent data loss in that case, the driver has to queue multiple read requests. That way, there will always be an outstanding request when the USB board generates an interrupt packet.

Of course, this is not the only issue. While all this is going on, there are race conditions, possible PNP and power events, and IO cancellation problems. Lucky for us, the framework will take care of all this behind the scenes.

USB Control commands

Control commands are a special case in USB communication. All USB devices have to have a control endpoint at index 0, regardless of what type of device it is. This endpoint is used for device configuration and all things that need to happen during initialization phase, like loading firmware, for example.

This means that endpoint 0 is always active, even before the device receives its configuration. A device also can't refuse to handle the control request immediately because the USB standard specifies them as high priority.

As you will see, when the driver sends control commands to the USB device using KMDF functions, it does not have to supply a USB pipe. That is because the request will always go to the correct control endpoint. The only thing that is needed is the USB device handle.

There are three different types of control request types:

  • Standard: The request is defined by the USB protocol itself.
  • Class: The request is defined by the specific device class.
  • Vendor: The request is defined by the vendor.

It is not my intention to give you an overview of all the requests that are required or allowed by the USB standard. That would lead us too far astray. Especially since the KMDF handles all the required requests for us during the initialization and configuration phase.

If you really want to know everything about these low level details, you can find the specifications online at the USB consortium.

Device IO controls

A user mode application that uses a device driver will often want to send special commands to the driver to make it do special things, to configure it, or to get status information. Read and write operations are not suited for this.

That is where device IO controls come into play. A device IO control is a special command that is sent to the device driver. The user mode interface for sending device IO controls looks like this:

BOOL DeviceIoControl(
  HANDLE hDevice,
  DWORD dwIoControlCode,
  LPVOID lpInBuffer,
  DWORD nInBufferSize,
  LPVOID lpOutBuffer,
  DWORD nOutBufferSize,
  LPDWORD lpBytesReturned,
  LPOVERLAPPED lpOverlapped
);

As you can see, a device IO control can (optionally) have input and output buffers. The most important parameter, however (from the driver's point of view), is dwIoControlCode. This numerical code will be used by the device driver to determine what it has to do.

The value itself is of little interest in the user mode application, but it can be used to learn a few things about the command. The 32 bits in the DWORD are divided into different parts. Each part has a special meaning to the system:

  • bit 31: Common. This bit is set for all drivers of non-predefined types.
  • bit 30 - 16: Device type. This value specifies if the device is of a predefined type or not. If the device is not of a predefined type, the value should be greater or equal to 0x8000.
  • bit 15 - 14: Required access. This value indicates the access level that the caller must have requested when opening the device handle. For example, if this field specifies read access, the IO control will only be sent to the driver if the caller opened the device with read access.
  • bit 13: Custom. This bit is set for function codes greater than 0x800.
  • bit 12 - 2: Function code. This value specifies the action the driver has to perform. If the function is vendor defined, this number should be greater than 0x800.
  • bit 1 - 0: Transfer type. This value indicates if the function uses buffered IO or direct IO for data transfer.

From this, we can conclude that not only are the opcodes simply used by the driver to determine what it has to do, but they also allow the driver programmer to configure access control and IO configuration.

Windows uses the control code to determine how it should move data to the driver, and to perform security checks. This makes it possible to restrict device usage to specific groups of users.

Implementing the code for configuring the device driver

The following chapters explain the different configuration and initialization phases of a USB device driver.

The driver entry point

If you have looked at the DriverEntry code of the WDF basic driver in my previous article, you'll notice that it looks exactly the same as the DriverEntry of this device driver. That is because like in many drivers, there is no global data to initialize or to clean up.

The only purpose of the DriverEntry function is to register the callback function for adding new devices.

NTSTATUS DriverEntry(
    IN PDRIVER_OBJECT  DriverObject, 
    IN PUNICODE_STRING  RegistryPath
    )
{
  WDF_DRIVER_CONFIG config;
  NTSTATUS status;

  WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd);

  status = WdfDriverCreate(
                      DriverObject,
                      RegistryPath,
                      WDF_NO_OBJECT_ATTRIBUTES,
                      &config,
                      WDF_NO_HANDLE);

  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDriverCreate failed with status 0x%08x\n", status));
  }

  return status;
}

Adding new devices

The EvtDeviceAdd function is executed by the framework for each new device that is attached to the system and registered to be handled by our driver.

Before the driver does anything else, it overrides some of the default PNP and power management functions of the KMDF framework. To be precise, the driver will implement its own version of the EvtDevicePrepareHardware, EvtDeviceD0Entry, and EvtDeviceD0Exit functions.

The driver configures its data IO to be buffered. The KMDF makes the difference between buffered and direct IO pretty transparent.

With buffered IO, there is an extra memory copy action between user space and kernel space, but the driver knows it can trust the buffer it gets. With direct IO, there is no extra memory but the driver needs to perform some checks to make sure it can use the supplied pointer for reading and writing.

The final initialization step is to create a device object using WdfDeviceCreate. The new device now has a representation in the KMDF framework.

Since the FX2 is a USB device, it is not unreasonable to assume that the user will disconnect it from the computer without using the 'safely remove hardware' option. To prevent any annoying system messages, the driver sets the Removable and SurpriseRemovalOK PNP properties to WdfTrue. That way, the OS knows that the driver is capable of handling this situation without any problems.

For reasons that I explain later on, the driver needs to store the state of the LED array on the FX2. In order to make this easier, a WDF memory object is wrapped around the D0LEDArrayState variable. This allows subroutines to supply a WDF memory handle to certain IO functions without having to create and delete WDFMEMORY objects.

Then the different device IO queues are created (see next chapter), and finally, the device interface is registered. User applications can find the device by enumerating all devices that export this interface.

NTSTATUS EvtDeviceAdd(
    IN WDFDRIVER  Driver,
    IN PWDFDEVICE_INIT  DeviceInit
    )
{
  NTSTATUS status;
  WDFDEVICE device;
  PDEVICE_CONTEXT devCtx = NULL;
  WDF_OBJECT_ATTRIBUTES attributes;
  WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
  WDF_DEVICE_PNP_CAPABILITIES pnpCapabilities;

  UNREFERENCED_PARAMETER(Driver);

  /*set the callback functions that will 
    be executed on PNP and Power events*/
  WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
  pnpPowerCallbacks.EvtDevicePrepareHardware = 
                     EvtDevicePrepareHardware;
  pnpPowerCallbacks.EvtDeviceD0Entry = EvtDeviceD0Entry;
  pnpPowerCallbacks.EvtDeviceD0Exit = EvtDeviceD0Exit;
  WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, 
                                &pnpPowerCallbacks);

  WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered);

  /*initialize storage for the device context*/
  WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, 
                                      DEVICE_CONTEXT);

  /*create a device instance.*/
  status = WdfDeviceCreate(&DeviceInit, &attributes, &device);  
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDeviceCreate failed with status 0x%08x\n", status));
    return status;
  }

  /*set the PNP capabilities of our device.
    we don't want an annoying
    popup if the device is pulled out of the USB slot.*/
  WDF_DEVICE_PNP_CAPABILITIES_INIT(&pnpCapabilities);
  pnpCapabilities.Removable = WdfTrue;
  pnpCapabilities.SurpriseRemovalOK = WdfTrue;
  WdfDeviceSetPnpCapabilities(device, &pnpCapabilities);
  
  devCtx = GetDeviceContext(device);

  /*create a WDF memory object 
    for the memory that is occupied by the
    WdfMemLEDArrayState variable in the device context.
    this way we have the value itself handy for debugging purposes, and
    we have a WDF memory handle that can be used for passing to the low
    level USB functions.
    this alleviates the need to getting the buffer at run time.*/
  status = WdfMemoryCreatePreallocated(WDF_NO_OBJECT_ATTRIBUTES,
                           &devCtx->D0LEDArrayState,
                           sizeof(devCtx->D0LEDArrayState),
                           &devCtx->WdfMemLEDArrayState);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfMemoryCreatePreallocated" 
      " failed with status 0x%08x\n", status));
    return status;
  }

  status = CreateQueues(device, devCtx);
  if(!NT_SUCCESS(status))
    return status;

  status = WdfDeviceCreateDeviceInterface(device, 
           &GUID_DEVINTERFACE_FX2, NULL);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDeviceCreateDeviceInterface failed" 
      " with status 0x%08x\n", status));
    return status;
  }

  return status;
}

Creating the IO queues

The code for creating the different IO queues has been put into a separate function to improve readability.

There are five queues used by the driver:

  • A parallel device IO control request queue. This will be the default entry for all IO control operations that are sent to the driver.
  • A serialized device IO control request queue. This is where the driver itself will queue IO control operations that have to be serialized.
  • A serialized write request queue. This is where the system is asked to send all write IO requests to.
  • A serialized read request queue. This is where the system is asked to send all read IO requests to.
  • A manual request queue. This is the queue where the driver will temporarily store IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE IO control requests until they can be completed.

There is no default IO handler in the driver. The result of this is that all requests that are not IO control, read, or write requests will automatically fail.

By default, the default queue will receive all IO requests unless dispatching for a specific request type is routed to a different queue. Rerouting the requests is done with the function WdfDeviceConfigureRequestDispatching.

It is not necessary to call WdfDeviceConfigureRequestDispatching for a manual queue because the driver will explicitly retrieve requests from the queue when the time is right. You will also notice that no request routing is specified for the serialized IO control request queue. That is because it is the driver itself that decides which requests are pushed into that queue.

NTSTATUS CreateQueues(WDFDEVICE Device, PDEVICE_CONTEXT Context)
{
  NTSTATUS status = STATUS_SUCCESS;

  WDF_IO_QUEUE_CONFIG ioQConfig;

  /*create the default IO queue. this one 
    will be used for ioctl request entry.
    this queue is parallel, so as to prevent 
    unnecessary serialization for
    IO requests that can be handled in parallel.*/
  WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQConfig,
                           WdfIoQueueDispatchParallel);
  ioQConfig.EvtIoDeviceControl = EvtDeviceIoControlEntry;
  status = WdfIoQueueCreate(Device,
                            &ioQConfig,
                            WDF_NO_OBJECT_ATTRIBUTES,
                            &Context->IoControlEntryQueue);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoQueueCreate failed with status 0x%08x\n", status));
    return status;
  }

  /*create the IO queue for serialize IO requests. This queue will be filled by
    the IO control entry handler with the requests that have to be serialized
    for execution.*/
  WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig,
                           WdfIoQueueDispatchSequential);
  ioQConfig.EvtIoDeviceControl = EvtDeviceIoControlSerial;
  status = WdfIoQueueCreate(Device,
                            &ioQConfig,
                            WDF_NO_OBJECT_ATTRIBUTES,
                            &Context->IoControlSerialQueue);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoQueueCreate failed with status 0x%08x\n", status));
    return status;
  }

  /*create the IO queue for write requests*/
  WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig,
                           WdfIoQueueDispatchSequential);
  ioQConfig.EvtIoWrite = EvtDeviceIoWrite;
  status = WdfIoQueueCreate(Device,
                            &ioQConfig,
                            WDF_NO_OBJECT_ATTRIBUTES,
                            &Context->IoWriteQueue);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoQueueCreate failed with status 0x%08x\n", status));
    return status;
  }

  status  = WdfDeviceConfigureRequestDispatching(Device,
                                                Context->IoWriteQueue,
                                                WdfRequestTypeWrite);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDeviceConfigureRequestDispatching failed with status 0x%08x\n",
      status));
    return status;
  }

  /*create the IO queue for read requests*/
  WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig,
                           WdfIoQueueDispatchSequential);
  ioQConfig.EvtIoRead = EvtDeviceIoRead;
  status = WdfIoQueueCreate(Device,
                            &ioQConfig,
                            WDF_NO_OBJECT_ATTRIBUTES,
                            &Context->IoReadQueue);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoQueueCreate failed with status 0x%08x\n", status));
    return status;
  }

  status  = WdfDeviceConfigureRequestDispatching(Device,
                                                Context->IoReadQueue,
                                                WdfRequestTypeRead);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDeviceConfigureRequestDispatching failed with status 0x%08x\n",
      status));
    return status;
  }

  /*create a manual queue for storing the IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE
      IO control requests. If a file handle associated with one or more requests
    in the queue is closed, the requests themselves are automatically removed
    from the queue by the framework and cancelled.*/
  WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig,
                           WdfIoQueueDispatchManual);
  status = WdfIoQueueCreate(Device,
                            &ioQConfig,
                            WDF_NO_OBJECT_ATTRIBUTES,
                            &Context->SwitchChangeRequestQueue);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoQueueCreate for manual queue failed with status 0x%08x\n", status));
    return status;
  }

  return status;
}

Preparing the hardware for operation

Because this function has to do quite a lot of things, it is broken up into several sub routines.

Before anything else, the driver has to initialize its connection to the USB device. If that succeeds, the different USB pipes have to be configured. After that, the power management for our driver can be set up.

In order to receive USB interrupts, the driver has to configure a continuous read operation. The framework will maintain a queue of always outstanding read requests for our driver, and execute the callback function EvtUsbDeviceInterrupt for each read request that gets completed.

The default number of outstanding read requests is 2. You can raise this number to maximum 10 requests, to prevent data loss if the device generates a high number of interrupts. For our driver, the default is OK.

It is worth mentioning that this same principle can be used for bulk request input endpoints. This could be useful, for example, for data acquisition devices that continuously have to stream data to the computer.

NTSTATUS EvtDevicePrepareHardware(
    IN WDFDEVICE    Device,
    IN WDFCMRESLIST ResourceList,
    IN WDFCMRESLIST ResourceListTranslated
    )
{
  NTSTATUS status;
  PDEVICE_CONTEXT devCtx = NULL; 
  WDF_USB_CONTINUOUS_READER_CONFIG interruptConfig;

  UNREFERENCED_PARAMETER(ResourceList);
  UNREFERENCED_PARAMETER(ResourceListTranslated);

  devCtx = GetDeviceContext(Device);

  status = ConfigureUsbInterface(Device, devCtx);
  if(!NT_SUCCESS(status))
    return status;

  status = ConfigureUsbPipes(devCtx);
  if(!NT_SUCCESS(status))
    return status;

  status = InitPowerManagement(Device, devCtx);
  if(!NT_SUCCESS(status))
    return status;

  /*set up the interrupt endpoint with a continuous read operation. that
    way we are guaranteed that no interrupt data is lost.*/
  WDF_USB_CONTINUOUS_READER_CONFIG_INIT(&interruptConfig,
                                        EvtUsbDeviceInterrupt,
                                        devCtx,
                                        sizeof(BYTE));
  status = WdfUsbTargetPipeConfigContinuousReader(
                                        devCtx->UsbInterruptPipe,
                                        &interruptConfig);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfUsbTargetPipeConfigContinuousReader " 
      "failed with status 0x%08x\n", status));
    return status;
  }

  return status;
}

Configuring the USB device

Before anything can be done with the USB device, the driver has to connect to the USB driver using the function WdfUsbTargetDeviceCreate. When this function is executed, a USB device object is created for our device and a connection to the bus driver is opened.

As I mentioned before, a configuration and an interface have to be selected. The FX2 has only one possible configuration which has only one interface. That means, the driver can use the WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE function to initialize the USB interface config structure.

The selection is then made active by executing WdfUsbTargetDeviceSelectConfig. No special attributes are necessary. The configured USB interface handle is saved in the device context.

NTSTATUS ConfigureUsbInterface(WDFDEVICE Device, PDEVICE_CONTEXT DeviceContext)
{
  NTSTATUS status = STATUS_SUCCESS;
  WDF_USB_DEVICE_SELECT_CONFIG_PARAMS usbConfig;

  status = WdfUsbTargetDeviceCreate(Device,
                                    WDF_NO_OBJECT_ATTRIBUTES,
                                    &DeviceContext->UsbDevice);

  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfUsbTargetDeviceCreate failed with status 0x%08x\n", status));
    return status;
  }

  /*initialize the parameters struct so that the device can initialize
    and use a single specified interface.
    this only works if the device has just 1 interface.*/
  WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE(&usbConfig);

  status = WdfUsbTargetDeviceSelectConfig(DeviceContext->UsbDevice,
                                          WDF_NO_OBJECT_ATTRIBUTES,
                                          &usbConfig);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfUsbTargetDeviceSelectConfig failed with status 0x%08x\n", status));
    return status;
  }

  /*put the USB interface in our device context so that we can use it in
    future calls to our driver.*/
  DeviceContext->UsbInterface =
    usbConfig.Types.SingleInterface.ConfiguredUsbInterface;

  return status;
}

Configuring the USB pipes

Now that the USB configuration and interface are selected, the data pipes can be configured.

The FX2 has three endpoints (not counting the control endpoint that the driver does not use directly): one interrupt endpoint, and two bulk data endpoints. The framework will handle the low level configuration of those endpoints. The driver itself only needs to iterate through the list of endpoints and determine what to do with them.

At the end, the driver checks if all three expected endpoints have been found. An error is generated if one or more expected endpoints are not found.

One thing to mention is the fact that, by default, the framework expects the driver to only perform USB transfers that are exact multiples of the USB transfer packet size. Since this is highly unlikely (or impossible for the interrupt endpoint), the driver disables that check.

NTSTATUS ConfigureUsbPipes(PDEVICE_CONTEXT DeviceContext)
{
  NTSTATUS status = STATUS_SUCCESS;
  BYTE index = 0;
  WDF_USB_PIPE_INFORMATION pipeConfig;
  WDFUSBPIPE pipe = NULL;

  DeviceContext->UsbInterruptPipe = NULL;
  DeviceContext->UsbBulkInPipe = NULL;
  DeviceContext->UsbBulkOutPipe = NULL;
  WDF_USB_PIPE_INFORMATION_INIT(&pipeConfig);
  do
  {
    pipe = WdfUsbInterfaceGetConfiguredPipe(DeviceContext->UsbInterface,
                                          index,
                                          &pipeConfig);
    if(NULL == pipe)
      break;

    /*none of our data transfers will have a guarantee that the requested
      data size is a multiple of the packet size.*/
    WdfUsbTargetPipeSetNoMaximumPacketSizeCheck(pipe);

    if(WdfUsbPipeTypeInterrupt == pipeConfig.PipeType)
    { 
      DeviceContext->UsbInterruptPipe = pipe;
    }
    else if(WdfUsbPipeTypeBulk == pipeConfig.PipeType)
    {
      if(TRUE == WdfUsbTargetPipeIsInEndpoint(pipe))
      {
        DeviceContext->UsbBulkInPipe = pipe;
      }
      else if(TRUE == WdfUsbTargetPipeIsOutEndpoint(pipe))
      {
        DeviceContext->UsbBulkOutPipe = pipe;
      }
    }
    index++;
  } while(NULL != pipe);

  if((NULL == DeviceContext->UsbInterruptPipe) ||
      (NULL == DeviceContext->UsbBulkInPipe) ||
      (NULL == DeviceContext->UsbBulkOutPipe))
  {
    KdPrint((__DRIVER_NAME
      "Not all expected USB pipes were found.\n"));
    return STATUS_INVALID_PARAMETER;
  }
  
  return status;
}

Configuring power management

How the driver initializes the power management depends on the capabilities of the device itself. For the FX2, we could assume that the capabilities are fixed, but the clean solution is to dynamically retrieve this information. This is done with the WdfUsbTargetDeviceRetrieveInformation function.

The data that is returned by this function is a WDF_USB_DEVICE_INFORMATION structure. This structure has three interesting parameters:

  • UsbdVersionInformation: This parameter holds the USB version that the device supports and a USB interface version number.
  • HcdPortCapabilities: A set of bit flags that identify HCD-supported port capabilities. Currently, there is only one flag: USB_HCD_CAPS_SUPPORTS_RT_THREADS. This flag indicates if the host controller supports real-time threads.
  • Traits: A bit mask that specifies the capabilities of the USB device.

Only the Traits parameter is of interest. In that bit mask, the driver can find if the device enables waking the system from a sleeping state, if the device is self-powered, and if the device is high-speed.

The only bit that has an effect on the driver is the WDF_USB_DEVICE_TRAIT_REMOTE_WAKE_CAPABLE flag. This flag indicates if the USB device supports wakeup from system sleep. For the FX2, this is the case.

There are two distinct properties to be configured: the S0 Idle settings, and the Sx Wake settings.

WdfDeviceAssignS0IdleSettings can be used to configure the idle time after which the device is brought to a low power state if the system is in the S0 state. Doing this prevents needless power consumption.

The S0 Idle settings structure is initialized with the WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT function. For USB devices, the capabilities flag IdleUsbSelectiveSuspend has to be used for enabling device sleep in the S0 system state. The default device power state the USB device will be powered down to is PowerDeviceD2.

The WdfDeviceAssignSxWakeSettings function specifies the device's ability to wake the system when both are in a low power state. It uses a WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS structure that contains the following parameters:

  • DxState: The lowest power state in which the device will be armed for wakeup. By default, this is the lowest D state in which the device is still capable of triggering system wakeup. For the FX2, this is PowerStateD2. This also means that the Sx to Dx mapping determines the deepest Sx state from which the device can trigger system wakeup.

    For example: if S2 is the deepest sleep state that still has a device power state equal to D2, then S2 is the deepest sleep state in which the device can trigger the system.

  • UserControlOfWakeSettings: This setting can be used to allow or disallow the user to enable or disable the wakeup feature.
  • Enabled: Enables or disables the wakeup feature.
NTSTATUS
InitPowerManagement(
    IN WDFDEVICE  Device,
    IN PDEVICE_CONTEXT Context)
{
  NTSTATUS status = STATUS_SUCCESS;
  WDF_USB_DEVICE_INFORMATION usbInfo;

  KdPrint((__DRIVER_NAME "Device init power management\n"));

  WDF_USB_DEVICE_INFORMATION_INIT(&usbInfo);
  status = WdfUsbTargetDeviceRetrieveInformation(
                                Context->UsbDevice,
                                &usbInfo);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfUsbTargetDeviceRetrieveInformation failed with status 0x%08x\n",
      status));
    return status;
  }
  
  KdPrint((__DRIVER_NAME  "Device self powered: %d",
    usbInfo.Traits & WDF_USB_DEVICE_TRAIT_SELF_POWERED ? 1 : 0));
  KdPrint((__DRIVER_NAME  "Device remote wake capable: %d",
    usbInfo.Traits & WDF_USB_DEVICE_TRAIT_REMOTE_WAKE_CAPABLE ? 1 : 0));
  KdPrint((__DRIVER_NAME  "Device high speed: %d",
    usbInfo.Traits & WDF_USB_DEVICE_TRAIT_AT_HIGH_SPEED ? 1 : 0));

  if(usbInfo.Traits & WDF_USB_DEVICE_TRAIT_REMOTE_WAKE_CAPABLE)
  {
    WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS idleSettings;
    WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS wakeSettings;

    WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT(&idleSettings,
                                               IdleUsbSelectiveSuspend);
    idleSettings.IdleTimeout = 10000;
    status = WdfDeviceAssignS0IdleSettings(Device, &idleSettings);
    if(!NT_SUCCESS(status))
    {
      KdPrint((__DRIVER_NAME
        "WdfDeviceAssignS0IdleSettings failed with status 0x%08x\n",
        status));
      return status;
    }
    
    WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS_INIT(&wakeSettings);
    wakeSettings.DxState = PowerDeviceD2;
    status = WdfDeviceAssignSxWakeSettings(Device, &wakeSettings);
    if(!NT_SUCCESS(status))
    {
      KdPrint((__DRIVER_NAME
        "WdfDeviceAssignSxWakeSettings failed with status 0x%08x\n",
        status));
      return status;
    }
  }

  return status;
}

Implementing the code for power management

The driver will start receiving power management as soon as the device object is configured. For the FX2, there are only two events of interest: EvtDeviceD0Entry and EvtDeviceD0Exit. There are other power events that can be handled, but these are not important for our driver.

Something that is not obvious from the code is the state of the IO queues when the device is powered on or off. That is because that is all being taken care of by the framework.

If the IO queues are power managed (which they are, in our case), then the framework will not let the device leave the D0 state as long as there are requests in the queue. Once the device is in a lower power state, the framework will stall all new requests instead of putting them in the queue.

If the device is in a low power state simply because it is idle, the framework will restore the device power state to D0 before delivering the request to the driver.

The power management functions are always called at IRQL=PASSIVE. However, that does not automatically mean that you can place them in pageable sections, or that you are allowed to access pageable data.

The reason for this is that the paging device may not be fully functional during the power state transition. As such, any attempt to access paged data can end in a bug-check.

Luckily, this behavior can be configured. The driver can call WdfDeviceInitSetPowerPageable to indicate that it wants to access pageable data during the power transition. In that case, the system will make sure that the power management functions of the driver are only executed if the page device is running.

The default is to allow paging, so unless you specify otherwise by calling WdfDeviceInitSetPowerNotPageable, you are free to put the power management functions in a pageable code section.

Device power up

Now that the hardware has been configured and the power management features have been set up, the device can enter its normal working state: PowerDeviceD0.

The USB device that was created previously has to be started to be able to perform USB communications. The WdfIoTargetStart function performs this action.

To get the IO target that is associated with the USB device, the framework provides the WdfUsbTargetPipeGetIoTarget function that retrieves the IO target that is associated with a specific USB pipe. Any of the configured USB pipes will do because they all use the same IO target.

The EvtDeviceD0Entry function is called each time the device enters the D0 state, regardless of the previous power state. An additional check is implemented in this function. If the previous state was PowerDeviceD3 (the power off state), the driver restores the state of the LED array.

NTSTATUS
EvtDeviceD0Entry(
    IN WDFDEVICE  Device,
    IN WDF_POWER_DEVICE_STATE  PreviousState
    )
{
  NTSTATUS status = STATUS_SUCCESS;
  PDEVICE_CONTEXT devCtx = NULL;

  KdPrint((__DRIVER_NAME "Device D0 Entry. Coming from %s\n",
    PowerName(PreviousState)));

  devCtx = GetDeviceContext(Device);
  status = 
    WdfIoTargetStart(WdfUsbTargetPipeGetIoTarget(
    devCtx->UsbInterruptPipe));
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoTargetStart failed with status 0x%08x\n", status));
    return status;
  }

  /*restore the state of the LED array 
    if the device is waking up from a
    D3 power state.*/
  if(PreviousState == PowerDeviceD3)
  {
    status = llSetLightBar(devCtx, 
             devCtx->WdfMemLEDArrayState);
  }

  return status;
}

Powering down the device

The power-down sequence is the reverse of the power-up sequence. If the target state is PowerDeviceD3, the state of the LED array is saved in the device context so that it can be restored again later.

When that is done, the USB device object that was created for our driver needs to be stopped so that it too can commence its power-down sequence. Any incomplete IO requests are left pending. That removes the need for restarting the continuous read operation that provides the driver with USB interrupts.

As soon as the IO target is restarted, the driver can receive interrupts again.

NTSTATUS
EvtDeviceD0Exit(
    IN WDFDEVICE  Device,
    IN WDF_POWER_DEVICE_STATE  TargetState
    )
{
  NTSTATUS status = STATUS_SUCCESS;
  PDEVICE_CONTEXT devCtx = NULL;

  devCtx = GetDeviceContext(Device);

  KdPrint((__DRIVER_NAME "Device D0 Exit. Going to %s\n",
    PowerName(TargetState)));

  /*save the state of the LED array if the device is waking up from a
    D3 power state.*/
  if(TargetState == PowerDeviceD3)
  {
    status = llGetLightBar(devCtx, devCtx->WdfMemLEDArrayState);
    if(!NT_SUCCESS(status))
      return status;
  }

  WdfIoTargetStop(WdfUsbTargetPipeGetIoTarget(devCtx->UsbInterruptPipe),
                  WdfIoTargetLeaveSentIoPending);

  return status;
}

Implementing IO

There has been a lot of activity already, just to get to the point where the driver is ready to receive IO requests and USB interrupts. As soon as the device power state is PowerDeviceD0, the IO queues will accept IO requests and execute the correct callback function.

Handling device IO control commands

Most actions that a USB device driver performs are received as device IO control requests. The reason for this is simple. A device typically has lots of features, and read and write operations can only be used for one thing: reading and writing.

All the other features have to be accessible somehow. That is what the IO control handler is for.

The function of a device IO control handler is simply to determine the correct function to execute, and to forward the request to that function. You can see this in the code below. Any IO control that is not eventually handled by our driver is completed with an error.

In order to provide a flexible and efficient handling mechanism, the IO control handling is split into two stages. The first stage is the EvtDeviceIoControlEntry function that will initially handle all requests that are sent to the driver. As you could see earlier, the IO control entry queue was created as a parallel queue, meaning that multiple requests can be served concurrently.

If the request does not have to be synchronized with other requests, it can be handled immediately. On the other hand, if it needs to be synchronized, it will be forwarded to the serialized IO control queue which will handle only one request at a time.

The beauty of this mechanism is that it allows for a quick execution of all non-synchronized requests while also providing synchronization for requests that have to be serialized.

VOID
EvtDeviceIoControlEntry(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request,
    IN size_t  OutputBufferLength,
    IN size_t  InputBufferLength,
    IN ULONG  IoControlCode
    )
{
  switch(IoControlCode)
  {
  case IOCTL_WDF_USB_GET_SWITCHSTATE:
    IoCtlGetSwitchPack(Queue, Request, 
           OutputBufferLength, InputBufferLength);
    break;
  case IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE:
    IoCtlGetSwitchPackChange(Queue, Request, 
           OutputBufferLength, InputBufferLength);
    break;
  default:
    {
      PDEVICE_CONTEXT devCtx = 
              GetDeviceContext(WdfIoQueueGetDevice(Queue));
      WdfRequestForwardToIoQueue(Request, 
              devCtx->IoControlSerialQueue);
    }
    break;
  }
}

The second stage of the IO handler is the serial IO control handler. It will handle any request that was not yet handled by the parallel handler. It will also fail any request it does not know.

As the functionality of the driver evolves over time, you can simply add IO control handling in the stage where it is most appropriate. So even if your driver has only IO controls that need to be serialized, it is still a good idea to use this mechanism because it allows you to cleanly add functionality if / when the requirements change.

VOID
EvtDeviceIoControlSerial(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request,
    IN size_t  OutputBufferLength,
    IN size_t  InputBufferLength,
    IN ULONG  IoControlCode
    )
{
  switch(IoControlCode)
  {
  case IOCTL_WDF_USB_SET_LIGHTBAR:
    IoCtlSetLightBar(Queue, Request, 
            OutputBufferLength, InputBufferLength);
    break;
  case IOCTL_WDF_USB_GET_LIGHTBAR:
    IoCtlGetLightBar(Queue, Request, 
            OutputBufferLength, InputBufferLength);
    break;
  default:
    WdfRequestComplete(Request, 
              STATUS_INVALID_PARAMETER);
    break;
  }
}

Getting the actual switch pack state

This is one of the requests that can be handled in parallel. It only atomically reads a value from the device context, so no serialization is needed.

The value of the switch pack is sent to the PC each time one of its switches changes its position. It is also sent when the device is powered up to its D0 state. The switch pack value is stored in the device context.

To get this value, the user application has to send an IO control request to the driver. This is the simplest IO control function in the driver. First, it checks to see if the output buffer is large enough to contain the switch pack state.

Then the output buffer pointer is retrieved so that the data can be copied into it. When that is done, the IO request can be completed. The number of copied bytes is supplied as completion information so that the correct number of bytes read is reported correctly to the application that sent the request.

VOID
IoCtlGetSwitchPack(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request,
    IN size_t  OutputBufferLength,
    IN size_t  InputBufferLength)
{
  NTSTATUS status = STATUS_SUCCESS;
  BYTE *outChar = NULL;
  size_t length = 0;
  PDEVICE_CONTEXT devCtx = NULL;

  UNREFERENCED_PARAMETER(InputBufferLength);

  if(OutputBufferLength < sizeof(BYTE))
  {
    KdPrint((__DRIVER_NAME
        "IOCTL_WDF_USB_GET_SWITCHSTATE" 
        " OutputBufferLength < sizeof(BYTE)\n"));
    WdfRequestComplete(Request, 
              STATUS_INVALID_PARAMETER);
    return;
  }

  status = WdfRequestRetrieveOutputBuffer(Request,
                                          sizeof(BYTE),
                                          &outChar,
                                          &length);

  if(NT_SUCCESS(status))
  {
    ASSERT(length >= sizeof(BYTE));
    devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue));
    *outChar = devCtx->ActSwitchPack;
  }

  WdfRequestCompleteWithInformation(Request, 
                      status, sizeof(BYTE));
}

Requesting a switch pack change notification

This is the second IO control that needs no synchronization. That is because the handler doesn't actually do anything.

Suppose an application needs to be constantly made aware of the latest value of the switch pack. One option would be to periodically send an IOCTL_WDF_USB_GET_SWITCHSTATE IO control to the driver to get the latest value, but there are several reasons why this is a bad idea.

It causes needless processing overhead. There is a lot of activity, while most of the time the results are the same as before. It also causes a lot of context switching that can harm performance.

There is a much better solution available: an IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE IO control.

The application sends such an IO control to the driver. Instead of completing this request, the driver puts it in the manual request queue and then forgets about it. Note that as soon as it is forwarded, the driver loses the request ownership, so it is not supposed to do anything with it after that. The request could be cancelled or completed, and trying to use the request handle would lead to a bug check.

If the request is synchronous, the calling thread is blocked until the request completes. If the request is sent asynchronous, the calling thread is not blocked but has to regularly check if the request has completed. This can be done in several ways but that is beyond the scope of this article.

Completing the request is done in the USB interrupt handler. Per interrupt packet, there is one IO request completed. The end result is that the user application has to perform only one IO operation per actual switch pack change, instead of wasting hundreds of IO operations by polling the switch pack state.

VOID
IoCtlGetSwitchPackChange(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request,
    IN size_t  OutputBufferLength,
    IN size_t  InputBufferLength)
{
  NTSTATUS status = STATUS_SUCCESS;
  PDEVICE_CONTEXT devCtx = NULL;

  UNREFERENCED_PARAMETER(InputBufferLength);
  UNREFERENCED_PARAMETER(OutputBufferLength);
  UNREFERENCED_PARAMETER(Queue);

  devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue));

  /*If the request is succesfull the
    request ownership is also transferred
    back to the framework.*/
  status = WdfRequestForwardToIoQueue(Request,
           devCtx->SwitchChangeRequestQueue);
  /*if the request cannot be forwarded
    it has to be completed with
    the appropriate status code because 
    the driver still owns the request.*/
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
        "WdfRequestForwardToIoQueue failed " 
        "with code 0x%08x.\n", status));
    WdfRequestComplete(Request, status);
  }
}

By now, you might be asking yourself the question, "This is all fine and dandy, but what happens with the IO request when the calling application closes its device handle?" I asked myself the same question.

The correct answer is, "Nothing that the driver has to care about." Really. It is that simple!

Truth be told, I was already experimenting with the EvtFileCleanup function to manually retrieve requests out of the manual queue and cancel them, when I found out that the framework does this for me, for free.

If there is an outstanding IO request in the queue when the associated file handle is closed, the IO request is cancelled and removed from the queue. The USB interrupt handler will never know.

The same is true if the device is removed from the system by pulling out the USB connector. All outstanding requests will be cancelled automatically.

Getting the state of the LED array

The IO control request for reading the state of the LED array is forwarded to the IoCtlGetLightBar function. I have omitted the description of this function because its control flow is exactly the same as for the previous IO control operation.

IoCtlGetLightBar first checks if the output buffer is large enough. After that, it executes the actual command. When that has finished, the request is completed with the status code of the executed command and the number of bytes that was read.

The only interesting thing here is the implementation of the llGetLightBar function that executes the low level command.

Before it does anything, it extracts the buffer pointer from the supplied WDF memory object. This is not needed for the actual USB operation, but the driver needs it afterwards to reformat the data packet.

A memory descriptor is then created for the WDF memory handle itself because the function for sending the request requires a memory descriptor instead of a WDFMEMORY handle.

The actual communication to get the LED state is implemented on the FX2 as a vendor control message. All USB control requests require a WDF_USB_CONTROL_SETUP_PACKET structure to hold the request information.

The driver initializes the control packet with the control request direction (BmRequestDeviceToHost), the command recipient (BmRequestToDevice), and the numerical value of the specific control command.

The control request is sent to the USB device in a synchronous fashion. I.e., the function will only return when the request has finished, or if there was an error. Note that the driver does not need to specify a USB pipe to use because the driver knows which endpoint to send the request to.

As with the switch pack state, the LED array state needs to be converted from its physical representation to a logical representation. The algorithm involved is different from the algorithm for the switch pack because the encoding is different.

NTSTATUS
llGetLightBar(
    IN PDEVICE_CONTEXT Context,
    IN WDFMEMORY State
    )
{
  NTSTATUS status = STATUS_SUCCESS;
  WDF_USB_CONTROL_SETUP_PACKET controlPacket;
  WDF_MEMORY_DESCRIPTOR memDescriptor;
  BYTE logicalVal = 0;
  BYTE *inChar = NULL;
  size_t length = 0;

  KdPrint((__DRIVER_NAME "entering llGetLightBar\n"));

  inChar = WdfMemoryGetBuffer(State, &length);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "Could not retrieve the lightbar memory pointer\n"));
    return status;
  }
  
  ASSERT(length >= sizeof(BYTE));
  ASSERT(NULL != inChar);

  /*initialize the descriptor that will be passed to the USB driver*/
  WDF_MEMORY_DESCRIPTOR_INIT_HANDLE(&memDescriptor, State, NULL);

  WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR(
                            &controlPacket,
                            BmRequestDeviceToHost,
                            BmRequestToDevice,
                            VC_GET_LIGHT_BAR,
                            0,
                            0);

  status = WdfUsbTargetDeviceSendControlTransferSynchronously(
                            Context->UsbDevice,
                            NULL,
                            NULL,
                            &controlPacket,
                            &memDescriptor,
                            NULL);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfUsbTargetDeviceSendControlTransferSynchronouslyfailed with status 0x%08x\n",
      status));
    return status;
  }

  /*translate the supplied physical value to a value that represents the
    values of the LEDs in the logical light array.*/
  logicalVal = ((*inChar & 0x1F) << 3) | ((*inChar & 0xE0) >> 5);
  KdPrint((__DRIVER_NAME "Original value = 0x%x, new value = 0x%x\n",
    *inChar, logicalVal));
  *inChar = logicalVal;
  
  return status;
}

Setting the state of the LEDs in the LED array

The code for setting the LED state is nearly identical to the code for getting it, so I am not going to repeat that here.

The only significant difference is that a different numerical control code is used (VC_GET_LIGHT_BAR instead of VC_SET_LIGHT_BAR), and that the LED array state is now converted from a logical value to a physical value before the request is sent.

Handling USB interrupts

The USB interrupt handler is the function that was registered to be called for every read request that succeeds for the continuous read on the interrupt endpoint.

This function is the only function in our driver that is ever called at IRQL=DISPATCH. This means that it should not block for any length of time, and it should only use functions that are safe at that IRQL. Last but not least, it should not access any pageable data. This also means that this function is the only function in our driver that is not placed in a pageable code section.

As you can see, EvtUsbDeviceInterrupt only takes the interrupt data and copies it into the value for the actual switch pack state.

The only additional thing that happens here is that the different bits in the switch pack are shuffled to a different position. The reason for this is that the incoming value is the switch pack as it was stored in hardware, rather than the logical ordering of the switches.

It is common for values like this to be ordered in a non-intuitive way because it is much easier and cheaper to do this in software than in hardware. The ordering could simply be caused by the fact that traces on the PCB have a wiring limitation.

When the data is converted, the driver checks if there is an outstanding IO request in the manual request queue. If there is such a request, it is completed. Only one request is completed per interrupt. Requests are put into the manual queue in the handler for the IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE IO control.

Since there is almost no delay between retrieving the IO control from the queue and completing it, nothing more needs to be done. If there was some lengthy processing to be done in between those two actions, the driver should enable cancellation of the request.

If you enable request cancellation, you have to supply a callback function that is called by the framework so that the driver can stop the request in a controlled manner. The driver then has to disable request cancellation before actually completing the request, to make sure that it is still allowed to touch it.

But in the case of this driver, that is not necessary because there is no delay between receiving the request ownership and the request completion.

VOID
EvtUsbDeviceInterrupt(
    WDFUSBPIPE  Pipe,
    WDFMEMORY  Buffer,
    size_t  NumBytesTransferred,
    WDFCONTEXT  Context
    )
{
  NTSTATUS status;
  BYTE temp;
  size_t size;
  PDEVICE_CONTEXT devCtx = Context;
  WDFREQUEST Request = NULL;
  BYTE *packState = WdfMemoryGetBuffer(Buffer, &size);

  UNREFERENCED_PARAMETER(Pipe);

  ASSERT(size == sizeof(BYTE));
  ASSERT(NumBytesTransferred == size);
  ASSERT(packState != NULL);
  
  temp = *packState;
  
  temp = (temp & 0x01) << 7 | 
         (temp & 0x02) << 5 | 
         (temp & 0x04) << 3 | 
         (temp & 0x08) << 1 | 
         (temp & 0x10) >> 1 | 
         (temp & 0x20) >> 3 | 
         (temp & 0x40) >> 5 | 
         (temp & 0x80) >> 7;

  KdPrint((__DRIVER_NAME "Converted switch pack from 0x%02x to 0x%02x\n",
    (ULONG)*packState, (ULONG)temp));

  devCtx->ActSwitchPack = ~temp;

  /*is there an io control queued? if so then complete the first one*/
  status = WdfIoQueueRetrieveNextRequest(devCtx->SwitchChangeRequestQueue,
                                         &Request);
  if(NT_SUCCESS(status))
  {
    BYTE* outBuffer;
    status = WdfRequestRetrieveOutputBuffer(Request,
                                            sizeof(BYTE),
                                            &outBuffer,
                                            NULL);

    if(NT_SUCCESS(status))
    {
      /*do not use the value in the device context,
        since that may already have
        changed because of a second interrupt
        while this one was handled.*/
      *outBuffer = temp;
      WdfRequestCompleteWithInformation(Request, 
                          status, sizeof(BYTE));
    }
    else
      WdfRequestComplete(Request, status);
    
    KdPrint((__DRIVER_NAME "Completed async pending IOCTL.\n"));
  }
}

Handling device read / write functionality

The last thing the driver is still missing is the read / write functionality. For the FX2, these are symmetrical. Everything that is written to the 'In' endpoint is looped back to the 'Out' endpoint. These endpoints are double buffered, so a new data packet can be sent while the previous packet is still being passed through.

If the data isn't read back, the write request will stall until the buffers are cleared again. At the application level, this means that there always has to be an outstanding read request that matches the write request in length.

Lucky for us, the driver doesn't have to care about this. This is the responsibility of the user application that uses this device driver.

Handling the write request

The driver cannot perform the actual write operation by itself. Rather, it has to ask the USB bus driver to perform a write request. For that reason, the driver reformats the incoming request to a write request for the USB IO target.

In order to be able to complete the request once it finishes, the EvtIoWriteComplete function is associated with the write request as a completion routine. It will be executed when the low level USB write request finishes.

If any of these intermediate actions fail, the request is failed immediately with the correct status code. Else, this function returns without changing the request. It is then up to the completion routine to do the rest.

VOID
EvtDeviceIoWrite(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request,
    IN size_t  Length
    )
{
  NTSTATUS status = STATUS_SUCCESS;
  PDEVICE_CONTEXT devCtx = NULL;
  WDFMEMORY requestMem;

  devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue));
  
  KdPrint((__DRIVER_NAME "Received a write request of %d bytes\n", Length));

  status = WdfRequestRetrieveInputMemory(Request, &requestMem);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfRequestRetrieveInputMemory failed with status 0x%08x\n", status));
    WdfRequestComplete(Request, status);
    return;
  }

  status = WdfUsbTargetPipeFormatRequestForWrite(
                                      devCtx->UsbBulkOutPipe,
                                      Request,
                                      requestMem,
                                      NULL);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfUsbTargetPipeFormatRequestForWrite " 
      "failed with status 0x%08x\n", status));
    WdfRequestComplete(Request, status);
    return;
  }
  WdfRequestSetCompletionRoutine(Request,
                                 EvtIoWriteComplete,
                                 devCtx->UsbBulkOutPipe);
  if(FALSE == WdfRequestSend(Request,
                            WdfUsbTargetPipeGetIoTarget(devCtx->UsbBulkOutPipe),
                            NULL))
  {
    KdPrint((__DRIVER_NAME "WdfRequestSend failed with status 0x%08x\n", status));
    status = WdfRequestGetStatus(Request);
    WdfRequestComplete(Request, status);
  }
  else
    return;
}

Completing the write request

The completion function for the write request is pretty simple. It gets the status and transfer length from the USB request completion parameters, and completes the request with that information.

VOID
EvtIoWriteComplete(
    IN WDFREQUEST  Request,
    IN WDFIOTARGET  Target,
    IN PWDF_REQUEST_COMPLETION_PARAMS  Params,
    IN WDFCONTEXT  Context)
{
  PWDF_USB_REQUEST_COMPLETION_PARAMS usbCompletionParams;

  UNREFERENCED_PARAMETER(Context);
  UNREFERENCED_PARAMETER(Target);

  usbCompletionParams = Params->Parameters.Usb.Completion;

  if(NT_SUCCESS(Params->IoStatus.Status))
  {
    KdPrint((__DRIVER_NAME "Completed the write request with %d bytes\n",
          usbCompletionParams->Parameters.PipeWrite.Length));
  }
  else
  {
    KdPrint((__DRIVER_NAME "Failed the read request with status 0x%08x\n",
          Params->IoStatus.Status));
  }
  WdfRequestCompleteWithInformation(Request,
                                    Params->IoStatus.Status,
                                    usbCompletionParams->Parameters.PipeWrite.Length);
}

Handling the read request

Read requests are virtually identical to write requests from this driver's point of view. The only difference is that another bulk pipe is used, and different completion parameters are read.

Testing the driver

You can download the device driver from the top of this page. For more detailed information on how to build and install the driver, you can read my previous article.

Also available for download is a demo application. The test application can enumerate all devices that export the GUID_DEVINTERFACE_FX2 device interface.

As soon as a handle to the device is opened, a secondary thread will initialize the switches on the user interface with the actual switch pack state, and then wait for switch change notifications.

The following features are also available to the user via push buttons:

  • Loop-back of a file through the device to another file on disk. During the data transfer, the LED array is used as a binary number that is incremented with each 10 USB packets that are received by the FX2.
  • Getting the LED array state. Each LED is represented by one bit that is visualized as a checkbox.
  • Setting the LED array state. The state of the checkboxes on the dialog will be set on the corresponding LEDs on the FX2.

All device errors will be popped up on a message box. It is no problem if you pull out the cable during operation. The current operations will fail gracefully, and the device handle will be closed.

If you test the application, you might notice that if you move the switches very fast, it is possible that the switches on the screen do not match the actual state on the FX2. This is simply because my application uses only one IO control for switch change notification.

If you want to receive all the notifications, your application has to make a queue of outstanding IO controls that gets filled up each time a previous one completes.

Conclusion

Phew, ...

I know this is a very long article. Thank you for reading it. I hope you enjoyed reading it as much as I enjoyed writing it and figuring it all out.

The reason this article is so long is that it is complete, or at least as complete as is possible without turning it into an encyclopedia or copying the entire DDK help collection.

This article describes all the issues that are involved with writing a full fledged USB device driver that uses control requests, USB interrupts, and bulk transfers.

With this article, you should be able to understand the concepts involved in USB device driver development. If you want to develop your own USB driver, then this article gives you a good place to start.

There is one thing to mention: the current design does not allow multiple applications to receive switch change notifications, since only one request is completed per interrupt. To add this functionality, I would have had to add and explain additional topics like file object handling and synchronization functions.

That would have made this article even longer, and the purpose of this article was to explain the USB mechanics. KMDF file object handling and other topics will have to wait for a follow up article.

To be honest, it took me far less time to develop the driver, the DLL, and the test application than it took to write this article. One of the most time consuming activities was to do all the research to make sure that this article is factually correct.

Please let me know if you should find any errors, ambiguities, mistakes, or other problems in this article so that I can keep it correct and up to date.

History

The following versions of this article have been released:

  • 1.0: Initial version of this article. Special thanks to Doron Holan and Vishal Manan for their feedback and suggestions.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here