Special Offers

Programming the Microsoft® Windows® Driver Model, Second Edition
Author Walter Oney
Pages 880
Disk 1 Companion CD(s)
Level All Levels
Published 12/16/2002
ISBN 9780735618039
Price $59.99
To see this book's discounted price, select a reseller below.

More Information

About the Book
Table of Contents
Sample Chapter
Related Series
Related Books
About the Author

Support: Book & CD

Rate this book
Barnes Noble Amazon Quantum Books


Chapter 13: Human Interface Devices

13   Human Interface Devices

A Human Interface Device (HID) is one that communicates with the host computer using structured reports. The HID class of devices consists primarily of devices that end users use to interact with computers. The class includes keyboards, mice, and game controllers of all kinds, but it can include any imaginable knob, switch, button, slider, exoskeletal device, or other type of control that a user might use to control a computer. Noninteractive devices such as bar code readers and measuring instruments might also be designed to fit into the HID class. In addition, HID devices can incorporate user-information components such as lights, displays, force feedback, and other indicators.

HID devices built for the Universal Serial Bus conform to the Device Class Definition for Human Input Devices. Additional specifications relevant to force feedback are in the USB Device Class Definition for Physical Interface Devices. HID relies on extensive sets of numeric constants, whose definition can be found in the HID Usage Tables specification. All of these specifications are available for free download from

Although the HID specifications are oriented toward USB implementations, any sort of device can function, in whole or in part, as a HID device. The important characteristic of a HID device, once again, is that the host is able to perform input and output operations using report packets that conform to an extremely flexible structure definition, called the report descriptor.

Applications access HID-compliant keyboards and mice only indirectly, by handling window messages whose character and content haven't fundamentally changed in over twenty years. Applications access other sorts of HID device through COM interfaces that are part of the DirectX component of Windows and through Win32 API calls.

Drivers for HID Devices

The Microsoft class driver for HID devices, HIDCLASS.SYS, provides the overall framework for WDM drivers that manage HID devices on all the Windows platforms. Microsoft also supplies a HIDCLASS minidriver named HIDUSB.SYS to handle USB devices whose device or interface descriptor indicates that they belong to the HID class. Consequently, if your USB device belongs to the HID class, you may not have to write a special-purpose driver at all because the Microsoft class driver and minidriver fully support the USB specifications.

If you're designing a USB device that includes some HID-like functionality, don't forget that you can make it a composite device by defining several interfaces. The generic parent driver will separate the functions of your device so that the system will load the standard Microsoft drivers for the HID function.

Microsoft also provides drivers for standard PS2 keyboards and mice, and for serial-port mice. These drivers, along with HIDCLASS, sit below class filter drivers named KBDCLASS and MOUCLASS, which present a consistent interface to higher-level components.

You might need to write a custom minidriver to replace HIDUSB.SYS if your USB device or interface provides or consumes structured reports but doesn't belong to the HID class. In such a case, your minidriver will furnish a faux HID descriptor to HIDCLASS, and it will also create structured reports matching that descriptor in response to input events.

Even with a true HID-class USB device, you might have to write your own minidriver to support custom functionality. I've used this approach to build drivers for several specialized devices, including a gaming mouse with lots of buttons and lights and a head-tracking device that delivers sensor values that must be transformed into position reports. In these cases, the devices are nominally HID-class USB devices, but my clients want the devices to deliver different reports from the ones generated by the firmware. It was not practical in these cases to put the custom functionality into firmware.

Finally, if you have a non-USB device (other than a standard keyboard or mouse) that includes HID-like functionality, a custom HIDCLASS minidriver is the only practical way to make that device accessible to DirectX and thence to existing applications.

Reports and Report Descriptors

A HID device transfers information in a block known as a report. The report contains bit and integer fields formatted according to a report descriptor. Much of the HID specification and related documents describe the contents of reports and report descriptors in great detail. I'll analyze two sample report descriptors here to help you understand the specifications.

Sample Keyboard Descriptor

To start with, I suggest that you download the so-called HID Descriptor Tool (DT.EXE) from The tool allows you to create and edit report descriptors using symbolic names. Figure 13-1 illustrates the user interface and one of the example descriptors that come with the tool.

Click to view graphic
Click to view graphic

Figure 13-1  Using the HID Tool to define a keyboard report descriptor.

The first item in the sample report descriptor establishes the usage page, which essentially specifies a namespace for interpreting certain numeric constants in subsequent elements of the descriptor. You need the HID Usage Tables document to interpret the numbers. For example, the usage code 6 means keyboard in the generic desktop page but sailing simulation device in the simulation controls page.

The second item specifies the usage for the next top-level collection in the descriptor. In the HID specification, a collection serves to group related data items. For example, a physical collection groups items collected at one geometric point, whereas an application collection groups items that might be familiar to applications. A further concept, the logical collection, allows related items to be grouped into a composite data structure, such as a byte count followed by data. These concepts are so abstract as to be nearly meaningless, and Microsoft assigns additional meaning, as follows:

  • A top-level collection, such as the one beginning with the third item in the keyboard sample, corresponds to an individually addressable entity. Acting as a bus driver, HIDCLASS creates a physical device object (PDO) for each top-level collection. The device identifier for the collection includes a generic compatible ID, based on the usage code. See Table 13-1. If the collection has any other usage, HIDCLASS won't create a compatible ID. Refer to Chapter 15 for more information about the importance of a compatible ID in locating a driver. The PDO then becomes the base of a PnP device stack for some type of device. Note that multiple top-level collections give rise to multiple device stacks. For this to work in practice, the device must use report identifiers to distinguish between the different collections.
  • A link collection is one nested within a top-level collection. Link collections provide an organizational hierarchy that applications can use to group related controls in a complex device. On a game pad, for example, one can use link collections to distinguish between buttons actuated by the left and right hands. There seems little point to this generality, however, when applications typically require end users to assign meanings to controls based on numbers rather than position in a hierarchy. But perhaps I just haven't seen enough applications and HID devices to make a comprehensive judgment.

