52964.fb2
This chapter looks at the design of the Plug and Play (PnP) system and how PnP drivers fit into a device stack. A device stack is the means by which several layers of drivers can work together to process user requests. The next chapter looks at how to implement Plug and Play in detail, and describes the Wdm2 example. As well as PnP, Wdm2 supports Power Management, as described in Chapter 10.
Plug and Play — in a different form — is available in Windows 95 for virtual device drivers (VxDs). NT 4 does not support Plug and Play. Therefore, the Windows 98 and Windows 2000 support for Plug and Play in the Windows Driver Model is new for both environments. If you have any old NT 4 kernel mode device drivers, Microsoft recommends that you update them to support Plug and Play and Power Management. However, supporting Plug and Play requires a major change to driver code, so you will have to judge whether it is worthwhile.
Although Plug and Play initially looks complicated, it is possible to use the code the Pnp.cpp module described in the next chapter for most drivers. Careful use of the LockDevice and UnlockDevice routines is all that is needed elsewhere.
From a user's point of view, Plug and Play is straightforward. You just plug in some new equipment and Windows finds the device and prompts you for the correct drivers. Then you play with your new device.
The main benefit for users is that there should be no fiddling with DIP switches or jumpers to configure the device. Instead, Windows does all the necessary setting up. This is where your device driver comes in.
1. A standard bus driver detects when a device is added.
2. The device identifiers are used to find your driver.
3. Your driver is loaded and told that a device has been added.
4. A further message tells you what hardware resources to use.
5. Your driver then talks to your device, possibly using the services of a standard driver.
When a piece of equipment is unplugged, Windows detects this and tells your driver that the device has gone.
This chapter first looks at the overall Plug and Play design. It then covers the PnP messages that a driver receives. The bulk of the chapter is spent looking at how drivers work together in device stacks to find devices and process user requests.
The Plug and Play system is designed to satisfy these two goals.
• Cope with new devices when they are added to the system. Devices that are configurable in software must be told what resources or addresses to use.
• Make it easier for drivers to access complicated devices by providing standard drivers. For a relatively complicated bus like the Universal Serial Bus, it makes sense to provide a standard driver that other drivers must use if they want to access the bus.
The Plug and Play system brings both these design problems together. The standard drivers detect when a device is added or removed from the system.
Although Microsoft's solution satisfies both the original design goals, it still has some drawbacks. First, it makes a typical driver more complicated. Second, the device stack structure is complicated to understand, although it is not too difficult to use.
The PnP Manager controls these four main elements of the design.
• Standard bus drivers detect when devices are added or removed.
• Device identifiers are used to determine which driver or drivers to load.
• The following messages are sent to drivers when device related events occur:
"A device has been added"
"Your device has been removed"
"Here are your device's resource assignments"
"Please stop your device while its resources are reassigned"
• Device stacks are constructed so user requests can be processed in stages.
The rest of this chapter discusses this design in detail. The following chapter shows how to put this design into practice. Chapter 11 describes the installation process: how drivers are located and the format of installation INF files.
Detecting Devices
The first aim of the PnP system is to cope with new devices as they added to the system. Table 8.1 summarizes the requirements and the Microsoft solution, together with its one main drawback.
Standard bus drivers detect devices, both at power up and afterwards when devices are added or removed. Each bus driver retrieves one or more identifiers from its devices. These identifiers are used to find an appropriate device driver by looking through all the available installation files. If a suitable driver cannot be found, the user is prompted for a driver disk.
The driver is loaded and its AddDevice entry point is called to tell it that a new device has been found.
The bus driver or the installation file details the hardware resources that a device needs. Arbiters are used to decide what resources to give to each device. A Start Device PnP message tells the driver its device's resource assignments. It can then start to talk to its device properly.
In some cases, the bus driver must perform additional arbitration steps. For example, in the Universal Serial Bus, a device initially responds at a default address. The USB bus driver must assign the device a free USB address before it can use the full bus protocol. In addition, the USB bus driver must reserve some of the bus bandwidth for some types of device. If there is not enough bandwidth available then the device cannot be used.
The main drawback of this design is that resource assignments can be taken away from a device temporarily when it is up and running. Ideally, any I/O operations that are in progress should only pause briefly during this process. This is a complete pain because any I/O operations in progress at the time have to be halted, and any new I/O requests should be held in a queue until the device is restarted. Halting and queuing requests is not a trivial task for device drivers.
Suppose an existing PnP device currently uses IRQ7 and I/O ports 0x378 to 0x37A. The device can also be configured in software to use IRQ5 and ports 0x278 to 0x27A. Suppose a new device is now added that can only use IRQ7 and I/O ports 0x300 to 0x30F. It is possible to accommodate both these devices if the first one has its interrupt line changed from IRQ7 to IRQ5. To make this happen, the first driver must be halted and restarted straightaway with the new resource assignments[16]. If this works, the second device can then be given its resource assignments.
Table 8.1 Plug and Play device detection
Aim | Detect all devices at power up. Load the most appropriate driver. Give each driver their resource assignments. For hot pluggable devices, cope as devices are added and removed |
Solution | Provide enumerator bus drivers to find new devices and detect when devices are added or removed. Each device provides identifiers which arc used to find the most appropriate driver. The bus driver or INF file provides the device's resource requirements. Provide arbiters to decide which resources to allocate to which device. Send messages to indicate device events. |
Drawback | Devices which are running may have to stopped so that their resources can be reassigned. |
Driver Layers
The second aim of the Plug and Play system is to make standard drivers available for other drivers to use.
At the lowest level, a bus or class driver talks directly to the hardware. One or more client drivers are layered on top of such system drivers. These client drivers work at the functional level. For example, a USB printer client driver uses the USB bus driver to send its messages. However, the messages are only understood by the printer firmware. The client driver therefore controls the printer functions, letting the USB system driver handle all the messy low-level communications details.
Each bus driver has one device object that represents the whole bus. The bus driver creates a new device object, called the Physical Device Object (PDO), for each device that it finds on its bus. It provides various device identifiers to the PnP Manager. The PnP Manager finds and loads the most appropriate driver, and calls its AddDevice routine.
The newly loaded driver is called a function driver, as it should know how to control its device's function. The function driver must create its own new device object, called a Functional Device Object (FDO) which stores its information about the device[17]. The FDO is created in a function driver's AddDevice routine. The AddDevice routine then goes on to layer itself above the bus driver PDO.
In this simple case, two device objects refer to the same physical device. The PDO is the bus driver's representation of the device, while the FDO stores the function driver information about the device.
Figure 8.1 shows the situation when two devices are attached to a single bus. The bus driver has two 'child' PDOs, one for each device. A suitable function driver has been found for each device. Each function driver has created an FDO to store information about its device.
The bus driver controls three different device objects. Each child PDO receives requests from the function driver layered about it. In addition, the bus driver has an FDO of its own which it uses to store information about the whole bus. A bus driver might handle child PDO requests by sending them to its FDO for processing.
Just to reiterate, each function driver knows how to control the functions in a device. However, a function driver usually uses the facilities of its underlying bus driver to talk to its device.
Even a virtual device driver like Wdm1 has a device stack. Wdm1 is a function driver and so its device object is an FDO. There is a bus driver underneath Wdm1, the system unknown driver. the unknown bus driver makes a PDO to represent each Wdm1 device. The Unknown driver does some important jobs, even though it does not correspond to a hardware bus. The following chapter describes the jobs that even a minimal bus driver like Unknown must do.
Figure 8.1 A bus driver, two devices, and their function drivers
Device Stacks
When one driver layers its device over another, it forms a device stack. Each device in the stack usually plays some part in processing user requests.
Figure 8.2 shows a generic device stack. A bus driver is always at the bottom of the stack. One or more function drivers do the main device control functions.
Filter drivers are used to modify the behavior of standard device drivers. Rather than rewrite a whole bus or class driver, a filter driver modifies its actions in the area of interest. As the figure shows, there are various types of filter driver. A bus filter driver acts on all devices that attach to a particular bus driver. Class filter drivers are installed in the stack for each device of the specified class. Finally, a device filter driver is installed for one particular device. Lower-level filter drivers are below the function driver, while upper-level drivers are above the function driver in the stack.
Filter drivers are only installed when a device is first installed, so they cannot be inserted once a device stack has been built.
Figure 8.2 Bus, function, and filter drivers
Monolithic and Layered Drivers
When a standard keyboard is found, Windows loads the appropriate driver. This talks directly to the keyboard through its controller electronics. The keyboard driver is described as monolithic because it takes over all the processing required to handle the keyboard (i.e., it does not use other drivers to do its job).
In contrast, USB drivers use a layered approach. To use a USB keyboard, the keyboard driver uses the services of the USB class drivers.
As shown in Chapter 21, the USB class drivers expose an upper edge that drivers higher than it must use. Such drivers can issue USB Request Blocks (URBs), which transmit or receive information from a USB device. The keyboard driver does not care how the USB class drivers do their job.
The situation for the USB class drivers themselves is probably slightly different[18]. They are told where the USB bus controller electronics live, and they make use of the services of the PCI bus driver. However, I suspect that the USB class drivers talk directly to their own hardware. Asking the PCI bus driver to write or read memory would be too slow.
A different approach would have been to let each USB driver talk directly to the USB hardware in a monolithic driver. This would lead to two problems. The first is that each USB driver would need a large amount of code. Duplicating this code would waste memory and writing large drivers is decidedly error prone. The second problem is almost of more significance: how would all these different drivers coordinate their activities? The answer is, of course, with difficulty. Using different layers of drivers solves both these problems.
This section looks at the messages that a PnP driver receives. Its AddDevice routine is called when a device is added. After that, messages are sent using the PnP IRP. The PnP messages have the same major function code IRP_MJ_PNP, but each message uses a different minor function code. Each message has different parameters in the IRP.
A PnP driver must implement an AddDevice routine and handle various PnP IRPs. Table 8.2 is a list of all the minor function codes. The first eight of these minor codes are the most important. The other minor function codes are usually just handled by bus drivers.
Table 8.2 Plug and Play minor function codes
Common PnP IRPs | |
---|---|
IRP_MN_START_DEVICE | Assign resources and start a device |
IRP_MN_QUERY_REMOVE_DEVICE | Ask if a device can be removed |
IRP_MN_CANCEL_REMOVE_DEVICE | Cancel a query remove request |
IRP_MN_REMOVE_DEVICE | Device has been unplugged or uninstalled Deallocate resources and remove a device |
IRP_MN_SURPRISE_REMOVAL (W2000 only) | A user has unexpectedly unplugged a device |
IRP_MN_QUERY_STOP_DEVICE | Ask if a device can be stopped |
IRP_MN_CANCEL_STOP_DEVICE | Cancel a query stop request |
IRP_MN_STOP_DEVICE | Stop a device for resource reallocation |
Unusual PnP IRPs | |
IRP_MN_QUERY_DEVICE_RELATIONS | Ask for PDOs that have certain characteristics |
IRP_MN_QUERY_INTERFACE | Let a driver export a direct-call interface |
IRP_MN_QUERY_CAPABILITIES | Ask about device capabilities (e.g., whether it can be locked or ejected) |
IRP_MN_QUERY_RESOURCES | Get a device's boot configuration resources |
IRP_MN_QUERY_RESOURCE_REQUIREMENTS | Ask what resources a device requires |
IRP_MN_QUERY_DEVICE_TEXT | Get a device's description or location string |
IRP_MN_FILTER_RESOURCE_REQUIREMENTS | Let filter and function drivers filter a device's resource requirements |
IRP_MN_READ_CONFIG | Read configuration space information |
IRP_MN_WRITE_CONFIG | Set configuration space information |
IRP_MN_EJECT | Eject the device from its slot |
IRP_MN_SET_LOCK | Set device locking state |
IRP_MN_QUERY_ID | Get hardware, compatible, and instance IDs for a device |
IRP_MN_QUERY_PNP_DEVICE_STATE | Set bits in a device state bitmap |
IRP_MN_QUERY_BUS_INFORMATION | Get type and instance number of parent bus |
IRP_MN_DEVICE_USAGE_NOTIFICATION | Notify whether a device is in the path of a paging, hibernation, or crash dump file. |
IRP_MN_QUERY_LEGACY_BUS_INFORMATION | Returns legacy bus information (W2000 only) |
Figure 8.3 shows the PnP states for a device and the messages that are sent to change state. The minor function code names are shortened in the figure (e.g., IRP_MN_START_DEVICE is shown as START_DEVICE). In this book, a PnP IRP with an IRP_MN_START_DEVICE minor function code is called a Start Device message. The other PnP function codes are given similar names.
When a device is added to the system, Windows finds the correct driver and calls its DriverEntry routine. Chapter 11 explains how Windows finds the correct drivers.
The PnP Manager then calls the driver's AddDevice routine to tell it that a device has been added. It is at this point that the driver makes its own device object, the Functional Device Object (FDO). However, the driver should not try to access its device hardware yet.
In due course, the driver receives an IRP_MN_START_DEVICE IRP that includes information about the resources the device has been assigned. It can then start talking properly to the device hardware.
If a device is about to be unplugged, Windows asks the driver if it is all right for the device to be removed using an IRP_MN_QUERY_REMOVE_DEVICE IRP. If the driver agrees, an IRP_MN_ REMOVE_DEVICE IRP is sent to remove the device. If the driver does not want its device removed (e.g., if it is in the middle of a long transfer) it fails the remove request. It is then sent an IRP_MN_CANCEL_REMOVE_DEVICE IRP to put it back in the started state.
If a user unexpectedly pulls out a device, the driver is sent an IRP_MN_REMOVE_DEVICE IRP in Windows 98 or an IRP_MN_SURPRISE_REMOVAL IRP in Windows 2000. You have to cope with interrupted transfers as well as you can.
The other main state change occurs when the PnP Manager wishes to reallocate some of the driver's resources. This might happen if a new device of some sort is plugged in, meaning that the resource assignments need to be juggled about. The PnP Manager asks to stop the driver temporarily while its resources are reassigned. Similarly, in response to remove requests, an IRP_MN_QUERY_STOP_DEVICE IRP asks if it is OK to stop your device. If it is, an IRP_MN_STOP_DEVICE IRP is issued to take the device into the stopped state. If not, an IRP_MN_ CANCEL_STOP_DEVICE IRP moves the device back into the started state. While stopped, the driver should not access its device. An IRP_MN_START_DEVICE IRP informs the driver of its new resources and starts the device again.
Figure 8.3 Plug and Play device states and messages
Note that a driver might receive a remove device message while in the stopped or awaiting resources states. These state changes are not shown in the diagram for the sake of clarity.
The next chapter looks in detail at how to handle these state changes. For the moment, I will look at device enumeration and then how layers of PnP drivers work together to form a device stack.
The enumeration process finds all the devices on a user's PC.
As you probably know, most of the hardware devices on a PC motherboard are found at fixed locations. For example, on most PCs, the keyboard controller is something that looks like an 8042 processor that can be accessed at I/O ports 0x60 and 0x64. The keyboard controller interrupts on line IRQ1.
This approach works satisfactorily for the one keyboard controller that every mother-board has. However, with serial ports and the old ISA bus, things soon started to get too complicated for most users. If an old ISA bus card just uses fixed addresses and interrupts, it could easily conflict with another card. An interim solution was to provide jumpers and switches on each card to make it configurable. However, configuring this hardware was too much for most mortals to cope with.
The solution to this problem is to have devices that are configurable by software. This means that a bus driver tells each card or device where it should be located, possibly according to assignments given by the PnP Manager. All the newer buses, such as PCI, USB, and IEEE 1394 are software-configurable. Some PnP ISA devices are configurable in software, as well.
Usually, this works as follows. When a card or device powers up, it is detected by its bus driver. The bus driver uses the slot or port number to interrogate the device. Some configuration information or a device descriptor tells the bus driver what sort of resources the card needs. The bus driver or the PnP Manager then allocates the resources appropriately and tells the card this information, either by sending it a command or by writing to its registers. The card then configures itself to respond at the addresses it has been given. The card or device can then start operating normally.
The PnP Configuration Manager sorts out the basic system resources for drivers: I/O ports, Memory addresses, DMA channels, and Interrupts. However, the bus resources required in a particular bus environment are usually controlled by the bus driver. For example, only the USB drivers know about bandwidth allocation on the USB bus. When a USB client driver tries to configure its device, the USB bus driver decides if its bandwidth requirements can be satisfied.
Some types of device can be reconfigured after they have powered up. Configurable hot-pluggable devices can be plugged in or unplugged while the computer is switched on. The appropriate bus driver detects these changes and allocates or deallocates the card or device's resources. However, subsidiary buses are usually designed so that resources do not need to be reassigned in mid-flow.
When Windows is started, it does not know which devices are attached to the computer. It can work out some basic information for itself, such as how much memory it has, but how does it find out about the rest?
Windows uses drivers to enumerate the available hardware. Enumeration means finding and listing any available devices. Arbitrators are then used to juggle all their resource requirements. An appropriate driver (or drivers) is found for each device. The drivers are then told which resources to use, and off they go.
Figure 8.4 shows how enumeration works. Enumeration starts at the lowest level. The root device in the PC finds the basic chips that are on the motherboard[19]. It finds any simple devices, such as built-in serial ports and the keyboard. It also finds the PCI adapter, the electronics that control the PCI bus.
The PCI bus driver then enumerates and configures any hardware it finds. First, it finds a bridge to the ISA bus. The driver for this then finds a PnP ISA sound card, and the sound card drivers are loaded.
The PCI bus driver also finds a USB bus controller. The USB drivers enumerate the USB bus and find a keyboard and printer attached. These are configured and the appropriate drivers are loaded.
Figure 8.4 Hardware enumeration
Figure 8.5 shows a device tree for part of my PC. A device tree is usually shown growing upwards from a root device at the bottom. The lower-level drivers are the ones that interact with the hardware.
As can be seen, the main keyboard driver can get information from two sources, either the legacy keyboard or a USB keyboard. The USB keyboard driver is layered above the HID and USB class drivers and the PCI bus driver. Whether the keyboard is legacy or USB, Win32 gets the same response: the drivers hide the hardware.
Figure 8.5 Device tree
The advantage of this approach is that each driver builds upon the work undertaken in lower layers, making it much easier to write most drivers. Indeed, it is the only way to write some types of drivers. If you are writing a USB driver, you must access your device through the USB class drivers. This means that you have to learn the specification of the relevant class driver. However, this is a far easier task than writing a huge monolithic driver that works with other similar drivers.
One possible drawback is that all this layering of drivers will take more processing time. While this is certainly a valid criticism, it is likely that a monolithic driver would use similar layering internally, so I/O should not take too much longer. More importantly, drivers that are easier to write are more likely to be stable.
It does not make sense to have huge monolithic drivers. Wherever possible, a series of driver layers are built up, each performing an appropriate task. The advantage of this layering is that it breaks up I/O into a series of manageable tasks. If each layer has a standard specification, it means that a whole layer can be replaced and the higher layers will not know the difference. The lower layers hide the implementation details.
Microsoft has helped this layering process by providing standard bus and class drivers that implement one driver layer for a whole class of devices. For example, the Human Input Device (HID) class driver provides all the common functionality that these devices must implement. A driver that wants to talk to a specific type of HID device will layer itself above the Windows HID class driver. It will make I/O requests to this HID class driver. The HID driver layers itself above other drivers that let it talk to the real world.
As an another example, the USB class drivers are layered internally. Two types of electronics can be used to interface a PC to a physical USB bus: the Open Host Controller Interface (OpenHCI) and the Universal Host Controller Interface (UHCI). Windows chooses either the OpenHCI.sys driver or the UHCD.sys driver as the layer to interface to the electronics. Each driver implements the same upper-edge functionality. Other internal layers of the USB class drivers are built upon this common base.
The PnP system has been designed to work with configurable devices and layers of drivers. A device stack represents the layers of drivers that process requests.
When a bus driver enumerates its bus, it gets the PnP Manager to call each new driver's AddDevice routine for each device it finds. The resources are assigned using the Start Device message. PnP messages are used to stop other drivers while the resources are being rejigged. Appropriate PnP messages are issued when a hot-pluggable device is removed.
The PnP system works with layers of drivers. I/O requests can be passed down to lower-level drivers for processing. A driver can then inspect the results from lower drivers and act accordingly. A driver can also generate new I/O requests to send to lower drivers.
Chapter 5 mentioned briefly that a PnP driver, such as Wdm1, has to deal with several different types of device object.
Each driver layer must create a device object to hold its information about a device. These device objects are arranged in a device stack, as shown in Figure 8.6. Note that it is the device objects that are directly connected together, not the drivers themselves (though each device object obviously knows with which driver it is associated). The arrangement is called a device stack, even though it does not correspond with the usual programmer definition of a stack.
The important point is that layers of device objects are built up. I/O requests are sent to the top of the stack and are gradually sent down the driver layers for processing. The results are sent back up the stack for post-processing.
In many cases, however, IRPs do not simply flow down to the bottom of the device stack and rise back up again. If one driver rejects a request, it can fail the IRPimmediately and send it back up straightaway. Another common scenario is as follows. Suppose one driver receives a read request. To process the read, it might have to issue two read requests to its underlying drivers. The driver issues these two requests in turn and then waits for the results before finally completing its own request (i.e. sending it back up the stack).
Figure 8.6 Device objects
As Figure 8.6 shows, the various device objects have different names. Each device object is, in fact, the same DEVICE_OBJECT structure. Different names are given to each type of device object simply to remind us what type of driver is involved.
The device object at the bottom of the stack is called the Physical Device Object (PDO). A PDO is created and serviced by the appropriate bus driver. For example, the USB bus driver provides the PDO for the USB keyboard and USB printer devices.
The main driver that services a device is called a function driver. An installation INF file may specify that more than one Function driver is put into the device stack.
Each function driver creates its own device object called a Functional Device Object (FDO). As Chapter 5 demonstrated, IoCreateDevice is used to create an FDO. After an FDO is created, it is usually attached to the device stack using IoAttachDeviceToDeviceStack. This returns a pointer to the next device down the stack that is stored in a field called NextStackDevice in the device extension. This typical device driver does not know where it is in the stack, so it passes any requests to NextStackDevice. In a simple case, the next device object might be the same as the PDO.
Some DDK examples refer to the next device in the stack as TopOfStack. I thought that this was slightly misleading, as the next device is not the top of all the device stack. Some other drivers call this field LowerDeviceObject.
A final category of device object is called a Filter Device Object (Filter DO). Filter device drivers are slipped into the driver stack as the stack is built to modify the behavior of other drivers.
The AddDevice routine in each function or filter driver is called whenever a new device stack is built. Each AddDevice routine is passed a pointer to the same bus driver PDO. AddDevice then makes an FDO that is then attached to the device stack. The order in which the driver AddDevice routines are called determines the order of drivers in the stack. In this way, the device stack is built from the bottom up. Similarly, when a device is removed, the device stack is deconstructed by removing the highest drivers first. The PDO serves as the anchor point for the whole device stack, as each driver in the stack is given the same PDO pointer.
This section uses a full example to illustrate two points. First, it shows how I/O requests are handled by a real driver stack. Second, it shows how drivers can have an upper edge that is different from that provided by lower layers.
Figure 8.7 shows how a USB keyboard might be used. A USB keyboard must be accessed via the HID class driver. The figure shows how two possible HID clients talk to the keyboard.
The items in this figure are not discussed in detail. The chapters on USB and HID will explain all. The major information flows are of concern just now.
The USB keyboard driver is a kernel mode HID client that sends requests to the HID class driver in the form of standard IRP_MJ_READ and IRP_MJ_WRITE IRPs. These must be in the right format to be recognized by the HID class driver. Alternatively, a user mode HID application can access the HID keyboard directly (rather than by waiting for standard Windows character messages). It does this using the Win32 ReadFile and WriteFile routines. These calls appear to the HID class driver as IRP_MJ_READ and IRP_MJ_WRITE IRPs. Again, these requests must be in the correct HID format.
Internally, the HID class driver uses one of its minidrivers to talk to the lower drivers. In this case, it is using its USB minidriver to talk to the top of the USB stack. Although it is not shown on the diagram, the main HID class driver actually uses Internal IOCTLs to request I/O from a minidriver.
The HID USB minidriver generates Internal IOCTLs to use the services of the USB class drivers. The most common Internal IOCTL has a control code of IOCTL_INTERNAL_USB_SUBMIT_URB. This submits a USB Request Block (URB) to the USB class driver. To get some input data, the minidriver will almost certainly use the URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER function code within a URB.
As mentioned before, the USB class drivers are layered internally. In this case, the diagram shows that the OpenHCI USB host controller driver is in use. The OpenHCI driver will make some use of its lower driver, the PCI bus driver. However, it will do most of its work by talking directly to the OpenHCI USB Controller hardware. It will read and write memory and controller registers. It will almost certainly handle interrupts and use DMA to transfer information.
The USB controller hardware itself is responsible for the very last stage of the keyboard I/O request handling. It generates the appropriate signals on the USB bus, according to the instructions given to it by the USB class drivers. The USB keyboard understands these signals and responds according to its specification.
When a key is pressed on the keyboard, information percolates its way back up through the chain of drivers, ending up at one of the client drivers as a keypress input report.
Figure 8.7 HID USB Keyboard I/O Request handling
Just to complicate matters, the previous example does not involve just one device stack. Instead, there are actually four, as shown in Table 8.3.
A device stack must always end at a Physical Device Object (PDO). Each bus driver creates a PDO for each device it finds.
1. Starting from the HID Keyboard driver's FDO, going down the stack, there is an FDO created by the HID class driver and a PDO created by the USB bus driver. These three device objects are in the first device stack.
2. The DDK documentation says that the USB class drivers are layered internally, with a hub driver at the top and a host controller driver at the bottom (OpenHCI, in this case).
3. The USB host controller was originally found by the PCI bus driver. Therefore, the host controller itself has an FDO that is layered on top of a PDO created by the PCI bus driver.
4. The PCI bus was originally found by the Windows root bus driver. The final device stack therefore consists of the PCI bus FDO layered on top of the root bus PDO.
This arrangement looks complicated. However, if you are trying to use only the USB class drivers, you do not really care how they process your requests. All the details of the other device stacks are hidden from you.
Table 8.3 USB Keyboard device stacks
Driver | Device stack |
---|---|
HID Keyboard driver | HID Keyboard device FDO |
HID class device FDO | |
PDO (created by USB hub bus driver) | |
USB Hub USB | Hub device FDO |
PDO (created by USB host controller bus driver) | |
USB Host controller | USB Host Controller device FDO |
PDO (created by PCI bus driver) | |
PCI Adapter | PCI device FDO |
PDO (created by root bus driver) |
This example shows how several standard Windows system drivers have been used in the device stack for a USB keyboard. Each driver writer usually needs to know only the specification of the next layer down[20]. The USB Keyboard driver writer needs to understand only the HID class driver specification. Knowledge of the different USB controllers and how to use them is definitely not required.
The upper edge of a driver is the specification of how to use it. The upper edge of the HID class driver responds to standard read and write IRPs. However, the upper edge of the USB class drivers only responds to Internal IOCTL IRPs. This is a sensible option for the USB class drivers, as it is not appropriate for user mode drivers to call them directly. User mode programs could not adhere to the timing requirements of the USB class drivers.
The example also shows that the upper edge presented by a driver does not determine its lower edge. The HID class drivers accept standard read and write IRPs. However, it implements these by sending Internal IOCTLs to its own minidriver. The USB minidriver implements its upper edge by sending URBs to the USB class drivers in its own Internal IOCTLs.
Thus, you have to look at some of the earlier example figures with care. Figure 8.5 shows function, filter, and bus drivers in a smooth hierarchy. In real life, the chain of events is much more complicated, as I/O requests are processed by several drivers.
A filter driver must have the same upper and lower edge because it must slip into the stack without affecting other drivers. A filter can modify or inspect the requests in which it is interested. However, all other requests must be passed down the stack unmodified. It is possible for a filter driver to perform some extra checking, which will mean that some IRPs are never passed down. However, it is vital that user mode applications or higher-level drivers can continue to function normally.
This chapter has presented the background for Plug and Play device drivers, the messages they handle, and how they fit into the WDM device stack. The next chapter looks at how to implement Plug and Play in practice and illustrates it in the Wdm2 device driver.
The first driver must disable its device interrupts, reprogram its hardware, and then enable interrupts again.
Both FDOs and PDOs use exactly the same DEVICE_OBJECT structure.
I don't know for sure because I don't know how they work. And I don't care!
For ACPI systems, the ACPI bus driver implements the root device.
However, note that the USB OpenHCI driver accesses hardware directly, once it has its configuration information from the PCI bus driver.