52964.fb2 Writing Windows WDM Device Drivers - читать онлайн бесплатно полную версию книги . Страница 24

Writing Windows WDM Device Drivers - читать онлайн бесплатно полную версию книги . Страница 24

Chapter 23HID Clients

This chapter shows how to use the Human Input Device (HID) model in Windows. It is easy to write a user mode application to talk to a HID device through the Windows HID class driver. If need be, you can write a kernel mode HID client to do the same job. A HID kernel mode client can be layered over the HID class driver in the normal way. Alternatively, it can use Plug and Play Notification to find any suitable HID devices. Windows provides routines that make it much easier to analyze HID Report descriptors and send and receive reports.

This chapter initially looks at how to write a user mode application, called HidKbdUser, to talk to a HID keyboard. This section describes all the Windows parsing routines that are available to both user mode and kernel mode HID clients.

Later, the HidKbd kernel mode HID client driver is described. This illustrates the Plug and Play Notification technique of finding all devices with a matching device interface. HidKbd finds any devices with a HID class driver device interface. If a HID keyboard is found, it makes a device called \\.\HidKbd available to user mode applications. The HidKbdTest application tests the HidKbd driver.

HID Class Driver

The HID class driver is the key to using HID in Windows. It uses HID minidrivers to talk to actual devices. It handles requests from clients, directing them to the correct minidriver.

When a new HID device is found by the Plug and Play Manager, the installation INF file eventually ensures that the HID class driver and the appropriate minidriver are loaded.

The HID class driver uses the minidriver to read the HID and Report descriptors. The HID class driver then creates a device object for each of the top-level collections described in the Report descriptor. Each device object is registered as belonging to the HID class device interface.

User mode and kernel mode clients can use the HID class device interface GUID to identify all the available HID devices. The client must get the device capabilities to see if the device is of interest. If you were looking for a HID keyboard, you would ignore a HID mouse.

Each request to the HID class driver eventually ends up as a call to the appropriate HID minidriver. Each minidriver knows how to send and receive HID reports, for example. This chapter does not describe how to write minidrivers; this subject is covered in the DDK.

HID Class Driver Characteristics

The HID class driver responds to Create, Close, Read, Write, and IOCTL IRPs. Read and Write requests are used for input, output, and feature reports. IOCTLs are used to retrieve HID device capabilities. User mode applications should not use the IOCTLs directly. Instead, various HidD… routines are supplied to make the job easier.

Ring Buffer

The HID class driver has a buffer for each device for input reports. This holds input reports until a client reads them. This is a "ring buffer", so if the reports are not read quickly enough, new reports may overwrite the old reports (i.e., data may get lost).

A kernel mode client can get and set the ring buffer size using two IOCTLs.

Multiple Open Handles

In Windows 98, more than one program can access a single HID device at the same time. For example, if a HID keyboard is attached then a Windows kernel mode driver will be loaded to talk to it. This driver ensures that any key presses are routed into the normal keyboard driver. You can also open up another handle to the device object in the HID class driver that represents the HID keyboard. Therefore, in W98 at least, any input reports are received on all open handles.

However in Windows 2000, the HID keyboard driver opens a HID keyboard for exclusive access. Therefore the HidKbdUser user mode program and the HidKbd driver are unable to talk to the keyboard. I was able to develop this code in the Beta 2 version of W2000, when the keyboard was not opened for exclusive access.

Windows HID Clients

Windows 98 and Windows 2000 have built in support for various types of common HID device (e.g., keyboards, mice, game ports, audio controls, and the DirectInput system). The standard installation INF files load the required drivers automatically. If you plug in a HID USB keyboard, for example, the relevant system drivers are loaded and you should be able to start typing straightaway[56].

Figure 23.1 shows the Windows 2000 drivers that are loaded to service a "legacy" keyboard and a HID USB keyboard. The situation is identical in Windows 98, apart from the driver filenames. For mouse input and game ports, the driver diagrams are almost identical. However, in Windows 2000, all the game port input is now done using HID with a HID joystick minidriver.

The Win32 subsystem still uses the standard keyboard driver, KbdClass.sys in W2000, to get all its keyboard input. This interrogates the 8042 legacy keyboard in the same way as NT 4 and earlier. However, the keyboard driver can also get its input from a HID keyboard. An intermediate driver, KbdHid.sys in W2000, is used. I assume that KbdHid.sys does all the hard work of interrogating the HID class driver, and it simply returns 8042 equivalent scan codes to the main keyboard driver.

The keyboard auto-repeat feature is not usually present in a HID keyboard. Either the KbdClass.sys or the KbdHid.sys driver must implement auto-repeat. Similarly, the Ctrl+Alt+Del key combination must be detected by one of these drivers. Quite correctly, it is not a feature of HID class driver.

I understand that KbdHid.sys driver always tries to keep two outstanding read requests for a keyboard: one to detect a key being depressed and one to detect a key being released. Combined with the HID class driver ring buffer, this technique should ensure that no key presses are lost.

In the future, it would be neater if Microsoft rearranged this keyboard driver layout. They would have to write a HID minidriver for the legacy 8042 port. All keyboard input would then come through the HID class driver. Although this approach might be slower, it should not matter for such a relatively slow device as a keyboard.

In addition, the figure shows that a user mode client can also access the HID keyboard as a HID device. It will receive regular HID input reports, as shown later. It will not receive 8042 equivalent scan codes. As I mentioned previously, all input reports are received by both KbdHid.sys and the user mode client.

Figure 23.1 Windows and a user mode client using a HID keyboard

Header Files

Table 23.1 shows the HID header files that you need to include. In the Windows 98 DDK, these files are in the src\hid\inc directory. In the Windows 2000 DDK, most headers are in the standard inc directory, while the others are in src\wdm\hid\inc.

Table 23.1 HID header files

hidсlass.hIOCTL codes
hidpddi.hOther parsing library routines
hidpi.hParsing routines (i.e., HidP… functions and structures)
hidport.hMinidriver header
hidsdi.hUser-mode HidD… functions
hidtoken.hReport descriptor item constants
hidusage.hStandard usage values

HID USB Minidriver

HID evolved from the USB specification, but is now a general purpose input device model. Nonetheless, HID fits neatly into the USB model. The HID class is one of the classes defined in the USB Specifications.

At present, the USB bus is one of the few places where HID devices can be found. The HID class driver talks to the USB device stack through the HidUsb.sys driver.

A HID USB device has the standard USB device, configuration, interface, and endpoint descriptors. In addition, it has the class-specific HID, Report, and Physical descriptors described in the previous chapter.

The standard USB device descriptor has zeroes for its class, subclass, and protocol. The standard interface descriptor has constant USB_DEVICE_CLASS_HUMAN_INTERFACE (0x03) in its bInterfaceClass field, iInterfaceSubClass is set to 1 if the device supports a boot protocol, described in the following text. In this case, bInterfaceProtocol is 1 for a keyboard and 2 for a mouse.

A USB request for the Configuration descriptor usually returns the HID descriptor before the endpoint descriptors[57]. The report and physical descriptors must be specifically asked for, using the following codes.

HID DescriptorHID_HID_DESCRIPTOR_TYPE0x21
Report descriptorHID_REPORT_DESCRIPTOR_TYPE0x22
Physical DescriptorHID_PHYSICAL_DESCRIPTOR_TYPE0x23

A HID USB device uses the default control pipe endpoint 0 and one interrupt pipe. The control pipe is used for standard USB purposes and for sending and receiving reports, etc. The interrupt pipe is used to return new input reports.

USB HID devices support the notion of an idle rate. Whenever an interrupt request is received from the host, the USB HID device can either return a report or a NAK to indicate that nothing has changed. In the former case, no input state may have changed and so host processing of the report will waste time. The idle rate determines how often the HID USB device returns a report. If at any time during the idle rate period, an input report changes, the HID USB device does return an input report at the next interrupt. The recommended idle rate for keyboards is 500ms and infinity for joysticks and mice.

The following HID-specific class requests are supported on the default pipe. GET_REPORT and SET_REPORT get or send a specific input, output, or feature report. An idle rate can be defined using SET_IDLE and read using GET_IDLE. SET_PROTOCOL and GET_PROTOCOL are used for boot devices, as described in the following text.

USB Boot Devices

A USB keyboard or mouse must be available during boot so the user can configure the BIOS settings, select the operating system, etc.

The BIOS can read the USB interface descriptor with relative ease. If it finds that iInterfaceSubClass field is 1, the device must be a keyboard or mouse that supports the boot protocol. The Report descriptor for boot keyboards and mice must be in a standard format. Therefore, the BIOS does not have to read and decode the Report descriptor. It simply uses the SET_PROTOCOL command to enable the standard reports.

A boot keyboard input report is usually eight bytes long, in the format described in the previous chapter. Other requirements for a boot keyboard are given in the HID specification.

