PHD Computer Consultants Ltd
... Windows Driver Model Article
Last modified: 16 June 1999.

This article and accompanying code are Copyright © 1998,1999 PHD Computer Consultants Ltd, www.phdcc.com. Approximately 4500 words.
This article was originally written for EXE Magazine and appeared in its January 1999 issue.

The source code for the HiImWdm example driver discussed in this article is available in the PHD download area. Download hiimwdm.zip and unzip in directory C:\EXEWdm.

You may be interested in reading Chris Cant's earlier article on writing Windows NT 3.51 and NT4.0, Writing Windows NT Device Drivers. Many of the techniques described in this article are still used in WDM device drivers.

Chris Cant's book on WDM device drivers is available now.

Check out PHD's other device driver resources. PHD sells its DebugPrint software that lets you include formatted print trace statements in your driver code, and a simple general purpose driver PHDIo.


Windows Driver Model (WDM) Device Drivers

Introduction

Glossary
APIApplication Programmer Interface
COMCommon Object Model
DDKDriver Development Kit
FDOFunctional Device Object
GUIDGlobally Unique Identifier
HIDHuman Input Device
IEEE 1394100Mbps+ serial bus, neé Firewire
INFInstallation information file
IOCTLI/O Control Code
IRPI/O Request Packet
ISAIndustry Standard Architecture PC bus
mofWMI class file
MSDNMicrosoft Developer Network
NTNew Technology
PDOPhysical Device Object
PnPPlug and Play
SDKSoftware Development Kit
URBUSB Request Block
USB12Mbps Universal Serial Bus
VC++Visual C++
W2000Windows 2000 (neé NT 5)
W98Windows 98
WBEMWeb-based Enterprise Management
WDMWindows Driver Model
WMIWindows Management Instrumentation
The Windows® Driver Model (WDM) lets you write a common device driver for Windows 98 and Windows 2000 (NT 5) - for some types of device.

This article will look at the development of a small virtual WDM device driver to illustrate the main principles without getting bogged down in the details. I mainly concentrate on giving an overview of the technology, rather than the minutiae of each line of code. Welcome to acronym-land.

WDM is an enhanced form of the NT 3.51 and NT 4 kernel-mode device driver model. The main structural changes are the addition of Plug and Play (PnP), Power management, Windows Management Interface (WMI) and Device Interface support. These features are described in this article, along with details of the necessary development environment.

However, almost of more significance, is the provision of a series of class drivers. A class driver does the bulk of the work for a specific area of functionality. There are class drivers for the Universal Serial Bus (USB), the IEEE 1394 (Firewire) bus, streaming devices and Human Interface Devices (HID). More on these later.

You still need to write a separate driver which calls the appropriate class driver. For example, you can write a driver to talk to a new USB device through the USB class driver using relatively straightforward USB Request Blocks (URBs), ie without having to worry about the details of talking to a bit of hardware. For HID devices, you do not even need to write a kernel-mode driver at all, as you can use Win32 HID client functions to access the device.

There are two ways of customising class drivers. The first is to write a filter driver, which can slot in above or below a class driver. Alternatively, minidrivers in various shapes and form can be used. The class driver does all the general processing while the minidriver just communicates with a specific type of hardware. For example, the Windows HID class driver is paired with the hidusb minidriver to talk to HID devices on the USB bus. You could, for example, write a new HID minidriver to provide an interface to the serial port version of your product.

Each class driver has an appropriate specification. The USB class driver responds to several internal IOCTLs, one of which is to process a URB. As another example, a HID minidriver must register certain call-backs to work with the HID class driver. So you need to consult the relevant class documentation in the Driver Development Kit (DDK).

Microsoft has managed to sneak COM Globally Unique Identifiers (GUIDs) into driver development. You can use these to define a private interface that identifies your particular device. GUIDs are sometimes also used by minidrivers to identify what facilities are supported.

Unfortunately, you will not be able to port your old NT device drivers to Windows 98 with no work. WDM relies on Plug and Play for resource assignment, which was not available in these old drivers. Some people will still need to write VxDs for Windows 98, and others will need to maintain their NT specific kernel-mode drivers for Windows 2000, eg for video drivers. See my earlier article in EXE magazine, October 1997 for details of NT kernel-mode driver development. In fact, I recommend that you re-read this article as it covers many concepts used here.

