SPOT the Geek and Windows CE Drivers

 

Mike Hall
Microsoft Corporation

March 12, 2004

Applies to:
    Microsoft® Windows® CE .NET

Summary: Sings the praises of new SPOT watches; explains drivers and gives instructions on building and testing a stream driver. (17 printed pages)

Contents

Microsoft Windows ChallengE
Understanding the Purpose of a Device Driver
Dynamic-Link Library
Creating a Stream Driver
Testing the Driver

Watches running Smart Personal Object Technologies (SPOT) became publicly available at this year's Consumer Electronics Show (CES). Fossil and Suunto are the first companies shipping products (a list of watches can be found on the MSN Direct Web site). You might be asking what this has to do with Microsoft® Windows® CE, and that's an excellent question to which I don't have a good answer.

This is a cool (read geeky) watch. I have an Abacus SPOT watch and am impressed with the watch and the MSN® Direct service. I'm currently signed up to receive weather (which for some reason always shows rain in Seattle!), stock quotes, news alerts, instant messages, and my calendar. Interestingly, the SPOT watch automatically adjusts for new time zones—how useful is that! I've been presenting at the Windows® Embedded Essentials events in Korea and Taiwan, so my SPOT watch was still set for Korea time. While I was waiting to clear customs at the Seattle airport, I noticed that the watch had adjusted to Redmond time and within minutes I started to receive news and calendar items.

You are probably wondering how to configure the service—it's actually extremely simple. Each watch contains a unique ID (think GUID). When you power up the watch for the first time the GUID is displayed on the screen. After you power up, you need to go to http://direct.msn.com and complete a registration form, which links your watch GUID to your passport account. Then you sign up for whichever services you are interested in. Right now news, weather, stock, calendar, new watch faces, and instant messages are supported. There are plans to expand on this lineup in the near future with additional channels.

OK, that's enough of SPOT, now let's take a look at device drivers on Windows CE.

Microsoft Windows ChallengE

The Microsoft Embedded Devices Group, in collaboration with the Institute of Electrical and Electronics Engineers, Inc. (IEEE) Computer Society International Design Competition (CSIDC) have invited student teams to participate in the first official Microsoft Windows ChallengE ("ChallengE"). The theme of the 2004 competition is "Making the World a Safer Place." Right now teams are building devices based on Windows CE .NET 4.2. Each team is responsible for building a custom operating system image and any required applications or drivers.

Since many teams are new to Windows CE, we've been receiving a number of questions about how to customize the operating system. Most teams are adding additional peripherals to their base hardware platform and have been asking about device drivers. Therefore, this month's article is going to take you through the process of creating, building, and testing a stream driver on Windows CE. One of the common ways of interfacing with a driver is through IO Control, or IOCTL, so we will also implement this functionality in the driver.

Understanding the Purpose of a Device Driver

Before we dig into the process of writing drivers, it may be good to understand the purpose of a device driver: Drivers abstract the underlying hardware from the operating system and better still from an application developer. An application developer shouldn't need to know the specifics of your display hardware, or your serial hardware. Windows exposes application programming interfaces (API) for a developer to call into the hardware but the developer does not need to know what the physical hardware is (that's the beauty of APIs).

Example APIs

For example, take writing to a serial port—an application developer simply:

  1. Calls CreateFile( ) on COMx
  2. Calls WriteFile( ) to write some bytes of data to the serial port
  3. Calls CloseHandle( ) to close the serial port

The same sequence of APIs works no matter what the underlying serial hardware is (or which Windows operating system you are running on).

The same is also true of other APIs. If we want to output a line to the display surface we would simply call PolyLine( ), or MoveToEx( ), LineTo( ). For the most part, as an application developer you don't need to know what display hardware is, there are APIs to call, which return the dimensions of the display surface, the color depth, and so on.

The good news is that developers have a consistent, well known set of APIs to call, which abstract their application from the underlying hardware. For computer application developers, this is crucial, because the application developer has no way of knowing whether the application will run on a laptop, Tablet PC, or desktop computer; or whether the computer is running at 1024x768 or 1600x1200. The application developer can query the screen resolution and color depth at runtime and therefore doesn't need to build an application that only runs on specific hardware.

A driver is simply a dynamic-link library (DLL), which is loaded into a parent process address space. The parent process can then call any of the interfaces exposed from the DLL. The driver is typically loaded by its parent process through a call to LoadLibrary( ) or LoadDriver( ). LoadDriver not only loads the DLL into the parent process address space but also makes sure the DLL isn't paged out.

How does a calling process know which APIs or functions are exposed from our DLL or driver? Easy, the parent process calls GetProcAddress( ), which takes the name of a function and the hInstance of the loaded DLL. The call returns a pointer to the function, if it exists, or NULL if the function is not exposed from the DLL.