A mouse boot input report is at least three bytes long. The first three bits are the mouse button states. The second and third bytes are the X and Y displacements.

User Mode HID Clients

The DDK recommends that you talk to HID devices using a user mode application, if possible. This makes sense, as it is usually far easier to write and debug a Win32 application than a device driver. See the next section if you really need to write a HID client device driver. A user mode HID application has three main jobs to do.

1. Find all HID devices.

2. For each HID device, inspect its capabilities to see if it is of interest.

3. Read HID input reports or write HID output reports when needed. Alternatively send and receive feature reports.

The HidUsbUser example in HidUsb\User\HidKbdUser.cpp illustrates these tasks. HidUsbUser looks for a HID keyboard, reads input keys until Esc is pressed, and then — hey hey — flashes the keyboard LEDs. HidUsbUser does not care what sort of HID keyboard is attached. All HID keyboards ought to respond in a similar way.

HidUsbUser uses various standard routines. The HidD… routines are available only to user mode applications. The "parsing routines" HidP… can be used by kernel mode clients as well. In user mode, all these routines are provided by Hid.dll. In kernel mode, the parsing routines are in HidParse.sys.

Finding HID Devices

The HID class driver registers all its device objects as having the HID device interface. HidUsbUser can, therefore, use the GetDeviceViaInterface routine to find all existing HID devices. Instead of using one of the book software GUIDs, HidUsbUser looks for the HID class GUID. Use HidD_GetHidGuid to find this GUID.

GUID HidGuid;

HidD_GetHidGuid(&HidGuid);

Going back to Chapter 5, you will see that GetDeviceViaInterface uses the SetupDi… routines to find a device that matches the given GUID. Previously, I have always looked for the first device with the desired device interface. HidUsbUser, however, looks through all the available HID devices looking for a HID keyboard.

GetDeviceViaInterface opens a handle to each device. HidUsbUser then gets each device's capabilities. If it is a HID keyboard, the main program carries on. Otherwise it closes the device handle, and loops again so that GetDeviceViaInterface can open the next HID device. HidUsbUser reports an error and gives up if a HID keyboard is not found.

Note that HidUsbUser only looks for an appropriate HID device when it starts. As it stands, it will not discover any new HID devices that suddenly are plugged in. The Win32 RegisterDeviceNotification function can be used to listen for Plug and Play Notification events in user mode applications. The Wdm2Notify application described in Chapter 9 shows how to use RegisterDeviceNotification to listen for device interface events. This can easily be modified to spot new HID class devices.

If the HID USB keyboard is unceremoniously unplugged while HidUsbUser is running, all I/O requests fail, and GetLastError returns 1167 (ERROR_PROCESS_ABORTED).

Getting HID Capabilities

HidUsbUser uses GetCapabilities to get a HID device's capabilities, as shown in Listing 23.1. In particular, it looks for a HID keyboard. If it finds a keyboard, GetCapabilities returns a preparsed data pointer and the maximum input and output report sizes. The preparsed data is the HID Report descriptor parsed into a format that makes it easier for the support routines to decode and encode reports. You will need to pass the preparsed data pointer whenever you call any of the HID parsing routines.

Some applications will simply want to look for the device vendor and product IDs. If you know the capabilities of your device, there is no need to do any more checking. However, you still need to get the preparsed data. If possible, you should write code that does not rely on specific vendor and product IDs.

Get the device ID using HidD_GetAttributes. This fills in the supplied HIDD_ATTRIBUTES structure. If successful, the VendorID, ProductID, and VersionNumber fields are filled in. For USB devices, these values come from the USB Device descriptor.

Use HidD_GetPreparsedData to obtain a pointer to the preparsed data. This time, Windows allocates the buffer and returns a pointer to you. Make sure that you keep the preparsed data pointer handy for the rest of your HID operations. When finished with it, free the preparsed data memory using HidD_FreePreparsedData.

The first step when analyzing your device capabilities is to call HidP_GetCaps, which fills in a HIDP_CAPS structure. The UsagePage and Usage fields in there tell you the usage information for the Report descriptor top-level collection. In GetCapabilities, this is all the information that is needed to determine if the device is a keyboard. It checks that these fields are HID_ USAGE_PAGE_GENERIC and HID_USAGE_GENERIC_KEYBOARD, respectively.

The HIDP_CAPS structure also contains much other useful information. First, it contains the maximum length of input, output, and feature reports in three fields. The first two of these values are saved, because they indicate how big a buffer to allocate to send and receive reports. The other HIDP_CAPS fields are used if you check the detailed control capabilities.

Listing 23.1 Getting a HID device's capabilities

bool GetCapabilities(HANDLE hHidKbd, PHIDP_PREPARSED_DATA& HidPreparsedData, USHORT& InputReportLen, USHORT& OutputReportLen) {

 // Get attributes, i.e. find vendor and product ids

 HIDD_ATTRIBUTES HidAttributes;

 if (!HidD_GetAttributes(hHidKbd, &HidAttributes)) {

  printf("XXX Could not get HID attributes\n");

  return false;

 }

 printf("HID attributes: VendorID=%04X, ProductID=%04X, VersionNumber=%04X\n",

  HidAttributes.VendorID, HidAttributes.ProductID, HidAttributes.VersionNumber);

 // Get preparsed data

 if (!HidD_GetPreparsedData( hHidKbd, &HidPreparsedData)) {

  printf("XXX Could not get HID preparsed data\n");

  return false;

 }

 // Work out capabilities

 HIDP_CAPS HidCaps;

 bool found = false;

 NTSTATUS status = HidP_GetCaps(HidPreparsedData, &HidCaps);

 if (status==HIDP_STATUS_SUCCESS) {

  printf("Top level Usage page %d usage %d\n", HidCaps.UsagePage, HidCaps.Usage);

  if (HidCaps.UsagePage==HID_USAGE_PAGE_GENERIC && HidCaps.Usage==HD_USAGE_GENERIC_KEYBOARD) {

   printf(" Found HID keyboard\n\n");

   found = true;

  }

  // Remember max lengths of input and output reports

  InputReportLen = HidCaps.InputReportByteLength;

  OutputReportLen = HidCaps.OutputReportByteLength;

  printf("InputReportByteLength %d\n", HidCaps.InputReportByteLength);

  printf("OutputReportByteLength %d\n", HidCaps.OutputReportByteLength);

  printf("FeatureReportByteLength %d\n\n", HidCaps.FeatureReportByteLength);

  printf("NumberLinkCollectionNodes %d\n\n", HidCaps.NumberLinkCollectionNodes);

  printf("NumberInputButtonCaps %d\n", HidCaps.NumberInputButtonCaps);

  printf("NumberInputValueCaps %d\n", HidCaps.NumberInputValueCaps);

  printf("NumberOutputButtonCaps %d\n", HidCaps.NumberOutputButtonCaps);

  printf("NumberOutputValueCaps %d\n", HidCaps.NumberOutputValueCaps);

  printf("NumberFeatureButtonCaps %d\n", HidCaps.NumberFeatureButtonCaps);

  printf("NumberFeatureValueCaps %d\n\n", HidCaps.NumberFeatureValueCaps);

  ShowButtonCaps("Input button capabilities", HidP_Input, HidCaps.NumberInputButtonCaps, HidPreparsedData);

  ShowButtonCaps("Output button capabilities", HidP_Output, HidCaps.NumberOutputButtonCaps, HidPreparsedData);

 }

 return found;

}

Why Get Button and Value Capabilities?

Some programs cannot just rely on the HIDP_CAPS UsagePage and Usage fields. For example, in the future, some fancy device may use these fields to say that it is a "speech interface". A "speech interface" might still be able to generate key presses (e.g., when a user says a word). Its detailed capabilities would show that it could indeed present data that is of interest.

Each control in a HID device is seen by Windows as being either a button or a value. Anything that has a HID usage is defined as being a button. Controls that take on any other values are called values.

Each of the keys on a keyboard has a HID usage. All keyboard keys are in usage page 7. Within that usage page, most of the usages from 0 to 255 have key definitions. However, most Western keyboards produce usage codes in two ranges. Usages 0 to 101 contain most standard keys, while usages 224 to 231 represent the modifier keys such as Shift, Ctrl, Alt, etc. To be extra careful, check that the HID device generates usages in these two ranges.

Similarly to double check that the HID device has the correct LEDs check that its output report contains buttons in usage page 8, for LEDs. Within this usage page, usage 1 corresponds to NumLock, usage 2 with CapsLock, and usage 3 with ScrollLock.

Getting Button Capabilities

The HIDP_CAPS structure has fields that tell you how many button and value capabilities there are for each type of report. For example, NumberInputButtonCaps tells you how many button capabilities there are for input reports.

