This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


January 1996

Microsoft Systems Journal Homepage

Automatically Configure Your Devices by Exploiting Plug and Play VxD Services

Walter Oney

Walter Oney is a freelance developer and software consultant based in Boston, MA. He is currently writing a book on systems programming in Windows 95 to be published in spring of 1996 by Microsoft Press. He can be reached on CompuServe at 73730,553.

Click to open or copy the PLUGPLAY project files.

ThePlugandPlaysubsystems of Windows® 95 are designed to simplify the lives of end users and ten-thumbed programmers faced with new or conflicting hardware. In my previous article I summarized the hardware features that allow Windows 95 and other operating systems (MS-DOS®) to determine what hardware populates a PC, and I described the role of the Configuration Manager VxD. Now I'll examine how Configuration Manager arbitrates the competing demands of hardware for resources such as I/O port addresses and interrupt request assignments. I'll show how a VxD learns what resources it's supposed to use. Finally, I'll describe how you can extend the functionality of the Windows 95 Device Manager by supplying a custom property page provider that works with your device driver.

Loading Drivers

Because this material is so dense, let me sum up the previous article before moving on to new concepts. Configuration Manager uses VxDs known as bus enumerators to determine what hardware is physically present. An internal root enumerator uses registry entries created by the Windows Setup program to count the "legacy" devices on the system. The legacy devices include all the devices detected by Setup and one top-level hardware bus. Each bus has a driver that enumerates the attached hardware (which may include other buses) to create device nodes (DEVNODEs for short) in a hierarchical hardware tree. Drivers for devices like SCSI controllers that contain other devices can augment the hardware tree by registering as enumerators and creating further DEVNODEs.

Two keys in the system registry describe each hardware device. The hardware key occupies a branch of HKLM\Enum. (HKLM is an abbreviation for the top-level HKEY_LOCAL_MACHINE branch of the registry.) The hardware key points to the software key for the device. The software key lies within a branch of HKLM\System\CurrentControlSet\Services\Class named for the class to which the device belongs. For example, the hardware key for my COM1 port is HKLM\Enum\Root\*PNP0500\0000, while the software key is HKLM\System\CurrentControlSet\Services\Class\ports\0000.

Configuration Manager initially locates the hardware key for a device when an enumerator creates a DEVNODE. The DEVNODE has an identifier that also names the hardware key. Configuration Manager uses the Driver named value within the hardware key in the Registry to find the software key. Using COM1 as an example again, Configuration Manager would find the name "ports\0000" in the driver value for COM1. Rather than now directly load a device driver, Configuration Manager obtains the name of a device loader from the DevLoader value in the software key. It then asks the device loader to dynamically load the device driver.

Configuration Manager's initial communication with a Plug and Play VxD uses the Directed_Sys_Control service of the Virtual Machine Manager to send a PNP_New_Devnode system control message to the driver's control procedure. Along with the message code, Configuration Manager sends the address of the DEVNODE for the device in question as well as a reason code explaining what the called driver is supposed to do. The code DLVXD_LOAD_ENUMERATOR means that Configuration Manager wants the driver to use the CM_Register_Enumerator service to register an enumeration procedure for the DEVNODE, which presumably describes a bus or controller to which other devices are attached. The DLVXD_ LOAD_DRIVER code asks the driver to call CM_ Register_Device_Driver to register as the driver. Finally, DLVXD_LOAD_DEVLOADER indicates that the called driver should locate and dynamically load the requisite driver VxD, which will thereupon receive its own PNP_New_Devnode(DLVXD_LOAD_DRIVER) call in order to register itself as the driver.

Plug and Play drivers often break the rules about handling PNP_New_Devnode. In the previous article, I showed a sample enumerator for the fictional School Bus. This enumerator follows the same pattern as Microsoft bus enumerators. That is, Configuration Manager calls it initially because of a DevLoader entry in a registry key and asks it to be the device loader for the bus. Ignoring this injunction, the School Bus enumerator registers itself as both an enumerator and driver for the bus. It doesn't actually load any other drivers at this point! The sample driver for an equally fictional Telepathic Input Device illustrated the same technique for a driver that masquerades as a device loader in order to be able to register itself with Configuration Manager as the device driver.

A Plug and Play device driver is really nothing more than a dynamically loadable VxD that contains a configuration function in addition to its normal I/O programming. The configuration function can be relatively complicated, but doesn't have to be. I'll talk about this function in detail later on. For now, I'll just mention that its basic purpose is to retrieve I/O resource assignments from Configuration Manager and to initialize the device and the driver's own logic based on those assignments. If a device uses an interrupt request line, for example, the driver's configuration function would determine which IRQ to use by asking Configuration Manager. If it's possible to program the hardware to use the assigned IRQ, the driver would do that from within its configuration function. (If it's impossible to program the IRQ, Configuration Manager assigns the one and only IRQ the hardware wants to use. The software driver would still work as I'm describing it.) The driver would then use VPICD_Virtualize_IRQ in the normal way to hook the hardware interrupt. In effect, the configuration function subsumes much of the initialization that Windows 3.1 VxDs used to do during Device_Init. Furthermore, Configuration Manager VxD services replace previous methods-such as INI file settings, a priori assumptions, or communication with real-mode drivers-of learning about resource assignments.