Currently it is not exactly clear whether WDM device drivers are binary compatible between W98 and W2000. The W98 DDK says that drivers can be compatible, while the W2000 DDK says that they are not necessarily binary compatible. The DDKs do claim that they should be source level compatible. However, for USB drivers, there are some facilities available in W2000 that are not available in W98.



Driver Development

Resources
Windows 98 DDK
  • Dr Iver
www.microsoft.com/ddk
MSDN
  • Windows 98 DDK
  • Windows 2000 DDK
  • Platform SDK
msdn.microsoft.com
Compuware Numega
  • SoftICE debugger
  • DriverAgent
  • DriverWorks
www.compuware.com/driverzone
www.vireo.com
www.numega.com
BlueWater Systems
  • WinDK
  • WinRT
www.bluewatersystems.com
Jungo
  • WinDriver
  • KernelDriver
www.jungo.com
Open Systems Research
  • OSR DDK
www.osr.com
PHD
  • DebugPrint trace tool

  • PHDIo General purpose driver
www.phdcc.com/debugprint

www.phdcc.com/phdio

You will need a Microsoft Driver Development Kit (DDK) or two to build drivers. However I have bundled a copy of the built driver with the source code, so you can try out the example driver without having a DDK. The DDKs are available for lengthy download online, but most driver developers want to have an MSDN Professional subscription to get the DDKs and the useful Platform SDK on CD, as well as the latest beta test versions of Windows, etc.

You can buy two different types of development tool to help you write drivers. The first type lets you control a general purpose driver in user-mode to handle many standard types of I/O. The second type gives you a C++ framework to base your driver on, with many useful classes and examples. Compuware Numega, BlueWater Systems, Inc. and Jungo provide products of both types. PHD sells general purpose drivers. Finally, Open Systems Research sells OSR DDK, a debugging aid that logs all your DDK function calls so that they can be viewed with their OSRTracer program.

The example driver comes with a suitable VC++ Visual Studio 97 workspace. The Makefile project assumes that you have the Windows 98 DDK in its default location of C:\98DDK and that the example source base directory is C:\EXEWdm. Alter the project settings if you use different directories.

The W2000 DDK provides the WinDbg tool to let you do source level debugging between two W2000 computers. However, the equivalent SoftICE tool from Compuware NuMega can be used on just one PC. For simple formatted print trace statements you can use PHD's DebugPrint software.



HiImWdm example

Table 1
HiImWdm source files
file description
HiImWdm.h Header
Init.cpp Driver Initialisation
Pnp.cpp Plug and Play
Power Management
Dispatch.cpp Read, write, etc.
WMI.cpp WMI handling
..\guid.h GUID definitions
HiImWdm.rc Version resource
HiImWdm.mof WMI class definition
Build and installation files
HiImWdmfree.inf Free build INF
HiImWdmchecked.inf Checked build INF
SOURCES List of files to build
..\BuildDrvr.bat Build batch file
makefile.inc mof compile and
Post build steps
Our device driver is called HiImWdm and the download zip hiimwdm.zip contains the source and built driver. Unzip the file in a directory called C:\EXEWdm.

The HiImWdm driver will run in Windows 98 and Windows 2000 without any new hardware. To test its operation, it implements a 4 byte shared memory buffer that can be read and written from a Win32 program. The "checked" build version also makes the buffer bytes available for inspection through WMI in W2000.

The sys directory contains the HiImWdm driver code. Table 1 lists the source code files and the files needed to build and install the driver. The "free" release version of the driver ends up in OBJ\i386\free\HiImWdm.sys with the "checked" debug version in OBJ\i386\checked\HiImWdm.sys

The exe directory contains a small test console application. Note that the VC++ 5 Setup API header and library is seriously out of date and will cause a compile to fail. The project is set up to use the C:\98DDK versions of these files. The W2000 DDK and Platform SDK versions are even newer.

The W98 DDK does not currently include the WMI headers and libraries. So I have made the free build WMI-free. You can compile the checked build with WMI under W2000. The W98 DDK does not have the mofcomp tool so I have removed this compile step.