GetCapabilities uses the ShowButtonCaps routine shown in Listing 23.2 to show what button capabilities there are for different types of report. The NumCaps parameter is passed the number of button capabilities that are expected for the specified type of report.

ShowButtonCaps first allocates an array of HIDP_BUTTON_CAPS structures called ButtonCaps. The call to HidP_GetButtonCaps fills in this array. Afterwards, ShowButtonCaps simply goes through each element in ButtonCaps and prints out the usages that can be returned. The HIDP_BUTTON_CAPS UsagePage field gives the usage page of all the buttons referred to in this structure. ReportId specifies the report in which these buttons are. If the IsRange BOOLEAN is TRUE, the Range.UsageMin and Range.UsageMax fields are valid. Otherwise, the Not Range. Usage field holds the only valid usage.

Each HIDP_BUTTON_CAPS structure has similar fields for the string descriptors and physical designators associated with controls. Finally, various link fields specify in which collection the buttons are. See the previous chapter for a description of collections.

Listing 23.2 Getting buttons capabilities

void ShowButtonCaps(char* Msg, HIDP_REPORT_TYPE ReportType, USHORT NumCaps, PHIDP_PREPARSED_DATA HidPreparsedData) {

 if (NumCaps==0) return;

 printf(" %s\n", Msg);

 HIDP_BUTTON_CAPS* ButtonCaps = new HIDP_BUTTON_CAPS[NumCaps];

 if( ButtonCaps==NULL) return;

 NTSTATUS status = HidP_GetButtonCaps(ReportType, ButtonCaps, &NumCaps, HidPreparsedData);

 if (status==HIDP_STATUS_SUCCESS) {

  for (USHORT i=0; i<NumCaps; i++) {

   printf("ButtonCaps[%d].UsagePage %d\n", i, ButtonCaps[i].UsagePage);

   if (ButtonCaps[i].IsRange) printf(".Usages %d..%d\n\n", ButtonCaps[i].Range.UsageMin, ButtonCaps[i].Range.UsageMax);

   else printf(".Usage %d\n\n", ButtonCaps[i].NotRange.Usage);

  }

 }

 delete ButtonCaps;

}

Here is an excerpt from the output produced by the ShowButtonCaps routine. It confirms that, in this case, the HID device has the desired controls. It could be fairly laborious if you had to check that all the right buttons were available, as the button capabilities could arrive in any order.

Input button capabilities

ButtonCaps[0].UsagePage 7

             .Usages 224..231

ButtonCaps[1].UsagePage 7

             .Usages 0..101

Output button capabilities

ButtonCaps[0].UsagePage 8

             .Usages 1..3

Use HidP_GetSpecificButtonCaps if you need to look for buttons in a different collection or search for a controls with a specific usage page or usage. This might be a better way of determining whether the controls you are interested in are supported.

Getting Value Capabilities

You can retrieve details of what control values are supported in a very similar way. Use HidP_GetValueCaps to get a list of all values in the top-level collection. Alternatively, HidP_GetSpecificValueCaps is used to look for values in a different collection, or search for controls with a specific usage page or usage. The information is stored in an array of HIDP_VALUE_CAPS structures.

Getting Collection Capabilities

The HidP_GetLinkCollectionNodes function is used to obtain details of all the collections in a top-level collection. It fills in an array of HIDP_LINK_COLLECTION_NODE structures. See the DDK for full details of how this array specifies the arrangement of collections within the top-level collection.

Reading Input Reports

Reading input reports from a HID device is straightforward. Listing 23.3 shows how this is done in the HidUsbUser main routine. It keeps reading input reports until the Esc key is pressed on the HID keyboard. See Listing 23.6 for some example output from this routine.

First, allocate and zero a buffer to receive a report, with the size given in the HIDP_CAPS structure. A keyboard input report is always eight bytes long. However, the InputReportByteLength field in HIDP_CAPS is one longer than this, as the first byte is used to indicate the report ID. For keyboard reports, this first byte will be zero, as report IDs are not used.

Use ReadFile to read the next available input report. If the device can return two different input reports then this call may obtain either of them. Suppose these two reports have different lengths. For the smaller input report, the returned count of bytes transferred will be less than the buffer size you passed.

Listing 23.3 Reading keyboard input reports

DWORD TxdBytes;

char* InputReport = new char[InputReportLen];

assert(InputReport!=NULL);

// Loop until Esc pressed on keyboard

do {

 if (!ReadFile( hHidKbd, InputReport, InputReportLen, &TxdBytes, NULL)) {

  printf("XXX Could not read value %d\n", GetLastError());

  break;

 } else if (TxdBytes==InputReportLen) {

  printf(" Input report %d:", InputReport[0]);

  for(USHORT i=1; i<InputReportLen; i++) printf(" %02X", InputReport[i]);

  printf("\n");

  DecodeInputUsages(InputReport, InputReportLen, HidPreparsedData);

 } else {

  printf("XXX Wrong number of bytes read: %d\n", TxdBytes);

  break;

 }

} while (InputReport[3]!=0x29);

delete InputReport;

What Buttons Were Set in My Report?

You could just look directly at the received buffer and work out what it means. However, the correct HID way to analyze reports is get the HID parsing routines to tell you what usages were set in the report. DecodeInputUsages, shown in Listing 23.4, does just this job. Indeed, it goes further by telling you what changes have occurred since the last input report. It prints out the usages that have just been "made" and the ones have just been "broken".

The HidP_GetButtonsEx function analyses an input report buffer and reports which button usages were set in the report. The output is an array of USAGE_AND_PAGE structures (i.e., usage page and usage values).

DecodeInputUsages must first find out the maximum size for this output array using HidP_MaxUsageListLength, so that a suitably sized array can be allocated. Actually, DecodeInputUsages cheats, as I know the maximum size in advance. I use the MaxPreviousUsages constant to declare fixed-size arrays; this avoids allocating and freeing buffers all over the place.

The maximum usage list length for a keyboard report is 14. This is made up of the six keys that the HID Report descriptor says can be pressed simultaneously, and the eight modifier keys, again that the Report descriptor says can be pressed simultaneously. Obviously, it is extremely unlikely that 14 keys can be pressed at the same time.

HidP_GetButtonsEx analyses the input report just received and fills in the array of USAGE_AND_PAGE structures, called Usages. The ValidUsages variable is filled with the number of valid elements in this array. DecodeInputUsages simply prints out the usage page and usage for each of these valid key presses.

The next job, if desired, is to work out what has changed since the last input report. The HidP_UsageListDifference function does this. It is passed the previous usage list and the current usage list as input. It fills in two further arrays, one with a list of usages that have been "made" (or just arrived), and one with a list of usages that have just "broken" (or gone away). DecodeInputUsages prints out these two arrays. Note that HidP_UsageListDifference deals with arrays of USAGE values, not USAGE_AND_PAGEs. In DecodeInputUsages all the input usages are in the keyboard usage page, so this is not a problem.

Listing 23.4 Decoding input report usages

const ULONG MaxPreviousUsages = 14;

USAGE_AND_PAGE Usages[MaxPreviousUsages];

USAGE PreviousUsages[MaxPreviousUsages];

void DecodeInputUsages(char* KbdReport, USHORT KbdReportLen, PHIDP_PREPARSED_DATA HidPreparsedData) {

 // Get max number of USAGE_ANQ_PAGEs required for all input reports in

 // top-level collection

 ULONG MaxUsages = HidP_MaxUsageListLength(HidP_Input, 0, HidPreparsedData);

 if (MaxUsages==0 || MaxUsages>MaxPreviousUsages) {

  printf("XXX Invalid HidP_MaxUsageListLength returned %d\n", MaxUsages);

  return;

 }

 // Get usages set in given keyboard report

 ULONG ValidUsages = MaxUsages;

 NTSTATUS status = HidP_GetButtonsEx(HidP_Input, 0, Usages, &ValidUsages, HidPreparsedData, KbdReport, KbdReportLen);

 if (status==HIDP_STATUS_SUCCESS) {

  USAGE CurrentUsages[MaxPreviousUsages];

  USAGE BreakUsages[MaxPreviousUsages];

  USAGE MakeUsages[MaxPreviousUsages];

  // Show current usages

  memset(CurrentUsages, 0, sizeof(CurrentUsages));

  printf(" Usages set: ");

  for (ULONG i=0; i<ValidUsages; i++) {

   printf( " %02X:%02X", Usages[i].UsagePage, Usages[i].Usage);

   CurrentUsages[i] = Usages[i].Usage;

  }

  // Work out differences compared to previous usages

  HidP_UsageListDifference(PreviousUsages, CurrentUsages, BreakUsages, MakeUsages, MaxUsages);

  // Print out usages broken and made

  printf(" (Break: ");

  for (i=0; i<MaxUsages; i++) {

   if (BreakUsages[i]==0) break;

   printf(" %02X", BreakUsages[i]);

  }

  printf(") (Make: ");

  for(i=0; i<MaxUsages; i++) {

   if (MakeUsages[i]==0) break;

   printf(" %02X", MakeUsages[i]);

  }

  printf(")\n\n");

  // Save previous usages

  memcpy(PreviousUsages, CurrentUsages, MaxUsages*sizeof(USAGE));

 }

}