In this article, I'll be mentioning many of the services Configuration Manager provides to VxD callers (see Figure 1). (Note that when you call these functions you must place the CM_ prefix in front of the name listed in Figure 1.)

Hardware Resources

Now, let's learn how Configuration Manager learns what resources areneeded by the complete collection of hardware, allocates those resources among the competing devices, and notifies the device drivers of the finished allocation. The resources are simply interrupt request line (IRQ) assignments, I/O port addresses, DMA channel numbers, and memory addresses. The architecture of Configuration Manager allows hardware makers to invent their own resource types, as I'll show later on.

Configuration Manager represents a device's resource requirements by a list of resource descriptor structures, one for each resource that the device needs. A collection of several resource descriptors forms a "logical configuration." Theoretically, a device might have several logical configurations, some of which are better for performance or end-user convenience than others. The priority of the logical configuration (see Figure 2) encodes this assessment.

The ability to have more than one logical configuration anticipates advances in hardware design. Eventually, you may see hardware that can operate in more than one mode depending on the availability of resources. For example, a video capture card might work best with mapped memory but might still operate (albeit more slowly) via DMA transfers. Such a device would have a DESIRED configuration that demands memory and a NORMAL or SUBOPTIMAL configuration that asks for a DMA channel in lieu of memory.

The enumerator that creates a DEVNODE should ordinarily also create the logical configuration(s) for the associated device. To continue along with the example I started last month, the School Bus enumerator might know that an attached Telepathic Input Device needs an IRQ in the range 0-7. It would tell Configuration Manager about this requirement by creating a logical configuration using code like that in Figure 3. CM_Create_DevNode creates a device node representing an attached Telepathic Input Device and stores the resulting DEVNODE address in the local device variable. CM_Add_Empty_Log_Conf creates a new (empty) logical configuration for the designated DEVNODE. Calls to CM_Add_Res_Des add resource descriptors to the logical configuration. This example indicates that the device needs an IRQ resource that can be satisfied by any of the IRQs described by the bit mask 0x00FF. That is, the device can use any IRQ in the range 0 through 7. You'll probably notice that I left out error checking after calls to CM_ADD_EMPLOY_LOG_CONF and CM_ADD_RES_DES. I originally did that because I thought the example was clearer without it. It turns out that a failure in either of these calls will leave the DEVNODE marked as having an out-of memory "problem."

Building Resource Descriptors

Figure 3 shows how an enumerator could create a logical configuration when the programmer knows at compile-time what resources the device requires. This would be a fairly unusual situation. Configuration requirements for legacy device normally appear in the LogConfig subkey of the device's hardware registry key. Windows Setup or the Device Installer puts them there based on configuration data in an INF file. The root enumerator uses the CM_Read_Registry_Log_Confs function to read them and convert them to Configuration Manager's internal format.

 CONFIGRET code = CM_Read_Registry_Log_Confs(devnode,0);

The ISA Plug and Play enumerator can read resource requirements directly from the Plug and Play cards. It then uses CM_ISAPNP_To_CM to convert the compressed data read from the cards into Configuration Manager's internal format. The EISA and PCI enumerators also have hardware-oriented methods to learn how the user has already configured cards attached to those buses.

Another situation worth mentioning is when the programmer decides to develop configuration information within the device driver. You might do this if you need to create the initial list requirements at run time. It turns out that Configuration Manager regards the configuration expressed in an INF file as immutable and doesn't allow it to be filtered at run time. To provide for dynamic changes to the requirements, therefore, you build logical configurations in the PNP_New_Devnode handler before you register your configuration function:

 CM_Add_Empty_Log_Conf(...);
CM_Add_Res_Des(...);
                    .
                    .
                    .
CM_Register_Device_Driver(...);

You must make these calls in this particular order because Configuration Manager may configure the device during the CM_Register_Device_Driver call.

Now I'll describe the format of the four standard resource descriptors. It's important to know that the descriptors are bidirectional in a sense. You tell Configuration Manager what resources you need by filling out a descriptor and attaching it to a logical configuration. Configuration Manager works internally with its own copies of descriptors in exactly the same format. You can later retrieve a pointer to one of Configuration Manager's internal resource descriptors by calling CM_Get_Next_Res_Des.

AnIRQ_DES structure describes a single IRQ:

 typedef struct IRQ_DES_s
    {
    WORD  IRQD_Flags;      // 00 shared/unshared flags
    WORD  IRQD_Alloc_Num;  // 02 IRQ actually allocated
    WORD  IRQD_Req_Mask;   // 04 maskofacceptableIRQs
    WORD  IRQD_Reserved;   // 06 (reserved)
    } IRQ_DES;             // 08

IRQD_Flags can have a value of fIRQD_Share to indicate that the device shares an IRQ or zero to indicate that the device needs an exclusive IRQ. IRQ_Req_Mask is a bit mask in which bit n is 1 if the device supports the use of IRQ n. A device that can handle any IRQ would use 0xFFFF for this parameter; a device that needs either IRQ 3 or IRQ 4 would use 0x0018. The IRQ_Alloc_Num field in one of Configuration Manager's internal IRQ descriptors eventually holds the IRQ actually assigned to the device.