Table 13-1   HIDCLASS-Compatible ID for Each Supported Usage

Usage PageUsageCompatible ID
Generic desktopPointer or mouseHID_DEVICE_SYSTEM_MOUSE
Joystick or game padHID_DEVICE_SYSTEM_GAME

Within the single top-level collection for the sample keyboard, the most important items are the main items named INPUT and OUTPUT. An INPUT item corresponds to a field in an input report, whereas an OUTPUT item corresponds to a field in an output report. There can also be FEATURE items that define fields in a feature report, but the keyboard sample doesn't include any of them. A number of global items precede the main items in order to describe the presentation and meaning of the data itself.

It's important to realize that INPUT, OUTPUT, and FEATURE report items can be interleaved in the report descriptor. The logical collection structure within a top-level collection isn't important in determining which data items appear together in a given report. Rather, the type of the items governs. Thus, the example keyboard descriptor mixes INPUT and OUTPUT items in a way that might suggest that there are five reports, or else a single bidirectional report. In reality, there is a single input report defined by the INPUT items and a single output report defined by the OUTPUT items.

The main items, along with all the qualifying global items, define the bit layout of a structured report. To visualize the report, assign bits from right to left and don't leave any unused bits for alignment purposes. Treat multibit values, including those that span byte boundaries, as little-endian (least significant bit on the right of the resulting picture). Subdivide the result into bytes, which the device transmits from right to left.

In the keyboard report, we have five data items in the collection, and they define an input report and an output report (see Figure 13-2):

  • An input item consisting of eight (REPORT_COUNT) single-bit values (REPORT_SIZE 1), each of which can vary from 0 (LOGICAL_MINIMUM) to 1 (LOGICAL_MAXIMUM). The meaning of the bits corresponds to keyboard usages (USAGE_PAGE) E0 through E7 (USAGE_MINIMUM and USAGE_MAXIMUM). In other words, byte 0 of the input report contains flag bits to indicate which of the shift-type keys on the keyboard are currently depressed.
  • A constant input item consisting of one (REPORT_COUNT) 8-bit value (REPORT_SIZE). This is byte 1 of the input report, and it's simply a placeholder that contains no valid data.
  • An output item consisting of five (REPORT_COUNT) single-bit (REPORT_SIZE) values. The LOGICAL_MINIMUM and LOGICAL_MAXIMUM values previously specified apply to these values because they haven't been overridden. The meaning of the bits is different, however: they correspond to LEDs (USAGE_PAGE) with labels such as Num Lock (USAGE_MINIMUM and LOGICAL_MAXIMUM). In other words, the low-order 5 bits of byte 0 of the output report contain flags to control LEDs for the toggling keys.
  • A constant output item consisting of one (REPORT_COUNT) 3-bit (REPORT_SIZE) value. These 3 bits pad out the output report to a full byte.
  • An input item consisting of six (REPORT_COUNT) 8-bit values (REPORT_SIZE), ranging from 0 through 101 (LOGICAL_MINIMUM and LOGICAL_MAXIMUM) and corresponding to keys on a standard 101-key keyboard (USAGE_PAGE, USAGE_MINIMUM, and USAGE_MAXIMUM). In other words, bytes 2 through 7 of the input report contain the codes for up to six keys that are being simultaneously held down.

Click to view graphic
Click to view graphic

Figure 13-2  Layout of keyboard input and output reports.

HIDFAKE Descriptor

Figure 13-3 illustrates the report descriptor used in the HIDFAKE sample driver in the companion content. This report descriptor has a few features that are different from the keyboard sample:

  • The top-level application's usage is "Gun Device" from the Gaming Controls page. This was an artificial choice that I made to avoid difficulty installing the sample driver. For any usage listed in Table 13-1, HIDCLASS will supply a compatible device identifier along with the device's specific ID. Windows XP will then prefer a signed driver matching the compatible ID to an unsigned driver (such as HIDFAKE.SYS) matching the device's specific ID. (See Chapter 15 for more information about how Windows XP chooses drivers.) It's nearly impossible to switch to the specific driver.
  • I used three logical collections within the main collection. The logical collections merely serve to highlight the three-report structure of the descriptor. The sample would work perfectly well without them.
  • The descriptor includes an input report and two feature reports. The input report (1) contains a single button usage. The first feature report (2) is for returning a driver version number, and the second feature report (3) is to allow the test applet to control the state of the fake button.

Click to view graphic
Click to view graphic

Figure 13-3  Using the HID Tool to define the HIDFAKE report descriptor.

HIDFAKE illustrates one fine point about report descriptors. Feature reports pretty much need to have identifying numbers because the HID specification calls for them in the Get_Report_Request and Set_Report_Request control pipe commands. If any report in a top-level collection has an identifier, all reports in that collection must. In reality, though, HIDFAKE models a notional device that has a real button report and no feature reports. I defined the feature reports as a way for the test applet to communicate "out of band" with the driver. If we were dealing with a real device, therefore, the driver would have to insert a report identifier in each input report that it read from the device.

HIDCLASS Minidrivers

As previously discussed, Microsoft supplies a driver (HIDUSB.SYS) for any USB device built according to the HID specification. This section describes how you can build a HIDCLASS minidriver for some other type of device that you want to have masquerade as HID.


The DriverEntry function for a HIDCLASS minidriver is similar to that in a regular WDM driver, but only up to a point. In this routine, you initialize the DRIVER_OBJECT data structure with pointers to AddDevice and DriverUnload routines as well as to dispatch routines for just three types of I/O request packet (IRP): IRP_MJ_PNP, IRP_MJ_POWER, and IRP_MJ_INTERNAL_DEVICE_CONTROL. Then you build a HID_MINIDRIVER_REGISTRATION structure and call HidRegisterMinidriver, which is one of the functions exported by HIDCLASS.SYS. Table 13-2 describes the fields in the HID_MINIDRIVER_REGISTRATION structure.