The HidP_GetButtons function can be used if you are only looking for buttons in a particular usage page. Both HidP_GetButtons and HidP_GetButtonsEx have parameters that let you look for buttons in a particular collection.

What Values Were Set in My Report?

Use the HidP_GetUsageValue, HidP_GetScaledUsageValue, or HidP_GetUsageValueArray functions to retrieve control values.

Sending Output Reports

You send HID output reports using the Win32 WriteFile function. While you can build the output buffer by hand, it is safer to use the HID parsing routines to fill in the buffer.

The SetLEDs function shown in Listing 23.5 shows how a single output report is sent to set the state of the HID keyboard LEDs. It has three optional parameters, which must contain the individual LED usages that you want set on. This line in the HidUsbUser main routine shows how to turn the ScrollLock LED on and turn the other LEDs off.

SetLEDs(hHidKbd, OutputReportLen, HidPreparsedData, HID_USAGE_LED_SCROLL_LOCK);

A keyboard output report consists of just one byte. However, as before, the output buffer must have an extra byte at the beginning for the report ID. This is set to zero in this case, as keyboards do not use report IDs. SetLEDs allocates an output buffer and zeroes it.

SetLEDs must now build an array of USAGEs, filled with any of its optional parameters that are non-zero. This array and its length are passed to HidP_SetButtons. HidP_SetButtons builds the output buffer in the correct format. SetLEDs then simply calls WriteFile to send off the output report.

You can call HidP_SetButtons more than once for a single output report. Indeed, you may also want to call one of the set value functions, if the output report must contain values. Set values into an output report using the HidP_SetUsageValue, HidP_SetScaledUsageValue, and HidP_SetUsageValueArray functions.

Be careful if you are using a HID device that has more than one output report. HidP_SetButtons can only build one output report at a time. HidP_SetButtons sets the report ID, if appropriate. If a second call to HidP_SetButtons tries to set a usage that is not in the first report, it fails with a suitable error code. However, if you start afresh with a new output report and repeat the second call to HidP_SetButtons, it should work this time. This whole process makes it particularly laborious to set output usages in a way that is truly independent of the device that is attached.

Listing 23.5 Setting keyboard LEDs using an output HID report

void SetLEDs(HANDLE hHidKbd, USHORT OutputReportLen, PHIDP_PREPARSED_DATA HidPreparsedData,

 USAGE Usage1/*=0*/, USAGE Usage2/*=0*/, USAGE Usage3/*=0*/) {

 // Build Output report from given usage(s)

 char* OutputReport = new char[OutputReportLen];

 assert(OutputReport!=NULL);

 memset(OutputReport, 0, OutputReportLen);

 USAGE UsageList[3];

 UsageList[0] = Usage1;

 UsageList[1] = Usage2;

 UsageList[2] = Usage3;

 ULONG UsageLength = 0;

 if (Usage1!=0) {

  UsageLength++;

  if (Usage2!=0) {

   UsageLength++;

   if (Usage3!=0) {

    UsageLength++;

   }

  }

 }

 // Convert usages into an output report

 NTSTATUS status = HidP_SetButtons(HidP_Output, HID_USAGE_PAGE_LED, 0, UsageList, &UsageLength, HidPreparsedData, OutputReport, OutputReportLen);

 if (status!=HIDP_STATUS_SUCCESS) {

  delete OutputReport;

  return;

 }

 printf(" Output report: ");

 for (ULONG i=1; i<OutputReportLen; i++) printf(" %02X", OutputReport[i]);

 printf("\n");

 // Send off output report

 DWORD TxdBytes;

 if (!WriteFile(hHidKbd, OutputReport, OutputReportLen, &TxdBytes, NULL))

  printf("XXX Could not write value %d\n", GetLastError());

 else if (TxdBytes==OutputReportLen) printf(" Wrote output report 0K\n");

 else printf("XXX Wrong number of bytes written: %d\n",TxdBytes);

 delete OutputReport;

}

Other User Mode HID Client Functions

User mode HID clients can send and receive HID feature reports. Use HidD_GetFeature to get a feature report. The first byte of the buffer must be set to the report ID that you want to receive. The received feature report can be analyzed using HidP_GetButtonsEx, etc., as usual. Use HidD_SetFeature to send a feature report. Beforehand, use HidP_SetButtons, etc., to build up the feature report in the correct format, as usual.

HidD_FlushQueue deletes all pending information from the input queue for this HID device.

The functions HidD_GetNumInputBuffers and HidD_SetNumInputBuffers may be available in the future to let you get or set the ring buffer size used by the HID class driver for this device[58].

Running HidKbdUser

If you have a HID keyboard, you can run HidKbdUser yourself. For the benefit of those who do not, Listing 23.6 shows some example output. As mentioned earlier, HidKbdUser will not run in W2000 as the HID keyboard device cannot be shared.

There is only one HID device in the system. It is opened and HidKbdUser finds that it is a HID keyboard, as the top-level usage page and usage are the correct values. HidKbdUser then prints out the capabilities of the device.

Test 2 reads any input reports from the HID keyboard. In this example, I pressed Ctrl+Alt+Del followed by A, B, C, and Esc. You can see how an input report is produced every time a key is pressed or released. The actual input report is in exactly the same format as the raw USB interrupt transfer shown in Table 21.9.

If you remember from Chapter 21, the USB example driver, UsbKbd, kept on receiving input interrupt data even if no state changes had occurred. The HID class driver sensibly filters out these redundant input reports and only returns data when a key is pressed or released.

Test 3 sends several output reports to flash the LEDs on the keyboard. HidKbdUser calls the SetLEDs function several times for different LED combinations with a short delay between each call.

Listing 23.6 Example HidKbdUser output

Test 1

Symbolic link is \\.\000000000000000b#{4d1e55b2-f16f-11cf-88cb-001111000030}

     Found HID device

     HID attributes: VendorID=046A, ProductID=0001, VersionNumber=0305

     Top level Usage page 1 usage 6

     Found HID keyboard

     InputReportByteLength 9

     OutputReportByteLength 2

     FeatureReportByteLength 0

     NumberLinkCollectionNodes 1

     NumberInputButtonCaps 2

     NumberInputValueCaps 0

     NumberOutputButtonCaps 1

     NumberOutputValueCaps 0

     NumberFeatureButtonCaps 0

     NumberFeatureValueCaps 0

     Input button capabilities

     ButtonCaps[0].UsagePage 7

                  .Usages 224..231

     ButtonCaps[1].UsagePage 7

                  .Usages 0..101

     Output button capabilities

     ButtonCaps[0].UsagePage 8

                  .Usages 1..3

Opened OK

Test 2

     Input report 0: 01 00 00 00 00 00 00 00 Left Ctrl pressed

     Usages set: 07:E0 (Break: ) (Make: E0)

     Input report 0: 05 00 00 00 00 00 00 00 Left Alt pressed

     Usages set: 07:E0 07:E2 (Break: ) (Make: E2)

     Input report 0: 05 00 63 00 00 00 00 00 Del pressed

     Usages set: 07:E0 07:E2 07:63 (Break: ) (Make: 63)

     Input report 0: 05 00 00 00 00 00 00 00 Del released

     Usages set: 07:E0 07:E2 (Break: 63) (Make: )

     Input report 0: 00 00 00 00 00 00 00 00 Left Ctrl & Alt released

     Usages set: (Break: E0 E2) (Make: )

     Input report 0: 00 00 04 00 00 00 00 00 A pressed

     Usages set: 07:04 (Break: ) (Make: 04)

     Input report 0: 00 00 00 00 00 00 00 00 A released

     Usages set: (Break: 04) (Make: )

     Input report 0: 00 00 05 00 00 00 00 00 B pressed

     Usages set: 07:05 (Break: ) (Make: 05)

     Input report 0: 00 00 00 00 00 00 00 00 B released

     Usages set: (Break: 05) (Make: )

     Input report 0: 00 00 06 00 00 00 00 00 C pressed

     Usages set: 07:06 (Break: ) (Make: 06)

     Input report 0: 00 00 00 00 00 00 00 00 C released

     Usages set: (Break: 06) (Make: )

     Input report 0: 00 00 29 00 00 00 00 00 Esc pressed

     Usages set: 07:29 (Break: ) (Make: 29)

Test 3

     Output report: 00

     Wrote output report OK

     Output report: 04

     Wrote output report OK

     Output report: 02

     Wrote output report OK

     Output report: 01

     Wrote output report OK

     Output report: 06

     Wrote output report OK

     Output report: 05

     Wrote output report OK

     Output report: 03

     Wrote output report OK

     Output report: 07

     Wrote output report OK

     Output report: 00

     Wrote output report OK