A memory resource descriptor employs a MEM_DES structure immediately followed in memory by one or more MEM_RANGE structures:

 typedef struct Mem_Des_s
    {
    WORD  MD_Count;      // 00 numberofMEM_RANGEs
    WORD  MD_Type;       // 02 alwaysequaltoMTypeRange
    ULONG MD_Alloc_Base; // 04 base of allocated memory
    ULONG MD_Alloc_End;  // 08 end of allocated memory
    WORD  MD_Flags;      // 0C flags
    WORD  MD_Reserved;   // 0E (reserved)
    } MEM_DES;           // 10

typedef struct Mem_Range_s
    {
    ULONG  MR_Align;     // 00 mask for base alignment
    ULONG  MR_nBytes;    // 04 byte count
    ULONG  MR_Min;       // 08 minimum address
    ULONG  MR_Max;       // 0C maximum address
    WORD   MR_Flags;     // 10 flags (same as MD_Flags)
    WORD   MR_Reserved;  // 12 (reserved)
    } MEM_RANGE;         // 14

MD_Count is the number of MEM_RANGE structures that follow the MEM_DES structure in memory. MD_Type currently holds MTypeRange, which is just the size of a MEM_RANGE structure. MD_Flags can include one of fMD_ROM or fMD_RAM plus one of fMD_24 or fMD_32. Within each MEM_RANGE structure, MR_Align specifies how the allocated memory must be aligned. For example, 0xFFFFF000 denotes page (4096-byte boundary) alignment. MR_nBytes contains the byte length of the required memory area. MR_Min and MR_Max indicate bounds on the base address. To insist that the allocated memory be in the adapter region of memory between A000:0 and the start of the BIOS, for example, specify 0x000A0000 and 0x000EFFFF for the minimum and maximum values. MR_Flags and MR_Reserved should both be zero.

The MD_Alloc_Base and MD_Alloc_End fields in one of Configuration Manager's internal memory descriptors will eventually hold the starting and ending addresses of the assigned block.

The resource descriptor for I/O port addresses, like that for memory, uses a header structure followed by an array of range structures:

 typedef struct IO_Des_s
    {
    WORD IOD_Count;       // 00 numberofIO_RANGEs
    WORD IOD_Type;        // 02 always IOType_Range
    WORD IOD_Alloc_Base;  // 04 allocated base address
    WORD IOD_Alloc_End;   // 06 allocated end of range
    WORD IOD_DesFlags;    // 08 flags
    BYTE IOD_Alloc_Alias; // 0A allocated alias offset
    BYTE IOD_Alloc_Decode;// 0B allocated decode mask
    } IO_DES;             // 0C
 typedef struct IO_Range_s
    {
    WORD IOR_Align;       // 00 mask for base alignment
    WORD IOR_nPorts;      // 02number of ports needed
    WORD IOR_Min;         // 04lowestallowableaddress
    WORD IOR_Max;         // 06highestallowableaddress
    WORD IOR_RangeFlags;  // 08flags
    BYTE IOR_Alias;       // 0Aalias offset
    BYTE IOR_Decode;      // 0Balias decode mask
    } IO_RANGE;           // 0C

The meaning of most of these fields is analogous to the similar fields in memory resource descriptors. For example, an ISA card needing 16 ports starting on a 16-byte boundary would specify the following:

 IOR_Align = 0xFFF0;
IOR_nPorts = 16;
IOR_Min = 0x0100;
IOR_Max = 0x03FF;

The Min and Max values follow from the fact that the standard ISA bus provides for only 10 bits of I/O port addressing and reserves address 00-FFH.

The DDK gives a confusing description of how to use the Alias and Decode fields to do a couple of different things. One use for these fields is to describe how many bits of I/O port address the card decodes. A standard ISA card, for example, decodes only the low-order 10 bits, meaning that addresses 3F8H, 7F8H, BF8H, and so on, are all equivalent ways of addressing the same physical hardware port. Other cards may use multiple addresses that happen to differ by 400H, 800H, or some other value larger than 1024 (210). This scheme is so complex that Microsoft is planning to change itforthesequelto Windows 95. The only settings you should use for these fields are those shown in Figure 4.

One setting (Alias = 255, Decode = 0) requires special explanation. Normally, the I/O arbitrator automatically reserves all the 10-bit synonyms of any port address in recognition of the way ISA cards decode addresses. That is, if a card receives ports 3F8-3FFH, the arbitrator will normally not give any other card addresses like 7F8H-7FFH or BF8H-BFFH whose 10 low-order bits are the same. In a system with both a PCI and an ISA bus, the primary bus is the PCI bus, and the ISA bus is attached via a bridge. The ISA bus never sees I/O addresses bigger than 3FFH, which means that PCI cards can use the 10-bit synonym addresses with impunity. The special Alias/Decode setting of 255/0 tells the I/O arbitrator to assign port addresses without concern over 10-bit synonyms in order to take advantage of this feature of the PCI bus.

Finally, you indicate a DMA resource with a DMA_RES structure:

 typedef struct DMA_Des_s
    {
    BYTE DD_Flags;          // 00 flags
    BYTE DD_Alloc_Chan;     // 01 allocated channel
    BYTE DD_Req_Mask;       // 02 mask for supported                            
                            // channels
    BYTE DD_Reserved;       // 03 (reserved)
    } DMA_RES;              // 04

DD_Flags indicates the width of the required channel: fDD_BYTE, fDD_WORD, or fDD_DWORD. DD_Req_Mask indicates which channels can be used. For example, 0x60 indicates the ability to use channel 5 or 6. The number of the eventual channel assignment will appear in the DD_Alloc_Chan field of a Configuration Manager internal DMA descriptor.