I would none-the-less have expected the checked WMI build to run under W98, as it is supposed to support WMI. (To install WBEM WMI, in Add/Remove Programs, Windows Setup tab, Internet Tools, check the Web-based Enterprise Mgmt box.) The Microsoft WBEM implementation seems to have been updated to include a WMI namespace that was not there before. Perhaps this update only runs properly in Windows 2000.



Install and Test

To install the driver, go to the Control Panel and select "Add New Hardware" or "Hardware wizard". Opt to select the hardware from a list. Select "Other devices" and "Have Disk". Browse to c:\EXEWdm\sys and install the "HiImWdm Example, free build, without WMI" driver. The driver is now copied to the Windows system32\drivers directory and the installation INF file is copied to one of the Windows INF directories.

The HiImWdm device should now appear in the Device Manager "Other devices" category. If you rummage around the registry you will find references to the driver, the device and the device interface that have been installed. The Windows Unknown GUID {4D36E97E...} appears in the first two cases, while the device interface uses the HiImWdm GUID {87472BA0...}. Be warned that W98 and W2000 use slightly different registry structures.

To test the driver, run TestWDM.exe in the C:\EXEWdm\exe\Release directory. TestWDM opens a handle to the first HiImWdm device and reads the buffer. The first time you run it, the buffer will probably have a value of zero. However, the next time it should be storing 0xABCDEF01, left over from the last write.

TestWdm goes on to write 0xABCDEF01 to the buffer and checks that this value can be read. It then checks that an incorrect write fails and finally closes the device handle.



Initialisation

Table 2
HiImWdm call-backs
call-back description
AddDevice PnP Add new device
Unload Driver unload
IRP_MJ_CREATE Win32 CreateFile
IRP_MJ_CLOSE Win32 close file
IRP_MJ_POWER Power management
IRP_MJ_PNP Plug and Play
IRP_MJ_READ Win32 reads
IRP_MJ_WRITE Win32 writes
IRP_MJ_DEVICE_CONTROL Win32 IOCTLs
IRP_MJ_SYSTEM_CONTROL WMI
The driver's main entry point is the DriverEntry routine in init.cpp. The main job here is to set up the series of call-back routine pointers shown in Table 2 so that the driver can be called again when appropriate. Some calls originate in the kernel. A Win32 program accesses the device as if it were a file, and these requests end up as driver calls as well.

A driver works primarily by processing I/O Request Packets (IRPs), so call-backs are defined for each of the IRP major functions that HiImWdm supports. Some of the IRPs come in different forms, eg the Plug and Play IRP_MJ_PNP has a IRP_MN_START_DEVICE minor code which indicates that this IRP requests you to start the device.

This example does not cope with IRP cancelling.



Plug and Play

Plug and Play (PnP) makes it easier for users to insert new devices into a computer as there should be no complicated hardware addresses to set up. Instead a Plug and Play device should be configurable in software. For a low level bus driver, its PnP resources are the different sets of IRQs, I/O space registers and memory-mapped addresses that it supports. Most bus types define their own scheme for making devices configurable. Some ISA devices are PnP configurable as well.

Device Stack

PnP builds a device stack from the bottom up. I shall now describe how the right drivers are found for USB devices. Details of the USB terminology are given later.

PnP uses enumerator drivers and arbiters to find devices and allocate resources to them. The root enumerator might find a PCI bus. The PCI bus enumerator might find a USB host controller. The USB host controller class/miniclass drivers are loaded, and, above them, the USB Hub driver for the embedded USB root hub. The USB hub then enumerates the USB bus.

When a USB device is first plugged in, it appears at the USB default address of zero. The act of inserting the device notifies the USB hub of a new device. It retrieves the USB device's descriptors and in due course allocates it a proper USB address.

Each device has a Hardware Id which is used to identify the appropriate driver. In the USB context, the Hardware Id is formed using the vendor's id and the product id. A fallback Compatible Id is also made available in case an appropriate driver is not found. For USB, this is the class and sub-class of the device. For example, Windows recognises the "HID keyboard USB device" class so that it can be used straight away without a vendor-supplied driver.

A driver's INF installation file lists a driver for each Hardware Id and Compatible Id supported. The INF file lists the files that need to be copied and the registry entries that should be made. W2000 specific sections are used to set up the driver service registry entry and any error logging features.