Test 4

     CloseHandle worked

Kernel Mode HID Clients

A device driver can talk to a HID device using the HID class driver. As mentioned previously, it is far easier to write a user mode application to control a HID device. However, you may find that it is necessary to write a HID client driver (e.g., if you need to implement an existing device API). A kernel client should be more efficient than a user mode client, though speed ought not to be a problem for most human input devices.

Client Types

A kernel mode HID client can take one of two main forms, depending on how it relates to devices. An "AddDevice" HID client uses installation files (as usual) to layer itself above the HID class driver for each device. Alternatively, "Plug and Play Notification" HID client driver is not initially associated with any one device. Instead, it receives notifications when a HID device arrives or disappears. A PnP Notification HID client makes its own device objects if a HID device of interest arrives.

As I have not examined Plug and Play Notification before, this chapter's example driver, HidKbd, uses this technique.

"AddDevice" HID Clients

An "AddDevice" HID client will look like all the previous WDM device drivers. The driver is loaded using installation INF files in the normal way. The driver's AddDevice routine is called when a suitable device is loaded. The driver must handle Plug and Play IRPs in the same way as usual.

An "AddDevice" HID client makes calls to the HID class driver by calling the NextStackDevice as usual. The HID class driver responds to read and write requests as well as various IOCTL IRPs.

As is usual for WDM drivers, your driver's upper edge may be completely different. For example, the kbdhid.sys system keyboard driver has an upper edge that reports 8042 equivalent key presses. However, kbdhid.sys makes HID requests on its lower edge to find the HID keyboard data.

Things can now get complicated. Note that user mode clients will still be able to find the HID device. If they try to send HID requests to the HID device, these requests will be routed to your driver first. If your driver implements some other upper edge, it will not recognize these requests. Or, if you are being very clever, you could recognize when HID operations have been requested and route them straight through to the HID class driver. If you do this job, you are acting as a HID filter driver.

All in all, it is probably easier if you do not use the "AddDevice" device technique when writing a HID client driver. Therefore, I shall take a close look at how to write a Plug and Play Notification client. However, kernel mode PnP Notification does not seem to work in Windows 98, so you will have to write an "AddDevice" client.

"PnP Notification" HID Clients

A PnP Notification HID client is usually an NT style driver. Therefore, it is not loaded as a result of a device being plugged in. As an NT style driver, it is usually loaded when the system boots up. Installation INF files are not used. Instead, you must write a custom installation program, as described in Chapter 11.

The DriverEntry routine calls the IoRegisterPlugPlayNotification function to ask to receive any device interface change events for a particular device interface GUID. The driver AbcUnload routine calls IoUnregisterPlugPlayNotification to indicate that it no longer wants to receive such notifications.

In this case, the device interface change callback is informed whenever any HID device is plugged into the system. The HidKbd code then interrogates the device to see if it is a HID keyboard. If it is, it creates a HidKbd device.

Just to complicate matters, HidKbd now has two options. First, it can layer itself over the HID device so that is becomes part of the device stack for this device. However, this suffers from the same drawbacks as the "AddDevice" technique, that user mode requests to the HID device would be routed to HidKbd.

The HidKbd driver takes the second option. HidKbd stores a pointer to the HID device object, but does not layer itself above the HID device. Both HidKbd and a user mode application can therefore call the HID class driver safely.

Plug and Play Notifications

Kernel mode drivers can register to receive three different types of notification events. As far as I can tell, none of these notifications work in Windows 98, as calling IoRegisterPlugPlayNotification seems to hang the system. Therefore, ail the subsequent discussion here applies to Windows 2000 only. The Beta 2 version of W2000 let the HidKbd driver access the HID keyboard. Later versions do not, so most of the code in HidKbd will now not run.

Table 23.2 shows how to call IoRegisterPlugPlayNotification. The EventCategory parameter specifies what type of event you want to receive. HidKbd only asks for device interface change events, EventCategoryDeviceInterfaceChange, and so passes the relevant GUID as the EventCategoryData parameter. It also specifies the PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES flag as the EventCategoryFlags parameter. This means that it receives notifications straightaway for any existing devices that support the given interface.

If you register to receive EventCategoryHardwareProfileChange events, you are supposed to receive hardware profile change events. The callback is told whether a Query Change, Change Complete, or Change Cancelled event occurred.

Registering for EventCategoryTargetDeviceChange events asks for notifications when a target device is removed. You must pass a PFILE_OBJECT as the EventCategoryFlags. The callback is told whether a Query Remove, Remove Complete, or Remove Cancelled event occurred. In my mind, there is a fundamental flaw to this notification. You must pass a file object to IoRegisterPlugPlayNotification. To have a file object pointer, you must have opened a file. If a file is open on a device, Windows 2000 automatically stops any PnP remove request for the device. When I tried it, my target device callback received a Query Remove event followed straightaway by a Remove Cancelled event. It seems as though registering for target device notifications automatically stops any remove requests from completing.

Table 23.2 IoRegisterPlugPlayNotification function

NTSTATUS IoRegisterPlugPlayNotification(IRQL==PASSIVE_LEVEL)
ParameterDescription
IN IO_NOTIFICATION_EVENT_CATEGORY EventCategoryEventCategoryDeviceInterfaceChange EventCategoryHardwareProfileChange or EventCategoryTargetDeviceChange
IN ULONG EventCategoryFlagsOptionally PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES
IN PVOID EventCategoryDataDevice GUID, NULL, or file object, respectively
IN PDRIVER_OBJECT DriverObjectThe driver object
IN PDRIVER_NOTIFICATION_CALLBACK_ROUTINE CallbackRoutineYour callback routine name
IN PVOID ContextContext to pass to your callback
OUT PVOID *NotificationEntryOutput value to pass to IoUnregisterPlugPlayNotification

Device Interface Change Notifications

Anyway, device interface change notifications do seem to work, so let's look at the HidKbd device interface change callback, HidKbdDicCallback shown in Listing 23.7. Each callback receives the context pointer and a notification structure pointer. For device interface change events, this is a pointer to a DEVICE_INTERFACE_CHANGE_NOTIFICATION structure.

The notification structure Event GUID field says what type of event has occurred. The SymbolicLinkName UNICODE_STRING field can be used to open a handle to the device.

When a new device arrives, Event contains GUID_DEVICE_INTERFACE_ARRIVAL For DeviceRemoval events, Event is GUID_DEVICE_INTERFACE_REMOVAL. HidKbdDicCallback uses the IsEqualGUID macro to detect each of these events. For Device Arrival events, the CreateDevice routine is called, and for Device Removals, DeleteDevice is called.

Listing 23.7 PnP device interface change notification callback

NTSTATUS HidKbdDicCallback(IN PVOID NotificationStructure, IN PVOID Context) {

 PDEVICE_INTERFACE_CHANGE_NOTIFICATION dicn = (PDEVICE_INTERFACE_CHANGE_NOTIFICATION)NotificationStructure;

 PDRIVER_OBJECT DriverObject = (PDRIVER_OBJECT)Context;

 if (IsEqualGUID(dicn->Event, GUID_DEVICE_INTERFACE_ARRIVAL)) {

  DebugPrint("Device arrival: XT", dicn->SymbolicLinkName);

  CreateDevice(DriverObject, dicn->SymbolicLinkName);

 } else if(IsEqualGUID(dicn->Event, GUID_DEVICE_INTERFACE_REMOVAL)) {

  DebugPrint("Device removal: %T", dicn->SymbolicLinkName);

  DeleteDevice(dicn->SymbolicLinkName);

 } else DebugPrint("Some other device event: %T", dicn->SymbolicLinkName);

 return STATUS_SUCCESS;

}

HidKbd Devices

Hold onto your hats, as the HidKbd device handling is a bit complicated.

Remember that the CreateDevice routine is called whenever a HID device is added to the system. HidKbd is just looking for a HID keyboard. However, it must cope if a HID device arrives that is not a keyboard, and if two HID keyboards arrive (it is possible).

HidKbd tries to make things simple by only coping with one keyboard. If a second HID keyboard arrives, it is ignored.

HidKbd creates its own device called \\.\HidKbd for the first HID keyboard that arrives. A user mode program can open a handle to this device and issue read requests. HidKbd handles these by calling the HID device to get an input report. HidKbd does not do anything for Write or IOCTL requests.

Has a HID Keyboard Been Found?

The CreateDevice routine shown in Listing 23.8 starts by checking to see if it has already found a HID keyboard. The HidKbdDo global variable stores a pointer to the HidKbd device object; if this is non-NULL, a suitable keyboard has already been found.

