Brief Device Drivers Overview
What is a device driver? According to its definition, it is software that abstracts the functionality of a physical or virtual device. For instance, network adapters are physical devices whereas a file system or virtual serial port are of the second type. A device driver manages all operations of such devices. In this article, I don’t pretend to make you an expert in device driver development, but rather give you an idea of how to use it for your own purposes as an application developer.
Under Windows CE, device drivers are trusted modules, but they aren’t required to run in kernel mode. You might need to sign your driver with a certificate to run it on WM 5.0.
A couple of words about the driver’s interface: Most Windows CE device drivers follow the common Windows philosophy and implement a Stream interface; in other words, they behave just like a file. Nevertheless, there are drivers with a different architecture; for example, for network or display adapters, keyboards, busses, ports, and so forth. I will show you “stream interface”-based device drivers on a simple power management driver as an example.
Stream drivers have to export the following functions:
- XXX_Init
- XXX_Deinit
- XXX_Open
- XXX_Close
- XXX_IOControl
- XXX_Seek
- XXX_Read
- XXX_Write
Actually, the first two functions are called during RegisterDevice and DeregisterDevice, respectively. All the rest of the implements stream the interface. I will discuss a device driver that also deals with power management, so you need the next two functions as well:
- XXX_PowerDown
- XXX_PowerUp
Now, let’s take a look at actual implementation and later usage.
Sample Power Driver
Your sample device driver will perform simple yet important operations: It will notify applications when device power is about to be ON and OFF. The very first sight on the driver’s code will lead you to the XXX_Init function:
PWRDRV_API DWORD PWR_Init(DWORD dwContext) { // initialize the critical section // (used for changing the messaging HWND) InitializeCriticalSection(&g_csMsg); // create messaging working thread if (g_hMessageThread==NULL) { g_hMessageThread = CreateThread(NULL, 0, MessageThreadProc, NULL, 0, &g_dwMessageThreadId); } return TRUE; // return non-zero value }
First of all, the “XXX” placeholder was replaced by “PWR”, which means that later on you can address this device as “PWR1:” and so forth. As was said above, this function is called during RegisterDevice. You will see how to use it later in this article.
So what does PWR_Init do? As you can see, only one simple thing—it starts a thread that periodically polls power state flags and in turn sends notifications if needed:
DWORD WINAPI MessageThreadProc(LPVOID lpParameter) { while (1) { if (g_bPoweredUp && !g_bMsgSent) { g_bMsgSent = TRUE; EnterCriticalSection(&g_csMsg); if (g_hMsgWnd!=NULL) { PostMessage(g_hMsgWnd, WM_PWR_POWERUP, 0, 0); } LeaveCriticalSection(&g_csMsg); } // wait some time before polling again Sleep(1000); } return 0; }
And finally, the PRW_Deinit function cleans up all resources:
PWRDRV_API BOOL PWR_Deinit(DWORD hDeviceContext) { if (g_hMessageThread!=NULL) { TerminateThread(g_hMessageThread, 0); } DeleteCriticalSection(&g_csMsg); return TRUE; }
In reality, you might want to add more initialization/deinitialization code, but for this simple purpose, it is enough. If you use such a driver from the application side, you have to call the open/close functions:
// Register power on notification driver DLL hPwrDevice = RegisterDevice( TEXT("PWR"), // device identifier prefix 1, // device identifier index TEXT("PWRDRV.DLL"), // device driver name 0); // instance information (passed // to XXX_Init) // open power on notification driver g_hPwrDll = CreateFile( TEXT("PWR1:"), // "special" file name GENERIC_READ|GENERIC_WRITE // desired access FILE_SHARE_READ|FILE_SHARE_WRITE, // sharing mode NULL, // security attributes // (=NULL) OPEN_ALWAYS, // creation disposition FILE_ATTRIBUTE_NORMAL, // flags and attributes NULL); // template file // (ignored) ... // Close the power on notification driver if (g_hPwrDll!=NULL) { CloseHandle(g_hPwrDll); } // Deregister the power on notification driver if (hPwrDevice!=NULL) { DeregisterDevice(hPwrDevice); } ...
The next step is to implement the PWR_Open function. Because your driver does nothing, almost all its interface functions will be empty. PWR_Open stands a bit aside, because here you can decide what kind of access to the device you are going to implement: single or multiple. So, by having as such simple code as:
PWRDRV_API DWORD PWR_Open(DWORD hDeviceConext, DWORD dwAccessCode, DWORD dwShareMode) { // return the same value for single access or a different one // for multiple return 1; }
the driver’s behavior can vary. You use the simplest case, so your driver provides single access only. For the implementation of all other stream functions, you will return an error.
Now, turn to core driver’s procedure, PWR_IOControl:
PWRDRV_API BOOL PWR_IOControl( DWORD hOpenContext, // open context handle DWORD dwCode, // I/O control code PBYTE pBufIn, // input buffer DWORD dwLenIn, // input buffer size PBYTE pBufOut, // output buffer DWORD dwLenOut, // output buffer size PDWORD pdwActualOut) // actual output bytes returned { BOOL bRet = FALSE; PDWORD pDatIn = (PDWORD) pBufIn; PDWORD pDatOut = (PDWORD) pBufOut; switch (dwCode) { case IOCTL_PWR_GET_POWERUP_STATE: // return power up notification value in the output buffer *pDatOut = g_bPoweredUp; *pdwActualOut = sizeof(g_bPoweredUp); bRet = TRUE; break; case IOCTL_PWR_RESET_POWERUP_STATE: // reset the power up notification value g_bPoweredUp = FALSE; bRet = TRUE; break; case IOCTL_PWR_ENABLE_POWERUP_MESSAGE: // enable the power up notification messaging by getting the // messaging HWND in the input buffer g_hMsgWnd = (HWND) *pDatIn; bRet = TRUE; break; case IOCTL_PWR_DISABLE_POWERUP_MESSAGE: // disable the power up notification messaging EnterCriticalSection(&g_csMsg); { g_hMsgWnd = NULL; } LeaveCriticalSection(&g_csMsg); bRet = TRUE; break; } return bRet; }
This function is a connection point between your driver and other applications. Regular code may call DeviceIOControl to talk to your driver. You normally provide an API header, something like this:
////////////////////////////////////////////////////////////// // // PWRAPI.H // // Definitions to be included by an application using the // power on notification driver, PWRDRV.DLL. // ////////////////////////////////////////////////////////////// // Power up detected windows message, sent to the application // by the driver #define WM_PWR_POWERUP (WM_USER+0x1000) // IO control functions to be used in DeviceIoControl calls to // the driver as the dwIoControlCode parameter. // // Get the current power up detection value. // The lpOutBuffer parameter should be of size DWORD and // will contain the boolean state of power up detection. #define IOCTL_PWR_GET_POWERUP_STATE 0x00000001 // Reset the power up detection value. // Requires no parameters. #define IOCTL_PWR_RESET_POWERUP_STATE 0x00000002 // Enable the power up detection messaging. // The lpInBuffer parameter should point to the HWND // (not contain the HWND itself) for the window to // receive the power up messages. #define IOCTL_PWR_ENABLE_POWERUP_MESSAGE 0x00000003 // Disable the power up detection messaging. // Requires no parameters. #define IOCTL_PWR_DISABLE_POWERUP_MESSAGE 0x00000004
Third-party applications use this API in the following manner:
DeviceIoControl( g_hPwrDll, // file handle to the driver IOCTL_PWR_ENABLE_POWERUP_MESSAGE, // I/O control code (enable // messages) &g_hWnd, // in buffer (HWND to send // messages to) sizeof(g_hWnd), // in buffer size NULL, // out buffer 0, // out buffer size &dwBytesReturned, // number of bytes returned NULL); // ignored (=NULL)
The last topic is notification handling. There is nothing new there because your driver simply posts a message to the given window. You may easily modify the code to send broadcast messages to all windows. Thus, a typical message handler for an MFC application may look like this:
... ON_MESSAGE(WM_PWR_POWERUP,OnPowerUp) ... LRESULT CYourClass::OnPowerUp(WPARAM wParam, LPARAM lParam) { // do whatever you want to here return 1; }
As a bottom line, such a mechanism allows you to utilize system services in your own applications when standard APIs don’t provide the required support.
Conclusion
This article overviewed a simple device driver and its usage from a regular application. Such a technique may allow your software to utilize a lot of things a standard OS API just can’t provide. Working together with the driver, the application gets additional functionality and can significantly empower itself.
Download
Download the accompanying code’s zip file here.
About the Author
Alex Gusev started to play with mainframes at the end of the 1980s, using Pascal and REXX, but soon switched to C/C++ and Java on different platforms. When mobile PDAs seriously rose their heads in the IT market, Alex did it too. Now, he works at an international retail software company as a team leader of the Mobile R department, making programmers’ lives in the mobile jungles a little bit simpler.