Programming Microsoft Windows Ce Net 3Rd [Electronic resources]

نسخه متنی -صفحه : 169/ 128
نمايش فراداده

Basic Drivers

Before I dive into how to write a device driver, we must take a brief look at how Windows CE handles drivers in general. Windows CE separates device drivers into three main groups: native, bus, and stream interface. Native drivers, sometimes called built-in drivers, are those device drivers that are required for the hardware and were created by the OEM when the Windows CE hardware was designed. Among the devices that have native drivers are the keyboard, the touch panel, and audio. These drivers might not support the generic device driver interface I describe shortly. Instead, they might extend the interface or have a totally custom interface to the operating system. Native drivers frequently require minor changes when a new version of the operating system is released. These drivers are designed using the Platform Builder product supplied by Microsoft. However these drivers are developed, they're tightly bound to the Windows CE operating system and aren't usually replaced after the device has been sold.

Bus drivers manage the system busses such as a PCI bus. PCMCIA, CompactFlash, and SDIO slots are also considered busses. Bus drivers are in charge of interrogating the hardware on the bus to determine what hardware is installed and allocating resources. The bus driver also asks the Device Manager to load the proper drivers for the hardware on the bus and provides a system-independent method of accessing the hardware registers without the device drivers from having to know the physical memory map of the system.

Stream interface device drivers (which are sometimes referred to as installable drivers) can be supplied by third-party manufacturers to support hardware added to the system. Although some Windows CE systems have a PCI bus for extra cards, the additional hardware is usually installed via a Personal Computer Memory Card International Association (PCMCIA), a CompactFlash, or a Secure Digital I/O (SDIO) slot. In this case, the device driver would use functions provided by the bus driver to access the hardware.

In addition, a device driver might be written to extend the functionality of an existing driver. For example, you might write a driver to provide a compressed or encrypted data stream over a serial link. In this case, an application would access the encryption driver, which would in turn use the serial driver to access the serial hardware.

Device drivers under Windows CE operate at the same protection level as applications. They differ from applications in that they're DLLs. Most drivers are loaded by the Device Manager process (Device.exe) when the system boots. All these drivers, therefore, share the same process address space. Some of the built-in drivers, on the other hand, are loaded by GWES.exe. These drivers include the display driver (DDI.dll) as well as the keyboard and touch panel (or mouse) drivers.

Driver Names

Stream interface device drivers are identified by a three-character name followed by a single digit, as in COM2: this scheme allows for 10 device drivers of one name to be installed on a Windows CE device at any one time. Instance values are numbered from one to nine, with the tenth instance having an instance number of zero. Here are a few examples of some three-character names currently in use:

COMSerial driver

ACMAudio compression manager

WAVAudio wave driver

CONConsole driver

When referencing a stream interface driver, an application uses the three-character name, followed by the single digit, followed by a colon (:). The colon is required under Windows CE for the system to recognize the driver name.

Bus drivers typically don't have a stream-style three-letter name. One consequence of this is that bus drivers are not accessible to applications such as stream drivers. Bus drivers are however loaded by the Device Manager and in most ways are loaded and managed like stream drivers.

The Device Driver Load Process

When Device.exe loads, it looks in the registry under [HKEY_LOCAL_ MACHINE]\Drivers for a string value named RootKey. This value points to the registry key that lists the drivers that should be loaded when the system boots. Traditionally, this key is named BuiltIn. In addition, an optional key named DLL can be present listing the registry enumerator, the DLL that actually reads and interprets the registry structure. If no DLL key is found, the default enumerator Regenum.dll is used.

The Device Manager then uses the registry enumerator to read the key specified by RootKey for the list of the drivers it must load when it initializes. This list is contained in a series of keys. The names of the keys don't matter—it's the values contained in the keys that define which drivers to load and the order in which to load them. Figure 22-1 shows the contents of the WaveDev key. The Wave driver is the audio driver.

Figure 22-1: The registry key for the Wave driver

The four values under this key are the basic four entries used by a device driver under Windows CE. The DLL key specifies the name of the DLL that implements the driver. This is the DLL that the registry enumerator loads. The Order value ranges from 0 through 255 and specifies the order in which the drivers are loaded. The registry enumerator loads drivers with lower Order values before drivers with higher Order values in the registry.

The Prefix value defines the three-letter name of the driver. This value is mandatory for stream drivers but typically not used for bus drivers. Applications that want to open this driver use the three-letter key with the number that Windows CE appends to create the device name. The Index value is the number that will be appended to the device name.

As the registry enumerator reads each of the registry keys, it loads the DLL specified, creates an Active key for the driver and then calls either ActivateDevice or ActivateDeviceEx to register the DLL as a device driver with the system. The registry enumerator maintains a table of device handles that are returned by ActivateDevice.