The first job is to open a connection to the HID device and see if it is HID keyboard. While CreateDevice could use ZwCreateFile to open a handle to the HID device, the IoGetDeviceObjectPointer routine is what is really needed. IoGetDeviceObjectPointer is passed the symbolic link for a device. If the symbolic link is found, IoGetDeviceObjectPointer issues a Create IRP to the device, passing an empty string as the IRP filename parameter[59]. IoGetDeviceObjectPointer returns two pieces of information: the device object pointer and the PFILE_OBJECT pointer.

HidKbd is going to use the HID device object pointer a lot. In addition, it needs a file object pointer when it eventually reads reports from (or writes reports to) the HID class driver. However, in the mean time, CreateDevice closes the file object pointer by calling ObDereferenceObject. Why is this done? If a file is open on a device, Windows 2000 will not let a device be removed. The file must be closed to let device removals take place.

CreateDevice now inspects the HID device capabilities using the GetCapabilities routine, which I describe later. If GetCapabilities finds a HID keyboard, HidKbdUser, like its user mode equivalent, returns a pointer to the preparsed data and the maximum input and output report lengths.

Creating the HidKbd Device

If a HID keyboard is found, CreateDevice can go on to create its own device object. However, it first calls ObReferenceObjectByPointer to reference the HID class driver device object. This ensures that the device object will not disappear from under our feet. When the HidKbd device is deleted, ObDereferenceObject is called to dereference the object. Note that referencing this device object does not stop it from processing removal requests successfully.

The next job is to allocate some memory for a copy of the HID device symbolic link name. This name is stored in a UNICODE_STRING field called HidSymLinkHame in the new device extension. The HidKbd Device Removal event handler only deletes a device if the correct underlying HID device is being removed.

HidKbd now sets up the device name and symbolic link names for the new HidKbd device. These are \Device\HidKbd and \DosDevices\HidKbd respectively, and so the device appears in Win32 as \\.\HidKbd.

HidKbd is finally ready to call IoCreateDevice, with most of the parameters set up as usual. However this time it passes the device type FILE_DEVICE_KEYBOARD, as this seems most appropriate. If the device is created successfully, the global variable, HidKbdDo, stores the device object. Next, CreateDevice sets up the device extension. Finally, CreateDevice calls IoCreateSymbolicLink to create the symbolic link that makes the device visible to Win32 applications.

Note that HidKbd did not call IoAttachDeviceToDeviceStack. If it did make this call, the HidKbd device would be layered over the HID device. Any user mode calls direct to the HID device would arrive at the HidKbd device first, which is not what is wanted.

As a last touch, note that CreateDevice sets up the StackSize field of the HidKbdDo device object. If HidKbd had called IoAttachDeviceToDeviceStack, this routine would have set the IRP stack size to be one greater than the HID device stack size. As it did not call IoAttachDeviceToDeviceStack, HidKbd has to do this same job. Later, HidKbd passes IRPs to the HID class driver for processing. Setting the stack size in this way ensures that there will be enough IRP stack locations available.

Listing 23.8 HidKbd CreateDevice routine

void CreateDevice( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING HidSymLinkName) {

 if (HidKbdDo!=NULL) {

  DebugPrintMsg("Already got HidKbdDo");

  return;

 }

 PFILE_OBJECT HidFileObject = NULL;

 PDEVICE_OBJECT HidDevice;

 NTSTATUS status = IoGetDeviceObjectPointer(HidSymLinkName, FILE_ALL_ACCESS, &HidFileObject, &HidDevice);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("IoGetDeviceObjectPointer failed");

  return;

 }

 // Close file object

 ObDereferenceObject(HidFileObject);

 // Inspect HID capabilities here

 PHIDP_PREPARSED_DATA HidPreparsedData = NULL;

 USHORT HidInputReportLen, HidOutputReportLen;

 if (!GetCapabilities(HidDevice, HidPreparsedData, HidInputReportLen, HidOutputReportLen)) {

  DebugPrintMsg("GetCapabilities failed");

  FreeIfAllocated(HidPreparsedData);

  return;

 }

 // Reference device object

 status = ObReferenceObjectByPointer(HidDevice, FILE_ALL_ACCESS, NULL, Kernel Mode);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("ObReferenceObjectByPointer failed");

  FreeIfAllocated(HidPreparsedData);

  return;

 }

 // Allocate a buffer for the device ext HidSymLinkName

 PWSTR HidSymLinkNameBuffer = (PWSTR)ExAllocatePool(NonPagedPool, HidSymLinkName->MaximumLength);

 if (HidSymLinkNameBuffer==NULL) {

  FreelfAllocated(HidPreparsedData);

  ObDereferenceObject(HidDevice);

  return;

 }

#define NT_DEVICE_NAME L"\\Device\\HidKbd"

#define SYM_LINK_NAME L"\\DosDevices\\HidKbd"

 // Initialise NT and Symbolic link names

 UNICODE_STRING deviceName, linkName;

 RtlInitUnicodeString(&deviceName, NT_DEVICE_NAME);

 RtlInitUnicodeString(&linkName, SYM_LINK_NAME);

 // Create our device object

 status = IoCreateDevice(DriverObject, sizeof(HIDKBD_DEVICE_EXTENSION), &deviceName, FILE_DEVICE_KEYBOARD, 0, FALSE, &HidKbdDo);

 if (!NT_SUCCESS(status)) {

  HidKbdDo = NULL;

  FreeIfAllocated(HidSymLinkNameBuffer);

  FreeIfAllocated(HidPreparsedData);

  ObDereferenceObject(HidDevice);

  return;

 }

 // Set up our device extension

 PHIDKBD_DEVICE_EXTENSION dx = (PHIDKBD_DEVICE_EXTENSION)HidKbdDo->DeviceExtension;

 dx->HidKbdDo = HidKbdDo;

 dx->HidDevice = HidDevice;

 dx->HidPreparsedData = HidPreparsedData;

 dx->HidSymLinkName.Length = 0;

 dx->HidSymLinkName.MaximumLength = HidSymLinkName->MaximumLength;

 dx->HidSymLinkName.Buffer = HidSymLinkNameBuffer;

 RtlCopyUnicodeString(&dx->HidSymLinkName, HidSymLinkName);

 // Create a symbolic link so our device is visible to Win32…

 DebugPrint("Creating symbolic link XT", &linkName);

 status = IoCreateSymbolicLink(&linkName, &deviceName);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("Could not create symbolic link");

  FreeIfAllocated(dx->HidSymLinkName.Buffer);

  IoDeleteDevice(HidKbdDo);

  ObDereferenceObject(HidDevice);

  HidKbdDo = NULL;

  return;

 }

 HidKbdDo->Flags |= DO_BUFFERED_IO;

 HidKbdDo->Flags &= ~DO_DEVICE_INITIALIZING;

 HidKbdDo->StackSize = HidDevice->StackSize+1;

 DebugPrintMsg("Device created OK");

}

Deleting the HidKbd Device

The HidKbd device must be deleted in two circumstances. First, if HidKbd is notified that the HID device has been removed. Second, if the HidKbd driver is unloaded. The DeleteDevice routine shown in Listing 23.9 handles both these cases. When the driver is unloaded the HidSymLinkName parameter is NULL. However, if a HID device is being removed, HidSymLinkName contains the symbolic link name of the device.

DeleteDevice first checks that a HidKbd device has been created. If one has and a device is being removed, DeleteDevice calls RtlCompareUnicodeString to see if the device name matches the one to which HidKbd refers. If it is a different HID device, nothing more is done.

Before the device is deleted, DeleteDevice must free any memory that is associated with it (i.e., the preparsed data and the buffer for the copy of the symbolic link name). DeleteDevice now remakes the HidKbd symbolic link name. The symbolic link name is deleted using IoDeleteSymbolicLink. ObDereferenceObject is called to deference the HID device object. Finally, IoDeleteDevice deletes the HidKbd device.

Listing 23.9 HidKbd DeleteDevice routine

void DeleteDevice(IN PUNICODE_STRING HidSymLinkName) {

 if (HidKbdDo==NULL) return;

 PHIDKBD_DEVICE_EXTENSION dx = (PHIDKBD_DEVICE_EXTENSION)HidKbdDo->DeviceExtension;

 if (HidSymLinkName!=NULL && RtlCompareUnicodeString(HidSymLinkName, &dx->HidSymLinkName, FALSE)!=0) {

  DebugPrintMsg("DeleteDevice: symbolic link does not match our device");

  return;

 }

 DebugPrintMsg("Deleting our device");

 FreelfAllocated(dx->HidPreparsedData);

 FreeIfAllocated(dx->HidSymLinkName.Buffer);

 // Initialise Symbolic link names

 UNICODE_STRING linkName;

 RtlInitUnicodeString(&linkName, SYM_LINK_NAME);

 // Remove symbolic link

 DebugPrint("Deleting symbolic link XT", &linkName);

 IoDeleteSymbolicLink(&linkName);

 ObDereferenceObject(dx->HidDevice);

 IoDeleteDevice(HidKbdDo);

 HidKbdDo = NULL;

}