Stream drivers expose a well known set of functions. Let's take the serial port example we used earlier. Since this is a stream driver we want to be able to write a stream of bytes to the device or read a stream of bytes from the device. Therefore we would expect the following set of functions to be exposed from our driver: Open, Close, Read, and Write. Stream drivers also expose some additional functions: PowerUp, PowerDown, IO Control, Init, and DeInit.

Creating a Stream Driver

Let's use Platform Builder to create a base operating system image for the Emulator platform. We can then add our DLL/driver project to the platform. I've created a platform called "DrvDemo," which is based on the "Internet Appliance" configuration, and we will set this to be a debug image so that we can trace the debug output from the functions in our driver.

Once the platform is built and downloaded (this shows the operating system boots and runs OK), we then need to create our skeleton driver. We will use the Platform Builder New Project or File menu item to create a WCE Dynamic Link Library. There is no difference in creating a DLL to expose functions or resources and a DLL to be used as a driver—the only difference is which functions the DLL exposes and how the DLL is registered and used on the platform.

(As an aside, one way to create internationalized applications is to create a base application that contains one set of core language strings, dialogs, and resources; then create a number of external DLLs, each of which contain the dialogs, strings, and resources for a specific locale. The application can then load the appropriate language resources on the fly. Additional languages can easily be added to the application by simply adding additional DLL files. This and other interesting topics are described in the book Developing International Software, by Microsoft Press, originally written by Nadine Kano (the second edition was updated by Dr. International, I guess a relative of the good Dr. GUI from MSDN.) Here's a link to the book on the Microsoft Press Web site. This title should be available at all good bookstores, and online through Amazon.com or Barnes & Noble.)

Right, back to the driver—I'm going to name the DLL project "MyDriver," a simple Windows CE DLL Project. This generates a "bare bones" DLL. Here's the code that's generated for us.

// MyDriver.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                )
{
    return TRUE;
}

Cool, eh? All that code written for us; what is left for us to do …? The DLL exposes one function, DllMain, which is the entry point to our DLL. The entry point for our DLL can be called in four situations: Process Attach, Process Detach, Thread Attach, and Thread Detach. The reason for calling the DLL entry point is passed into DllMain through the DWORD ul_reason_for_call parameter. We can switch on this to determine why the entry point has been called, like so:

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                )
{
   switch ( ul_reason_for_call )
   {
      case DLL_PROCESS_ATTACH:
         OutputDebugString(L"MyDriver - DLL_PROCESS_ATTACH\n");
      break;
      case DLL_PROCESS_DETACH:
         OutputDebugString(L"MyDriver - DLL_PROCESS_DETACH\n");
      break;
      case DLL_THREAD_ATTACH:
         OutputDebugString(L"MyDriver - DLL_THREAD_ATTACH\n");
      break;
      case DLL_THREAD_DETACH:
         OutputDebugString(L"MyDriver - DLL_THREAD_DETACH\n");
      break;   }
    return TRUE;
}

Functions

Here's the list of functions we need to expose for our driver, where XXX_ denotes the name of the driver as exposed to the operating system. For a serial driver this would be COM, where serial port 1 would be exposed as COM1. Our driver will expose its name to the operating system as DEM (for DEMo driver).

Function Description
XXX_Close Closes the device context identified by hOpenContext.
XXX_Deinit Called by the Device Manager to de-initialize a device.
XXX_Init Called by the Device Manager to initialize a device.
XXX_IOControl Sends a command to a device.
XXX_Open Opens a device for reading, writing, or both. An application indirectly invokes this function when it calls CreateFile to open special device file names.
XXX_PowerDown Ends power to the device. It is useful only with devices that can be shut off under software control.
XXX_PowerUp Restores power to a device.
XXX_Read Reads data from the device identified by the open context.
XXX_Seek Moves the data pointer in the device.
XXX_Write Writes data to the device.

So our functions are:

DEM_Close, DEM_Deinit, DEM_Init, DEM_IOControl, DEM_Open, DEM_PowerDown, DEM_PowerUp, DEM_Read, DEM_Seek, and DEM_Write.

Our driver is exposed as DEM1: to the operating system. OK, let's implement some functionality for the driver:

First DEM_Init( )—the function definition for this function is as follows:

DWORD DEM_Init( LPCTSTR pContext, LPCVOID lpvBusContext);

This takes as its first parameter, a pointer to a string containing the registry path to the active key for the stream interface driver. With our driver being loaded by the operating system, we get passed the following string (HKLM) Drivers\Active\02. We can use the Remote Registry Editor to view the contents of this key.

Figure 1: The Remote Registry Editor

The "Key" item in the registry points to the location of the registry information for this driver: HKLM\Drivers\BuiltIn\Sample. We can use this information to load any registry specific information for this driver. For example, if this were a display driver we would use this information to determine screen refresh rate or screen resolution. Since this is a simple driver, we don't have any configuration information to load from the registry.