When a device is added, it is put at the top of a stack of devices, for example, above the HID class/minidriver driver pair, the USB class driver and the PCI bus driver. Any I/O requests are sent to the top of the device stack. Each layer of the stack can do some processing on the request, or pass it down the stack. A driver can attach a completion routine to an IRP so that it can handle the IRP after it has been processed by the rest of the stack below it.

Note that a driver can have an "upper edge" interface which is totally different to the facilities it uses in the next lower driver. A device might have a streaming upper edge, but make calls to the USB system to do its job.

Device Objects

Figure 1
Stack of device objects
Device stack objects
A WDM driver has to cope with three different types of device object as shown in figure 1. The bus driver object at the bottom of the device stack is called the Physical Device Object (PDO). Each WDM driver must make its own Functional Device Object (FDO) for each device. Finally, you must remember the next device down the stack. This is so you know where to pass a request when you need to send it for processing down the stack of drivers.

Listing 1 shows how HiImWdm's FDO is made in the AddDevice routine using the IoCreateDevice call. AddDevice then attaches to the top of the stack above the PDO using IoAttachDeviceToDeviceStack. The returned FDO device object has a pointer to some memory for us to use, the device extension. The HiImWdm device extension structure is defined in HiImWdm.h. Our AddDevice stores the PDO, FDO and the NextDevice in the device extension.

When our device is removed, our FDO is detached from the stack and deleted.

Listing 1
AddDevice routine Device object handling
// Create our Functional Device Object in fdo
status = IoCreateDevice(DriverObject,
	sizeof(WDM_DEVICE_EXTENSION),
	NULL,	// No Name
	FILE_DEVICE_UNKNOWN,
	0,
	FALSE,	// Not exclusive
	&fdo);

// Remember pdo and fdo in our device extension
PWDM_DEVICE_EXTENSION dx = (PWDM_DEVICE_EXTENSION)fdo->DeviceExtension;
dx->pdo = pdo;
dx->fdo = fdo;

// Attach to the driver stack below us
dx->NextDevice = IoAttachDeviceToDeviceStack(fdo,pdo);

// Set fdo flags appropriately
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
fdo->Flags |= DO_BUFFERED_IO;


Plug and Play States

A PnP driver goes through several different states as devices are added, removed or stopped to allow for resource reallocation.

When a new device is loaded, the PnP Manager eventually finds the relevant driver for a device. The driver is loaded and its DriverEntry routine called.

Figure 2 shows the different states that a PnP device can then go through. First a driver's AddDevice routine is called so that the driver can create its FDO and attach it to the stack. However, do not talk to your device yet!

Figure 2
Plug and Play States
Plug and Play States

When your driver receives a IRP_MJ_PNP request with the IRP_MN_START_DEVICE minor function code, all the relevant hardware resources have been assigned. Normally you must pass this request down the driver stack first so that the bus driver can process it, ie make the device available to you. Your IRP completion routine can talk to your device as the IRP travels back up the driver stack. Indeed, you may well want to send more requests down the stack to configure your device, before finally completing the original start device IRP.

Similar considerations apply to stopping or removing devices, ie do any of your work to stop the device before you send the IRP down the stack.

The other PnP states are used to cope with device removal requests and stop requests. Both these have query IRPs to let you reject the request for the moment. You might say "no" if you have a read or write request in progress. You should store any further ordinary I/O requests in a queue while a device is stopped, and reject them if a device is removed. If a user brutally unplugs a device then you must cope with a IRP_MN_REMOVE_DEVICE in Windows 98 or a IRP_MN_SURPRISE_REMOVAL in Windows 2000.

Plug and Play Flags

Proper PnP handling requires the use of various flags, etc., in the FDO device extension. You will usually need a flag to indicate that the device has been started. Check this flag before performing any I/O requests.

I/O operations can be in progress when a device is removed or stopped. You must ensure that the remove or stop request waits until the current I/O is cancelled or finished. Drivers usually do this by having a usage count and StoppingEvent event in each device extension. The usage count is incremented atomically when an I/O operation is started and decremented when it completes. A remove request checks that the usage count is zero. Otherwise it waits for the StoppingEvent to be signalled when an I/O operation completes.

While your device is stopped for resource reallocation, you should hold any incoming I/O requests in a queue for processing when the device is restarted.

