In this section, I'll describe some additional details about power management, including flags you might need to set in your device object, controlling your device's wake-up feature, arranging for power-down requests after your device has been idle for a predetermined time, and optimizing context restore operations.
Three flag bits in a device object—see Table 8-6—control various aspects of power management. After you call IoCreateDevice in your AddDevice function, all three of these bits will be set to 0, and you can set one or more of them depending on circumstances.
Table 8-6. Power-management flags in DEVICE_OBJECT.
Flag | Brief Description |
---|---|
DO_POWER_PAGABLE | Driver's IRP_MJ_POWER dispatch routine must run at PASSIVE_LEVEL |
DO_POWER_INRUSH | Powering on this device requires a large amount of current |
DO_POWER_NOOP | Device doesn't participate in power management |
Set the DO_POWER_PAGABLE flag if your dispatch function for IRP_MJ_POWER requests must run at PASSIVE_LEVEL. The flag has the name it does because, as you know, paging is allowed at PASSIVE_LEVEL only. If you leave this flag set to 0, the Power Manager is free to send you power requests at DISPATCH_LEVEL. In fact, it always will do so in the current release of Windows 2000.
Set the DO_POWER_INRUSH flag if your device draws so much current when powering up that other devices should not be allowed to power up simultaneously. The problem solved by this flag is familiar to people who've experienced multiple simultaneous spikes of electricity demand at the end of a power outage—having all your appliances trying to cycle on at the same time can blow the main breaker. The Power Manager guarantees that only one inrush device at a time will be powered up. Furthermore, it sends power requests to inrush devices at DISPATCH_LEVEL, which implies that you may not also set the DO_POWER_PAGABLE flag.
The system's ACPI filter driver will set the INRUSH flag in the PDO automatically if the ASL description of the device so indicates. All that's required for the system to properly serialize inrush power is that some device object in the stack have the INRUSH flag set, so you won't need to set the flag in your own device object too. If the system can't automatically determine that you require inrush treatment, however, you would need to set the flag yourself.
Set the DO_POWER_NOOP flag if your driver isn't managing hardware and needn't participate in power management. When PoCallDriver sees this flag set in a device object, it simply completes the IRP with STATUS_SUCCESS without even calling the corresponding driver's dispatch routine.
The settings of the PAGABLE and INRUSH flags need to be consistent in all the device objects for a particular device. If the PDO has the PAGABLE flag set, every device object should also have PAGABLE set. Otherwise, a bug check with the code DRIVER_POWER_STATE_FAILURE may occur. (It's legal for a PAGABLE device to be layered on top of a non-PAGABLE device, just not the other way around.) If a device object has the INRUSH flag set, neither it nor any lower device objects should be PAGABLE, or else an INTERNAL_POWER_ERROR bug check will occur. If you're writing a disk driver, don't forget that you may change back and forth from time to time between pagable and nonpagable status in response to device usage PnP notifications about paging files.
Some devices have a hardware wake-up feature, which allows them to wake up a sleeping computer when an external event occurs. See Figure 8-15. The power switch on the current crop of PCs is such a device. So are many modems and network cards, which are able to listen for incoming calls and packets, respectively.
Figure 8-15. Examples of devices that wake the system.
If your device has a wake-up feature, your function driver has additional power management responsibilities beyond the ones we've already discussed. The first additional responsibility is to handle the IRP_MN_WAIT_WAKE flavor of IRP_MJ_POWER. Most devices don't need to do any processing in their dispatch functions for WAIT_WAKE requests beyond installing a standard I/O completion routine and passing the IRP down the driver stack. The bus drivers for the USB and Peripheral Component Interconnect (PCI) bus, for example, implement the bus specifications for arming, disarming, and detecting wake-up. More explicitly, if your device doesn't have additional features related to device wake-up beyond the ones prescribed by the relevant bus specification, you don't need any special processing.
You want to fail IRP_MN_QUERY_POWER requests that specify a power state incompatible with your wake-up feature. If the query is for a system state, compare the proposed new state with the SystemWake field in the device capabilities structure, which gives the lowest system state from which your device can wake up the system. If the query is for a device state, compare the proposed new state with the DeviceWake field, which gives the lowest device state from which your device can issue the wake-up signal. If the result of the comparison shows that the proposed power state is too low, fail the query with STATUS_INVALID_DEVICE_STATE. Otherwise, process the query in the way I've already discussed.
You need to originate an IRP_MN_WAIT_WAKE at appropriate times. To do this, call PoRequestPowerIrp as illustrated by this code fragment:
typedef struct _DEVICE_EXTENSION { PIRP WaitWakeIrp; }; NTSTATUS SomeFunction(...) { ... POWER_STATE junk; junk.SystemState = pdx->devcaps.SystemWake; status = PoRequestPowerIrp(pdx->Pdo, IRP_MN_WAIT_WAKE, junk, (PREQUEST_POWER_COMPLETE) WaitWakeCallback, pdx, &pdx->WaitWakeIrp); ... } |
The last extra responsibility related to wake-up is to cancel the WAIT_WAKE IRP when it's no longer needed using code like this:
PIRP Irp = (PIRP) InterlockedExchangePointer(&pdx->WaitWakeIrp, NULL); if (Irp) IoCancelIrp(Irp); |
For most devices, you need to perform three tasks when the WAIT_WAKE completes. You should nullify the member of the device extension structure that points to the active WAIT_WAKE IRP. That will prevent some other part of your driver from thinking that the WAIT_WAKE is still active. You should initiate a device set-power IRP to restore power to your device. Some devices might need to perform some sort of device-specific operation to disarm the device's wake-up feature at this point, too. Finally, you might want to automatically reissue a WAIT_WAKE so that your device's wake-up feature remains armed for the future. The first of these tasks—nullifying the WAIT_WAKE IRP pointer—ought to be done in a standard I/O completion routine that your dispatch routine installs. The other two tasks—repowering your device and requesting a new WAIT_WAKE IRP—should be done in the callback routine (WaitWakeCallback in my fragment) that you specify in your call to PoRequestPowerIrp.
NOTE
It looks to me as though it's very difficult to be 100 percent sure that you're calling IoCancelIrp for your WAIT_WAKE request with a valid pointer. You could decide to cancel the IRP a nanosecond before your I/O completion routine nullifies your cached pointer to the IRP. The completion process could run its course, ending with a call to IoFreeIrp from inside the Power Manager as soon as your callback routine returns. Thereafter, IoCancelIrp or the bus driver's cancel routine could try to work with the now-invalid IRP. This is the same "tiger on Main Street" problem that I discussed in Chapter 5. Between us, I and one of the Microsoft developers who reviewed this code came up with an elegant solution that's unfortunately too big to fit in the margin. Please refer to the GENERIC sample on the companion disc.
In the preceding section, I showed you how to launch a WAIT_WAKE IRP, how to cancel one, and what to do when one completes. You should be wondering when you should launch this IRP in the first place.
The first part of the answer to "when?" is that you need a way to know whether the end user wants your device's wake-up feature to be armed. Your driver should arm the wake-up feature unless the end user says not to. The end user will interact with some sort of user interface element (such as a control panel applet similar to POWCPL.DLL) to indicate whether your wake-up feature should be armed when the system powers down. The user interface element communicates in turn with your driver, either by using a private IOCTL interface or by setting a WMI control. You then remember the arm/disarm setting. At some point in the evolution of Windows 2000, user-mode programs will perhaps be able to use the so far unimplemented RequestDeviceWakeup and CancelDeviceWakeupRequest APIs to trigger WMI calls to your driver.
The second part of the answer concerns when you invoke PoRequestPowerIrp to request the WAIT_WAKE. The DDK indicates that you may request a WAIT_WAKE at any time when your device is in the D0 state and a device power transition is not in progress. Good times are when you're told by the end user to enable your wake-up feature and when you process a system power query that will reduce the device power state.
You should disable wake-up (and cancel an outstanding WAIT_WAKE) whenever you're told to do so by the end user and also when you process an IRP_MN_STOP_DEVICE request.
As a general matter, the end user would prefer that your device not draw any power if it isn't being used. You can register with the Power Manager to be sent a low-power device IRP when your device remains idle for a specified period. The mechanics of the idle detection scheme involve two service functions: PoRegisterDeviceForIdleDetection and PoSetDeviceBusy.
To register for idle detection, make this service function call:
pdx->idlecount = PoRegisterDeviceForIdleDetection(pdx->Pdo, ulConservationTimeout, ulPerformanceTimeout, PowerDeviceD3); |
The first argument to PoRegisterDeviceForIdleDetection is the address of the PDO for your device. The second and third arguments specify timeout periods measured in seconds. The conservation period will apply when the system is trying to conserve power, such as when running on battery power. The performance period will apply when the system is trying to maximize performance, such as when running on AC power. The fourth argument specifies the device power state into which you want your device to be forced if it's idle for longer than whichever of the timeout periods applies.
The return value from PoRegisterDeviceForIdleDetection is the address of a long integer that the system uses as a counter. Every second, the Power Manager increments that integer. If it reaches the appropriate timeout value, the Power Manager sends you a device set-power IRP indicating the power state you registered. At various places in your driver, you'll reset this counter to 0 to restart the idle detection period:
if (pdx->idlecount) PoSetDeviceBusy(pdx->idlecount); |
PoSetDeviceBusy is a macro in the WDM.H header file that uncritically dereferences its pointer argument to store a 0. It turns out that PoRegisterDeviceForIdleDetection can return a NULL pointer, so you should check for NULL before calling PoSetDeviceBusy.
Now that I've described what PoSetDeviceBusy does, you can see that its name is slightly misleading. It doesn't tell the Power Manager that your device is "busy," in which case you'd expect to have to make another call later to indicate that your device is no longer "busy." Rather, it indicates that, at the particular instant you use the macro, your device is not idle. I'm not making this point as a mere semantic quibble. If your device is busy with some sort of active request, you'll want to have logic that forestalls idle detection. So, you might want to call PoSetDeviceBusy from many places in your driver: from various dispatch routines, from your StartIo routine, and so on. Basically, you want to make sure that the detection period is longer than the longest time that can elapse between the calls to PoSetDeviceBusy that you make during the normal processing of a request.
NOTE
PoRegisterSystemState allows you to prevent the Power Manager from changing the system power state, but you can't use it to forestall idle timeouts. Besides, it isn't implemented in Windows 98, so calling it is contraindicated for drivers that need to be portable between Windows 2000 and Windows 98.
Picking the idle timeout values isn't necessarily simple. Certain kinds of devices can specify -1 to indicate the standard power policy timeout for their class of device. At the time of this writing, only FILE_DEVICE_DISK and FILE_DEVICE_MASS_STORAGE devices are in this category. While you'll probably want to have default values for the timeout constants, their values should ultimately be under end user control. Underlying the method by which a user gives you these values is a tale of considerable complexity.
Unless your device is one for which the system designers planned a generic idle detection scheme, you'll need to provide a user-mode component that allows the end user to specify timeout values. To fit in best with the rest of the operating system, that piece should be a property page extension to the Power control panel applet. That is, you should provide a user-mode DLL that implements the IShellPropSheetExt and IShellExtInit COM interfaces. This DLL would fit the general description of a shell extension DLL, which is the topic you would research if you wanted to learn all the ins and outs of writing this particular piece of user interface software.
Learning about COM in general and shell extension DLLs in particular seems to me like a case of the tail wagging the dog insofar as driver programming goes. So the WDMIDLE sample on the companion disc includes a shell extension DLL (POWCPL.DLL) that you can copy and adapt. If you install this sample, you'll start noticing a new property page in the Power Options property sheet. See Figure 8-16. POWCPL.DLL uses the user-mode functions we discussed in Chapter 2, "Basic Structure of a WDM Driver," to enumerate all the devices that have registered a GUID_WDMIDLE interface, and it presents their "friendly" names in a list box. It uses a private I/O control (IOCTL) scheme—see Chapter 9, "Specialized Topics"— to query and alter the idle timeout constants used by WDMIDLE.SYS. Using IOCTLs for this purpose gives you a workable scheme for both Windows 2000 and Windows 98. Another possible method uses the COM interfaces that are part of WMI. (See Chapter 10, "Windows Management Instrumentation.") This method is a great deal more cumbersome and doesn't work in the original release of Windows 98, which is why I didn't code POWCPL.DLL to use it.
Figure 8-16. The property page for idle devices.
On the driver side of the user interface is a handler for IRP_MJ_DEVICE_CONTROL to answer queries and honor requests to alter power management settings. The end user expects that settings, once specified, will remain in effect in subsequent sessions. The driver therefore needs to record the current values of the constants in the registry by using the functions I discussed in Chapter 3, "Basic Programming Techniques." Furthermore, at StartDevice time, the driver needs to read those persistent settings from the registry to initialize the driver according to the user's expectations.
All of these details, though important to delivering a polished product, are rather tangential to the issues of power management that I'm discussing in this chapter, so I won't discuss the code here.
If you implement idle detection, you'll also have to provide a way to restore power to your device at some later time—no one else will do it for you. I wrote a function named SendDeviceSetPower to deal with this detail. You would have code like this in the dispatch function for an IRP that needs power:
1 2 3 4 |
NTSTATUS DispatchWrite(IN PDEVICE_OBJECT fdo, IN PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; if (pdx->idlecount) PoSetDeviceBusy(pdx->idlecount); if (pdx->powerstate > PowerDeviceD0) { NTSTATUS status = SendDeviceSetPower(fdo, PowerDeviceD0, FALSE); if (!NT_SUCCESS(status)) return CompleteRequest(Irp, status, 0); } IoMarkIrpPending(Irp); StartPacket(&pdx->dqReadWrite, fdo, Irp, OnCancel); return STATUS_PENDING; } |
In general, we get read and write requests in an arbitrary thread context, so we should not block that thread. When we power ourselves back on, therefore, we return without waiting for the power-up operation to finish. The DEVQUEUE takes care of starting the request when power is finally back.
The SendDeviceSetPower helper routine calls PoRequestPowerIrp directly. The resulting device IRP gets handled in the same way as we've already discussed.
You might want to use an optimization technique in connection with removing and restoring power to your device. Two background facts will help you make sense of the optimization technique. First, the bus driver doesn't always power down a device even when it receives a device set-power IRP. This particular bit of intransigence arises because of the way computers are wired together. There might be one or more power channels, and there might be any random collection of devices wired to any given channel. These devices are said to share a power relation. A particular device can't be powered down unless all the other devices on the same power channel are powered down as well. So, to use the macabre example that I sometimes give my seminar students, suppose the modem you want to power down happens to share a power channel with your computer's heart-lung machine—the system can't power down your modem until the bypass operation is over.
The second background fact is that some devices require a great deal of time to change power. To return to the previous example, suppose that your modem were such a device. At some point, you received and passed along a device set-power request to put your modem to sleep. Unbeknownst to you, however, the bus driver didn't actually power down the modem. When the time comes to restore power, you could save some time if you knew that your modem hadn't lost power. That's where this particular optimization comes into play.
At the time you remove power, you can create and send a power request with the minor function code IRP_MN_POWER_SEQUENCE to the drivers underneath yours. Even though this IRP is technically an IRP_MJ_POWER, you use IoAllocateIrp instead of PoRequestPowerIrp to create it. You still use PoStartNextPowerIrp and PoCallDriver when you handle it, though. The request completes after the bus driver stores three sequence numbers in an array you provide. The sequence numbers indicate how many times your device has been put into the D1, D2, and D3 states. When you're later called upon to restore power, you create and send another IRP_MN_POWER_SEQUENCE request to obtain a new set of sequence numbers. If the new set is the same as the set you captured at power-down time, you know that no state change has occurred and that you can bypass whatever expensive process would be required to restore power.
Since IRP_MN_POWER_SEQUENCE simply optimizes a process that will work without the optimization, you needn't use it. Furthermore, the bus driver needn't support it, and you shouldn't treat failure of a power-sequence request as indicative of any sort of error. The GENERIC sample on disc actually includes code to use the optimization, but I didn't want to further complicate the textual discussion of the state machine by showing it here.