ActivateDevice creates a new key under [HKEY_LOCAL_MACHINE\Drivers\Active and initializes it. It then finds a free index for the driver if one wasn't specified in the original registry key. ActivateDevice then calls RegisterDevice to complete the load. RegisterDevice loads the driver in memory using the LoadDevice function. LoadDevice is similar to LoadLibrary but loads the entire DLL into memory and locks the pages so they can't be discarded. RegisterDevice then attempts to get function pointers to the 10 external entry points in the driver. For named, stream, drivers, the entry points Init, Deinit, Open, Close, and at least one of the Read, Write, Seek, or IOControl entry points must exist or the driver load fails. For unnamed bus drivers, RegisterDevice tries to get all 10 entry points, but fails only if the Init and Deinit functions can't be found.

Once the entry points have been saved, RegisterDevice calls the driver's Init function. If Init returns a nonzero value, the driver is added to the device chain and RegisterDevice returns. If Init returns zero, the driver is unloaded and the driver initialization fails.

Although this is the standard load procedure, another registry value can modify the load process. If the driver key contains a Flags value, the load process can change in a number of ways. The following values are currently valid for the Flags value:

DEVFLAGS_UNLOADUnload the driver after the call to Init returns.

DEVFLAGS_LOADLIBRARYUse LoadLibrary to load the driver instead of LoadDriver.

DEVFLAGS_NOLOADDon't load the driver at all.

DEVFLAGS_NAKEDENTRIESThe driver entry points aren't prefixed by the driver name.

Another way the driver load process can be modified depends on the now-deprecated registry value named Entry. If this value is found, the DLL is loaded, and then, instead of calling ActivateDevice, the system calls the entry point in the driver named in Entry. The driver itself is then responsible for calling the ActivateDevice function if it's to be registered as a driver with the system.

If the Entry value is present, another value, Keep, can also be specified. Specifying the Keep value tells the system not to unload the driver after it calls the driver's entry point. This arrangement allows the driver DLL to avoid calling RegisterDevice and therefore avoid being a driver at all. Instead, the DLL is simply loaded into the process space of Device.exe.

Device drivers can also be loaded manually by applications. The preferred function for loading a device driver is ActivateDeviceEx prototyped as

HANDLE ActivateDeviceEx (LPCWSTR lpszDevKey, LPCVOID lpRegEnts, 
DWORD cRegEnts, LPVOID lpvParam);

The first parameter is the name of a registry key under [HKEY_LOCAL_ MACHINE] where the driver information is saved. The format of the registry key is identical to the format discussed earlier. The next two parameters, lpRegEnts and cRegEnts, describe an array of REGINI structures that define a series of registry values that will be added to the device's Active key. Generally, adding values is done only for bus drivers. The final parameter is a pointer that is passed to the device driver's Init function when the driver is loaded. This pointer can point to any device-specific information. The driver must use the new, two-parameter definition of the Init function to receive this value.

The return value from ActivateDeviceEx is the handle to the instance of the device. If the return value is zero, the load failed. In this case, use GetLastError to determine why the function failed. The returned handle can't be used to read or write to the device; instead, the driver should be opened with CreateFile. The handle should be saved in case the driver needs to be unloaded in the future.

An older method of loading a driver is RegisterDevice. RegisterDevice is dangerous because drivers loaded with this function will not have an Active key associated with the driver. The only reason for discussing the function at all is that it doesn't require a registry key to load the driver, which can be handy when writing a quick and simple test program that loads and later unloads the driver.

RegisterDevice is prototyped as

HANDLE RegisterDevice (LPCWSTR lpszType, DWORD dwIndex, 
LPCWSTR lpszLib, DWORD dwInfo);

The first two parameters are the three-character prefix of the driver and the instance number of the device. To load COM3, for example, lpszType would point to the string COM and dwIndex would have a value of 3. If an instance of the driver is already loaded the function will fail, so it's important to check the return value to see whether the function fails and determine why the failure occurred.

The lpszLib parameter identifies the name of the DLL that implements the driver. The final parameter, dwInfo, is passed to the driver in the Init call in the dwContext value. Because most drivers expect the dwContext value to point to a string naming a registry key, this value should at least point to a zero-terminated null string. RegisterDevice returns the handle to the instance of the driver if the load was successful and zero otherwise.

A driver can be unloaded with

BOOL DeregisterDevice (Handle hDevice);

The only parameter is the handle that was returned with ActivateDeviceEx or RegisterDevice.

Enumerating the Active Drivers

The most reliable way to find a device driver is to use FindFirstFileEx and set the fSearchOp parameter to FindExSearchLimitToDevices. Using the search string * and repeatedly calling FindNextFile results in a list of the stream drivers loaded. Unfortunately, there's a bug in the implementation of FindFirstFileEx in the original Pocket PCs. When you used the FindExSearchLimitToDevices fSearchOp parameter with FindFirstFileEx, the original Pocket PCs would throw an exception. The only way to catch this is to bracket the call to FindFirstFileEx with a __try, __except block. As a result, a more general method to search for device drivers is to simply check the registry.

The more general method for determining what drivers are loaded onto a Windows CE system is to look in the registry under the key \Drivers\Active under HKEY_LOCAL_MACHINE. The Device Manager dynamically updates the subkeys contained here as drivers are loaded and unloaded from the system. Contained in this key is a list of subkeys, one for each active driver loaded with ActivateDevice. The contents of these subkeys might change in future versions of Windows CE, but knowing what these subkeys contain can be helpful in some situations.

The name of the key is simply a placeholder; the values inside the keys are what indicate the active drivers. Figure 22-2 shows the registry key for the COM1 serial driver.

Figure 22-2: The registry's active list values for the serial device driver for COM1

In Figure 22-2, the Name value contains the official five-character name (four characters plus a colon) of the device. The Hnd value is a handle used internally by Windows CE. The interesting entry is the Key value. This value points to the registry key where the device driver stores its configuration information. This second key is necessary because the active list is dynamic, changing whenever a device is installed. Instead, the driver should open the registry key specified by the Key value in the active list to determine the driver's permanent configuration data. The configuration data for the serial driver is shown in Figure 22-3.

Figure 22-3: The registry entry for the serial driver

You can look in the serial driver registry key for such information as the name of the DLL that actually implements the driver, the three-letter prefix defining the driver name, the order in which the driver wants to be loaded, and something handy for user interfaces, the friendly name of the driver. Not all drivers have this friendly name, but when they do, it's a much more descriptive name than COM2 or NDS1.

Drivers for PCMCIA or CompactFlash cards have an additional value in their active list key. The PnpId value contains the Plug and Play ID string that was created from the card's ID string. Some PCMCIA and CompactFlash cards have their PnpId strings registered in the system if they use a specific device driver. If so, a registry key for the PnpId value is located in the Drivers\PCMCIA key under HKEY_LOCAL_MACHINE. For example, a PCMCIA card that had a PnpId string This_is_a_pc_card would be registered under the key \Drivers\PCMCIA\This_is_a_pc_card. That key may contain a FriendlyName string for the driver. Other PCMCIA cards use generic drivers. For example, most CompactFlash storage cards use the ATADISK driver registered under \Drivers\PCMCIA\ATADISK.

Reading and Writing Device Drivers

Applications access device drivers under Windows CE through the file I/O functions, CreateFile, ReadFile, WriteFile, and CloseHandle. The application opens the device using CreateFile, with the name of the device being the five-character (three characters plus digit plus colon) name of the driver. Drivers can be opened with all the varied access rights: read only, write only, read/write, or neither read nor write access.

Once a device is open, data can be sent to it using WriteFile and can read from the device using ReadFile. As is the case with file operations, overlapped I/O isn't supported for devices under Windows CE. The driver can be sent control characters using the function DeviceIoControl. The function is prototyped this way:

BOOL DeviceIoControl (HANDLE hDevice, DWORD dwIoControlCode,
LPVOID lpInBuffer, DWORD nInBufferSize,
LPVOID lpOutBuffer, DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped);

The first parameter is the handle to the opened device. The second parameter, dwIoControlCode, is the IOCTL (pronounced eye-OC-tal) code. This value defines the operation of the call to the driver. The next series of parameters are generic input and output buffers and their sizes. The use of these buffers is dependent on the IOCTL code passed in dwIoControlCode. The lpBytesReturned parameter must point to a DWORD value that will receive the number of bytes returned by the driver in the buffer pointed to by lpOutBuffer.

Each driver has its own set of IOCTL codes. If you look in the source code for the example serial driver provided in the Platform Builder, you'll see that the following IOCTL codes are defined for the COM driver. Note that these codes aren't defined in the Windows CE SDK because an application doesn't need to directly call DeviceIoControl using these codes.

IOCTL_SERIAL_SET_BREAK_ON

IOCTL_SERIAL_SET_BREAK_OFF

IOCTL_SERIAL_SET_DTR

IOCTL_SERIAL_CLR_DTR

IOCTL_SERIAL_SET_RTS

IOCTL_SERIAL_CLR_RTS

IOCTL_SERIAL_SET_XOFF

IOCTL_SERIAL_SET_XON

IOCTL_SERIAL_GET_WAIT_MASK

IOCTL_SERIAL_SET_WAIT_MASK

IOCTL_SERIAL_WAIT_ON_MASK

IOCTL_SERIAL_GET_COMMSTATUS

IOCTL_SERIAL_GET_MODEMSTATUS

IOCTL_SERIAL_GET_PROPERTIES

IOCTL_SERIAL_SET_TIMEOUTS

IOCTL_SERIAL_GET_TIMEOUTS

IOCTL_SERIAL_PURGE

IOCTL_SERIAL_SET_QUEUE_SIZE

IOCTL_SERIAL_IMMEDIATE_CHAR

IOCTL_SERIAL_GET_DCB

IOCTL_SERIAL_SET_DCB

IOCTL_SERIAL_ENABLE_IR

IOCTL_SERIAL_DISABLE_IR

As you can see from the fairly self-descriptive names, the serial driver IOCTL functions expose significant function to the calling process. Windows uses these IOCTL codes to control some of the specific features of a serial port, such as the handshaking lines and timeouts. Each driver has its own set of IOCTL codes. I've shown the preceding ones simply as an example of how the DeviceIoControl function is typically used. Under most circumstances, an application has no reason to use the DeviceIoControl function with the serial driver. Windows provides its own set of functions that then call down to the serial driver using DeviceIoControl.

Okay, we've talked enough about generic drivers. It's time to sit down to the meat of the chapter—writing a driver.