Getting HID capabilities

The HidKbd GetCapabilities routine is largely the same as its equivalent in HidKbdUser. It returns true if the HID device capabilities indicate that it is a HID keyboard.

However, GetCapabilities must obtain the device attributes and the preparsed data in a different way from HidKbdUser. The GetPreparsedData routine shown in Listing 23.10 does this job.

GetPreparsedData uses two IOCTLs to obtain the information needed from the HID class driver. IOCTL_HID_GET_COLLECTION_INFORMATION returns the attributes and the size of memory buffer required for the preparsed data. Next, IOCTL_HID_GET_COLLECTION_DESCRIPTOR is used to get the preparsed data itself. GetPreparsedData returns a pointer to the preparsed data memory. This memory must eventually be freed.

The CallHidIoctl routine is used to issue the two IOCTLs. This works in a very similar way to the CallUSBDI routine shown in Listing 21.2. It calls IoBuildDeviceIoControlRequest to build the IOCTL passing the IOCTL code, the output buffer details and a completion event. CallHidIoctl calls the HID class driver using IoCallDriver, waiting until it completes using the event, if necessary.

Listing 23.10 HidKbd GetPreparsedData routine

bool GetPreparsedData(IN PDEVICE_OBJECT HidDevice, OUT PHIDP_PREPARSED_DATA HidPreparsedData) {

 HID_COLLECTION_INFORMATION HidCi;

 NTSTATUS status = CallHidIoctl(HidDevice, IOCTL_HID_GET_COLLECTION_INFORMATION, &HidCi, sizeof(HidCi));

 if (!NT_SUCCESS(status)) {

  DebugPrint("IOCTL_HID_GET_COLLECTION_INFORMATION failed %x", status);

  return false;

 }

 DebugPrint("HID attributes: VendorID=%4x, ProductID=%4x, VersionNumber=%4x",

  HidCi.VendorID, HidCi.ProductIO, HidCi.VersionNumber);

 ULONG PreparsedDatalen = HidCi.DescriptorSize;

 DebugPrint("PreparsedDatalen %d", PreparsedDatalen);

 HidPreparsedData = (PHIDP_PREPARSED_DATA)ExAllocatePool(NonPagedPool, PreparsedDatalen);

 if (HidPreparsedData==NULL) {

  DebugPrintMsg("No memory");

  return false;

 }

 status = CallHidIoctl(HidDevice, IOCTL_HID_GET_COLLECTON_DESCRIPTOR, HidPreparsedData, PreparsedDatalen);

 if (!NT_SUCCESS(status)) {

  DebugPrint("IOCTL_HID_GET_COLLECTION_DESCRIPTOR failed %x", status);

  return false;

 }

 return true;

}

Opening and Closing the HidKbd Device

The DDK documentation for the HID class driver read and write handler says that the IRP file object pointer must be valid. HidKbd obtained a file object using IoGetDeviceObjectPointer when it first found a HID device. However, this file handle was closed because it stops the HID device from being removed.

When a user mode application opens a handle to a HidKbd device, the Create IRP handler receives another file object pointer. This same file object pointer is passed in subsequent Read, Write, and Close IRPs, etc.

The HidKbd Create IRP handler, HidKbdCreate, therefore, has to tell the HID class driver about this new file object pointer. It does this by passing the Create IRP to the HID class driver. This is actually very easy to do by putting this extra code in the HidKbdCreate routine.

As HidKbd does not need to process the IRP afterwards, there is no need to set a completion routine.

// Forward IRP to HID class driver device

IoSkipCurrentIrpStackLocation(Irp);

return IoCallDriver(dx->HidDevice, Irp);

The HidKbd Close IRP handler, HidKbdClose, has exactly the same lines in it. This tells the HID class driver that the file handle is being closed.

A side effect of making the HID class driver open a handle for the device is that Windows 2000 will not let the HID device be removed for the duration. This is a perfectly acceptable behavior.

Reading and Writing Data

Our HID kernel mode client is now finally ready to read and write data.

HidKbd currently only supports reading of input reports. The Read IRP expects the provided buffer to be big enough. For a keyboard-input report, the buffer must be at least nine bytes long. The first byte will be 0, with the eight bytes of the input report in the remaining bytes. HidKbd makes no attempt to analyze the data in the same way as HidKbdUser. Instead, it simply returns all the information to the user mode application.

The main Read IRP handler, HidKbdRead, eventually calls ReadHidKbdInputReport, shown in Listing 23.11. ReadHidKbdInputReport is passed the precious file object pointer and a pointer to the buffer. It returns a count of the number of bytes transferred.

ReadHidKbdInputReport looks similar to the CallUSBDI and CallHidIoctl routines described before. This time HidKbd must issue a read request to the HID class driver, so it uses IoBuildSynchronousFsdRequest kernel call to build a suitable Read IRP. An event can be used to wait synchronously for the IRP to be completed, so ReadHidKbdInputReport must be called at PASSIVE_LEVEL IRQL.

By default, IoBuildSynchronousFsdRequest does not insert a file object pointer into the IRP. Therefore, HidKbd must do this job by hand. It calls IoGetNextIrpStackLocation to get the stack location that will be seen by the next driver, the HID class driver. ReadHidKbdInputReport then simply stores the PFILE_OBJECT in the stack FileObject field.

Finally, HidKbd runs IoCallDriver to call the HID class driver. If the IRP is still pending when this call returns, ReadHidKbdInputReport waits for the event to become signalled when the IRP does complete.

I have left out one small part of the story. The DDK says that HID class drivers use Direct I/O for their input and output buffers, not Buffered I/O. Luckily, IoBuildSynchronousFsdRequest sorts this out for us. It checks if the called driver uses Direct I/O. If it does,it allocates the required MDL for the passed input or output buffer (and deallocates it on completion).

Listing 23.11 ReadHidKbdlnputReport routine

NTSTATUS ReadHidKbdInputReport(PFILE_OBJECT FileObject, PVOID Buffer, ULONG& BytesTxd) {

 PHIDKBD_DEVICE_EXTENSION dx = (PHIDKBD_DEVICE_EXTENSION)HidKbdDo->DeviceExtension;

 BytesTxd = 0;

 if (HidKbdDo==NULL) return STATUS_NO_MEDIA_IN_DEVICE;

 IO_STATUS_BLOCK IoStatus;

 IoStatus.Information = 0;

 KEVENT event;

 LARGE_INTEGER FilePointer;

 FilePointer.QuadPart = 0i64;

 // Initialise IRP completion event

 KeInitializeEvent(&event, NotificationEvent, FALSE);

 PIRP Irp = IoBuildSynchronousFsdRequest(IRP_MJ_READ, dx->HidDevice,

  Buffer, dx->HidInputReportLen, &FilePointer, &event, &IoStatus);

 if (Irp==NULL) return STATUS_INSUFFICIENT_RESOURCES;

 // Store file object pointer

 PIO_STACK_LOCATI0N IrpStack = IoGetNextIrpStackLocation(Irp);

 IrpStack->FileObject = FileObject;

 // Call the driver and wait for completion if necessary

 NTSTATUS status = IoCallDriver(dx->HidDevice, Irp);

 if (status == STATUS_PENDING) {

  KeWaitForSingleObject(&event, Suspended, KernelMode, FALSE, NULL);

  status = IoStatus.Status;

 }

 // return IRP completion status

 DebugPrint("ReadHidKbdInputReport: status %x", status);

 BytesTxd = IoStatus.Information;

 return status;

}

Permanently Allocated IRP

A kernel mode HID client is likely to be reading many input reports. Rather than building up a suitable IRP for each call, it is more efficient to have one at the ready all the time. However, this approach is a bit more complicated to set up. The HidKbd driver has this alternative code commented out.

When a HidKbd device is created, it must allocate the IRP that will be reused in all subsequent read and write requests. The SetupHidIrp routine, shown in Listing 23.12, calls IoAllocateIrp to obtain a suitable IRP pointer from the I/O Manager. As IRPs have a variable number of stack locations, SetupHidIrp must pass the desired stack size. The second parameter to IoAllocateIrp should be FALSE for intermediate drivers.

It also makes sense to preallocate a buffer for input and output reports. SetupHidIrp works out the size of buffer needed and allocates it from the nonpaged pool. The final preparatory step is to allocate an MDL for this buffer. Remember that the HID class driver uses Direct I/O and so needs an MDL passed in Read and Write IRPs. The call to IoAllocateMdl makes a suitable MDL out of the buffer pointer.

Listing 23.12 SetupHidIrp routine