We need to return a DWORD handle to a device context if our driver initializes correctly. In our case the function will return the value of Hex 1234 - 0x1234. The function would return NULL if the driver failed to initialize.

DWORD DEM_Init( LPCTSTR pContext, LPCVOID lpvBusContext)
{
OutputDebugString(L"MyDriver - DEM_Init - Context: ");
OutputDebugString(pContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Init\n");
return 0x1234;
}

Next the DEM_Open function—the function definition is as follows:

DWORD DEM_Open(DWORD hDeviceContext,DWORD AccessCode,DWORD ShareMode )

The Open function is passed to the DeviceContext that we returned from the Init function, along with the access code, and the share mode. These functions map directly to the parameters from the CreateFile Win32® API. In our case the driver will always open, so we simply return a valid OpenContext, in this case the value Hex 5678 0x5678.

DWORD DEM_Open( DWORD hDeviceContext, DWORD AccessCode, DWORD ShareMode )
{
OutputDebugString(L"MyDriver - DEM_Open\n");
OutputDebugString(L"hDeviceContext - ");
DBGOut(hDeviceContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Open\n");
return 0x5678;
}

Now DEM_Close—here's the function definition:

BOOL DEM_Close( DWORD hOpenContext )

The Close function simply gets the OpenContext. In our case we're simply returning TRUE, which indicates the driver closed correctly.

BOOL DEM_Close( DWORD hOpenContext )
{
OutputDebugString(L"MyDriver - DEM_Close\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Close\n");

return TRUE;
}

Now we get to the interesting functions: Read, Write, Seek, and IOCTL. Let's start with Write—how's this going to work? For our demo driver we want an application to write some data (perhaps a string) to the driver. We will cache the data and then return this to the application when we call the Read interface. This is interesting because the data is cached across application runtimes; so we could run our application, call the Write function on our driver, close the application, and some time later reload the application, call the Read function that then returns the data to the application. Of course a "real" stream driver would be interfacing with hardware to read and write data.

Write Function

DEM_Write—here's the function definition:

DWORD DEM_Write( DWORD hOpenContext, LPCVOID pBuffer, DWORD Count )

The Write function is passed our OpenContext handle, a pointer to a buffer, and a count of the number of bytes to be written. Remember this is bytes, not characters. Even though we might be passing a Unicode string, let's say "HELLO," this is five unicode characters or 10 bytes. When living in the Unicode world, we need to be careful of the difference between bytes and characters. It's not enough to call lstrlen( ) to get the string length. For our sample string above, this would write five bytes or "Hel," which is not the desired result. (Perhaps it's time to order a copy of Developing International Software, which describes single byte character sets, double byte character sets, and Unicode in detail.)

OK, so here's our Write function code. Let's walk through the code—we use OutputDebugString to write some useful entry point information to the debug window in Platform Builder. We then stumble across a variable that's not been seen before. Let me introduce you to hMem. hMem is a driver global of type HANDLE, which is defined as HANDLE hMem=NULL;.

When we call the Write routine we check to see if hMem has been allocated. If it has, we call LocalFree to free up the existing content, then allocate enough memory to store the incoming data, and memcpy to copy the inbound buffer to the memory allocated in the function. The Write function is expected to return the number of bytes written. So, in this case we return the number of bytes we've been passed by the application.

DWORD DEM_Write( DWORD hOpenContext, LPCVOID pBuffer, DWORD Count )
{
OutputDebugString(L"MyDriver - DEM_Write\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

if (NULL != hMem) {
   LocalFree(hMem);
}

hMem=LocalAlloc(LPTR,Count);
memcpy(hMem,pBuffer,Count);
dwCount=Count;

OutputDebugString(L"MyDriver - ~ DEM_Write\n");
return Count;
}

Read Function

That's Write function completed. We would expect the Read function to be very similar—here's the function definition:

DWORD DEM_Read( DWORD hOpenContext, LPVOID pBuffer, DWORD Count )

The Read function returns the number of bytes written, or if we fail to read any bytes, it returns -1 (0xffff). Since an application developer may try to read bytes from our driver before calling the Write function, we need to return 0xffff if we don't have any data stored. In our demo driver we output some debug information and check our hMem. If this is NULL, we simply return 0xffff. If this is not NULL, then we obviously have some data to return. We set our return value to be the number of bytes to return to the application, copy our stored data into the buffer passed into the Read function, and then return.

Note  The Read function is passed a pointer to a buffer and a count that defines the size of the buffer. In our Read function, we really should check to make sure the buffer is big enough to return the stored data.

DWORD DEM_Read( DWORD hOpenContext, LPVOID pBuffer, DWORD Count )
{
DWORD dwRetCount=0xffff;      // default to error
OutputDebugString(L"MyDriver - DEM_Read\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");
if (NULL != hMem) {
   dwRetCount=dwCount;
   memcpy(pBuffer,hMem,dwCount);
}
OutputDebugString(L"MyDriver - ~ DEM_Read\n");

return dwRetCount;
}

Void Functions

The PowerUp and PowerDown functions are both Void functions. Right now these simply output some debug information. (Interestingly, I used the PowerUp function recently in a PocketPC driver to keep the PocketPC alive when an external keyboard was connected. The original keyboard driver didn't keep the device alive, so after 30 seconds of typing the device would turn off—how frustrating is that? So I wrote a "shim" driver that kept the PocketPC alive.)

Here are the PowerUp and PowerDown functions:

void DEM_PowerUp( DWORD hDeviceContext )
{
OutputDebugString(L"MyDriver - DEM_PowerUp\n");
OutputDebugString(L"hDeviceContext - ");
DBGOut(hDeviceContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_PowerUp\n");
}

void DEM_PowerDown( DWORD hDeviceContext )
{
OutputDebugString(L"MyDriver - DEM_PowerDown\n");
OutputDebugString(L"hDeviceContext - ");
DBGOut(hDeviceContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_PowerDown\n");
}

IO Control Function

So, that leaves the IOCTL function. Why do we need this? The stream driver exposes the Read, Write, Open, and Close functions. If you need to add any additional functionality that doesn't fit with these functions, you have the ability to call the IO Control function, passing in a special driver defined "code" and input and output buffers. Right now, our demo driver takes a string into the Write function, and returns the stored string in the Read function. So what could we do with an IOCTL? Hey, how about reversing the string … sounds like a plan. First we need to define an IOCTL "code" that will be used by the calling application and by the driver.

Both the application and driver contain the following define:

#define IOCTL_DRIVER_DEMO   42

We will pass this value from our application to the driver (more on the application code in a minute). Let's take a look at the IOCTL code in the driver.

The first thing to note about the IOCTL code is that we will output some debug information in the entry point to the function. We then "switch" on the dwCode—in our case we're looking for the value 42 or IOCTL_DRIVER_DEMO. When we boot our operating system image, we will see the IOCTL for our driver is called even before our application is loaded.

In the IOCTL_DRIVER_DEMO switch statement, we reverse the incoming string, and then copy to the output buffer, which, in this case, is the same as the input buffer.

BOOL DEM_IOControl( DWORD hOpenContext, DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut )
{
OutputDebugString(L"MyDriver - DEM_IOControl\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

switch (dwCode) {
   case IOCTL_DRIVER_DEMO:
   {
      OutputDebugString(L"DRIVER DEMO IOCTL...\n");
      // reverse the string...
      HANDLE hTemp=LocalAlloc(LPTR,dwLenIn+1);
      memset(hTemp,0x00,dwLenIn+1);
      TCHAR *tcOut=(TCHAR*)hTemp;
      TCHAR *tcIn=(TCHAR*)pBufIn;
      DWORD dwChars=dwLenIn/2;
      for (DWORD x=0;x < dwChars;x++) {
         tcOut[x]=tcIn[dwChars-x-1];
      }
      memcpy(pBufOut,hTemp,dwLenIn);
      LocalFree(hTemp);
      *pdwActualOut=dwLenIn;
   }
   break;
   default:
      OutputDebugString(L"Unknown IOCTL\n");
   break;
}

OutputDebugString(L"MyDriver - ~ DEM_IOControl\n");
return TRUE;
}

That's it; our driver code is now complete. Unfortunately the driver doesn't expose any functions at this time. We need to add a .def file to our project. You can create a .def file using Notepad or the Platform Builder editor, and then add it to the project from the Project menu (click Insert and then Files).

Here's how the .def file looks:

LIBRARY MyDriver

EXPORTS
   DEM_Init
   DEM_Deinit
   DEM_Open
   DEM_Close
   DEM_IOControl
   DEM_PowerUp
   DEM_PowerDown
   DEM_Read
   DEM_Write
   DEM_Seek

That's it; our driver code is finished. Here's the complete listing:

// MyDriver.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"

DWORD DEM_Init(LPCTSTR pContext, LPCVOID lpvBusContext);
BOOL DEM_Deinit( DWORD hDeviceContext );
DWORD DEM_Open( DWORD hDeviceContext, DWORD AccessCode, DWORD ShareMode );
BOOL DEM_Close( DWORD hOpenContext );
BOOL DEM_IOControl( DWORD hOpenContext, DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut );
void DEM_PowerUp( DWORD hDeviceContext );
void DEM_PowerDown( DWORD hDeviceContext );
DWORD DEM_Read( DWORD hOpenContext, LPVOID pBuffer, DWORD Count );
DWORD DEM_Write( DWORD hOpenContext, LPCVOID pBuffer, DWORD Count );
DWORD DEM_Seek( DWORD hOpenContext, long Amount, WORD Type );

#define IOCTL_DRIVER_DEMO   42

// Not exposed by the Device Driver 
void DBGOut(DWORD dwValue);

HANDLE hMem=NULL;
DWORD dwCount;

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                )
{
switch ( ul_reason_for_call )
{
   case DLL_PROCESS_ATTACH:
      OutputDebugString(L"MyDriver - DLL_PROCESS_ATTACH\n");
   break;
   case DLL_PROCESS_DETACH:
      OutputDebugString(L"MyDriver - DLL_PROCESS_DETACH\n");
   break;
   case DLL_THREAD_ATTACH:
      OutputDebugString(L"MyDriver - DLL_THREAD_ATTACH\n");
   break;
   case DLL_THREAD_DETACH:
      OutputDebugString(L"MyDriver - DLL_THREAD_DETACH\n");
   break;
}
return TRUE;
}

DWORD DEM_Init( LPCTSTR pContext, LPCVOID lpvBusContext)
{
OutputDebugString(L"MyDriver - DEM_Init - Context: ");
OutputDebugString(pContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Init\n");
return 0x1234;
}

BOOL DEM_Deinit( DWORD hDeviceContext )
{
OutputDebugString(L"MyDriver - DEM_Deinit\n");

OutputDebugString(L"MyDriver - ~ DEM_Deinit\n");
return TRUE;
}

DWORD DEM_Open( DWORD hDeviceContext, DWORD AccessCode, DWORD ShareMode )
{
OutputDebugString(L"MyDriver - DEM_Open\n");
OutputDebugString(L"hDeviceContext - ");
DBGOut(hDeviceContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Open\n");
return 0x5678;
}

BOOL DEM_Close( DWORD hOpenContext )
{
OutputDebugString(L"MyDriver - DEM_Close\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Close\n");

return TRUE;
}

BOOL DEM_IOControl( DWORD hOpenContext, DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut )
{
OutputDebugString(L"MyDriver - DEM_IOControl\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

switch (dwCode) {
   case IOCTL_DRIVER_DEMO:
   {
      OutputDebugString(L"DRIVER DEMO IOCTL...\n");
      // reverse the string...
      HANDLE hTemp=LocalAlloc(LPTR,dwLenIn+1);
      memset(hTemp,0x00,dwLenIn+1);
      TCHAR *tcOut=(TCHAR*)hTemp;
      TCHAR *tcIn=(TCHAR*)pBufIn;
      DWORD dwChars=dwLenIn/2;
      for (DWORD x=0;x < dwChars;x++) {
         tcOut[x]=tcIn[dwChars-x-1];
      }
      memcpy(pBufOut,hTemp,dwLenIn);
      LocalFree(hTemp);
      *pdwActualOut=dwLenIn;
   }
   break;
   default:
      OutputDebugString(L"Unknown IOCTL\n");
   break;
}

OutputDebugString(L"MyDriver - ~ DEM_IOControl\n");
return TRUE;
}

void DEM_PowerUp( DWORD hDeviceContext )
{
OutputDebugString(L"MyDriver - DEM_PowerUp\n");
OutputDebugString(L"hDeviceContext - ");
DBGOut(hDeviceContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_PowerUp\n");
}

void DEM_PowerDown( DWORD hDeviceContext )
{
OutputDebugString(L"MyDriver - DEM_PowerDown\n");
OutputDebugString(L"hDeviceContext - ");
DBGOut(hDeviceContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_PowerDown\n");
}

DWORD DEM_Read( DWORD hOpenContext, LPVOID pBuffer, DWORD Count )
{
DWORD dwRetCount=0xffff;      // default to error
OutputDebugString(L"MyDriver - DEM_Read\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");
if (NULL != hMem) {
   dwRetCount=dwCount;
   memcpy(pBuffer,hMem,dwCount);
}
OutputDebugString(L"MyDriver - ~ DEM_Read\n");

return dwRetCount;
}

DWORD DEM_Write( DWORD hOpenContext, LPCVOID pBuffer, DWORD Count )
{
OutputDebugString(L"MyDriver - DEM_Write\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

if (NULL != hMem) {
   LocalFree(hMem);
}

hMem=LocalAlloc(LPTR,Count);
memcpy(hMem,pBuffer,Count);
dwCount=Count;

OutputDebugString(L"MyDriver - ~ DEM_Write\n");

return Count;
}

DWORD DEM_Seek( DWORD hOpenContext, long Amount, WORD Type )
{
OutputDebugString(L"MyDriver - DEM_Seek\n");
OutputDebugString(L"hOpenContext - ");
DBGOut(hOpenContext);
OutputDebugString(L"\n");

OutputDebugString(L"MyDriver - ~ DEM_Seek\n");

return 0;
}


void DBGOut(DWORD dwValue)
{
TCHAR tcTemp[10];
wsprintf(tcTemp,L"%ld",dwValue);
OutputDebugString(tcTemp);
}

Testing the Functions

We can confirm that the functions needed for our DLL are correctly exported by using the Dumpbin application, which ships with Platform Builder. Simply open a build release window (from the Build menu, click Open Build Release Directory), and run the command line Dumpbin /exports mydriver.dll. The results from running Dumpbin on my sample driver follow:

Figure 2: Dumpbin results

So far our driver is just a DLL that exports some functions. We could load the DLL from an application by calling LoadLibrary, and get the address of any of the functions by calling GetProcAddress( ). We can then call the individual functions. We want this to be a driver, so we need to modify the platform registry to include the settings needed to register the driver.

Registry Entries

So far, we've probably seen the Feature View (the initial view of the operating system), the Class View, and the File View. We can modify the registry by using the Parameter View. I'm going to modify the Project.reg file. Here's what I've added to my registry:

 [HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Sample]
    "Dll" = "mydriver.Dll"
    "Prefix" = "DEM"
    "Index" = dword:1
    "Order" = dword:0
    "FriendlyName" = "Demo Driver"
    "Ioctl" = dword:0

The registry entries define: the file name of the driver mydriver.dll, the prefix for the driver as this is exposed to applications DEM, the friendly name of the driver "Demo Driver," and the Order and Index.

Order

The Registry Entry Order DWORD used for drivers gives the system developer the opportunity to set the relative load sequence for all drivers. All drivers with Order = 0 are loaded first, followed by drivers of Order = 1,2,… Within Order = 0, drivers are loaded as they appear in the registry. Order allows the developer to ensure drivers with dependencies to load in the proper sequence. For example, if you created a driver that required a serial port driver be available, and it loaded at Order = 0, then you would place your driver at Order = 1.

Index

The Registry Entry Index allows the developer to specify the numeric portion of the driver name in the file system. File system names are a combination of the driver prefix as detailed in the driver registry settings and an indexed value that enumerates multiple drivers with the same prefix. By default, the first driver with a prefix of COM would be assigned file system name COM1 and the next driver would be given COM2. In order to ensure that your driver is always loaded as COM2 you would need to provide an Index = 2.

So, what's the big deal with order and index prefix? The drivers are all going to make it into the file system anyhow. Why can't I just put them in the registry in the correct order and not have to worry about this? The reason is "Registry Saving." Each time the registry for a device is saved and reloaded, the registry entries for the entire system are reversed. Registry entries are loaded into the system through a linked list by placing the next entry on the tail of the list. Registry entries are saved by starting at the front of the list working to the tail, causing all entries within a key to be reversed.

As a simple example, this reversal will cause non-indexed serial ports to reverse in order. COM2 will become COM1 after the next registry save and reload sequence. Some early developers tried to save the registry twice to avoid using the Index and Order registry entries, but they soon went MAD. Both the Order and Index device driver registry entries allow the developer to ensure the system loads are consistent and dependable. As an exercise look at the registry entries on your system and sketch order and index for all drivers on your system—answers on a postcard should be mailed to …

Testing the Driver

Now that our driver is written, we can check to make sure the driver is registered and available to the operating system. There are a number of ways we can do this. We can use the Windows CE Remote System Information application to provide a list of devices exposed to the operating system. In the image that follows, we can see that DEM1: is listed as an available device. The details on the right side of the Remote System Information application show the Friendly name, Prefix, DLL name, and so on.

Figure 3: Windows CE Remote System Information window

We could examine the debug output as the driver loads. This shows the driver being initialized and various functions being called. We can also use the list of loaded modules to determine that our driver is loaded.

Figure 4: List of loaded modules

We also need to write an application to test out the driver. The application needs to call:

  • CreateFile on DEM1:
  • WriteFile to write some information to the driver
  • CloseHandle to close the driver

We've added debug information to the driver, so as we call CreateFile, WriteFile, ReadFile, we would expect to see the Open, Write, and Close functions being called in the driver.

// MyApp.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include "resource.h"

#include <commctrl.h>

#define MAX_LOADSTRING 100

#define IDC_CMDBAR 0x101
HWND hWndCmdBar;

void WriteToDriver( );
void ReadFromDriver( );
void HandleIOCTL( );

#define IOCTL_DRIVER_DEMO   42

// Global Variables:
HINSTANCE hInst;            // current instance
TCHAR szTitle[MAX_LOADSTRING];                        // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING];                        // The title bar text

// Forward declarations of functions included in this code module:
ATOM            MyRegisterClass(HINSTANCE hInstance);
BOOL            InitInstance(HINSTANCE, int);
LRESULT CALLBACK   WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR     lpCmdLine,
                     int       nCmdShow)
{
    // TODO: Place code here.
   MSG msg;
   HACCEL hAccelTable;

   // Initialize global strings
   LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
   LoadString(hInstance, IDC_MYAPP, szWindowClass, MAX_LOADSTRING);
   MyRegisterClass(hInstance);

   // Perform application initialization:
   if (!InitInstance (hInstance, nCmdShow)) 
   {
      return FALSE;
   }

   hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_MYAPP);

   // Main message loop:
   while (GetMessage(&msg, NULL, 0, 0)) 
   {
      if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
      {
         TranslateMessage(&msg);
         DispatchMessage(&msg);
      }
   }

   return msg.wParam;
}

//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
//  COMMENTS:
//
//    This function and its usage is only necessary if you want this code
//    to be compatible with Win32 systems prior to the 'RegisterClassEx'
//    function that was added to Windows 95. It is important to call this function
//    so that the application will get 'well formed' small icons associated
//    with it.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
   WNDCLASS wc;

    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = (WNDPROC) WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = 0;
    wc.hCursor = 0;
    wc.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = 0;
    wc.lpszClassName = szWindowClass;

   return RegisterClass(&wc);
}

//
//   FUNCTION: InitInstance(HANDLE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;

   hInst = hInstance; // Store instance handle in our global variable

   hWnd = CreateWindow(szWindowClass, szTitle, WS_VISIBLE,
      0, 0, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

//
//  FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND   - process the application menu
//  WM_PAINT   - Paint the main window
//  WM_DESTROY   - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   PAINTSTRUCT ps;
   HDC hdc;
   TCHAR szHello[MAX_LOADSTRING];
   LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);

   switch (message) 
   {
      case WM_COMMAND:
         switch(LOWORD(wParam)) {
            case ID_FILE_EXIT:
               PostQuitMessage(0);
            break;
            case ID_DRIVER_WRITE:
               WriteToDriver( );
            break;
            case ID_DRIVER_READ:
               ReadFromDriver( );
            break;
            case ID_DRIVER_IOCTL:
               HandleIOCTL( );
            break;
         }
      break;
      case WM_CREATE:
         hWndCmdBar=CommandBar_Create(hInst,hWnd,IDC_CMDBAR);
         CommandBar_InsertMenubar(hWndCmdBar,hInst,IDR_MENU,0);
      case WM_PAINT:
         hdc = BeginPaint(hWnd, &ps);
         // TODO: Add any drawing code here...
         RECT rt;
         GetClientRect(hWnd, &rt);
         DrawText(hdc, szHello, _tcslen(szHello), &rt, DT_CENTER);
         EndPaint(hWnd, &ps);
         break;
      case WM_DESTROY:
         PostQuitMessage(0);
         break;
      default:
         return DefWindowProc(hWnd, message, wParam, lParam);
   }
   return 0;
}

void WriteToDriver( )
{
DWORD dwWritten;
TCHAR *tcString=L"Demo String...";
HANDLE hDrv=CreateFile(L"DEM1:",GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if (INVALID_HANDLE_VALUE == hDrv) {
   OutputDebugString(L"Failed to open Driver...\n");
} else {
   WriteFile(hDrv,(LPVOID)tcString,lstrlen(tcString)*sizeof(TCHAR),&dwWritten,NULL);
}
CloseHandle(hDrv);
}

void ReadFromDriver( )
{
DWORD dwRead;
TCHAR tcTemp[30];
HANDLE hDrv=CreateFile(L"DEM1:",GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if (INVALID_HANDLE_VALUE == hDrv) {
   OutputDebugString(L"Failed to open Driver...\n");
} else {
   memset(tcTemp,0x00,30*sizeof(TCHAR));
   ReadFile(hDrv,tcTemp,30,&dwRead,NULL);
   MessageBox(NULL,tcTemp,L"Demo Data",MB_OK);
}
CloseHandle(hDrv);
}

void HandleIOCTL( )
{
HANDLE hDrv=CreateFile(L"DEM1:",GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
TCHAR tcBuffer[10];
DWORD dwBytesReturned;

lstrcpy(tcBuffer,L"Hello");

BOOL bRet=DeviceIoControl(
  hDrv, 
  IOCTL_DRIVER_DEMO, 
  tcBuffer, 
  lstrlen(tcBuffer)*sizeof(TCHAR), 
  tcBuffer, 
  lstrlen(tcBuffer)*sizeof(TCHAR),
  &dwBytesReturned, 
  NULL);

MessageBox(NULL,tcBuffer,L"IOCTL Test",MB_OK);
CloseHandle(hDrv);

}

There are three functions we're interested in: Write, Read, and IOCTL.

Here's the Write function: the steps are identical to writing data to a serial port. We use CreateFile to open DEM1:, check the return value to make sure we have a valid handle. Then we use WriteFile( ) to write data to the driver and CloseHandle( ) to complete the write operation.

void WriteToDriver( )
{
DWORD dwWritten;
TCHAR *tcString=L"Demo String...";
HANDLE hDrv=CreateFile(L"DEM1:",GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if (INVALID_HANDLE_VALUE == hDrv) {
   OutputDebugString(L"Failed to open Driver...\n");
} else {
   WriteFile(hDrv,(LPVOID)tcString,lstrlen(tcString)*sizeof(TCHAR),&dwWritten,NULL);
}
CloseHandle(hDrv);
}

Reading from the driver is as simple as writing to it—we set up a buffer, call CreateFile on DEM1: (note the use of OPEN_EXISTING_FILE and GENERIC_READ). Then we call ReadFile and CloseHandle.

void ReadFromDriver( )
{
DWORD dwRead;
TCHAR tcTemp[30];
HANDLE hDrv=CreateFile(L"DEM1:",GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if (INVALID_HANDLE_VALUE == hDrv) {
   OutputDebugString(L"Failed to open Driver...\n");
} else {
   memset(tcTemp,0x00,30*sizeof(TCHAR));
   ReadFile(hDrv,tcTemp,30,&dwRead,NULL);
   MessageBox(NULL,tcTemp,L"Demo Data",MB_OK);
}
CloseHandle(hDrv);
}

The IOCTL is the oddball function. It doesn't follow the same convention as reading and writing, both of which appear the same as reading and writing to a file in the file system. We still get a handle to the driver in the same way as writing or reading from the driver using CreateFile, and close in the same way using CloseHandle( ). Here's the IOCTL code: we call the function DeviceIoControl( ), which takes the handle to our driver (returned from CreateFile), the "code" we want to call in the driver, the pointers to input and output buffers, and the size of input and output buffers.

void HandleIOCTL( )
{
HANDLE hDrv=CreateFile(L"DEM1:",GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
TCHAR tcBuffer[10];
DWORD dwBytesReturned;

lstrcpy(tcBuffer,L"Hello");

BOOL bRet=DeviceIoControl(
  hDrv, 
  IOCTL_DRIVER_DEMO, 
  tcBuffer, 
  lstrlen(tcBuffer)*sizeof(TCHAR), 
  tcBuffer, 
  lstrlen(tcBuffer)*sizeof(TCHAR),
  &dwBytesReturned, 
  NULL);

MessageBox(NULL,tcBuffer,L"IOCTL Test",MB_OK);
CloseHandle(hDrv);

}

So, that's all there is to it: we created a DLL, added the required functions to expose "stream driver" functionality, registered the driver so that it loads correctly, and checked that the driver has loaded through the Remote System Information tool (I knew there had to be a good reason for this tool shipping with Windows CE!). We also checked the Windows CE Debug output to show the driver being loaded and various functions being called. To wrap up this exercise we created a Win32 application that calls the Win32 APIs CreateFile, ReadFile, WriteFile, and CloseHandle. This calls down into our driver and calls the Open, Read, Write, and Close functions. So, what's next? In our sample we don't touch hardware, we simply show how to implement the basic stream driver functionality. Next we could add support for power management and could touch real hardware.

While writing this month's "Get Embedded" article I had an interesting thought: Our driver is loaded by the device driver manager device.exe. Let's say we found a problem with the driver and needed to modify one line of code. In this case we need to shut down the operating system, modify the driver source, rebuild the driver, and re-link the operating system. We know that our driver is dynamically loaded by the device driver manager, so why shouldn't we create an intermediate driver that loads our "real" driver?

Here's the thought process—our intermediate driver simply exposes the functions DEM_Open, DEM_Close, DEM_Read, etc. On initialization the "intermediate" driver calls LoadLibrary on our "real" driver and then calls GetProcAddress on the entry points of our real driver. When the intermediate driver functions are called they simply pass through to the real driver. I know what you are thinking … how does this help us? If the intermediate driver loads our real driver, then we're in the same situation as when device.exe loads our real driver, right? Perhaps not … what if we implement an IOCTL in our stream driver that calls FreeLibrary on our real driver? At this point device.exe is happy, and it can still call down into our dummy driver. We're also happy, because we can modify the source of our real driver, rebuild and call a second IOCTL in our intermediate driver to reload the real driver. Neat, huh! This will be left to you as the reader to implement and test … shouldn't take more than a few minutes…

Thanks to Nat Frampton for the article review. Now on to next month … I bet you are itching to find out what's planned for next month's article (that makes two of us). I'm thinking we should hit some headless issues, because there have been a number of questions in the newsgroups, on chats, and in meetings with folks at the Windows Embedded Essentials events. There should be enough content there to take us through the next couple of months.