HiImWdm currently only provides minimal PnP support. It responds to AddDevice and REMOVE_DEVICE calls, ie to make and delete its FDO. It does not use any PnP flags to queue IRPs during stops nor check remove requests.

Device Interfaces

A device must be accessible to the kernel or Win32 code for it to be of any use. The old NT device driver model uses explicit symbolic links to provide a name which a Win32 application could open.

While this technique is still available, WDM lets you use device interfaces instead. A device interface uses a GUID to identify the interface that a driver implements. In HiImWdm, we just use the WDM_GUID GUID to identify the fact that we are a HiImWdm device. In other cases, a driver might use a standard GUID to indicate that it implements a particular COM interface.

Our AddDevice routine calls IoRegisterDeviceInterface to register the link between WDM_GUID and our FDO. It then has to enable it using IoSetDeviceInterfaceState. When our device is removed, the WdmPnp routine disables the device interface.

IoRegisterDeviceInterface actually makes a symbolic link to our device. The actual link name is a long string which includes our GUID. Win32 programs like our TestWdm use various SetupDi... functions (such as SetupDiGetClassDevs) to find all devices which support a particular GUID. Eventually it can get the symbolic link name which it can pass to CreateFile to open a handle to our HiImWdm device.


Power Management

Device drivers play an important part of Power Management. The idea is to have shorter start-up and shutdown times by not turning off the computer completely. Power Management policies can also help to conserve battery life and might result in quieter running of the computer.

There are six system power states defined, S0 to S5, with S0 being fully on and S5 shutdown. Each device can be in one of four power states called D0 to D3, with D0 fully on.

A device might decide to reduce its own power level, eg if a disk has not been accessed for 5 minutes. Alternatively, the Power Manager can request that the whole system power down (it assumes that the system can always power up). A device that is sleeping (S1-S3) or hibernating (S4) can wake the computer up, eg if a modem receives an incoming call.

Power Management is done with the IRP_MJ_POWER IRP. The Power Manager uses the IRP_MN_QUERY_POWER minor code to see if a driver can go into a specified state, and then IRP_MN_SET_POWER to actually request that state. If you are going to fail a power request then complete the IRP immediately. Otherwise it is up to the bus driver to complete the power IRP.

Drivers like HiImWdm that do not implement a power policy should just pass the power IRPs down the stack. This means calling PoStartNextPowerIrp, IoSkipCurrentIrpStackLocation and then PoCallDriver. Note the call to PoCallDriver rather than IoCallDriver to call the next driver.

If a driver does handle power IRPs then proceed as follows. The Power Manager initiates a IRP_MN_QUERY_POWER IRP to set a new system power state. If your device needs to change to a different device power state that is appropriate for the given system power state, then you need to send yourself a power IRP to do this. Yes, that's right, you call PoRequestPowerIrp to tell yourself to change power state. Once this IRP has completed its rites of passage, you can pass on (or complete) the original system power IRP.

If you get an I/O request while powered down, you must send a set power IRP and wait for its completion, before handling the request.


WMI

Windows Management Instrumentation (WMI) lets administrators tend your device. So your driver must provide information and events to user-mode applications. Methods in the driver can be invoked.

WMI is a part of the Web-Based Enterprise Management (WBEM) initiative. The Win32 implementation of WBEM has various providers of information, giving access to the registry, the NT event log, Win32 information and WMI.

For WMI, you can define your own custom data and event blocks in a C++ class-like file, compiled into a resource using the mofcomp tool. Each WMI block is identified by a GUID. A WBEM Object Browser tool lets you view all the WMI blocks on a computer, or across the network. Alternatively, custom user mode programs can be written to access WMI, either using Java APIs or COM ActiveX interfaces.

Windows 2000 drivers can and should still send NT events to the system log. However WMI is supposed to be available in Windows 98, giving WMI broader appeal. As stated above, at the moment I have only got the WMI to run in Windows 2000.

The HiImWdm code in wmi.cpp shows how to provide a custom WMI data event called HiImWdmInformation. This contains the first ULONG from the read/write common buffer and the symbolic name for the device interface.