void SetupHidIrp(IN PHIDKBD_DEVICE_EXTENSION dx, IN CCHAR StackSize) {

 // Work out maximum size of input and output reports

 dx->HidMaxReportLen = dx->HidInputReportLen;

 if (dx->HidOutputReportLen > dx->HidMaxReportLen) dx->HidMaxReportLen = dx->HidOutputReportLen;

 DebugPrint("Setting up HidIrp etc %d", dx->HidMaxReportLen);

 if( dx->HidMaxReportLen==0) return;

 dx->HidReport = ExAllocatePool(NonPagedPool, dx->HidMaxReportLen);

 if (dx->HidReport==NULL) return;

 dx->HidIrp = IoAlIocateIrp(StackSize, FALSE);

 if (dx->HidIrp==NULL) return;

 dx->HidReportMdl = IoAllocateMdl(dx->HidReport, dx->HidMaxReportLen, FALSE, FALSE, NULL);

 if (dx->HidReportMdl==NULL) {

  IoFreeIrp(dx->HidIrp);

  dx->HidIrp = NULL;

 }

}

When the HidKbd device is removed, the IRP, the buffer memory, and the MDL must be freed. Listing 23.13 shows how the RemoveHidIrp routine does this job using the IoFreeMdl, IoFreeIrp, and ExFreePool routines.

Listing 23.13 RemoveHidIrp routine

void RemoveHidIrp(IN PHIDKBD_DEVICE_EXTENSION dx) (

 DebugPrintMsg("Removing HidIrp etc");

 if (dx->HidReportMdl!=NULL) {

  IoFreeMdl(dx->HidReportMdl);

  dx->HidReportMdl = NULL;

 }

 if (dx->HidIrp!=NULL) {

  IoFreeIrp(dx->HidIrp);

  dx->HidIrp = NULL;

 }

 if (dx->HidReport!=NULL) {

  ExFreePool(dx->HidReport);

  dx->HidReport = NULL;

 }

}

I can now discuss how to use this preallocated IRP. Listing 23.14 shows the replacement ReadHidKbdInputReport routine. This time, it cannot use IoBuildSynchronousFsdRequest, so the IRP and its stack must be built by hand.

The IoInitializeIrp call is used to initialize the IRP. IoInitializeIrp incorrectly clears the IRP AllocationFlags field, so this must be preserved. In W2000, IoReuseIrp correctly reinitialises the IRP. ReadHidKbdInputReport then stores the MDL for the buffer in the IRP MdlAddress field. As before, it calls IoGetNextIrpStackLocation to get the next stack location. ReadHidKbdInputReport must set up all the stack parameters carefully: the MajorFunction, the Parameters.Read fields, and the FileObject.

Finally, ReadHidKbdInputReport needs to set a completion routine so that it knows when the IRP has completed. It passes an event to the completion routine. The completion routine sets the event into the signalled state when it is run). ReadHidKbdInputReport waits until the event is set (i.e., when the IRP has been completed by the lower driver. Assuming that the HID driver has returned data, the final job is to copy the data into the user's buffer, using RtlCopyMemory.

The ReadComplete completion routine returns STATUS_MORE_PROCESSING_REQUIRED. This stops the I/O Manager from deleting the IRP. The IRP will be reused so it must not be deleted.

Listing 23.14 New ReadHidKbdInputReport routine

NTSTATUS ReadHidKbdInputReport(PHIDKBD_DEVICE_EXTENSION dx, PFILE_OBJECT FileObject, PVOID Buffer, ULONG& BytesTxd) {

 BytesTxd = 0;

 if (HidKbdDo==NULL || dx->HidIrp==NULL || dx->HidReport==NULL) {

  DebugPrintMsg("No HidIrp");

  return STATUS_INSUFFICIENT_RESOURCES;

 }

 RtlZeroMemory(dx->HidReport, dx->HidMaxReportLen);

 // Initialise IRP completion event

 KEVENT event;

 KeInitializeEvent(&event, NotificationEvent, FALSE);

 // Initialise IRP

 UCHAR AllocationFlags = dx->HidIrp->AllocationFlags;

 IoInitializeIrp(dx->HidIrp, IoSizeOfIrp(HidKbdDo->StackSize), HidKbdDo->StackSize);

 dx->HidIrp->AllocationFlags = AllocationFlags;

 dx->HidIrp->MdlAddress = dx->HidReportMdl;

 PIO_STACK_LOCATION IrpStack = IoGetNextIrpStackLocation(dx->HidIrp);

 IrpStack->MajorFunction = IRP_MJ_READ;

 IrpStack->Parameters.Read.Key = 0;

 IrpStack->Parameters.Read.Length = dx->HidInputReportLen;

 IrpStack->Parameters.Read.ByteOffset.QuadPart = 0;

 IrpStack->FileObject = FileObject;

 IoSetCompletionRoutine(dx->HidIrp, (PIO_COMPLETION_ROUTINE)ReadComplete, &event, TRUE, TRUE, TRUE);

 NTSTATUS status = IoCallDriver(dx->HidDevice, dx->HidIrp);

 if (status == STATUS_PENDING) {

  KeWaitForSingleObject(&event, Suspended, KernelMode, FALSE, NULL);

  status = dx->HidIrp->IoStatus.Status;

 }

 // return IRP completion status

 DebugPrint("ReadHidKbdInputReport: status %x", status);

 BytesTxd = dx->HidIrp->IoStatus.Information;

 if (BytesTxd>0) RtlCopyMemory(Buffer, dx->HidReport, BytesTxd);

 return status;

}

NTSTATUS ReadComplete(IN PDEVICE_OBJECT fdo, IN PIRP Irp, IN PKEVENT Event) {

 KeSetEvent(Event, 0, FALSE);

 return STATUS_MORE_PROCESSING_REQUIRED;

}

Even Better …

Two problems exist with the permanently allocated IRP solution I have just presented. The first is that the driver will not cope with two "simultaneous" read requests as it uses the same buffer in each call. A quick fix to this problem would be to allow only one read request at a time. The next best solution is dropping the shared buffer; an MDL must then be allocated for the user buffer in each read request.

In fact, the best solution is not to use a permanently allocated IRP, but to reuse the Read IRP. If the HidKbd device uses Direct I/O, the operating system will even do the MDL allocation. In this version, the ReadHidKbdInputReport routine only needs to set up the next IRP stack location appropriately. In fact, calling IoCopyCurrentIrpStackLocationToNext will probably do this job just fine.

The second problem with both the earlier techniques of calling the HID class driver is that they can be inefficient. In both the earlier cases, the call to KeWaitForSingleObject forces the current thread to block waiting for the event to become signalled. As HidKbd may operate in the context of a user thread, this may stop any other overlapped operations from running.[60] The solution to this problem is to modify the completion routine. If the completion routine completes the original Read IRP, there is no need for ReadHidKbdInputReport to wait for the IRP completion event.

This technique should be used wherever possible. The HidKbd Create and Close IRP use this technique as they pass their IRPs to the HID class driver, which completes them in due course. However, it is probably still worth using events in the CallHidIoctl routine for two reasons. The first is that HidKbd needs to know the IRP results. Secondly, my guess is that the HID class driver will be able to complete these IOCTLs straightaway, as it should already have the information at hand.

The CallUSBDI routine in the UsbKbd driver is a candidate for this technique, as it is more than likely that the USB class drivers will take some time to process a USBDI request. However, it is usually the case that the USBDI call results are needed. Processing the results in a completion routine is just about possible. However, this will probably lead to code that is very complicated. In the end, it is probably simplest to leave the UsbKbd code as it is.

Other HID Class IOCTLs

The DDK header files define several other HID IOCTLs. However, some of these are used by the HID class driver when it talks to a minidriver. It is not clear if any of these are available to HID clients.

Conclusion

This chapter concludes my look at the Human Input Device class driver. It is straightforward to write a user mode application to communicate with a HID device. A Win32 program looks for devices that use the HID device interface. They must then interrogate each device to see if their capabilities are of interest. The program can then read input reports and send output reports.

If need be, you can write a kernel mode HID client. It is best if you use the Plug and Play Notification technique to find any devices that support the HID device interface. Then, you can get the device capabilities and send and receive reports in a broadly similar way to user mode applications.

Both user mode and kernel mode HID clients can make use of the HID parsing routines. These make it much easier to find HID device's capabilities and to generate and understand reports.


  1. In Windows 98, I found that I could not type on my HID keyboard after a reboot.

  2. This is called Draft#4 compliance. An older Draft#3-compliant device returns the HID descriptor after the endpoint descriptors.

  3. Kernel mode HID clients can get and set the ring buffer size using two IOCTLs.

  4. The device to which you are connecting must accept a Create IRP call with an empty filename.

  5. So far, we have not considered how the kernel calls our driver. However, to be most efficient, we should not block while processing an IRP for very long. Blocking in a Power or PnP IRP handler or a system thread is fine.