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:
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.
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.
The list of prerequisites for using the code is very short:
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.
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.
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.
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.
There are four types of data transfer in the USB protocol. Each has its own purpose:
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 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.
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:
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.
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:
Collapse Copy Code
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:
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.
The following chapters explain the different configuration and initialization phases of a USB device driver.
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.
Collapse Copy Code
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; }
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.
Collapse Copy Code
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; }
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:
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.
Collapse Copy Code
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; }
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.
Collapse Copy Code
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; }
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.
Collapse Copy Code
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; }
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.
Collapse Copy Code
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; }
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. Collapse Copy Code
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; }
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.
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.
Collapse Copy Code
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; }
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.
Collapse Copy Code
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; }
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.
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.
Collapse Copy Code
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.
Collapse Copy Code
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; } }
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.
Collapse Copy Code
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)); }
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.
Collapse Copy Code
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.
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.
Collapse Copy Code
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; }
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.
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.
Collapse Copy Code
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")); } }
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.
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.
Collapse Copy Code
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; }
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.
Collapse Copy Code
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); }
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.
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:
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.
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.