Table 13-2   Fields in the HID_MINIDRIVER_REGISTRATION Structure

Field NameDescription
Revision(ULONG) Minidriver sets this field to HID_REVISION, which currently equals 1.
DriverObject(PDRIVER_OBJECT) Minidriver sets this field to the same value passed as the DriverObject argument to DriverEntry.
RegistryPath(PUNICODE_STRING) Minidriver sets this field to the same value passed as the RegistryPath argument to DriverEntry.
DeviceExtensionSize(ULONG) Size in bytes of the device extension structure used by the minidriver.
DevicesArePolled(BOOLEAN) TRUE if the minidriver's devices need to be polled for reports. FALSE if the devices spontaneously send reports when data becomes available.

The only field whose meaning isn't completely straightforward is the DevicesArePolled flag. Most devices will spontaneously generate a report whenever the end user does something, and they'll notify the host via some sort of interrupt. For this kind of device, you set the DevicesArePolled flag to FALSE. HIDCLASS will then attempt to keep two IRPs (called ping-pong IRPs) active to read reports. Your minidriver is expected to queue the IRPs and complete them in order when the device interrupts.

Some devices don't spontaneously generate reports. For that kind of device, set the DevicesArePolled flag to TRUE. HIDCLASS will then issue IRPs to read reports in a timing loop. Your minidriver reads report data from the device only in response to each IRP. Higher-level components, such as an application using DirectX interfaces, can specify the polling interval. Think twice before setting DevicesArePolled to TRUE: most devices require it to be FALSE.

Here's a nearly complete example of the DriverEntry function in a HIDCLASS minidriver:

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,
  DriverObject->DriverExtension->AddDevice = AddDevice;
  DriverObject->DriverUnload = DriverUnload;
  DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] =
  DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
  DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
  RtlZeroMemory(&reg, sizeof(reg));
  reg.Revision = HID_REVISION;
  reg.DriverObject = DriverObject;
  reg.RegistryPath = RegistryPath;
  reg.DeviceExtensionSize = sizeof(DEVICE_EXTENSION);
  reg.DevicesArePolled = FALSE;  // <== depends on your hardware
  return HidRegisterMinidriver(&reg);

Driver Callback Routines

Most of the class/minidriver interfaces in Windows XP involve a set of callback routines that the minidriver specifies in a registration call made from DriverEntry. Most class drivers completely take over the management of the DRIVER_OBJECT while handling the registration call. This means that the class drivers each install their own AddDevice and DriverUnload functions and their own IRP dispatch routines. The class drivers then make calls to the minidriver callback routines to carry out vendor-specific actions.

HIDCLASS operates this way as well, but with a twist. When you call HidRegisterMinidriver, HIDCLASS installs its own function pointers in your DRIVER_OBJECT, just as most class drivers would. Instead of using a set of callback routines whose addresses your minidriver provides in the HID_MINIDRIVER_REGISTRATION structure (there are none), it remembers the AddDevice and DriverUnload pointers and the addresses of your dispatch routines for PNP, POWER, and INTERNAL_DEVICE_CONTROL requests. These minidriver routines do not have exactly the same functions as like-named routines in regular WDM drivers, though. I'll explain in this section how to write these callback routines.

AddDevice Callback

The AddDevice callback in a HIDCLASS minidriver has a prototype similar to that of a regular AddDevice function:


There are two important differences between the minidriver callback and the regular function:

  • The device object argument refers to a function device object (FDO) that HIDCLASS has already created.
  • Prior to Windows XP, HIDCLASS ignored the return code from the minidriver callback.

Since HIDCLASS creates the FDO before calling your minidriver AddDevice callback, you don't need to call IoCreateDevice or, indeed, to do practically any of the things that you normally do in a WDM AddDevice function. Rather, you just need to initialize your device extension structure and return. Versions of Windows prior to Windows XP will ignore the return code from this callback. Consequently, if an error arises in your AddDevice callback, you need to set a flag in your device extension that you can inspect at StartDevice time:

