Writing a Windows CE Stream Device Driver
As I mentioned earlier, Windows CE device drivers are simply DLLs. So on the surface, writing a device driver would seem to be a simple matter of writing a Windows CE DLL with specific exported entry points. For the most part, this is true. You have only a few issues to deal with when writing a Windows CE device driver.A device driver isn't loaded by the application communicating with the driver. Instead, the Device Manager, Device.exe, loads most drivers, including all stream drivers. This state of affairs affects the driver in two ways. First, an application can't simply call private entry points in a driver as it can in a DLL. The only way an application could directly call an entry point would be if it called LoadLibrary and GetProcAddress to get the address of the entry point so the entry point could be called. This situation would result in the DLL that implemented the driver (notice I'm not calling it a driver anymore) being loaded in the process space of the application, not in the process space of the Device Manager. The problem is that this second copy of the DLL isn't the driver—it's the DLL that implemented the driver. The difference is that the first copy of the DLL (the driver)—when properly loaded by the Device Manager—has some state data associated with it that isn't present in the second copy of the DLL loaded by the application. Perversely, the calls to LoadLibrary and GetProcAddress will succeed because the driver is a DLL. In addition, calling the entry points in the driver results in calling the correct code. The problem is that the code will be acting on data present only in the second copy of the DLL, not in the proper data maintained by the driver. This situation can, and usually does, result in subtle bugs that can confuse and even lock up the hardware the driver is managing. In short, never interact with a driver by calling LoadLibrary and GetProcAddress.The second effect of the driver being loaded by the Device Manager is that if a driver DLL is used for more than one instance of a piece of hardware, for example, on a serial driver being used for both COM1 and COM2, the Device Manager will load the DLL only once. When the driver is "loaded" a second time, the driver's initialization entry point, COM_Init, is simply called again.The reason for this dual use of the same DLL instance is that under Windows CE a DLL is never loaded twice by the same process. Instead, if an application asks to load a DLL again, the original DLL is used and a call is made to DllMain to indicate that a second thread has attached to the DLL. So if the Device Manager, which is simply another process under the operating system, loads the same driver for two different pieces of hardware, the same DLL is used for both instances of the hardware.Drivers written to handle multiple instances of themselves must not store data in global variables because the second instance of the driver would overwrite the data from the first instance. Instead, a multi-instance driver must store its state data in a structure allocated in memory. If multiple instances of the driver are loaded, the driver will allocate a separate state data structure for each instance. The driver can keep track of which instance data structure to use by passing the pointer to the instance data structure back to the Device Manager as its "handle," which is returned by the device driver's Init function.One final issue with Windows CE device drivers is that they can be reentered by the operating system, which means that a driver must be written in a totally thread-safe manner. References to state data must be protected by critical sections, interlock functions, or other thread-safe methods.
The Stream Driver Entry Points
A stream driver exposes 10 external entry points—summarized in the following list—that the Device Manager calls to talk to the driver. I'll describe each entry point in detail in the following sections.
xxx_Init Called when an instance of the driver is loaded
xxx_DeinitCalled when an instance of the driver is unloaded
xxx_Open Called when a driver is opened by an application with CreateFile
xxx_Close Called when a driver is closed by the application with Closehandle
xxx_Read Called when the application calls ReadFile
xxx_Write Called when the application calls WriteFile
xxx_Seek Called when the application calls SetFilePointer
xxx_IOControl Called when the application calls DeviceIoControl
xxx_PowerDownCalled just before the system suspends
xxx_PowerUp Called just before the system resumes
The xxx preceding each function name is the three-character name of the driver if the driver has a name. For example, if the driver is a COM driver, the functions are named COM_Init, COM_Deinit, and so on. For unnamed drivers, those without a prefix value specified in the registry, the entry points are the name without the leading xxx, as in Init and Deinit. Also, although the preceding list describes applications talking to the driver, there's no reason one driver can't open another driver by calling CreateFile and communicate with it just as an application can.
xxx_Init
When the Device Manager first loads an instance of the driver, the Device Manager calls the driver's Init function. The Init function has one of two prototypes. For newer drivers built for Windows CE .NET 4.0 and later, the prototype is
DWORD XXX_Init (LPCTSTR pContext, LPCVOID lpvBusContext);
The first parameter, pContext, typically contains a pointer to a string identifying the Active key created by the Device Manager for the driver. I say typically because an application using RegisterDevice can load the device to pass any value, including 0, in this parameter. The moral of the story is to look for a string but plan for the dwContext value to point to anything. The second parameter is a pointer to driver specific data structure. This pointer is actually whatever the fourth parameter to ActivateDeviceEx so it can be used for whatever data needs to be passed from the caller of ActivateDeviceEx to the driver.The legacy prototype of the Init function is prototyped as
DWORD XXX_Init (DWORD dwContext);
Here again, the first, and this time only, parameter almost always contains a pointer the name of the Active key in the registry. Although the newer function prototype is recommended, drivers using the old Init prototype work just as well.The driver should respond to the Init call by verifying that any hardware that the driver accesses functions correctly. The driver should initialize the hardware, initialize its state, and return a nonzero value. If the driver detects an error during its initialization, it should set the proper error code with SetLastError and return 0 from the Init function. If the Device Manager sees a 0 return value from the Init function, it unloads the driver and removes the Active key for the driver from the registry.The device driver can pass any nonzero value back to the Device Manager. The typical use of this value, which is referred to as the device context handle, is to pass the address of a structure that contains the driver's state data. For drivers that can be multi-instanced (loaded more than once to support more than one instance of a hardware device), the state data of the driver must be independently maintained for each instance of the driver.
xxx_Deinit
The Deinit entry point is called when the driver is unloaded. This entry point must be prototyped as
BOOL XXX_Deinit (DWORD hDeviceContext);
The single parameter is the device-context value the driver returned from the Init call. This value allows the driver to determine which instance of the driver is being unloaded. The driver should respond to this call by powering down any hardware it controls and freeing any memory and resources it owns. The driver will be unloaded following this call.
xxx_Open
The Open entry point to the driver is called when an application or another driver calls CreateFile to open the driver. The entry point is prototyped as
DWORD XXX_Open (DWORD hDeviceContext, DWORD AccessCode, DWORD ShareMode);
The first parameter is the device context value returned by the Init call. The AccessCode and ShareMode parameters are taken directly from CreateFile's dwDesiredAccess and dwShareMode parameters and indicate how the application wants to access (read/write or read only) and share (FILE_SHARE_READ or FILE_SHARE_WRITE) the device. The device driver can refuse the open for any reason by simply returning 0 from the function. If the driver accepts the open call, it returns a nonzero value.The return value is traditionally used, like the device context value returned by the Init call, as a pointer to an open context data structure. If the driver allows only one application to open it at a time, the return value is usually the device context value passed in the first parameter. This arrangement allows all the functions to access the device context structure directly, because one of these two values—the device context or the open context value—is passed in every call to the driver. The open context value returned by the Open function is not the handle returned to the application when the CreateFile function returns.Windows CE typically runs on hardware that's designed so that individual components in the system can be separately powered. Windows CE drivers that are designed to work without the Power Manager typically power the hardware they control only when the device is opened. The driver then removes power when the Close notification is made. This means that the device will be powered on only when an application or another driver is actually using the device.
xxx_Close
The Close entry point is called when an application or driver that has previously opened the driver closes it by calling CloseHandle. The entry point is prototyped as
BOOL XXX_Close (DWORD hOpenContext);
The single parameter is the open context value that the driver returned from the Open call. The driver should power down any hardware and free any memory or open context data associated with the open state.
xxx_Read
The Read entry point is called when an application or another driver calls ReadFile on the device. This entry point is prototyped as
DWORD XXX_Read (DWORD hOpenContext, LPVOID pBuffer, DWORD Count);
The first parameter is the open context value returned by the Open call. The second parameter is a pointer to the calling application's buffer, where the read data is to be copied. The final parameter is the size of the buffer. The driver should return the number of bytes read into the buffer. If an error occurs, the driver should set the proper error code using SetLastError and return <;$MI>1. A return code of 0 is valid and indicates that the driver read no data.A device driver should program defensively when using any passed pointer. The following series of functions tests the validity of a pointer:
BOOL IsBadWritePtr (LPVOID lp, UINT ucb);
BOOL IsBadReadPtr (const void *lp, UINT ucb);
BOOL IsBadCodePtr (FARPROC lpfn);
The parameters are the pointer to be tested and, for the Read and Write tests, the size of the buffer pointed to by the pointer. Each of these functions verifies that the pointer passed is valid for the use tested. However, the access rights of a page can change during the processing of the call. For this reason, always couch any use of the pBuffer pointer in a __try, __except block. This will prevent the driver from causing an exception when the application passes a bad pointer. For example, you could use the following code:
DWORD xxx_Read (DWORD dwOpen, LPVOID pBuffer, DWORD dwCount) {
DWORD dwBytesRead;
// Test the pointer.
if (IsBadReadPtr (pBuffer, dwCount)) {
SetLastError (ERROR_INVALID_PARAMETER);
return -1;
}
__try {
dwBytesRead = InternalRead (pBuffer, dwCount);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
SetLastError (ERROR_INVALID_PARAMETER);
return -1;
}
return dwBytesRead;
}
In the preceding code, the pointer is initially tested by using IsBadReadPtr to see whether it's a valid pointer. The code that actually performs the read is hidden in an internal routine named InternalRead. If that function throws an exception, presumably because of a bad pBuffer pointer or an invalid dwCount value, the function sets the error code to ERROR_INVALID_PARAMETER and returns <;$MI>1 to indicate that an error occurred.
xxx_Write
The Write entry point is called when the application that has opened the device calls WriteFile. The entry point is prototyped as
DWORD XXX_Write (DWORD hOpenContext, LPCVOID pBuffer, DWORD Count);
As with the Read entry point, the three parameters are the open context value returned by the Open call, the pointer to the data buffer containing the data, and the size of the buffer. The function should return the number of bytes written to the device or <;$MI>1 to indicate an error.
xxx_Seek
The Seek entry point is called when an application or driver that has opened the driver calls SetFilePointer on the device handle. The entry point is prototyped as
DWORD XXX_Seek (DWORD hOpenContext, long Amount, WORD Type);
The parameters are what you would expect: the open context value returned from the Open call, the absolute offset value that is passed from the SetFilePointer call, and the type of seek. There are three types of seek: FILE_BEGIN seeks from the start of the device, FILE_CURRENT seeks from the current position, and FILE_END seeks from the end of the device. The Seek function has limited use in a device driver but it is provided for completeness.
xxx_PowerDown
The PowerDown entry point is called when the system is about to suspend. For legacy drivers without a power management interface, the device driver should power down any hardware it controls and save any necessary hardware state. The entry point is prototyped as
void XXX_PowerDown (DWORD hDeviceContext);
The single parameter is the device context handle returned by the Init call.The device driver must not make any Win32 API calls during the processing of this call. Windows CE allows only two functions, SetInterruptEvent and CeSetPowerOnEvent, to be called during the PowerDown notification. SetInterruptEvent tells the kernel to signal the event that the driver's interrupt service thread is waiting for. SetInterruptEvent is prototyped as
BOOL SetInterruptEvent (DWORD idInt);
The single parameter is the interrupt ID of the associated interrupt event.The function CeSetPowerOnEvent is prototyped as
BOOL CeSetPowerOnEvent (HANDLE hEvt);
The parameter is the handle of an event that will be signaled when the system resumes.
xxx_PowerUp
The PowerUp entry point is called when the system resumes. Legacy drivers without a power management interface can use this notification to know when to power up and restore the state to the hardware it controls. The PowerUp notification is prototyped as
void XXX_PowerUp (DWORD hDeviceContext);
The hDeviceContext parameter is the device context handle returned by the Init call. As with the PowerDown call, the device driver can make no Win32 API calls during the processing of this notification.Although the PowerUp notification allows the driver to restore power to the hardware it manages, well-written drivers restore only the minimal power necessary for the device. Typically, the driver will power the hardware only on instruction from the Power Manager.
xxx_IOControl
Because many device drivers don't use the Read, Write, Seek metaphor for their interface, the IOControl entry point becomes the primary entry point for inter- facing with the driver. The IOControl entry point is called when a device or application calls the DeviceIOControl function. The entry point is prototyped as
BOOL XXX_IOControl (DWORD hOpenContext, DWORD dwCode, PBYTE pBufIn,
DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
PDWORD pdwActualOut);
The first parameter is the open context value returned by the Open call. The second parameter, dwCode, is a device-defined value passed by the application to indicate why the call is being made. Unlike Windows NT/2000/XP, Windows CE does very little processing before the IOCTL code is passed to the driver. This means that the device driver developer ought to be able to pick any values for the codes. However, this behavior might change in the future so it's prudent to define IOCTL codes that conform to the format used by the desktop versions of Windows. Basically, this means that the IOCTL codes are created with the CTL_CODE macro, which is defined identically in the Windows Driver Development Kit and the Windows CE Platform Builder. The problem with application developers creating conforming IOCTL code values is that the CTL_CODE macro might not be defined in some SDKs. So, developers are sometimes forced to define CTL_CODE manually to create conforming IOCTL codes.The next two parameters describe the buffer that contains the data being passed to the device. The pBufIn parameter points to the input buffer that contains the data being passed to the driver; the dwLenIn parameter contains the length of the data. The next two parameters are pBufOut and dwLenOut. The parameter pBufOut contains a pointer to the output buffer, and dwLenOut contains the length of that buffer. These parameters aren't required to point to valid buffers. The application calling DeviceIoControl might possibly pass 0s for the buffer pointer parameters. It's up to the device driver to validate the buffer parameters given the IOCTL code being passed.The final parameter is the address of a DWORD value that receives the number of bytes written to the output buffer. The device driver should return TRUE if the function was successful and FALSE otherwise. If an error occurs, the device driver should return an error code using SetLastError.The input and output buffers of DeviceIoControl calls allow for any type of data to be sent to the device and returned to the calling application. Typically, the data is formatted using a structure with fields containing the parameters for the specific call.The serial driver makes extensive use of DeviceIoControl calls to configure the serial hardware. For example, one of the many IOCTL calls is one to set the serial timeout values. To do this, an application allocates a buffer, casts the buffer pointer to a pointer to a COMMTIMEOUTS structure, fills in the structure, and passes the buffer pointer as the input buffer when it calls DeviceIoControl. The driver then receives an IOControl call with the input buffer pointing to the COMMTIMEOUTS structure. I've taken the serial driver's code for processing this IOCTL call and shown a modified version here:
BOOL COM_IOControl (PHW_OPEN_INFO pOpenHead, DWORD dwCode,
PBYTE pBufIn, DWORD dwLenIn,
PBYTE pBufOut, DWORD dwLenOut,
PDWORD pdwActualOut) {
BOOL RetVal = TRUE; // assume success
COMMTIMEOUTS *pComTO;
switch (dwCode) {
case IOCTL_SERIAL_SET_TIMEOUTS :
if ((dwLenIn < sizeof(COMMTIMEOUTS)) || (NULL == pBufIn)) {
SetLastError (ERROR_INVALID_PARAMETER);
RetVal = FALSE;
break;
}
pComTO = (COMMTIMEOUTS *)pBufIn;
ReadIntervalTimeout = pComTO->ReadIntervalTimeout;
ReadTotalTimeoutMultiplier = pComTO->ReadTotalTimeoutMultiplier;
ReadTotalTimeoutConstant = pComTO->ReadTotalTimeoutConstant;
WriteTotalTimeoutMultiplier = pComTO->WriteTotalTimeoutMultiplier;
WriteTotalTimeoutConstant = pComTO->WriteTotalTimeoutConstant;
break;
}
return RetVal;
}
Notice how the serial driver first verifies that the input buffer is at least the size of the timeout structure and that the input pointer is nonzero. If either of these tests fails, the driver sets the error code to ERROR_INVALID_PARAMETER and returns FALSE. Otherwise, the driver assumes that the input buffer points to a COMMTIMEOUTS structure and uses the data in that structure to set the timeout values. Although the preceding example doesn't enclose the pointer access in __try, __except blocks, a more robust driver might.The preceding scheme works fine as long as the data being passed to or from the driver is all contained within the structure. However, if you pass a pointer in the structure and the driver attempts to use the pointer, an exception will occur. To understand why, you have to remember how Windows CE manages memory protection across processes. (At this point, you might want to review the first part of Chapter 21.)As I explained in the preceding chapter, when a thread in an application is running, that application is mapped to slot 0. If that application allocates a buffer, the returned pointer points to the buffer allocated in slot 0. The problem occurs when the application passes that pointer to a device driver. Remember that a device driver is loaded by Device.exe, so when the device driver receives an IOControl call, the device driver and Device.exe are mapped into slot 0. The pointer passed from the application is no longer valid because the buffer it pointed to is no longer mapped into slot 0.
If the pointer is part of the parameter list of a function—for example, the pBufIn parameter passed in the DeviceIoControl—the operating system automatically converts, or maps, the pointer so that it points to the slot containing the calling process. Because any buffer allocated in slot 0 is also allocated in the application's slot, the mapped pointer now points to the buffer allocated before the application made the DeviceIoControl call.The key is that when an application is running, slot 0 contains a clone of the slot it was assigned when the application was launched. So any action to slot 0 is also reflected in the application's slot. This cloning process doesn't copy memory. Instead, the operating system manipulates the page table entries of the processor to duplicate the memory map for the application's slot in slot 0 when that application is running.The operating system takes care of mapping any pointers passed as parameters in a function. However, the operating system can't map any pointers passed in structures during a DeviceIoControl call because it has no idea what data is being passed to the input and output buffers of a DeviceIoControl call. To use pointers passed in a structure, the device driver must manually map the pointer.You can manually map a pointer using the following function:
LPVOID MapPtrToProcess (LPVOID lpv, HANDLE hProc);
The first parameter is the pointer to be mapped. The second parameter is the handle of the process that contains the buffer pointed to by the first parameter. To get the handle of the process, a driver needs to know the handle of the application calling the driver, which you can query by using the following function:
HANDLE GetCallerProcess (void);
Typically, these two functions are combined into one line of code, as in
pMapped = MapPtrToProcess (pIn, GetCallerProcess());
The application can also map a pointer before it passes it to a device driver, although this is rarely done. To do this, an application queries its own process handle using
HANDLE GetCurrentProcess (void);
Although both GetCurrentProcess and GetCallerProcess are defined as returning handles, these are actually pseudohandles and therefore don't need to be closed. For programmers using eMbedded Visual C++ to build a driver, MapPtrToProcess and GetCallerProcess are not prototyped in the standard include files. If you want to use these functions without warnings, add function prototypes to the include files for the driver.
As an example, assume a driver has an IOCTL function to checksum a series of buffers. Because the buffers are disjointed, the pointers to the buffers are passed to the driver in a structure. The driver must map each pointer in the structure, checksum the data in the buffers, and return the result, as in the following code:
#define IOCTL_CHECKSUM 2
#define MAX_BUFFS 5
typedef struct {
int nSize;
PBYTE pData;
} BUFDAT, *PBUFDAT;
typedef struct {
int nBuffs;
BUFDAT bd[MAX_BUFFS];
} CHKSUMSTRUCT, *PCHKSUMSTRUCT;
DWORD xxx_IOControl (DWORD dwOpen, DWORD dwCode, PBYTE pIn, DWORD dwIn,
PBYTE pOut, DWORD dwOut, DWORD *pdwBytesWritten) {
switch (dwCode) {
case IOCTL_CHECKSUM:
{
PCHKSUMSTRUCT pchs;
DWORD dwSum = 0;
PBYTE pData;
int i, j;
// Verify the input parameters.
if (!pIn || (dwIn < sizeof (CHKSUMSTRUCT)) ||
!pOut || (dwOut < sizeof (DWORD))) {
SetLastError (ERROR_INVALID_PARAMETER);
return FALSE;
}
// Perform the checksum. Protect against bad pointers.
pchs = (PCHKSUMSTRUCT)pIn;
__try {
for (i = 0; (i < pchs->nBuffs) && (i < MAX_BUFFS); i++) {
// Map the pointer to something the driver can use.
pData = (PBYTE)MapPtrToProcess (pchs->bd[i].pData,
GetCallerProcess());
// Checksum the buffer.
for (j = 0; j < pchs->bd[i].nSize; j++)
dwSum += *pData++;
}
// Write out the result.
*(DWORD *)pOut = dwSum;
*pdwBytesWritten = sizeof (DWORD);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
SetLastError (ERROR_INVALID_PARAMETER);
return FALSE;
}
}
return TRUE;
default:
SetLastError (ERROR_INVALID_PARAMETER);
return FALSE;
}
SetLastError (err);
DEBUGMSG (ZONE_FUNC, (DTAG TEXT("GEN_IOControl--\r\n")));
return TRUE;
}
In the preceding code, the driver has one IOCTL command, IOCTL_CHECKSUM. When this command is received, the driver uses the structures passed in the input buffer to locate the data buffers, map the pointers to those buffers, and perform a checksum on the data they contain.The 10 entry points that I described in this section, from Init to IOControl, are all that a driver needs to export to support the Windows CE stream driver interface. Now let's look at how IOCTL commands have been organized.
Device Interface Classes
In a generic sense, the driver is free to define any set of commands to respond to in the IOControl function. However, it would be nice if drivers that implement similar functions agreed on a set of common IOCTL commands that would be implemented by all the common drivers. In addition, there is additional functionality that all drivers may optionally implement. For drivers that implement this common functionality, it would be convenient if they all responded to the same set of IOCTL commands.
Driver interface classes are a way to organize and describe these common IOCTL commands. For example, Windows CE defines a set of IOCTL commands that are used by the Power Manager to control the power use of a driver. Drivers that respond to these power management IOCTLs are said to support the power management interface class. The list of driver interface classes grows with each release of Windows CE, but here is a short summary:
Power Management interface
Block Driver interface
Card services interface
Keyboard interface
NDIS miniport interface
Generic Stream interface
In addition to grouping like sets of IOCTL commands, device drivers can advertise their support of one or more interfaces. Other drivers, or even applications, can be informed when a driver is loaded that supports a given interface. Interface classes are uniquely identified with a GUID defined in the Platform Builder include files. Unfortunately, the GUID definitions are distributed across the different include files relevant to the different driver types related to the specific interface so finding them can be a challenge.
Advertising an Interface
Drivers that support a given interface need to tell the system that they support it. Advertising support for an interface can be accomplished in a couple of ways. First, the registry key specifying the driver can contain an IClass value that specifies one or more GUIDs identifying the interface classes the driver supports. For drivers that support a single interface, the IClass value is a string. For drivers that support multiple interfaces, the IClass value is a multi-z string with each individual string containing a GUID.A driver can manually advertise an interface by calling AdvertiseInterface defined as
BOOL AdvertiseInterface (const GUID* devclass, LPCWSTR name, BOOL fAdd);
The first parameter is the GUID for the interface being advertised. The second parameter is a string that uniquely identifies the name of the driver. The easiest way to do this is to provide the name of the driver, such as DSK1:. Recall that the name of a driver can be found in its Active key. The last parameter, fAdd, should be TRUE if the interface is now available and FALSE if the interface is no longer available. It is important to advertise the removal of the interface if the driver is being removed. Otherwise the Device Manager won't free the memory used to track the interface.
Monitoring for an Interface
Applications or drivers can ask to be notified when a driver advertises an interface being either created or removed. To be notified, a message queue should be created with read access. Set the maximum message length to MAX_DEVCLASS_NAMELEN. The message queue handle is then passed to the RequestDeviceNotifications function defined as:
HANDLE RequestDeviceNotifications (const GUID* devclass, HANDLE hMsgQ,
BOOL fAll);
The first parameter is a string representing the GUID of the interface that the application or driver wants to monitor. The string PMCLASS_GENERIC_DEVICE provides a method for being notified when any power-managed stream device is loaded or unloaded. This parameter can be set to NULL to receive all notifications. However, it isn't recommended to monitor all interfaces for performance reasons. The second parameter is the handle to the previously created message queue. The final parameter is a Boolean that should be set to TRUE to receive all past notifications or FALSE to only receive notifications from the time of the call forward.After the call, the application or driver should create a thread to block on the message queue handle that will be signaled when a message is inserted in the queue. The message format depends on the specific notification being sent.To stop the notifications, call the function StopDeviceNotifications prototyped as
BOOL StopDeviceNotifications (HANDLE h);
The only parameter is the handle returned by RequestDeviceNotifications. The interface class scheme provides a handy way for a developer to know what IOCTL commands to support for a given driver. The classic example of this system is power management. The power management methodology was radically redesigned with the release of Windows CE .NET 4.0. However, the stream interface couldn't be changed without causing all the drivers to be redesigned. Instead, the new power management support was exposed through a newly defined power management interface class.
Device Driver Power Management
Windows CE .NET introduced a new Power Manager that greatly increased the power management capabilities of the systems. The basics of this Power Manager are discussed in Chapter 21. Device drivers support the Power Manager by exposing a power management interface that allows the Power Manager to query the power capabilities of the device and to control its state. The control of the Power Manager is tempered by the actual response of the driver, which might not be in position to change its power state at the time of the request.
Power Management Functions for Devices
The power state of a device is defined to be one of the following:
D0Device fully powered. All devices are fully powered and running.
D1Device is fully functional, but in a power-saving mode.
D2Device is in standby.
D3Device is in sleep mode.
D4Device is unpowered.
These power states are defined in CEDEVICE_POWER_STATE enumeration, which also defines additional values for PwrDeviceUnspecified and PwrDeviceMaximum.When a device wants to set its own power state, it should call the DevicePowerNotify function defined as
DWORD DevicePowerNotify (PVOID pvDevice, CEDEVICE_POWER_STATE DeviceState,
DWORD Flags);
The pvDevice parameter points to a string naming the device driver to change. The second parameter is CEDEVICE_POWER_STATE enumeration. The dwDeviceFlags parameter should be set to POWER_NAME.When changing its own power state, the device should not immediately change to the state requested in the SetDevicePower call. Instead, the device should wait until it is instructed to change its power state through an IOCTL command sent by the Power Manager. The driver should not assume that just because it requests a given state that the Power Manager will set the device to that state. There might be system reasons for leaving the device in a higher power state.Now let's look at the IOCTL commands that are sent to a device driver that supports the power management interface class.
IOCTL_POWER_CAPABILITIES
This IOCTL command is sent to query the power capabilities of the device. The input buffer of the IoControl function is filled with a POWER_RELATIONSHIP structure that describes any parent-child relationships between the driver and a bus driver. The output buffer contains a POWER_CAPABILITIES structure that should be filled in by the driver. The structure is defined as
typedef struct _POWER_CAPABILITIES {
UCHAR DeviceDx;
UCHAR WakeFromDx;
UCHAR InrushDx;
DWORD Power[5];
DWORD Latency[5];
DWORD Flags;
} POWER_CAPABILITIES, *PPOWER_CAPABILITIES;
The DeviceDx field is a bitmask that indicates which of the power states, from D0 to Dn, the device driver supports. The WakeFromDx field is also a bitmask. This field indicates which of the device states the hardware can wake from if an external signal is detected by the device. The InrunshDx field indicates which entries of the Power array are valid. The Power array contains entries that specify amount of power used by the device, in milliwatts, for each given power state. The Latency array describes the amount of time, in milliseconds, that it takes the device to return to the D0 state from each of the other power states. Finally, the Flags field should be set to TRUE if the driver wants to receive an IOCTL_REGISTER_POWER_RELATIONSHIP command to manage other child devices.The level of detail involved in filling out the POWER_CAPABILITIES structure can be intimidating. Many drivers only fill out the first field, DeviceDx, to at least indicate to the system which power levels the device supports and set the remaining fields to zero.
IOCTL_REGISTER_POWER_RELATIONSHIP
This command is sent to a driver that wants to control the power management of any child drivers. During this call, the parent driver can inform the Power Manager of any devices it controls.
IOCTL_POWER_GET
This command is sent to the device to query the current power state of the device. The output buffer points to a DWORD that should be set to one of the CEDEVICE_POWER_STATE enumeration values.
IOCTL_POWER_QUERY
This command is sent to ask the device whether it will change to a given power state. The input buffer points to a POWER_RELATIONSHIP structure while the output buffer contains a CEDEVICE_POWER_STATE enumeration containing the power state that the Power Manager wants the device to enter. If the device wishes to reject the request, it should set the CEDEVICE_POWER_STATE enumeration to PwrDeviceUnspecified. Otherwise, the Power Manager assumes the driver is willing to enter the requested power state. The driver shouldn't enter the state on this command. Instead it should wait until it receives an IOCTL_POWER_SET command. Be warned that the simple implementation of the Power Manager in Windows CE doesn't call this IOCTL, so a driver shouldn't depend on receiving this command before an IOCTL_POWER_SET command is received.
IOCTL_POWER_SET
This command is sent to instruct the device to change to a given power state. The input buffer points to a POWER_RELATIONSHIP structure whereas the output buffer contains a CEDEVICE_POWER_STATE enumeration containing the power state that the device should enter. The device should respond by configuring its hardware to match the requested power state.