Tom and Glenn are principal engineers at Phoenix Technologies, where Tom specializes in Plug and Play and Glenn specializes in advanced technologies. They can be contacted at [email protected] and [email protected], respectively.
One of the first things you read in the Microsoft Hardware Design Guide for Windows 95 is that the guide "emphasizes the Plug and Play concept." This raises a number of questions, not the least of which is, What exactly is Plug and Play? The easy answer is that Plug and Play (PnP) is an architecture implemented in Windows 95 that's designed to eliminate some of the hardware-configuration hassles of installing peripherals such as soundcards, CD-ROM drives, graphics subsystems, and the like. Of course, to make this automatic hardware detection and configuration possible, PnP has a few requirements: For instance, your system motherboard, system BIOS, and peripheral devices all must be PnP compliant at the outset. In short, PnP is a collection of methods for determining and controlling system-resource usage. The problem with short answers, however, is that they tend to generate more questions.
For instance, what are system resources? To PnP, system resources are memory address space, I/O address space, IRQs, and DMAs. Memory address space can be further divided into what's below and above 1M. I/O can be divided into that below 400h (and all its aliases we haven't managed to escape) and that above. DMA consists of the 8- and 16-bit varieties, and IRQs can sometimes be shared.
Resources are allocated to or claimed by motherboard devices and plug-in bus devices of one of several families that may or may not include ISA, MCA, EISA, PCMCIA, VESA-VL, or PCI. Often, a motherboard device is actually a bus device soldered directly to the motherboard. And don't forget that configuration mechanisms (jumpers, switches, ROM setup, standard configuration utilities, and custom configuration utilities) are varied and sometimes inconsistent with each another.
In an effort to make the PC universe more pleasant, a number of hardware and software companies have developed the PnP standard for the determination and control of all system resources. Where practical, PnP also shows which device is consuming what resource, and where the device is located.
PnP has been shipping on many motherboard BIOSs in one form or another for over a year. There are two major types:
- Conflict detection and resolution (CDR).
- Run-time services (RTS).
The PnP header, which checksums to 0, contains a signature, length, data pointers, and code-entry points; see Table 1. There should be no more than one such header in the entire F000 segment. These entry points are far called from real or 16-bit protected mode. In this article, we'll focus on these code-entry points.
The code-entry points take stack-based parameters. While function number is the first parameter, additional parameters are function specific. This can be complex to declare properly in C; the source code accompanying this article serves as an example of declaring it so that it's easy to call.
The PnP RTS specification allows all functions to be optional. Both the RTS and CDR specs are available in source-code form; see "Availability," page 3. To save BIOS space, these functions may be implemented with little or no error checking. It is important that the calling application not pass unexpected input.
The most widely implemented functions (as well as those required for Windows 95 PnP compatibility) are the device-node services--GetNumberOfDeviceNodes, GetDeviceNode, and SetDeviceNode. These services allow a program to determine the resources used by the motherboard. Configuration utilities can use this information to preclude configuration conflicts. System-information utilities can use this information to augment their automatic hardware-detection algorithms.
Device nodes contain the Descriptor Block of Used Resources and the Descriptor Block of Possible Resources. The first states the resources the device is currently configured to use, while the second states the possible configurations the device can accept. Both are in a tagged, byte-oriented format that encodes length in a standard way. Thus, new descriptors can be added such that existing code can skip over them without getting lost. The supplied code contains routines to parse this format.
Reading a device node tells you if it can be set programmatically. If so, the SetDeviceNode call can be used. Nodes must be checked before a set call is issued since the function itself is not required to perform qualification of the input block. Neither is it required by the spec to return an error on any form of invalid input. The preferred method of reading a device node is as follows:
- Get the device node using the Descriptor Block of Used Resources as a template.
- Select the desired configuration from the Descriptor Block of Possible Resources. This configuration should not conflict with any other device-node resource usage.
- Set the device node (checking for error return).
- Get the device node again, verifying that the requested configuration was set.
PnP operates on the principle that everything in the PC universe is generically detectable. PnP ISA and PCI have configuration registers that can be read, and RTS supplies the device nodes to determine what's on the motherboard. Any other devices are classified as ISA Legacy Devices or Statically Allocated Resources. ("Legacy" devices are expansion cards that do not support the PnP spec. Windows 95 includes an "Add New Hardware" wizard for configuring legacy ISA cards in machines with a PnP BIOS. The system assigns resources to legacy cards before anything else.)
PnP also needs to determine a resource's usage. There are two accepted methods for this, and only one is implemented in any given system, so applications need to handle both. The first is to use the Get and Set Statically Allocated Resource functions, which store only those resources that are not generically detectable. Consequently, there is no information about how the user wants things configured--only about the areas unavailable to configurable devices.
The second way of determining resource usage is to use the Extended System Configuration Data (ESCD) functions: GetESCDInformation, ReadESCD, and WriteESCD. ESCD is an EISA-compatible format that includes structure definitions to store information about device nodes, PnP ISA, and PCI hardware. ESCD stores statically allocated resource information and the Last Working Configuration (LWC) of configurable devices. This allows the system to abbreviate the CDR process and ensure consistent device placement from boot to boot. This is important when device drivers for configurable systems get their placement information from a static source such as a command-line parameter in CONFIG.SYS. Device drivers for PnP or PCI devices should never do this but they sometimes do.
ESCD allows run-time utilities (such as the Device Manager in Windows 95) to request specific device placements. This may allow indirect control of the priority of boot devices, or difficult-to-place configurations can be determined by operating-system or application-level tools.
PNPDEMO.EXE demonstrates PnP RTS by showing how to find the $PnP header. The program uses the Real Mode Entry Point and calls several PnP services.
The existence of a PnP BIOS is determined by the presence of a valid $PnP header contained within the F000 segment of the BIOS. This header contains several pieces of information, but PNPDEMO focuses only on the Real Mode Entry Point. By calling this real-mode location with the appropriate parameters placed on the stack, the caller can derive all of the devices on the motherboard, the state of the docking station, the number of PnP ISA cards, and resource information about the entire system. Moreover, through this interface, the caller can configure or disable motherboard devices directly in an industry-standardized, platform-independent fashion. Note that PNPDEMO is not an alternative to the PnP BIOS specification; it merely demonstrates many of a PnP BIOS's capabilities.
After locating the PnP header and determining the entry point, a program makes calls by placing the appropriate parameters on the stack and calling the entry point's designated location. The first parameter to all functions is the function number. Table 2 lists the functions and their numerical designation.
PNPDEMO lets the user call all but the Set or Write type functions. These functions (which you can find in the appropriate source files) should be tightly controlled by more-sophisticated software. Note that although the files have the .CPP extension to indicate that they contain C++, most of the code is actually straight C and only utilizes a few subtle C++ features.
PNPDEMO.CPP. Within each of the following functions, each PnP Run-time Service's parameters is defined and used in a sample call; see Example 1(a). PnpSummary displays the current allocation of all of the device nodes by enumerating the list and parsing the allocated resources, as in Example 1(b).
PNPBIOS.CPP. FindPnpBIOS scans the F000 segment to locate a valid PnP BIOS Header. PnpGetHeader displays the contents of this header; see Example 2(a). The routines in Example 2(b) illustrate how to parse the contents of a device node, its header, allocated resources, and possible resources.
PNPISA.CPP. The routines in the PNP-ISA.CPP module provide the detailed resource parsing in complete and summarized forms; see Example 3.
UTILS.CPP. EisaId converts the DWORD ID fields to their uncompressed string value; see Example 4. Each ID contains both a vendor designation and a model number. In the case of many motherboard device nodes, this ID indicates a compatible standard device. For example, PNP0401 indicates a 16550-compatible serial port.
It's been said that anything making the user's interaction with PCs easier ends up making the programmer's job harder. There's no better example of this than the Windows 95 PnP expansion architecture. With promises of PnP features such as support for dynamic reconfiguration events, our jobs won't get any easier.
Table 1: PnP header contents.
Copyright © 1995, Dr. Dobb's Journal
Field Offset Length Value
Signature 00h 4 BYTES $PnP (ASCII)
Version 04h BYTE 10h
Length 05h BYTE 21h
Control field 06h WORD Varies
Checksum 08h BYTE Varies
Event-notification 09h DWORD Varies
Real-mode 16-bit 0Dh WORD Varies
offset to entry point
Real-mode 16-bit 0Fh WORD Varies
16-bit protected-mode 11h WORD Varies
offset to entry point
16-bit protected-mode 13h DWORD Varies
code-segment base address
OEM device identifier 17h DWORD Varies
Real-mode 16-bit 1Bh WORD Varies
16-bit protected-mode 1Dh DWORD Varies
data-segment base address
Table 2: PnP RTS functions.
0 Get number of device nodes.
1 Get device node.
2 Set device node.
3 Get event.
4 Send message.
5 Get docking-station information.
9 Set statically allocated resources.
A Get statically allocated resources.
B Get APM 1.1 table.
40 Get PnP ISA information.
41 Get ESCD information.
42 Read ESCD.
43 Write ESCD.
Example 1: (a) Defining and calling typical PnP run-time service functions; (b) using the PnpSummary function.
int PnPFnGetNodeInfo(int argc, char * argv)
int PnPFnGetNode(int argc, char * argv)
int PnPFnSetNode(int argc, char * argv)
int PnPFnGetEvent(int argc, char * argv)
int PnPFnSendMessage(int argc, char * argv)
int PnPFnGetDockInfo(int argc, char * argv)
int PnPFnSetStaticResources(int argc, char * argv)
int PnPFnGetStaticResources(int argc, char * argv)
int PnPFnGetApmTable(int argc, char * argv)
int PnPFnGetIsaConfig(int argc, char * argv)
int PnPFnGetEscdInfo(int argc, char * argv)
int PnPFnGetEscd(int argc, char * argv)
int PnPFnSetEscd(int argc, char * argv)
int PnpSummary (int argc, char* argv)
Example 2: Using FindPnpBIOS to locate a valid PnP BIOS header; (b) displaying the contents of the PnP BIOS header.
WORD FindPnpBios ()
int PnpGetHeader ()
void DumpDevNodeHeader (BYTE ** ppbDevNode)
void DumpDevNodeSumHeader (BYTE ** ppbDevNode)
void DumpDevNodeSumAllocated (BYTE ** ppbDevNode)
void DumpDevNodeAllocated (BYTE ** ppbDevNode)
void DumpDevNodePossibles (BYTE ** ppbDevNode)
void DumpDevNodeCompatIds (BYTE ** ppbDevNode)
void DumpDevNode (BYTE * pbDevNode)
void DumpDevNodeSummaryLine (BYTE * pbDevNode)
Example 3: Detailing resource parsing in two different forms: complete and summarized.
void DumpIsaResources (BYTE **ppbIsa)
void DumpIsaSumResources (BYTE **ppbIsa, int iLongSum)
Example 4: EisaId converts the DWORD ID fields to their uncompressed string value.
char * EisaId (DWORD dwCompressedID, char * pszBuffer)
Copyright © 1995, Dr. Dobb's Journal