typedef struct _DEVICE_EXTENSION {
  NTSTATUS AddDeviceStatus;

Another issue to be aware of is that the FDO's DeviceExtension pointer is the address of an extension structure that's private to HIDCLASS. The first few members of that private structure are mapped by the HID_DEVICE_EXTENSION structure in the DDK:

typedef struct _HID_DEVICE_EXTENSION {
  PDEVICE_OBJECT PhysicalDeviceObject;
  PDEVICE_OBJECT NextDeviceObject;
  PVOID MiniDeviceExtension;

To find your device extension, you must follow this pointer chain:


You use similar constructions to get the PDO address and to get what I call the LowerDeviceObject in this book. (HIDCLASS calls it the NextDeviceObject.) Being such a lazy typist, I usually define macros to make my life easier while I'm writing the minidriver:

#define PDO(fdo) (((PHID_DEVICE_EXTENSION) ((fdo)->DeviceExtension)) \
#define LDO(fdo) (((PHID_DEVICE_EXTENSION) ((fdo)->DeviceExtension)) \

Using these macros and the preceding fragment of a DEVICE_EXTENSION structure, your minidriver's AddDevice callback might look like this:

  <initialization code for DEVICE_EXTENSION members>
pdx->AddDeviceStatus = status;  // <== whatever is left over
  return status; // in case you're running in >= XP

The point of returning a real status code from AddDevice is that in Windows XP and later systems, HIDCLASS will fail its own AddDevice call if you do, and that will short-circuit the initialization of your device. But since HIDCLASS ignores the code in earlier versions of the operating system, you need to provide a way for your StartDevice function to return an error code.

DriverUnload Callback

HIDCLASS calls your minidriver DriverUnload callback as a subroutine from its own DriverUnload routine. If you created any global objects in your DriverEntry routine, you'll clean those up in the DriverUnload callback.

DispatchPnp Callback

You specify the DispatchPnp callback as if it were the dispatch function for IRP_MJ_PNP, by setting an array element in the driver object's MajorFunction table. HIDCLASS calls your callback as a subroutine while handling Plug and Play IRPs of various types. Your callback routine can perform most of the same operations that a function driver would perform for this type of IRP. See Chapter 6 for full details. There are two exceptions:

  • Your IRP_MN_START_DEVICE handler needs to test the error flag set by your AddDevice callback (I called it AddDeviceStatus in the earlier fragment) and to fail the IRP if the flag indicates an error. This is how you cope with the fact that HIDCLASS ignores the return code from AddDevice in versions of Windows prior to Windows XP.
  • Your IRP_MN_REMOVE_DEVICE handler does not call IoDetachDevice or IoDeleteDevice. Instead, it should simply release any resources that were allocated by the AddDevice callback. HIDCLASS itself will take care of detaching and deleting the FDO.

The HIDFAKE sample driver uses GENERIC.SYS. Its DispatchPnp routine therefore looks like this:

  return GenericDispatchPnp(PDX(fdo)->pgx, Irp);

Apart from using the PDX macro to locate the device extension structure, this code is the same as would appear in a regular function driver that uses GENERIC.SYS. The RemoveDevice, StartDevice, and StopDevice functions are different from regular ones, though:

HIDFAKE itself has no code at the points labeled A, B, and C. If you use this sample as a template for your own minidriver, you'll write code to do the following:

  1. Clean up any resources (such as memory, lookaside lists, and so on) allocated in AddDevice. HIDFAKE has no such resources.
  2. Configure the device as discussed in previous chapters. HIDFAKE has no hardware and therefore has nothing to do in this step.
  3. Deconfigure the device by reversing the steps done in StartDevice. Since HIDFAKE does nothing in StartDevice, it doesn't need to do anything here either.
DispatchPower Callback

You specify the DispatchPower callback as if it were the dispatch routine for IRP_MJ_POWER, by setting an array element in the driver object's MajorFunction table. HIDCLASS calls your callback as a subroutine while handling power IRPs of various types. In most cases, your callback should simply pass the IRP down to the next driver without performing any other actions because HIDCLASS contains all the power-management support needed by typical devices (including WAIT_WAKE support).

If you set the DevicesArePolled flag to FALSE in your call to HidRegisterMinidriver, HIDCLASS will cancel its ping-pong IRPs before forwarding a power request that reduces power. If you have simply piggybacked on these IRPs to send requests further down the PnP stack, you therefore won't need to worry about cancelling them. If you have cached pointers to these IRPs somewhere, you should provide a cancel routine.

Here's an example of the DispatchPower callback in a HIDCLASS minidriver:

  1. You needn't do anything special with any power IRP except a SET_POWER for a device power state.
  2. When restoring power, you install a completion routine before forwarding the IRP down the stack.
  3. When removing power, you save any context information before forwarding the IRP. To deal with the possibility that HIDCLASS might lower power in steps (for example, first D2 and later D3), you also need to keep track of the current device power state. Whether or not your device has context information to save, this is also the time to cancel any subsidiary IRPs that your driver has issued, terminate polling threads, and so on. HIDCLASS will be calling you at PASSIVE_LEVEL in a system thread that you're allowed to block if necessary while performing these tasks.
  4. As usual, you call PoCallDriver to forward the IRP. You need not call PoStartNextPowerIrp because HIDCLASS has already done so.
  5. The completion routine is called only after the bus driver completes a Set-D0 operation. Your device has now been repowered, and you can reverse the steps you performed when you removed power. Since you're potentially running at DISPATCH_LEVEL and in an arbitrary thread, however, you must perform these steps without blocking the current thread.

DispatchInternalControl Callback

You specify the DispatchInternalControl callback as if it were the dispatch routine for IRP_MJ_INTERNAL_DEVICE_CONTROL, by setting an array element in the driver object's MajorFunction table. HIDCLASS calls your callback routine as a subroutine in order to obtain reports and other information or to provide instructions to your minidriver. You can program this callback as if it were an ordinary IRP dispatch routine handling the control codes listed in Table 13-3.

Table 13-3   HIDCLASS Minidriver Control Operations

Internal Control CodeDescription
IOCTL_GET_PHYSICAL_DESCRIPTORGets USB-standard physical descriptor
IOCTL_HID_GET_DEVICE_ATTRIBUTESReturns information about device as if it were USB
IOCTL_HID_GET_DEVICE_DESCRIPTORReturns a USB-standard HID descriptor
IOCTL_HID_GET_FEATUREReads a feature report
IOCTL_HID_GET_INDEXED_STRINGReturns a USB-standard string descriptor
IOCTL_HID_GET_STRINGReturns a USB-standard string descriptor
IOCTL_HID_GET_REPORT_DESCRIPTORReturns a USB-standard report descriptor
IOCTL_HID_READ_REPORTReads a report conforming to the report descriptor
IOCTL_HID_SEND_IDLE_NOTIFICATIONIdles the device (new in Windows XP)
IOCTL_HID_SET_FEATUREWrites a feature report

I'll discuss these control operations in detail in the next section of this chapter. They all share several common features, however:

  • In general, HIDCLASS can call your DispatchInternalControl callback at any interrupt request level (IRQL) less than or equal to DISPATCH_LEVEL and in an arbitrary thread. These facts imply that your callback, and all the data objects it uses, must be in nonpaged memory. Furthermore, you cannot block the calling thread. If you create subsidiary IRPs to communicate with your hardware, you must use asynchronous IRPs. Finally, any driver to which you send an IRP directly from this callback must be able to tolerate receiving the IRP at DISPATCH_LEVEL. As a point of information, the standard SERIAL.SYS driver allows IRPs at DISPATCH_LEVEL, and the USB bus driver allows you to send USB request blocks (URBs) at DISPATCH_LEVEL as well.
  • Most of the control operations use METHOD_NEITHER, which means that the input and output data buffers are found in the stack Parameters.DeviceIoControl.Type3InputBuffer and the IRP UserBuffer fields, respectively.
  • The control operations are heavily oriented toward the USB specification for HID devices. If you're writing a HIDCLASS minidriver, it's probably because you have either a nonstandard USB device or some other type of device altogether. You'll therefore have to fit your device into the USB model. For example, you'll have to come up with dummy vendor and product identifier values, dummy string descriptors, and so on.
  • The IRPs you receive in this callback have sufficient IO_STACK_LOCATION slots for you to pass the IRP down the PnP stack to your own bus driver. This fact allows you to piggyback on the control IRP to carry out a device-specific function that requires an IRP.

Here's a skeleton for coding this callback function in a minidriver:

NTSTATUS DispatchInternalControl(PDEVICE_OBJECT fdo, PIRP Irp)
  ULONG info = 0;
  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
  ULONG cbin =
  ULONG cbout =
  ULONG code =
  PVOID buffer = Irp->UserBuffer;
  switch (code)
  if (status != STATUS_PENDING)
    CompleteRequest(Irp, status, info);
  return status;

Internal IOCTL Interface

The major interface between HIDCLASS and a minidriver is through the DispatchInternalControl callback summarized at the end of the preceding section on callback routines. In this section, I'll describe how to perform each of the control operations, in the order in which HIDCLASS normally presents them. Note that HIDCLASS doesn't invoke this callback at all until after the minidriver successfully completes an IRP_MN_START_DEVICE request.


HIDCLASS sends an IOCTL_HID_GET_DEVICE_ATTRIBUTES request as part of its processing of the IRP_MN_START_DEVICE request and conceivably, at other times, to obtain information that a USB device records in its device descriptor. The UserBuffer field of the IRP points to an instance of the following structure, which you should complete:

typedef struct _HID_DEVICE_ATTRIBUTES {
  ULONG  Size;
  USHORT VendorID;
  USHORT ProductID;
  USHORT VersionNumber;
  USHORT Reserved[11];

For example, you can complete the structure in the context of the skeletal DispatchInternalControl routine shown earlier:

  if (cbout < sizeof(HID_DEVICE_ATTRIBUTES))
  #define p ((PHID_DEVICE_ATTRIBUTES) buffer)
  RtlZeroMemory(p, sizeof(HID_DEVICE_ATTRIBUTES));
  p->Size = sizeof(HID_DEVICE_ATTRIBUTES);
  p->VendorID = 0;
  p->ProductID = 0;
  p->VersionNumber = 1;
  #undef p
  info = sizeof(HID_DEVICE_ATTRIBUTES);

If your device is simply a nonstandard USB device, it's obvious which values you should supply for the VendorID, ProductID, and VersionNumber fields of this structure: the idVendor, idProduct, and bcdDevice fields from the real device descriptor. If your device isn't a USB device, you have to come up with dummy values. I used 0, 0, and 1, respectively, in this code fragment, and those choices will suffice for every type of HID device except a joystick. For a joystick device, you need to pick unique values that match what you specify in the OEM registry subkey you create for the joystick. I have no advice about how to pick those values.


HIDCLASS sends an IOCTL_HID_GET_DEVICE_DESCRIPTOR request as part of its processing of the IRP_MN_START_DEVICE request and conceivably, at other times, in order to obtain a description of the device's HID characteristics. The UserBuffer field of the IRP points to an instance of a USB-standard HID descriptor structure, which you should complete:

typedef struct _HID_DESCRIPTOR {
  UCHAR   bLength;
  UCHAR   bDescriptorType;
  UCHAR   bCountry;
  UCHAR   bNumDescriptors;
    UCHAR   bReportType;
    USHORT  wReportLength;
    } DescriptorList [1];

Notwithstanding the apparent generality of this structure, HIDCLASS currently reserves sufficient space for only one element in the DescriptorList array, and it has to be the report descriptor. The Microsoft developers recommend that you nevertheless inspect the size of the output buffer and arrange your code to copy any additional descriptors—such as a physical descriptor—that you might have.

Your job in the minidriver is to fill in the descriptor structure as if you were a USB-standard HID device. For example:

  #define p ((PHID_DESCRIPTOR) buffer)
  if (cbout < sizeof(HID_DESCRIPTOR))
  RtlZeroMemory(p, sizeof(HID_DESCRIPTOR));
  p->bLength = sizeof(HID_DESCRIPTOR);
  p->bDescriptorType = HID_HID_DESCRIPTOR_TYPE;
  p->bCountry = 0;
  p->bNumDescriptors = 1;
  p->DescriptorList[0].bReportType = HID_REPORT_DESCRIPTOR_TYPE;
  p->DescriptorList[0].wReportLength = sizeof(ReportDescriptor);
  #undef p
  info = sizeof(HID_DESCRIPTOR);

The only aspect of this code that isn't going to be the same from one driver to the next is the length you specify for the wReportLength member of the single DescriptorList entry you provide. This value should be the length of whatever real or dummy report descriptor you'll deliver in response to the IOCTL_HID_GET_REPORT_DESCRIPTOR request.


HIDCLASS sends an IOCTL_HID_GET_REPORT_DESCRIPTOR request as part of its processing of the IRP_MN_START_DEVICE request and conceivably, at other times, in order to obtain a USB-standard HID report descriptor. The UserBuffer field of the IRP points to a buffer as large as you indicated would be necessary in your reply to an IOCTL_HID_GET_DEVICE_DESCRIPTOR request.

Suppose you have a static data area named ReportDescriptor that contains a report descriptor in standard format. You could handle this request this way:

  if (cbout < sizeof(ReportDescriptor))
  RtlCopyMemory(buffer, ReportDescriptor,
  info = sizeof(ReportDescriptor);

Your first step in building the report descriptor is to design the report layout. The USB specification for HID devices makes it seem that you're pretty much free to design any sort of report you want, with the implication that Windows will somehow just figure out what to do with the resulting data. In my experience, however, you really don't have that much freedom. The Windows components that process keyboard and mouse input are used to receiving reports that meet certain expectations. Applications, such as games, differ greatly in their ability to decode joystick reports. I've also found that the HIDPARSE driver, which HIDCLASS uses to parse a HID descriptor, is rather fussy about which apparently conforming descriptors it will actually accept. Consequently, my advice is to closely mimic an existing Microsoft device when designing reports for common devices.

One of the options when you save your work in the HID Tool is to create a C-language header file, like this one (corresponding to the descriptor shown in Figure 13-3):

char ReportDescriptor[64] = {
  0x05, 0x05,  // USAGE_PAGE (Gaming Controls)
  0x09, 0x03,  // USAGE (Gun Device )
  0xa1, 0x01,  // COLLECTION (Application)
  0xa1, 0x02,  //   COLLECTION (Logical)
  0x85, 0x01,  //     REPORT_ID (1)
  0x05, 0x09,  //     USAGE_PAGE (Button)
  0x09, 0x01,  //     USAGE (Button 1)
  0x15, 0x00,  //     LOGICAL_MINIMUM (0)
  0x25, 0x01,  //     LOGICAL_MAXIMUM (1)
  0x75, 0x01,  //     REPORT_SIZE (1)
  0x95, 0x01,  //     REPORT_COUNT (1)
  0x81, 0x02,  //     INPUT (Data,Var,Abs)
  0x75, 0x07,  //     REPORT_SIZE (7)
  0x81, 0x03,  //     INPUT (Cnst,Var,Abs)
  0xc0,        //   END_COLLECTION
  0xa1, 0x02,  //   COLLECTION (Logical)
  0x85, 0x02,  //     REPORT_ID (2)
  0x05, 0x01,  //     USAGE_PAGE (Generic Desktop)
  0x09, 0x30,  //     USAGE (X)
  0x25, 0xff,  //     LOGICAL_MAXIMUM (-1)
  0x75, 0x20,  //     REPORT_SIZE (32)
  0xb1, 0x02,  //     FEATURE (Data,Var,Abs)
  0xc0,        //   END_COLLECTION
  0xa1, 0x02,  //   COLLECTION (Logical)
  0x85, 0x03,  //     REPORT_ID (3)
  0x05, 0x09,  //     USAGE_PAGE (Button)
  0x09, 0x01,  //     USAGE (Button 1)
  0x25, 0x01,  //     LOGICAL_MAXIMUM (1)
  0x75, 0x01,  //     REPORT_SIZE (1)
  0xb1, 0x02,  //     FEATURE (Data,Var,Abs)
  0x75, 0x07,  //     REPORT_SIZE (7)
  0xb1, 0x03,  //     FEATURE (Cnst,Var,Abs)
  0xc0,        //   END_COLLECTION
  0xc0         // END_COLLECTION

You can simply include this header file in your driver to define the ReportDescriptor you return from IOCTL_HID_GET_REPORT_DESCRIPTOR.


IOCTL_HID_READ_REPORT is the workhorse operation of a HIDCLASS minidriver. HIDCLASS issues this request to obtain a raw HID report. HIDCLASS uses the raw reports to satisfy IRP_MJ_READ and IOCTL_HID_GET_INPUT_REPORT requests issued to it from higher-level components, including user-mode applications that call ReadFile, HidD_GetInputReport, IDirectInputDevice8::GetDeviceData, or IDirectInputDevice8::Poll.

A minidriver can employ any of several strategies to provide reports:

  • If your device is a programmed I/O (PIO) type of device attached to a traditional bus such as Peripheral Component Interconnect (PCI), you can perhaps perform hardware abstraction layer (HAL) function calls to derive data for a structured report. You'll then immediately complete the IOCTL_HID_READ_REPORT request.
  • If your device attaches to a traditional bus and uses a hardware interrupt to notify the host when report data is available, you need to implement a scheme for satisfying requests with reports when they become available. Using an interlocked list lets you read and save report data in an interrupt service routine (ISR). Other schemes would require your ISR to queue a deferred procedure call (DPC), which would then read and save report data.
  • If your device is a nonstandard USB device, you can perhaps submit a single URB to derive data from which you can compose a structured report. You can piggyback the URB on the IOCTL_HID_READ_REPORT request if your device's raw report is no bigger than the report HIDCLASS is expecting. In this case, your dispatch routine will presumably allocate memory for the URB from nonpaged memory, install a completion routine, and forward the IRP down the PnP stack to the USB bus driver. Your completion routine will free the URB, reformat the report data and set IoStatus.Information equal to the size of the reformatted report, and return STATUS_SUCCESS to allow the IRP to complete.
  • In still other situations, you may need to pend the IOCTL_HID_READ_REPORT request while you perform one or more I/O operations to fetch raw data from your device, which you then reformat into the desired report packet. With this design, you have the usual issues associated with caching a pointer to the IOCTL_HID_READ_REPORT request in a cancel-safe way and with cancelling whatever subsidiary IRPs you create.

No matter what scheme you devise, your driver will implement this IRP by filling the UserBuffer buffer with a report. For example:

  if (cbout < <size of report>)
  <obtain report data>
RtlCopyMemory(buffer, <report>, <size of report>);
  info = <size of report>;

Bear in mind that if your report descriptor includes more than one report, the report data you return to HIDCLASS begins with a 1-byte report identifier.


HIDCLASS issues the IOCTL_HID_WRITE_REPORT request to service IRP_MJ_WRITE and IOCTL_HID_SET_OUTPUT_REPORT requests issued from a higher-level component, such as a user-mode application that calls WriteFile, HidD_SetOutputReport, or IDirectInputDevice8::SendDeviceData.

Output reports are commonly used to set indicators of various kinds, such as LEDs and panel displays. Your job in a minidriver is to transmit the output report data to the device or to simulate the operation of a HID device receiving such a report by some means. USB devices implement the class-specific control-pipe command Set_Report_Request (or else they define an interrupt-out endpoint) for receiving output reports, but your architecture may call for a different approach.

Unlike other HIDCLASS internal control operations, IOCTL_HID_WRITE_REPORT uses METHOD_BUFFERED. This means that the AssociatedIrp.SystemBuffer field of the IRP contains the address of the output data and that the Parameters.DeviceIoControl.OutputBufferLength field of the IO_STACK_LOCATION contains its length.


HIDCLASS issues the IOCTL_HID_GET_FEATURE and IOCTL_HID_SET_FEATURE requests in order to read or write a so-called feature report. An application might trigger these requests by calling HidD_GetFeature or HidD_SetFeature, respectively.

You can embed feature reports within a report descriptor. According to the HID specification, feature reports are useful for getting and setting configuration information rather than for polling the device on a regular basis. With a USB-standard device, the driver uses Get_Report_Request and Set_Report_Request class-specific commands to implement this functionality. In the minidriver for some other kind of HID device, you need to provide some sort of analogue if your report descriptor includes feature reports.

These I/O control (IOCTL) operations are also the way Microsoft would prefer you perform out-of-band communication between an application and a HID minidriver. Bear in mind that HIDCLASS doesn't allow anyone to open a handle to the device itself (handles may be opened only to top-level collections) and fails any nonstandard control operations that it happens to receive. Without resorting to sleazy methods, as to which I won't say anything, there is actually no other way for an application and a HIDCLASS minidriver to communicate.

The "output" buffer for this request is an instance of the following structure:

typedef struct _HID_XFER_PACKET {
  PUCHAR reportBuffer;
  ULONG  reportBufferLen;
  UCHAR  reportId;

HIDCLASS uses the same structure for both GET_FEATURE and SET_FEATURE requests, and it sets Irp->UserBuffer to point to it in both cases too. In fact, the only difference between the two requests is that the length of the structure (a constant) is in the InputBufferLength parameter for SET_FEATURE and in the OutputBufferLength parameter for GET_FEATURE. (You won't even care about this difference. Since HIDCLASS is a trusted kernel-mode caller, there's no particular reason to validate the length of this parameter structure.)

Your job when handling one of these requests is to decode the reportId value, which designates one of the feature reports your driver supports. For a GET_FEATURE request, you should place up to reportBufferLen bytes of data in the reportBuffer buffer and complete the IRP with IoStatus.Information set to the number of bytes you copy. For a SET_FEATURE request, you should extract reportBufferLen bytes of data from the reportBuffer buffer.

Here's a skeleton for handling these two requests:

  #define p ((PHID_XFER_PACKET) buffer)
  switch (p->reportId)
    if (p->reportBufferLen < sizeof(FEATURE_REPORT_XX))
    RtlCopyMemory(p->reportBuffer, FeatureReportXx,
    info = sizeof(FEATURE_REPORT_XX);
  #undef p
  #define p ((PHID_XFER_PACKET) buffer)
  switch (p->reportId)
    if (p->reportBufferLen > sizeof(FEATURE_REPORT_YY))
    RtlCopyMemory(FeatureReportYy, p->reportBuffer,
  #undef p

In these fragments, FEATURE_CODE_XX and FEATURE_CODE_YY are placeholders for manifest constants that you would define to correspond to feature report identifiers in your device's scheme. FEATURE_REPORT_XX and FEATURE_REPORT_YY are structures that include an identifier byte and the actual report data, and FeatureReportXx and FeatureReportYy are instances of those structures.


HIDCLASS sends an IOCTL_GET_PHYSICAL_DESCRIPTOR when a higher-level component requests the physical descriptor for a device. Physical descriptors provide information about which part or parts of the body activate one or more controls on a device. If you have a nonstandard HID device for which this request is relevant, you'll need to support the request by returning a dummy descriptor meeting the HID specification, Section 6.2.3. For example:

  if (cbout < sizeof(PhysicalDescriptor))
  PUCHAR p =
    (PUCHAR) MmGetSystemAddressForMdlSafe(Irp->MdlAddress);
  if (!p)
    PhysicalDescriptor, sizeof(PhysicalDescriptor));
  info = sizeof(PhysicalDescriptor);

Note that this IOCTL uses METHOD_OUT_DIRECT instead of METHOD_NEITHER.

In addition, bear in mind the following statement in the HID specification: "Physical descriptors are entirely optional. They add complexity and offer very little in return for most devices."


HIDCLASS sends an IOCTL_HID_GET_STRING to retrieve a string describing the manufacturer, product, or serial number of a device. A user-mode application can trigger this IOCTL by calling HidD_GetManufacturerString, HidD_GetProductString, or HidD_GetSerialNumberString. The strings correspond to optional strings specified by the device descriptor of a standard USB device. A parameter to the operation indicates which string you should return, and in which language.

A skeleton for handling this control operation is as follows:

  #define p ((PUSHORT) \
  USHORT istring = p[0];
  LANGID langid = p[1];
  #undef p
  PWCHAR string = NULL;
  switch (istring)
    string = <manufacturer name>;
    string = <product name>;
    string = <serial number>;
  if (!string)
  ULONG lstring = wcslen(string);
  if (cbout < lstring * sizeof(WCHAR))
  RtlCopyMemory(buffer, string, lstring * sizeof(WCHAR));
  info = lstring * sizeof(WCHAR);
  if (cbout >= info + sizeof(WCHAR))
    ((PWCHAR) buffer)[lstring] = UNICODE_NULL;
    info += sizeof(WCHAR);

Some of the key points about this code fragment are these:

  • Like most other minidriver IOCTL requests, this one uses METHOD_NEITHER. In the context of the DispatchInternalControl callback presented earlier, buffer is the output buffer to be filled.
  • The serial number, if there is one, should be unique for each device.
  • The minidriver should fail the request with STATUS_INVALID_PARAMETER if an invalid string index appears and STATUS_INVALID_BUFFER_SIZE if a buffer is supplied but is too small to hold the entire string.
  • The minidriver returns the whole string or nothing. It appends a null terminator to the string if the output buffer is big enough.

The DDK doesn't specify what to do if the requested language isn't one of the ones your device or minidriver happens to support. I would suggest failing the request with STATUS_DEVICE_DATA_ERROR to mimic what a real USB device is supposed to do. If, however, the unsupported language is 0x0409 (American English), I recommend returning a default string of some kind—perhaps even the first language in your list of supported languages—because HIDCLASS always uses 0x0409 for the language id parameter in Windows XP and earlier versions of the system.


HIDCLASS sends an IOCTL_HID_GET_INDEXED_STRING to retrieve a string whose USB-standard index and language identifiers are specified. A user-mode program can trigger this IOCTL by calling HidD_GetIndexedString. You handle this request much like IOCTL_HID_GET_STRING except for these two points:

  • This control operation uses a curious mix of two buffering methods: the input data containing the string index and the language id is in stack->Parameters.DeviceIoControl.Type3InputBuffer (as would be true of a METHOD_NEITHER request), and the output buffer is described by the memory descriptor list (MDL) at Irp->MdlAddress, as would be true of a METHOD_OUT_DIRECT request.
  • The string index in the low-order 16 bits of the Type3InputBuffer is a USB-standard string index instead of a constant such as HID_STRING_ID_IMANUFACTURER.

The purpose of this request is to allow applications to retrieve string values corresponding to string usages in a HID report. USB devices may make up to 255 string values accessible in this way. With a nonstandard USB device or a non-USB device, your minidriver needs to provide an analogue if the report descriptor contains string usages.


HIDCLASS sends an IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST to power down an idle device. With a real USB device, this request dovetails with the USB selective suspend feature discussed in the preceding chapter.

The input buffer for this METHOD_NEITHER request is an instance of the following structure:

  PVOID IdleContext;

where HID_SEND_IDLE_CALLBACK is declared as follows:

typedef void (*HID_IDLE_CALLBACK)(PVOID Context);

Note that this structure is identical in layout and meaning to the one used with USB selective suspend. In fact, if your device happened to be a USB device, you could just forward the IRP down the stack after changing the function code:

  stack = IoGetNextIrpStackLocation(Irp);
  stack->Parameters.DeviceIoControl.IoControlCode =
  return IoCallDriver(pdx->LowerDeviceObject, Irp);

If your device is not a USB device, however, you should call back right away to HIDCLASS and complete the IRP, as shown here:


Calling back tells HIDCLASS that it can immediately power the device down.

Windows 98/Me Compatibility Notes

The footprint of history is heavy on the HID architecture in Windows 98/Me because of the necessity of supporting legacy methods of handling keyboards, mice, and joysticks. In general, it's been my experience that each new attempt to port a working HID minidriver from Windows XP has provided new opportunities for learning and reverse engineering. I plainly don't know everything there is to know in this area, but I'll describe two situations I encountered and how I dealt with them.


If you're writing a minidriver for fake hardware, such as HIDFAKE attempts to do, you must special-case the handling of IRP_MN_QUERY_ID. Left to itself, the root enumerator succeeds this IRP but provides a NULL list of identifiers. HIDCLASS then induces a crash deep in the Virtual Machine Manager (VMM). Here's the code in HIDFAKE to cope with this problem:

  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
  if (win98 
    && stack->MinorFunction == IRP_MN_QUERY_ID 
    && !NT_SUCCESS(Irp->IoStatus.Status))
    PWCHAR idstring;
    switch (stack->Parameters.QueryId.IdType)
    case BusQueryInstanceID:
      idstring = L"0000";
    case BusQueryDeviceID:
      idstring = L"ROOT\\*WCO0D01";
    case BusQueryHardwareIDs:
      idstring = L"*WCO0D01";
      return CompleteRequest(Irp);
    ULONG nchars = wcslen(idstring);
    ULONG size = (nchars + 2) * sizeof(WCHAR);
    PWCHAR id = (PWCHAR) ExAllocatePool(PagedPool, size);
    if (!id)
      return CompleteRequest(Irp, STATUS_INSUFFICIENT_RESOURCES);
    wcscpy(id, idstring);
    id[nchars + 1] = 0;
    return CompleteRequest(Irp, STATUS_SUCCESS, (ULONG_PTR) id);
  return GenericDispatchPnp(PDX(fdo)->pgx, Irp);

(Note the use of two override versions of my CompleteRequest helper here.)

Actually, you need to do something special with the BusQueryHardwareIDs even in Windows XP because HIDCLASS omits the creation of compatible IDs if the bus driver fails the request, which the root enumerator will do. You can't create a fake device of one of the standard classes unless you know this trick.


In Windows, there are two different code paths for interpreting the axis and button information for a joystick. One code path relies on the HID descriptor. Another relies on settings in the OEM registry key. If these code paths don't produce consistent results, you end up with a nonfunctional joystick in that every attempt to read its position generates an error. I know of no way except trial and error to get past this problem. The one time I had to do it for a client, I ended up writing an elaborate HID simulator that we could quickly program to create new joystick devices with specified attributes. After some number of iterations, we ended up with a working device. I'd be hard pressed to reproduce the effort.

Last Updated: December 12, 2002
Top of Page