Resource Arbitrators

Resource arbitration is the process by which Configuration Manager assigns resources to all of the devices that need them. Components known as "resource arbitrators" implement more-or-less elaborate algorithms for examining each device's needs and capabilities and for harmonizing those needs as far as possible. Microsoft provides arbitrators for the four standard resources (IRQ, DMA, port, and memory), and it exposes a great many APIs that are primarily useful to arbitrators. In fact, fifteen of the 81 Configuration Manager services listed in Figure 1 are used only by resource arbitrators. These include eleven functions for manipulating range list objects that can be used to describe port or memory address ranges for the port and memory arbitrators.

Configuration Manager is extensible to other resource types beyond the standard four, too. To illustrate how this works, suppose the Telepathic Input Device needed a special Telepathic Channel, of which only a limited number are available on any system. As a responsible hardware vendor, I ask Microsoft to assign a 10-bit OEM identifier number to use in forming a resource identifier. Suppose that number were 0x10 (0 through 0x0F being reserved by Microsoft). I combine this number with a 5-bit index denoting a particular resource. If this were the sixth resource I ever defined (prolific, aren't I?), I'd end up with a resource identifier like this:

 #define ResType_Telepath ((0x10 << 5) | 5)

Bit 15 of a resource identifier (0x8000) is ResType_Ignored_Bit and means that the resource so identified has no arbitrator. Bits 16-31 of a DWORD aren't used in resource identifiers. Microsoft doesn't believe very many vendors will ever be defining private resources, so the scheme I just outlined ought to provide sufficient flexibility.

I put the arbitrator for the Telepathic Channel resource into SCHOOL.VXD (see Figure 5) because it was convenient. During the initialization of this bus driver, the arbitrator is registered with Configuration Manager:

 BOOL OnSysDynamicDeviceInit()
    {                         // OnSysDynamicDeviceInit
    CM_Register_Arbitrator(&arbid, ResType_Telepath,
                           OnArbitrateTelepath, 0,
                           NULL, ARB_GLOBAL);
    return TRUE;
    }                         // OnSysDynamicDeviceInit

This registers OnArbitrateTelepath as the resource arbitration function for the resource and saves an arbitrator identifier in the REGISTERID variable named arbid. This identifier is used as the driver is unloaded to deregister the arbitrator:

 BOOL OnSysDynamicDeviceExit()
    {                         // OnSysDynamicDeviceExit
    if (arbid)
        CM_Deregister_Arbitrator(arbid, 0);
    return TRUE;
    }                         // OnSysDynamicDeviceExit

The previous code fragment registers a global arbitrator for a resource that might be used by many different devices. In most cases where you define private resources, you will probably register a local arbitrator for a resource that's specific to a bus or other single DEVNODE. To do this, use ARB_LOCAL as the last argument to CM_Register_Arbitrator and supply the DEVNODE address as the next-to-last argument. One advantage of using a local arbitrator is that Configuration Manager will automatically deregister the arbitration function if it removes the DEVNODE, thereby saving you the trouble of remembering to do so.

An arbitration function looks something like this:

 CONFIGRETOnArbitrateTelepath(ARBFUNC af,ULONGrefdata,
                             DEVNODE devnode, 
                             NODELIST_HEADER h)
    {                           // OnArbitrateTelepath
    switch (af)
        {                       // select on function

.

.

.

         }                       // select on function
    }                           // OnArbitrateTelepath

The af arbitration function is one of those listed in Figure 6, refdata is whatever reference data was supplied as the fourth argument to Register_Arbitrator (zero in the example), devnode is whatever DEVNODE was the fifth argument to Register_Arbitrator, and h is a pointer to a struct nodelistheader_s that in turn points to the head and tail of a list describing the device nodes needing configuration (see Figure 7). Each node in the list points to a DEVNODE and to a logical configuration. The logical configuration in turn contains resource descriptors, some of which may be for the resource you're arbitrating. The function should return CR_SUCCESS for all of the standard function codes unless there's really an error, but should return CR_DEFAULT for any unknown function code. Configuration Manager can use the CR_DEFAULT return to test version compatibility in future releases of Windows.

The basic resource assignment algorithm uses the TEST function to perform trial allocations for a list of logical configurations. Once all arbitrators have generated a working set of assignments, Configuration Manager performs a SET function to make the trial allocations permanent. I can best explain how to implement the TEST function by describing the sample:

 case ARB_TEST_ALLOC:        // af == 0
    {                       // ARB_TEST_ALLOC
    ALLOCPLACE place = {((NODELISTHEADER)h)->nlh_Head,
                         NULL};
    sortnodes((NODELISTHEADER) h);
    free_copy = free_map;
    release((NODELISTHEADER) h, &free_copy);
     if (allocate(&place, &free_copy))
        return CR_SUCCESS;
    else
        return CR_FAILURE;
    }                       // ARB_TEST_ALLOC

The ALLOCPLACE structure is one I defined so my "nextres" helper function can keep track of which resource descriptor is currently under consideration. That function steps from one telepathic resource descriptor to the next within a given DEVNODE's trial logical configuration and from one DEVNODE to the next.

The sortnodes helper function sorts the linked list of NODELIST elements. This is not as hard as it sounds because Configuration Manager exports a service function to do most of the work. The arbitrator walks the node list and sets the nl_ulSortDWord fields to an ordinal value indicating how easy or hard it will be to satisfy the demands of the device represented by the node. In this simple example, it's reasonable to use a sorting ordinal equal to the number of different channels the device is able to use. Fussier devices end up with lower ordinals than more flexible devices. Then the arbitrator calls CM_SortNodeList to sort the list based on the ordinals.

The release helper function releases the telepath resources currently used by the devices in the node list. This function should work with the allocated logical configuration (if any) belonging to each of the DEVNODEs in the node list. Since I'm performing a trial allocation, I actually operate on a copy of whatever data structure describes the current (real) allocation. A simple bit map indicating which of 32 possible channels is currently free suffices for this resource. Therefore, making a copy of the current allocation amounts to a simple assignment statement.

The allocate helper function walks through the node list in an attempt to simultaneously satisfy the demands of all devices. These demands are stated in one or more of the resource descriptors attached to a logical configuration for the devices. Configuration Manager can, in general, call the arbitrator once for each permutation of logical configurations for all devices. I used an algorithm that selects the next possible channel for a given resource descriptor and then recursively calls allocate to attempt to satisfy all remaining resource demands. In a perfect world, allocate will always succeed and return TRUE. Configuration Manager will then issue an SET request to commit the trial allocation:

 case ARB_SET_ALLOC:         // af == 2
    free_map = free_copy;
    free_copy = 0xDEADBEEF;
    return CR_SUCCESS;

The mildly odoriferous constant 0xDEADBEEF is traditionally used in Configuration Manager programming to indicate an uninitialized variable.

In the imperfect real world, it won't always be possible to satisfy every device. You might have two devices that can only use channel number zero. In such a case, allocate will return FALSE and the arbitrator will fail the test allocation by returning CR_FAILURE. Configuration Manager will issue a RELEASE request to allow the arbitrator to discard the now-useless trial allocation, and it will then attempt trial allocations using any alternative logical configurations that may exist. If no allocation is possible, Configuration Manager will disable one of the devices. Device Manager will report the conflict so the user understands why the disabled device can't be used.

There are two other arbitration functions that result in trial allocations: RETEST and FORCE. To implement RETEST, the arbitrator loops over all the resource descriptors just as it would for a TEST function. These descriptors are left over from boot-time assignment or from some previous TEST function, however, so they should record a resource assignment. RETEST therefore checks to see if the assigned resource is free in the trial allocation map and, if so, reassigns it. The most common use of RETEST is to record the configuration with which the device booted and find out whether the boot configuration contains inherent conflicts. Another use is to return to a suboptimal configuration that Configuration Manager found but bypassed in a failed attempt to find a better one. Configuration Manager also sends RETEST and SET requests with no trial logical configurations just to release the currently allocated resources.

FORCE works the same way as RETEST but must not fail. An arbitrator receives a FORCE request when the end user forces a particular resource choice through the Control Panel. The end user is in charge and may force you to assign resources in a particular way even though doing so doesn't make obvious sense. The ability to force a configuration provides an important safety valve in a potentially rigid automated environment, but can also allow a naive user to make mistakes.

Since neither RETEST nor FORCE examines any alternative assignments, there's no point in sorting the node list.

The Configuration Function

Once Configuration Manager has figured out how to assign resources to the hardware, the device drivers' configuration functions come into their own. It's this function that differentiates well-behaved Windows 95 device drivers from drivers in earlier versions of Windows. The main purpose of the function is to initialize the driver and the hardware to use newly assigned resources. Configuration Manager also calls the configuration function to handle a variety of other events (see Figure 8).

The CONFIG_START event is where all the action is. Configuration Manager calls the configuration function to process this event after assigning resources. The first job of the function is to determine which resources it now owns. If the device only uses the standard four resources, one function call will suffice (see Figure 9). In Figure 9, CM_Get_Alloc_Log_Conf retrieves information about the allocated logical configuration into a CMCONFIG structure (see Figure 10). This structure records up to nine memory base addresses and lengths, up to twenty I/O base addresses and counts, up to seven IRQ numbers and flags, and up to seven DMA channel numbers and attributes. In the example, the TELEPATH driver extracts its IRQ assignment from the expected first element of the bIRQRegisters array.

In previous versions of Windows, a primary purpose for writing a VxD was to virtualize hardware so that Windows DRV drivers and multiple MS-DOS®-based applications could share it transparently. For instance, Ring 3 drivers running in virtual machines hooked interrupt 9 and performed port I/O operations just as if they were talking to a real keyboard, but the Virtual Keyboard Device (VKD) driver multiplexed this activity so that keystrokes went where the user intended.

Virtualization is still important in Windows 95 for compatibility. MS-DOS-based applications continue to operate in their traditional egocentric ways. Moreover, many hardware vendors have been unable as yet to provide fully 32-bit VxD drivers and must continue to rely on real-mode solutions crafted for previous versions of the MS-DOS and Windows operating systems. Newer drivers typically provide a private API for applications to use in accessing devices, however. These drivers handle all physical I/O operations entirely within 32-bit protected mode. As I discuss the various CMCONFIG structure elements, I'll give a sketch of how a VxD goes about virtualizing or directly using the standard resources.

Memory Assignments

The CMCONFIG structure in Figure 10 records up to nine memory block assignments. The wNumMemWindows member indicates how many memory blocks were assigned, and entries in dMemBase record their base addresses. The dMemLength vector entries give the lengths of the memory blocks, and wMemAttrib elements specify block attributes. The base addresses are physical addresses and may, in general, occupy any location in the 32-bit address space. Most often, devices use memory in the adapter region of the first megabyte-that is, addresses in the range 000A0000 through 000FFFFF. The attributes word indicates the location of the assigned memory and can contain either fMD_ROM or fMD_RAM and either fMD_24 or fMD_32.

Virtualizing a device, such as the standard video display, that uses memory-mapped I/O requires the VxD to provide physically separate memory for each virtual machine. In addition, the VxD needs to swap each VM's private copy in and out of the physical device memory to swap ownership of the device. At a conceptual level, for example, the Virtual Display Device swaps virtual and real video buffers each time the user switches to a new full-screen MS-DOS session. The SHELL VxD drives the process of the state swapping by sending Set_Device_Focus system control messages as the user's focus changes.

Accessing physical memory directly from a VxD is not possible, since all addresses are subject to page translation. The normal way to access memory in the first megabyte is to add the virtual address to the CB_High_Linear value for the virtual machine in question. This gives a 32-bit address that unambiguously refers to the memory belonging to a single virtual machine.

Accessing physical device memory outside the first megabyte is a bit more complicated. In Windows 3.1, you use _MapPhysToLinear to obtain a virtual address describing a specified physical address. That service, which still works in Windows 95, modifies the system page tables as necessary to make the physical memory accessible. Unfortunately, the modification to the page tables is permanent, and you can't reclaim the virtual addresses if the physical memory moves later. If you start and stop the device several times during one Windows session at different physical addresses, you therefore run the risk of exhausting the page table. To solve this problem, you use _page Reserve to reserve a block of linear address space, _PageCommitPhys to commit the physical device memory to that block of linear addresses, and _LinPageLock to lock the page tables that map the memory so you can access the memory at interrupt time. When the device stops, you reverse these steps by calling _LinPageUnLock, _PageDecommit, and _PageFree. (Note: the DDK documentation for _PageCommitPhys incorrectly says the _LinPageLock won't allow you to access the memory at interrupt time. In fact, you can access the memory at interrupt time if you've locked the memory.)

I/O Port Assignments

The CMCONFIG structure records up to 20 I/O base addresses. The wNumIOPorts field indicates how many blocks of port addresses have been assigned to the device. Each assignment includes a base address (wIOPortBase element) and a port count (wIOPortLength element).

Virtualizing a device accessed through I/O ports often requires the VxD to trap the ports by calling Install_IO_Handler. Port trapping relies on the I/O permission mask attached to the one and only task state segment Windows uses. Setting a bit in the mask to 1 prevents virtual machine software from accessing the corresponding port directly. IN and OUT instructions instead cause GP faults that the VMM routes to the installed I/O handler, which can then simulate the operations or manage contention for unshareable devices.

Since VxDs run in the most privileged Ring 0, they can perform I/O operations to any port no matter how the I/O permission mask happens to be set.

IRQ Assignments

The CMCONFIG structure records up to seven IRQ assignments. The wNumIRQs field indicates how many IRQs have been assigned to the device, and entries in bIRQRegisters indicate which ones. The bIRQAttrib array records attributes for each IRQ; the only current attribute is the fIRQD_Share flag, which indicates that the IRQ is shared with other devices.

In real-mode under MS-DOS, you hook a hardware interrupt in two steps. First, you install the address of a service routine in the interrupt vector table at 0:0. For example, IRQ 10 interrupts on INT 72H in a standard PC. Second, you unmask the IRQ by writing to the Programmable Interrupt Controller's (PICs) interrupt mask register (IMR). Since there are two PICs on a standard PC, there are two IMRs at port addresses 21H and A1H. These are 8-bit ports in which a 1 bit indicates the masked state that prevents interrupt signals on the associated request line from actually reaching the central processor.

You must perform the same two steps in a VxD to hook a hardware interrupt in Windows 3.x or Windows 95, but you don't do so directly. The VPICD (Virtual PIC Device) driver virtualizes the interrupt controller for the entire system, and it exports services that other VxDs use for interrupt handling. VPICD_Virtualize_IRQ takes the place of an explicit interrupt vector replacement, and VPICD_Physically_Unmask performs the necessary operations with the mask register. VxDs always use these services instead of directly modifying the protected-mode interrupt descriptor table or directly performing I/O to the mask register.

When you virtualize an IRQ, you supply a function pointer table. One of the functions is a hardware interrupt procedure that VPICD calls shortly after fielding a hardware interrupt. Modern device drivers will handle the interrupt entirely in 32-bit protected mode and will therefore eventually call VPICD_Phys_EOI to mark the end of the interrupt. In previous versions of Windows, hardware interrupt handlers were often very short procedures whose only purpose was to reflect the interrupt to a real-mode handler previously installed by CONFIG.SYS.

DMA Assignments

The CMCONFIG structure records up to seven DMA channel assignments. The wNumDMAs field indicates how many channels have been assigned to the device, and the bDMALst array contains their numbers. The wDMAAttrib array indicates the width of the assigned channels as fDD_BYTE, fDD_WORD, or fDD_DWORD.

Devices that use DMA channels don't normally need to provide specially for virtualizing DMA transfers. That's the responsibility of the VDMAD driver, which traps the DMA controller ports for real-mode applications. VDMAD also translates virtual addresses to locked physical addresses and handles physically discontiguous transfers. Protected-mode applications that wish to do DMA transfers use the VirtualDMAServices API to communicate with VDMAD.

A VxD that wants to initiate a DMA transfer must use VDMAD services instead of directly programming the DMA channel. It might, for example, use VDMAD_Lock_DMA_Region,VDMAD_Set_Region_Info,VDMAD_Set_Phys_State, andVDMAD_Phys_Unmask_Channel to initiate transfers.

Other Configuration Issues

Since Configuration Manager allows hardware vendors to define their own I/O resource types, it also gives driver writers a way to determine private resource assignments. This isn't as easy as with the four standard resource types, because it requires the driver to explicitly loop over resource descriptors. In general, the driver would contain code like the following:

 LOG_CONF logconf;
RES_DES hres;
CM_Get_First_Log_Conf(&logconf devnode,ALLOC_LOG_CONF);
hres = (RES_DES) logconf;
while (CM_Get_Next_Res_Des(&hres, hres, restype,
                           NULL, 0) == CR_SUCCESS)
    { // for each restype resource
      // [extract assignment from  "hres->" structure]
    } // for each restype resource

Here restype denotes the private resource type index. This could also be one of the standard resource types like ResType_IRQ, but there's not much point in writing this sort of elaborate code to determine which IRQ has been assigned when CM_Get_Alloc_Log_Conf is so much more convenient. CM_Get_First_Log_Conf here retrieves the one and only allocated logical configuration for the device. CM_Get_Next_Res_Des loops over all the restype resource descriptors. You start the loop by giving it a logical configuration handle; thereafter, you give it a resource descriptor handle, and it returns the next one. The resource handles this function returns are just the Ring 0 flat addresses of the resource descriptor structures in whatever format the resource arbitrator happens to use. For example, the handle to an IRQ resource descriptor is simply the address of an IRQ_DES structure.

Configuration Manager can send a variety of other events to a device's configuration function. Besides START, the FILTER, REMOVE, and STOP events are particularly important. FILTER allows a driver to examine and modify the resource descriptors in its logical configurations before resources are actually assigned. You might want to handle this event, for example, if your parent DEVNODE's enumerator is too optimistic about which IRQs your device can actually handle. REMOVE means that your device is being logically removed from the system. You should respond by terminating your use of the assigned I/O resources. In addition to not performing any more I/O operations, you should call the inverses of the services you used earlier, such as _PageFree and VPICD_Force_Default_Behavior. STOP asks you to stop using your configuration as well; in practice, I see REMOVE requests even in situations (like disabling the device from Device Manager) where I'd expect to see STOP instead. In the sample TELEPATH driver, therefore, I treat STOP and REMOVE the same way.

Working with the End User

Everything I've said in these two articles until this point concerns VxD-level programming. There's one additional aspect of Plug and Play that relates to application-level programming that I'll talk about here: how you can extend Device Manager's device property sheet with custom information.

From within the Windows 95 Control Panel, you can launch the System applet and then select the Windows 95 Device Manager (see Figure 11). Selecting a particular device and clicking on the Properties button brings up a property sheet that describes that device (see Figure 12). The standard property sheet contains pages labeled General, Driver, and Resources. If your device has additional information to report to the user, you can write a custom property sheet provider to display it. I'll show you one simple way to report which telepathic channel belongs to a Telepathic Input Device (see Figure 13).

Figure 11 Device Manager

Figure 12 Device Property Sheet

Figure 13 Simple Custom Property Page

To create custom property pages for Device Manager, you'll create a small 16-bit (more on this later) DLL that exports an EnumPropPages function. This function should create one or more standard Windows 95 property pages. Your DLL should also contain the resources that define the layout of your custom pages and the dialog functions that manage the pages at run time. Finally, you arrange to add an EnumPropPages named value to the software key for your device driver.

Figure 14 shows SCHOOLUI.DLL, which is the custom property page provider for Telepath devices. SCHOOLUI.C is the only source program in this DLL, and it contains only two functions: EnumPropPages and StatusDlgProc. The purpose of EnumPropPages is to define the custom property page for the device. StatusDlgProc is the dialog procedure for that page, and it uses Configuration Manager calls to determine which telepathic channel gets assigned to the device.

I want to explain the unusual aspects of SCHOOLUI.DLL. Even though Windows 95 provides great support for Win32®-based applications, this DLL is a 16-bit program. This is partly because Device Manager itself is a 16-bit applet and because SCHOOLUI needs to make calls to CONFIGMG.VXD's protected-mode API, which is only available to 16-bit callers. SCHOOLUI.C includes some header files that come from the Windows 95 DDK. In fact, the DDK includes a complete set of 16-bit Windows header files, including WINDOWS.H and COMMCTRL.H, in addition to the 32-bit headers normally used by VxDs. SCHOOLUI includes SETUPX.H, which is primarily devoted to declarations used by custom installation programs but has some declarations needed here; SETUPX.H also includes the 16-bit version of the standard Windows 95 PRSHT.H header, which defines property sheet structures. SCHOOLUI also contains the following statements that include VxD header files:

 #define Not_VxD 
#include <vmm.h>
#define MIDL_PASS
#include <configmg.h>

The #define statements give you just the declarations from VMM.H and CONFIGMG.H that are useful to 16-bit Windows-based programs. You end up with declarations that let you call Configuration Manager functions using source code that's almost identical to what you would use in a VxD.

The EnumPropPages function creates a property page using the regular CreatePropertySheetPage function. The only part of this operation that's out of the ordinary is this statement:

 status.lParam = (LPARAM) 
                pdi->dnDevnode;

The pdi variable is a function argument that points to a DEVICE_INFO structure (defined in SETUPX.H). The only member of that structure I'm interested in here is dnDevnode, which is the Ring 0 flat address of the DEVNODE whose properties are going to be reported. The indicated statement saves this DEVNODE address in the lParam member of the property page descriptor, from whence it can later be extracted by the dialog procedure.

You're probably wondering what use a 16-bit, Ring 3 DLL has for a Ring 0 flat pointer to an undocumented data structure. The WM_INITDIALOG handler in the StatusDlgProc shows how to use this pointer to discover information about the device. The lParam parameter for WM_INITDIALOG is the address of a copy of the PROPSHEETPAGE structure originally used to define the property page, and the lParam member of that structure is the DEVNODE address you originally put there. So, the following statement retrieves the (flat) DEVNODE address:

 DEVNODE devnode = (DEVNODE) 
((LPPROPSHEETPAGE) lParam)->lParam;

You can pass this address as an argument to Configuration Manager functions in what looks like the normal way for a VxD. For example, you can get a copy of the telepathic resource structure for your device as follows:

 CM_Get_First_Log_Conf(&logconf, devnode, 
                      ALLOC_LOG_CONF);
CM_Get_Next_Res_Des(&hres, (RES_DES) logconf,
                    ResType_Telepath, NULL, 0);
CM_Get_Res_Des_Data(hres, &res, sizeof(res), 0);

This is a Ring 3 DLL, not a VxD, so how can it call Configuration Manager? When Not_VxD is defined, CONFIGMG.H defines all of the CM_XXX APIs as inline functions that use Configuration Manager's protected-mode API entry point (the one you discover using the standard INT 2FH, Function 1684H interface). The API entry in turn calls service functions within Configuration Manager. The only thing you have to do differently from a VxD is use CM_Get_Res_Des_Data to get the data pointed to by the resource handle (which is another Ring 0 flat pointer you can't use directly).

