As anyone who has attempted one can attest, writing a USB device driver using WDM is
truly a difficult task. The PnP nature of the devices cause your driver to push at every edge of the PnP state machine, and, as if it
weren't hard enough, power management becomes even more complicated with poorly documented USB-specific rules.
WDMUSB Drivers Are Dead. Long Live KMDFUSB Drivers!
That is why it is with great pleasure
we are pleased to announce the demise of the WDM USB driver. Not from any sort of, "if you write your USB driver in WDM it won't load
on Windows" edict, mind you, but simply as a matter of practicality. Thus, this article will tour you around the various aspects of KMDF
that make writing a USB driver downright pleasant and sounds the death knell for WDM USB drivers. Trust us, folks, write a USB driver in
KMDF and you will never go back.
The OSRUSBFX2 Sample
As we walk through the various parts of the framework that are relevant
to USB driver writers, we may occasionally want to show some sample code of how they're used. Instead of foisting yet another sample onto
the world, we've decided to simply use the OSRUSBFX2 sample provided with the WDK. While we're not 100% in love with it, it is correct
and probably familiar to most of the readers out there.
The Objects
While a KMDF USB driver is likely to use a wide range of KMDF objects, there are three
key objects that are specific to USB drivers:
-
WDFUSBDEVICE - This object represents a USB I/O target, which is a special type of KMDF I/O target that accepts USB
control transfers.
-
WDFUSBINTERFACE - This object is KMDF's abstraction of a USB interface, which is used to get or set interface information.
-
WDFUSBPIPE - This object represents a pipe I/O target, which is a special type of KMDF I/O target that accepts USB
bulk and interrupt transfers.
The following sections will discuss these objects in further detail.
WDFUSBDEVICE
During a USB driver's EvtDevicePrepareHardware processing, it will need
to create the WDFUSBDEVICE object used for all control transfer operations on the device. This is done by simply making a call
to WdfUsbTargetDeviceCreate, as seen in Figure 1.
//
// Create a USB device handle so that we can communicate with the
// underlying USB stack. The WDFUSBDEVICE handle is used to query,
// configure, and manage all aspects of the USB device.
// These aspects include device properties, bus properties,
// and I/O creation and synchronization.
//
status = WdfUsbTargetDeviceCreate(Device,
WDF_NO_OBJECT_ATTRIBUTES,
&pDeviceContext->UsbDevice);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP,
"WdfUsbTargetDeviceCreate failed with Status code %!STATUS!\n", status);
return status; }
Figure 1 - WdfUsbTargetDeviceCreate
|
Once you've created your WDFUSBDEVICE, there are several operations you're able to perform
with it. However, the most common operations will be selecting a device configuration and performing control transfers.
Selecting a Device Configuration
Selecting your device's
configuration is a simple matter of calling WdfUsbTargetDeviceSelectConfig and supplying an
appropriate WDF_USB_DEVICE_SELECT_CONFIG_ PARAMS structure. If your device is like most devices and only uses a
single interface, configuring your device is a snap. Just initialize the configuration parameters using the
provided WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE function and
call WdfUsbTargetDeviceSelectConfig, as shown in Figure 2.
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE( &configParams);
status = WdfUsbTargetDeviceSelectConfig(pDeviceContext->UsbDevice,
WDF_NO_OBJECT_ATTRIBUTES,
&configParams);
if(!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP,
"WdfUsbTargetDeviceSelectConfig failed %!STATUS!\n",
status);
return status;
}
Figure 2 - WdfUsbTargetDeviceSelectConfig
|
NOTE: If you're in the boat of needing to support multiple interfaces, you're
going to have to do a bit more work. We'll leave figuring that out as an exercise for the (unfortunate) reader, with the hint that
the steps are documented within the WDK documentation along with some marginally useful sample code.
Once you have successfully configured your device, you can find your WDFUSBINTERFACE object within the configuration
parameters structure supplied to WdfUsbTargetDeviceSelectConfig. We'll be discussing the uses of that object in
a later section.
Performing Control Transfers
Once your device is configured, it's likely that you'll
want to perform some control transfers on your device. There are a couple of different ways to send control transfers to your device,
but no matter which method you pick, the keys to the operation will be your WDFUSBDEVICE and the WDF_ USB_CONTROL_SETUP_PACKET
structure.
The WDFUSBDEVICE we already have, so it's simply a matter of initializing a WDF_USB_CONTROL_SETUP_ PACKET.
Like most other structures within KMDF, there are several different functions available to you for initializing one of these structures.
Currently, the following initialization functions are available:
- WDF_USB_CONTROL_SETUP_PACKET_INIT
- WDF_USB_CONTROL_SETUP_PACKET_INIT_ CLASS
- WDF_USB_CONTROL_SETUP_PACKET_INIT_ FEATURE
- WDF_USB_CONTROL_SETUP_PACKET_INIT_GET_STATUS
- WDF_USB_CONTROL_SETUP_PACKET_INIT_ VENDOR
Which one you pick will depend on what type of control transfer you are sending.
For example, on the OSRUSBFX2 device there is a vendor command available for retrieving the state of the LED bar graph. Therefore,
the WDF_USB_CONTROL_SETUP_PACKET structure used to send the command is initialized with the _VENDOR flavor, as seen
in Figure 3.
WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR(&controlSetupPacket,
BmRequestDeviceToHost,
BmRequestToDevice,
USBFX2LK_READ_BARGRAPH_DISPLAY, // Request
0, // Value
0); // Index
Figure 3 - Setting up a Control Transfer
|
Once you've built your WDF_USB_CONTROL_SETUP_ PACKET, it's just a small
matter of sending the request to your WDFUSBDEVICE. You can either do this by formatting an existing WDFREQUEST
with WdfUsbTargetDeviceFormatRequestForControlTransfer and sending it with WdfRequestSend
(which will give you the option of sending the request asynchronously), or you can shorthand it by simply
calling WdfUsbTargetDeviceSendControlTransfer Synchronously, such as in Figure 4.
WDF_MEMORY_DESCRIPTOR_INIT_BUFFER(&memDesc,
BarGraphState,
sizeof(BAR_GRAPH_STATE));
status = WdfUsbTargetDeviceSendControlTransferSynchronously(
DevContext->UsbDevice,
WDF_NO_HANDLE, // Optional WDFREQUEST
NULL, // PWDF_REQUEST_SEND_OPTIONS
&controlSetupPacket,
&memDesc,
&bytesTransferred);
if(!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, DBG_IOCTL,
"GetBarGraphState: Failed - 0x%x \n", status);
} else {
TraceEvents(TRACE_LEVEL_VERBOSE, DBG_IOCTL,
"GetBarGraphState: LED mask is 0x%x\n", BarGraphState->BarsAsUChar);
}
Figure 4 - Sending the Control Transfer...WdfUsbTargetDeviceSendControlTransferSynchronously
|
Other WDFUSBDEVICE Operations
There are lots of other thing you can
do with a WDFUSBDEVICE object, such as cycle the USB port it is attached to or retrieve various device descriptors. For a complete list of
valid operations consult the WDK index for functions starting with WdfUsbTargetDeviceXxx.
WDFUSBINTERFACE
Again assuming a single interface, after your driver has successfully
called WdfUsbTargetDeviceSelectConfig you get a handle to your WDFUSBINTERFACE object from within the
supplied configuration parameters. We can see the OSRUSBFX2 sample getting its pointer in Figure 5.
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE( &configParams);
status = WdfUsbTargetDeviceSelectConfig(pDeviceContext->UsbDevice,
WDF_NO_OBJECT_ATTRIBUTES,
&configParams);
if(!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP,
"WdfUsbTargetDeviceSelectConfig failed %!STATUS!\n",
status);
return status;
}
pDeviceContext->UsbInterface =
configParams.Types.SingleInterface.ConfiguredUsbInterface;
Figure 5 - Obtaining Handle to WDFUSBINTERFACE Object (Within Configuration Parameters)
|
Once you've acquired your WDFUSBINTERFACE, there are several operations you're able to
perform with it. However, the most common operation will be using it to acquire handles to your various WDFUSBPIPE objects.
Acquiring Your WDFUSBPIPE Objects
Acquiring your WDFUSBPIPE objects is a simple
matter of determining the number of pipes your interface has and calling KMDF to get a pointer to each pipe's WDFUSBPIPE. You'll
want to store pointers to each of these WDFUSBPIPE objects within your per-device context because you'll need them later for any
bulk or interrupt transfers. We can see how the OSRUSBFX2 sample does this in Figure 6.
numberConfiguredPipes = configParams.Types.SingleInterface.NumberConfiguredPipes;
// // Get pipe handles // for(index=0; index < numberConfiguredPipes; index++) {
WDF_USB_PIPE_INFORMATION_INIT(&pipeInfo);
pipe = WdfUsbInterfaceGetConfiguredPipe(
pDeviceContext->UsbInterface,
index, //PipeIndex,
&pipeInfo
);
//
// Tell the framework that it's okay to read less than
// MaximumPacketSize
//
WdfUsbTargetPipeSetNoMaximumPacketSizeCheck(pipe);
if(WdfUsbPipeTypeInterrupt == pipeInfo.PipeType) {
TraceEvents(TRACE_LEVEL_INFORMATION, DBG_IOCTL,
"Interrupt Pipe is 0x%p\n", pipe);
pDeviceContext->InterruptPipe = pipe;
}
if(WdfUsbPipeTypeBulk == pipeInfo.PipeType &&
WdfUsbTargetPipeIsInEndpoint(pipe)) {
TraceEvents(TRACE_LEVEL_INFORMATION, DBG_IOCTL,
"BulkInput Pipe is 0x%p\n", pipe);
pDeviceContext->BulkReadPipe = pipe;
}
if(WdfUsbPipeTypeBulk == pipeInfo.PipeType &&
WdfUsbTargetPipeIsOutEndpoint(pipe)) {
TraceEvents(TRACE_LEVEL_INFORMATION, DBG_IOCTL,
"BulkOutput Pipe is 0x%p\n", pipe);
pDeviceContext->BulkWritePipe = pipe;
}
}
Figure 6 - Acquiring WDFUSBPIPE Objects
|
Other WDFUSBINTERFACE Operations
There are lots of other
things you can do with a WDFUSBINTERFACE object, such as select an alternate interface setting or retrieve its USB interface descriptor
per the USB spec. For a complete list of valid operations consult the WDK index for functions starting
with WdfUsbInterfaceXxx.
WDFUSBPIPE
The WDFUSBPIPE object is what your driver will use to perform all bulk and interrupt
I/O on your device. Once you have acquired your WDFUSBPIPE objects via the WDFUSBINTERFACE object, the most common operations you will
perform on the WDFUSBPIPE objects will be single I/O operations and/or continuous I/O operations.
Performing Single I/O Operations
A common scenario under which your driver might
perform, say, a bulk read on the device is as follows:
A user application sends your driver a read request. Your driver converts this request into a bulk read and sends
the bulk read to your device.
This is such a common scenario in fact that KMDF practically does all the work for you. In the above scenario,
all your driver will need to do is call WdfUsbTargetPipeFormat RequestForRead, specifying the user's WDFREQUEST
and data buffer, then forward the user request to your device with WdfRequestSend. We can see the OSRUSBFX2 driver
performing these steps within its EvtIoRead callback in Figure 7.
status = WdfRequestRetrieveOutputMemory(Request, &reqMemory);
if(!NT_SUCCESS(status)){
TraceEvents(TRACE_LEVEL_ERROR, DBG_READ,
"WdfRequestRetrieveOutputMemory failed %!STATUS!\n", status);
goto Exit;
}
//
// The format call validates to make sure that you are reading or
// writing to the right pipe type, sets the appropriate transfer flags,
// creates an URB and initializes the request.
//
status = WdfUsbTargetPipeFormatRequestForRead(pipe,
Request,
reqMemory,
NULL // Offsets
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, DBG_READ,
"WdfUsbTargetPipeFormatRequestForRead failed 0x%x\n", status);
goto Exit;
}
WdfRequestSetCompletionRoutine(
Request,
EvtRequestReadCompletionRoutine,
pipe);
//
// Send the request asynchronously.
//
if (WdfRequestSend(Request,
WdfUsbTargetPipeGetIoTarget(pipe),
WDF_NO_SEND_OPTIONS) == FALSE) {
...
Figure 7 - Performing a Bulk Read
|
Performing Continuous I/O Operations
It is a common
paradigm in USB drivers to "hang" an I/O request on the USB device so that there is always a request waiting when the device
has data to present. When the I/O request completes, your driver performs device specific processing on the data and then
resubmits the I/O request to the device. Further complicating the matter, drivers typically need to have multiple I/O requests
pending on the device so that they don't lose the next set of data while working on the previous set of data.
In order to facilitate USB devices that require this programming model be used, KMDF supports something
called a continuous reader on both bulk read and interrupt read pipes. The idea behind the continuous read is
that your driver supplies a callback to be called when the device has reported bulk or interrupt data, depending on the
configured pipe type. This way, your driver deals with the data reported by the device while the framework deals with
keeping a set of requests active on the device and ready to receive the data.
The OSRUSBFX2 device supports an interrupt endpoint that monitors changes to
the device's switch pack. The sample driver configures a continuous reader that receives the device reported data at the time
of the interrupt. In Figure 8 we can see how the sample driver initializes the continuous reader on the WDFUSBPIPE,
while Figure 9 shows the actual code of the continuous reader callback.
WDF_USB_CONTINUOUS_READER_CONFIG_INIT(&contReaderConfig,
OsrFxEvtUsbInterruptPipeReadComplete,
DeviceContext, // Context
sizeof(UCHAR)); // TransferLength
//
// Reader requests are not posted to the target automatically.
// Driver must explictly call WdfIoTargetStart to kick start the
// reader. In this sample, it's done in D0Entry.
// By defaut, framework queues two requests to the target
// endpoint. Driver can configure up to 10 requests with CONFIG macro.
//
status = WdfUsbTargetPipeConfigContinuousReader(DeviceContext->InterruptPipe,
&contReaderConfig);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP,
"OsrFxConfigContReaderForInterruptEndPoint failed %x\n",
status);
return status;
}
Figure 8 - Initialization of the Continuous Reader
|
VOID
OsrFxEvtUsbInterruptPipeReadComplete(
WDFUSBPIPE Pipe,
WDFMEMORY Buffer,
size_t NumBytesTransferred,
WDFCONTEXT Context
)
{
...
switchState = WdfMemoryGetBuffer(Buffer, NULL);
TraceEvents(TRACE_LEVEL_INFORMATION, DBG_INIT,
"OsrFxEvtUsbInterruptPipeReadComplete SwitchState %x\n",
*switchState);
pDeviceContext->CurrentSwitchState = *switchState;
...
}
Figure 9 - The Continuous Reader Callback
|
Other WDFUSBPIPE Operations
The WDFUSBPIPE object
supports a rich set of operations that can be performed on the pipe, including routines for performing synchronous I/O on the pipe
and the ability to send your own USB Request Block (URB) to the pipe. For a complete list of valid operations consult the WDK
index for functions starting with WdfUsbTargetPipeXxx.
It Rocks!
Clearly the KMDF designers had USB on the brain when creating the
framework. There's so much built in support for typical USB operations that you almost forget how much of a pain WDM was.
And, don't forget, this is on top of having all your PnP and power management code written for you. So, get crackin' and
convert your old, crufty WDM drivers to KMDF!