HiImWdm calls IoWMIRegistrationControl when a device is added or removed. It sets up a WMILIB_CONTEXT structure in its device extension with various call-backs. When the IRP_MJ_SYSTEM_CONTROL IRP is received, HiImWdm calls WmiSystemControl to do preliminary processing. The QueryWmiRegInfo call-back is called to register our WMI data block. QueryWmiDataBlock is called to return the actual WMI data.


USB

Figure 3
USB Logical Structure
USB Logical Structure
Finally, let's have a brief overview of USB and HID device drivers.

USB is for lowish speed devices. USB is a half-duplex 12 Mbps serial bus, with 5V lines to provide a small amount of power to basic devices. The USB data bits are grouped into 1ms frames, which are the basis of bandwidth allocation. Hub devices allow further function devices to be plugged in, even when switched on. A new PC usually has one root hub with 2 USB downstream ports.

Figure 3 shows the logical structure of a USB device, with endpoints grouped into interfaces and configurations. Almost all USB devices have just one configuration and most have just one interface. Windows needs a separate USB client driver for each interface.

Each endpoint can transfer one of four types of data: control, interrupt, bulk and isochronous. A connection to an endpoint is called a pipe. Transfers on endpoint 0 (the Default Pipe) have a standard format.

Each USB device has a series of descriptors which describe its logical structure. The presence of additional class descriptors indicates that the device is of a certain standard type, eg printer, HID, hub, display etc. If Windows detects a USB device with a HID descriptor, then it automatically fires up the HID system drivers to interrogate the device. I guess that Windows will support other device classes in due course.

In the mean time, to control your USB device, you will need to write a WDM USB client kernel-mode driver. The Windows USB class driver is controlled using a series of internal IOCTLs. The most useful of these allows you to send off USB Request Blocks (URBs) for processing. There are currently 39 different URB operations that you can request, allowing you to get descriptors, perform all the different transfer types, etc.



HID

Figure 4
Possible HID driver stack
Possible HID driver stack
A Human Input Device (HID) is not some gruesome force-feeding contraption. Instead it is an abstract model for most types of input device that people could use to control their computers. HID lets you output information as well, eg to set the LEDs on a keyboard. Apparently there is a standard HID set of controls for a magic carpet!

Windows has built in support for various HID devices, eg keyboards and mice. Microsoft seems to be using HID more and more to get user input, as it provides an abstraction layer above the actual hardware interface. If both a HID keyboard and an old PC keyboard are attached then you can use them both.

Although HID was born as a USB extension class, it can now stand alone as long as the right HID descriptors are presented to the HID class driver. A USB minidriver is supplied as standard, but you can write other minidrivers if you wish.

Figure 4 shows one possible HID driver stack configuration. A USB HID device is plugged in. The USB HID minidriver provides the interface between the USB and HID class drivers. HID clients can either run in the kernel (like the Windows keyboard drivers, Vkd.vxd/kbdhid.vxd or Kbdclass.sys/Kbdhid.sys) or in Win32 user mode.

A HID device describes its capabilities primarily in a Report Descriptor. Input, Output and Feature reports are described. Each report consists of a series of bit or data controls, possibly grouped into collections. Each control or collection has a "usage", a standard definition of what it does. A keyboard must produce exactly the right reports for it to be recognised by Windows and your BIOS.

In the general case, a HID client uses the Windows HID parsing routines to determine what usages a device is capable of producing. When it received an actual HID report it uses more Windows routines to determine what values were returned.

Table 3
HID keyboard report definition
Keyboard Collection
	Input: 8 single-bit modifier keys
	Output: 5 single-bit LEDs
	Output: 3 single bits for padding
	Input: 6 data bytes for scan codes
Table 3 shows a brief summary of the standard HID keyboard reports. The modifier keys are used for left Ctrl, left Alt, etc. The scan codes represent the keys that are pressed simultaneously. A keyboard report is generated whenever a key is pressed or released.



Conclusion

You must write a WDM driver if you want to support some of the latest technologies like USB, HID and IEEE 1394. However you still need to write separate video drivers for W98 and W2000.

Supporting Plug and Play and Power management does make a driver more complicated. However this extra work is usually more than offset by having standard class drivers to do most of the detailed hardware interactions for you. And it is certainly nice to be able to write one driver that works in Windows 98 and Windows 2000.


Author

Chris Cant
Director, PHD Computer Consultants Ltd
Email: [email protected]
Web: www.phdcc.com