You have to do one more thing to create your custom property page, and that is to update the registry. When Device Manager wants to display a device's property sheet, it looks in the device's software key for an EnumPropPages entry. This named value designates a DLL and, optionally, the name of a function within that DLL that will create custom pages for the property sheet. By default, the function name is EnumPropPages. So, to create this custom page, I put an entry into my INF file that added the right entry to the registry. The resulting software key is shown in Figure 15.

Figure 15 EnumPropPages entry in a Registry key

I showed you an especially trivial example of a custom property page. You're obviously only limited by your imagination and the requirements of your device in what you could put into a custom page. You can also replace the standard General and Resources pages if you wish. To do this, define and add your replacement page as usual and set either (or both) of the DI_GENERALPAGE_ADDED or DI_RESOURCEPAGE_ADDED flags in the Flags member of the DEVICE_INFO structure passed to EnumPropPages as an argument. The right place to report the telepathic channel assignment, for example, would be in the Resource Settings list box that forms part of the Resources page (see Figure 16). Unfortunately, if you want to replace this relatively complicated page, you must duplicate all of its functionality, including presenting conflict information, responding to requests to change resource assignments, and so on. This is probably too much work, especially since the resulting code has to be maintained forever as Device Manager evolves.

Figure 16 Resources Property Page

Errata

There's a few things I'd like to correct from the first article. First of all, the listing for S_CTL.ASM won't assemble as shown-it's missing a couple backslashes. The one that appears in this article is correct and applies to both articles. The listing for TELEPATH.C has duplicate OnConfigure functions. Ignore the first one-it's empty. Once again, rely on the listing in this article. (You needn't worry about having retrieved the wrong source code from an MSJ bulletin board, since the correct version was uploaded, and is the same for both articles.)

The statement last month on page 38 that Configuration Manager is "able to respond to dynamic events like power-on or undocking" by virtue of being continuously present during your Windows session is misleading. Rather, being present allows Configuration Manager to handle docking and undocking events and other "hot" changes in hardware that happen without first powering down.

I learned after the article had gone to press that CONFIG$ isn't the actual agent that reads the Windows 95 registry during MS-DOS startup. IO.SYS does that and gives the information to CONFIG$ for relay to CONFIGMG.VXD. Furthermore, CONFIGMG doesn't need to rescan the registry to try to reinterpret the friendly name of a hardware profile because CONFIG$ supplies the numeric identifier as well as the name. Both of these factoids are pretty much meaningless to most programmers, but it's good to have the record straight.

Where To Go Next

In these two articles, I've described the basics of Plug and Play, starting with a discussion of how hardware identifies itself to operating systems like Windows 95. I showed examples of the major VxD components that work with Configuration Manager to detect and configure hardware and the corresponding drivers: a bus enumerator, a device loader, a device driver, and a resource arbitrator. For more information, consult the relevant bus specifications and the PNP.DOC file that comes with the Windows 95 DDK; then build and test your own sample programs. I hope you'll find those other resources more accessible after reading this and my previous article.

From the January 1996 issue of Microsoft Systems Journal.