52964.fb2
Let's finally talk to some real hardware. The next chapters discuss the WdmIo and PHDIo drivers. These general-purpose drivers give Win32 programmers access to simple hardware devices. You can do basic reads and writes of hardware ports. The drivers also let you perform interrupt driven I/O. There is just enough functionality to let a user mode application send output to a standard LPT printer port. Although this is not the recommended way to talk to a printer port[34], it does show that these drivers can be used in a real application.
The generic nature of the WdmIo and PHDIo drivers does make them slightly more complicated to understand. However, a complete and genuinely useful driver is worth having. Using a generic driver as an example also shows all the steps that a you need to use it as the basis of a more advanced driver.
To make these drivers easier to understand, this chapter looks at the interface they make available to Win32 programmers. I will use the test programs WdmIoTest and PHDIoTest, which send a test message out a parallel port to a printer, as an example. This chapter also looks at the installation and resource allocation issues for these drivers.
The following chapters explain how the WdmIo and PHDIo drivers work. They explain the following topics in detail.
• Queuing IRPs for serial hardware I/O
• StartIo routines
• Cancel routines for queued IRPs
• Handling the Cleanup IRP
• Interrupt driven I/O
• Deferred Procedure Calls
• Timers for time-outs
• Custom timers
• NT hardware detection and allocation
The WdmIo driver is a Plug and Play driver for Windows 98 and Windows 2000. It is based on most of the previous drivers. However, it does not support Power Management, WMI, or NT events. Its installation INF file specifies the resources that are available to a WdmIo device. A WdmIo device must have one (and only one) I/O port address range. It usually has one interrupt line resource as well. This is needed if interrupt-driven reads or writes are to be used. More than one WdmIo device can be installed, provided that separate INF files with different resources are used.
The PHDIo driver is an NT style driver, primarily for NT 3.51, NT 4, and Windows 2000. However, it works perfectly well in Windows 98. When the driver is loaded, it creates one device called \\.\PHDIo. The I/O port and interrupt resources are specified as a part of the filename used to open a handle to this device. Again, one I/O port must be given, and one interrupt can be specified, if need be.
Win32 programs must open a handle to a WdmIo or PHDIo device. WdmIo devices are accessed using the device interface identified by WDMIO_GUID. The one PHDIo device has a symbolic link name of \\.\PHDIo. However, the filename passed to CreateFile must include a definition of the resources required.
Having opened a handle to a WdmIo or PHDIo device, the Win32 program can use several IOCTLs as well as issuing read and write requests. Finally, it must close the device handle.
Table 15.1 lists the supported IOCTLs, including whether they need input or output parameters.
Most of the IOCTLs pass an input buffer that contains one or more PHDIO commands and their parameters.
IOCTL_PHDIO_RUN_CMDS is used to run a set of commands straightaway. The three following IOCTLs store the commands that are used later in the processing of read and write requests. Finally, IOCTL_PHDIO_GET_RW_RESULTS retrieves the command results and output from the last read or write.
Table 15.1 WdmIo and PHDIo IOCTL codes
IOCTL | Input | Output | Description |
---|---|---|---|
IOCTL_PHDIO_RUN_CMDS | Yes | Optional | Run the passed commands |
IOCTL_PHDIO_CMDS_FOR_READ | Yes | No | Store the commands to read a byteand store in the read buffer |
IOCTL_PHDIO_CMDS_FOR_READ_START | Yes | No | Store the commands that start theread process |
IOCTL_PHDIO_CMDS_FOR_WRITE | Yes | No | Store the commands to output a byte from the write buffer |
IOCTL_PHDIO_GET_RW_RESULTS | No | Yes | Get the command results of the last read or write operation |
A Win32 program passes a block of commands in an IOCTL input buffer. These commands are either run straightaway or are run one or more times during the processing of read or write requests.
A command is a single byte, followed by one or more parameter bytes. Table 15.2 lists all the commands, their parameters, and their output. The Ioctl.h header in the WdmIo\Sys directory contains the command definitions. Note that all operations are currently byte-sized. However, the two top bits of the command byte are reserved to indicate 16-bit word transfers and 32-bit long word transfers. If either of these bits is currently set, the command is not run and command processing is aborted. All command parameters are BYTEs. A BYTE is an unsigned 8-bit value, so you cannot use negative values.
Most of the commands are fairly self-explanatory. They are best explained using examples in the context of a sample application, WdmIoTest, that outputs data to the LPT printer parallel port.
Table 15.2 WdmIo and PhdIo commands
Command | Input parameters | Output | Description |
---|---|---|---|
PHDIO_OR | reg, Value | Read register, OR with value, and write back. Use to set bit(s) | |
PHDIO_AND | reg, Value | Read register, AND with value, and write back. Use to clear bit(s) | |
PHDIO_XOR | reg, Value | Read register, XOR with value, and write back. Use to toggle bit(s) | |
PHDIO_WRITE | reg, Value | Write value to a register | |
PHDIO_READ | reg | Value | Read value from a register |
PHDIO_DELAY | delay | Delay for given microseconds. Delay must be 60µs or less | |
PHDIO_WRITES | reg, count, Values, delay | Write values to same register with delay (<=60µs) | |
PHDIO_READS | reg, count, delay | Values | Read values from same register with delay (<=60µs) |
PHDIO_IRQ_CONNECT | reg, mask, Value | Connect to interrupt | |
PHDIO_TIMEOUT | seconds | Specify time-out for reads and writes | |
PHDIO_WRITE_NEXT | reg | Write next value from write buffer | |
PHDIO_READ_NEXT | reg | Store next value in read buffer |
The WdmIoTest Win32 application uses the WdmIo driver to output a couple of lines of text to a printer on the old Centronics LPT1 parallel printer port. The source and executable for this program are in the book software WdmIo\Exe directory. However, you will not be able to run this program until you have installed the WdmIo driver and configured your system correctly, as described later.
A parallel port responds at three addresses in I/O space in an x86 PC. It also generates an interrupt (when enabled) on one interrupt line. The LPT1 port traditionally lives at I/O space addresses 0x378 to 0x37A and generates ISA interrupt IRQ7.
A basic parallel port has the three registers as listed in Table 15.3. Signals with #at the end of their name use negative logic. For example, if the Status BUSY# bit is read as zero (low), the printer is busy.
The printer is ready to accept data if the Status BUSY# and ONLINE bits are high.
To write data to the printer, write the byte to the Data port. Wait 1µs for the data lines to settle. Set the Control port STROBE output high for at least 0.5µs. The printer signals that it is busy as BUSY# is read as low. When it is ready for another byte, it pulses ACK# low briefly and BUSY# goes high again.
The ACK# pulse signals the parallel port electronics to interrupt the processor (provided ENABLE_INT is high). I have no clear documentation to say what is exactly supposed to happen now. One source says that bit 2 of the Status register is set low to indicate that it has caused the interrupt. However, the two printers that I tried did not set this bit low. It seems that you just have to assume that if the right interrupt arrives (e.g., IRQ7), the correct parallel port caused the interrupt. It seems that reading the Status register clears the interrupt.
A printer may sometimes also pulse ACK# low and so generate an interrupt at other times, such as when it has finished initializing itself, when it is switched off, when it goes off-line, or when it runs out of paper. A dedicated parallel port driver might well take special action for these circumstances. The code here just needs to ensure that the WdmIo driver reads the Status port to clear the interrupt.
As you may be aware, more sophisticated versions of the parallel port are available. For example, when configured in the appropriate mode, a parallel port can input information via the Data port. In addition, some non-standard devices can be connected to a basic parallel port. These might allow input of 4-bits at a time using the Status port. Dongles are hardware devices that are used to verify that some software is licensed to run on a computer. They are designed to respond when their manufacturer's software talks to them in some special way. At all other times, they are designed to pass all parallel port signals to the printer and vice versa.
Table 15.3 Parallel port registers
Offset | Access | Register |
---|---|---|
0 | Read/Write | Data |
1 | Read only | Status |
Bit 3 | ONLINE | |
Bit 5 | OUT_OF_PAPER | |
Bit 6 | ACK# | |
Bit 7 | BUSY# | |
2 | Read/Write | Control |
Bit 0 | STROBE | |
Bit 2 | INIT# | |
Bit 3 | SELECT | |
Bit 4 | ENABLE_INT | |
Bit 6 | 1 | |
Bit 7 | 1 |
The WdmIoTest application uses the WdmIo driver to write a brief message to the printer. It outputs information to the console screen to indicate its progress as it performs the following steps. Steps 2-8 all involve DeviceIoControl or WriteFile calls to the WdmIo driver.
1. Open a handle to the WdmIo device. The GetDeviceViaInterface routine is used as before to open a handle to the first device with a WDMIO_GUID device interface.
2. Disable the interrupts and connect to an interrupt.
3. Initialize the printer.
4. Send commands for writing each byte.
5. Read the status every second until the printer is ready. Time-out after 20 seconds.
6. Write a message to the printer.
7. Get the write results.
8. Disable the interrupt.
9. Close the handle.
PHDIoTest does exactly the same job as WdmIoTest, except that it obtains a handle to the PHDIo device in a different way. It calls CreateFile directly, passing the required resources in the filename, as follows.
HANDLE hPhdIo = CreateFile("\\\\.\\PHDIo\\isa\\io378,3\\irq7\\override", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
Table 15.4 shows the different resource specifiers that may be used as a part of the PHDIo filename. The first specifier must be \isa. The other specifiers can be in any order. An I/O port specifier must be given. Use the \override specifier with caution and never use it in a commercial release. All letters in the filename must be in lower case.
Table 15.4 PHDIo filename resource elements
Element | Required | Description |
---|---|---|
\isa | Mandatory | Initial string: Isa bus |
\io<base>,<length> | Mandatory | I/O ports <base> and <length> in hex |
\irq<number> | Optional IRQ | <number> in decimal |
\override | Optional | Use these resources even if they cannot be allocated |
Listing 15.1 shows the commands that initialize the printer. It also shows how DeviceIoControl is called with the IOCTL_PHDIO_RUN_CMDS IOCTL code to run these commands.
First, three constants are defined to represent the offsets to each register in the parallel port electronics.
The InitPrinter BYTE array stores the commands to initialize the printer. Each line has one command and its parameters. First, the Control port INIT# line is set low by writing 0xC8 using the PHDIO_WRITE command. The PHDIO_DELAY command then waits for 60µs. The INIT# signal is set high. The write value of 0xDC also selects the printer and enables interrupts. A further delay of 60us completes the operation.
The DeviceIoControl Win32 function is used to issue the IOCTL_PHDIO_RUN_CMDS IOCTL. This IOCTL runs the given commands straightaway. The InitPrinter array is passed as the input to the IOCTL. IOCTL_PHDIO_RUN_CMDS can optionally be passed an output buffer.
If there is an output buffer that is big enough, the first two 16-bit words in the output indicate any problems that the WdmIo driver found. The first word is an error code. The second word is the zero-based index into the command buffer in which the problem was found. Both are zero if there were no problems. The possible error codes are also found in Ioctl.h in the WdmIo\Sys directory.
The WdmIoTest code rather sloppily does not bother to check the returned error code. If using this driver for real, make sure that you check all error codes.
Listing 15.1 WdmIo Test issuing commands to run straightaway
const BYTE PARPORT_DATA = 0;
const BYTE PARPORT_STATUS = 1;
const BYTE PARPORT_CONTROL = 2;
BYTE InitPrinter[] = {
PHDIO_WRITE, PARPORT_CONTROL, 0xC8, // Take INIT# low
PHDIO_DELAY, 60, // Delay 60us
PHDIO_WRITE, PARPORT_CONTROL, 0xDC, // INIT# high, select printer,
// enable interrupts
PHDIO_DELAY, 60, // Delay 60us
};
int main(int argc, char* argv[]) {
// …
DWORD BytesReturned;
WORD rv[3];
if (DeviceIoControl(hWdmIo, IOCTL_PHDIO_RUN_CMDS,
InitPrinter, length(InitPrinter), // Input
rv, sizeof(rv), // Output
&BytesReturned, NULL)) {
printf(" InitPrinter OK. rv=%d at %d\n", rv[0], rv[13);
} else {
printf("XXX InitPrinter failed %d\n",GetLastError());
goto fail;
}
// …
Reading Data
If you use the PHDIO_READ or PHDIO_READS commands, you must provide an output buffer that is big enough to receive the read data. Remember that the first four bytes of the output buffer are always used for the error code and location.
Listing 15.2 shows how the ReadStatus commands are issued. It simply reads a byte value from the Status port. After DeviceIoControl has returned, the fifth byte of the output buffer contains the Status register contents. WdmIoTest checks that the BUSY# and ONLINE signals are 1 before continuing.
Listing 15.2 Reading data
BYTE ReadStatus[] = {
PHDIO_READ, PARPORT_STATUS, // Read status
};
int main(int argc, char* argv[]) { //…
DWORD BytesReturned;
WORD rv[3];
if (DeviceIoControl(hWdmIo, IOCTL_PHDIO_RUN_CMDS,
ReadStatus, length(ReadStatus), // Input
rv, sizeof(rv), // Output
&BytesReturned, NULL)) {
PBYTE pbrv = (PBYTE)&rv[2];
printf(" ReadStatus OK. rv=%d at %d status=%02X\n", rv[0], rv[l], *pbrv);
if ( (*pbrv&0x88)==0x88) {
busy = false;
break;
}
}
The WriteFile Win32 call is used to pass output data to the WdmIo driver. However, it needs to know how to process the data and handle interrupts. Two steps are required before WriteFile is called. First, connect to the interrupt. Second, pass WdmIo a series of commands that it will run to send the first byte and process each write interrupt.
Connecting to an Interrupt
When the WdmIo device is started, it is told which interrupt to use. However, it does not connect to the interrupt (i.e., install its interrupt handler), as it does not yet know how to handle the interrupt.
The ConnectToInterrupts commands shown here are used to initialize WdmIo's interrupt handling. The first write command tells the parallel port hardware not to generate interrupts. The second command tells the WdmIo driver to use a time-out of 10 seconds when processing subsequent WriteFile (and ReadFile) requests. WdmIo must have a time-out; a default of 10 seconds is used if no PHDIO_TIMEOUT command is given.
BYTE ConnectToInterrupts[] = {
PHDIO_WRITE, PARPORT_CONTROL, 0xCC, // Disable interrupts
PHDICO_TIMEOUT, 10, // Write time-out in seconds
PHDIO_IRQ_CONNECT, PARPORT_STATUS, 0x00, 0x00, // Connect to interrupt
};
The last command, PHDIO_IRQ_CONNECT, connects the WdmIo driver to its interrupt. As mentioned before, the actual interrupt number is passed as a resource when the WdmIo device is started. WdmIo starts servicing a hardware interrupt by reading a hardware register; in this case, the Status register is read. It must determine whether the interrupt was caused by its hardware or not.
The two final parameters to the PHDIO_IRQ_CONNECT command are a mask and a value. The register contents are ANDed with the mask and compared to the value, as shown in the following WdmIo code snippet. If they are not equal, the interrupt must be intended for another driver.
// See if interrupt is ours
UCHAR StatusReg = ReadByte(dx, dx->InterruptReg);
if ((StatusReg&dx->InterruptRegMask) != dx->InterruptRegValue) return FALSE; // Not ours
Suppose the Status register really did reset its bit 2 to 0 when it generated an interrupt. Specifying 0x04, as the mask would isolate bit 2. If the ANDed result is equal to 0x00, the interrupt is ours. Therefore, specifying a PHDIO_IRQ_CONNECT mask of 0x04 and value of 0x00 would have correctly detected when the parallel port interrupted.
However, as stated earlier, I found that there is no interrupt indication in the Status register. I simply have to assume that if an interrupt arrives, it came from the correct parallel port. To persuade the WdmIo code to continue regardless, a mask of 0x00 and a value of 0x00 was specified in the ConnectToInterrupts code.
Storing the Write Byte Commands
The WdmIo driver needs to be told a series of commands that write a single byte of data. These commands are used both to write the first output byte and to process an interrupt to send another byte.
IOCTL_PHDIO_CMDS_FOR_WRITE is used to store the commands used to write data. This IOCTL is issued using DeviceIoControl in the normal way. The commands are in the input buffer. No output buffer need be specified.
Listing 15.3 shows the WriteByte commands that WdmIoTest tells WdmIo to use to write a data byte. The first PHDIO_WRITE command ensures that the STROBE output signal in bit 0 of the Control register is off. The PHDIO_WRITE_NEXT command is used to write the next byte in the output buffer to the Data port. PHDIO_DELAY is used to delay for 1µs while the output signals settle. The STROBE signal is then set. A delay of 1µs is used before turning STROBE off again. A last delay of 1µs is introduced just to be on the safe side. Finally, the Status register is read; I shall show later on how to access this value.
Listing 15.3 Stored write byte commands
BYTE WriteByte[] = {
PHDIO_WRITE, PARPORT_CONTROL, 0xDC, // Ensure STROBE off
PHDIO_WRITE_NEXT, PARPORT_DATA, // Write next byte
PHDIO_DELAY, 1, // Delay 1us
PHDIO_WRITE, PARPORT_CONTROL., 0xDD, // STROBE on
PHDIO_DELAY, 1, // Delay 1us
PHDIO_WRITE, PARPORT_C0NTROL. 0xDC, // STROBE off
PHDIO_DELAY, 1, // Delay 1us
PHDIO_READ, PARPORT_STATUS, // Read status
};
The PHDIO_WRITE_NEXT command is crucial to these data transfer commands. The WdmIo driver keeps track of the current position in the output buffer passed by WriteFile. WdmIo correctly runs the WriteByte commands until all the bytes have been transferred.
Writing Data
The WdmIoTest program is finally ready to output a message to the printer. The following code snippet shows that WriteFile is used in the normal way to output data.
char* Msg = "Hello from WdmIo example\r\nChris Cant, PHD Computer Consultants Ltd\r\n";
DWORD Ten = strlen(Msg);
if (!WriteFile(hWdmIo, Msg, Ten, &BytesReturned, NULL)) printf("XXX Could not write message %d\n", GetLastError());
else if (BytesReturned==len) printf(" Write succeeded\n");
else printf("XXX Wrong number of bytes written: %d\n", BytesReturned);
The WdmIo driver uses the WriteByte commands to output the first byte, H. It then expects an interrupt when each byte has been printed. The WriteByte commands are run again by the interrupt handler to output each following byte. Assuming all goes well, when an interrupt is received after the last byte has been sent, WdmIo completes the WriteFile call successfully.
If there is a problem, WriteFile returns an error. The most likely error is ERROR_NOT_READY, which is returned if the write times out. If there is a problem running the write byte commands, ERROR_GEN_FAILURE is returned. Retrieve the WriteFile results to find the source of the problem.
Getting WriteFile Results
Each time the write commands are run, WdmIo gives its command processor a 5-byte output buffer. The first two words (4 bytes) are used for the error code and location. The fifth byte is filled with any output data that the commands produce. In WdmIoTest, the WriteByte commands read the Status register just after each byte is output. If the write commands attempt to output more than one byte, the command run will be aborted with error code PHDIO_NO_OUTPUT_ROOM.
However, what is really wanted is the value of the Status register after each byte is processed by the printer. At this point, the Status register contains some useful information, such as whether the printer is off-line or has run out of paper.
While WdmIoTest can just issue the ReadStatus commands again, it is more convenient if the Status register value is returned along with the command output data. Therefore, the register that the interrupt handler read is stored as a sixth byte in the WriteFile results buffer.
IOCTL_PHDIO_GET_RW_RESULTS is used to obtain the write (and read) results. Table 15.5 recaps the contents of the 6-byte buffer that is returned. Listing 15.4 shows how DeviceIoControl is used to read and display the results. When WdmIoTest is run successfully, the cmd status is 0x5F and the int status is 0xDF. This is expected. When a byte has just been output to the printer, BUSY# (the top bit of the Status register) goes low. When it has been printed, BUSY# goes high and an interrupt is generated.
Note that the command output buffer and the last interrupt register value locations are reused each time an interrupt occurs and the commands are run. This is not a problem. As soon as any fault occurs, processing stops with the most useful information in the results buffer.
Table 15.5 Read/Write results
Bytes | Description |
---|---|
2 | Command error code |
2 | Command error offset |
1 | Command output value |
1 | Last interrupt register |
Listing 15.4 Getting Write/Read command results
if (DeviceIoControl(hWdmIo, IOCTL_PHDIO_GET_RW_RESULTS,
NULL, 0, // Input
rv, sizeof(rv), // Output
&BytesReturned, NULL)) {
printf(" Get RW Results OK. rv=%d at %d\n", rv[0], rv[l]);
BYTE* pbuf = (BYTE*)(&rv[2]);
printf(" cmd status=%02x\n", pbuf[0]);
printf(" int status=%02x\n", pbuf[l]);
}
WdmIoTest finally disables interrupts by running the DisableInterrupts commands and closes its handle to the WdmIo device.
Reading data is a process similar to writing data. This time, two sets of commands need to be passed to WdmIo before the actual ReadFile is issued. The first set of commands starts the read process. The second set is used to process interrupts. Two sets are needed, as it is highly likely that different commands will be needed.
Use IOCTL_PHDIO_CMDS_FOR_READ_START to tell WdmIo which commands to use to start the read process. Typically, these commands might simply enable input interrupts from the device. IOCTL_PHDIO_CMDS_FOR_READ passes WdmIo the commands used to process each interrupt.
The ReadFile call passes the read buffer. The WdmIo driver can only handle fixed length transfers. However, a time-out of only one or two seconds could be used to ensure that errors are caught reasonably quickly.
As before, IOCTL_PHDIO_GET_RW_RESULTS is used to get the read results.
Reading data is not illustrated in WdmIoTest.
Installing a WdmIo device for a parallel port is a bit trickier than all the previous drivers. This is because Windows should already have installed its own driver to service the port. It is possible to run WdmIo for a parallel port in Windows 98. However, I found it was not possible in Windows 2000. The PHDIo driver can be forced to run in W2000 to talk to a parallel port.
The WdmIoLpt1Checked.inf and WdmIoLpt1Free.inf installation files are designed for a parallel port that, by default, lives at address 0x0378 and generates ISA interrupt IRQ7. Other configurations are made available for the user to select.
The best way to install WdmIo in Windows 98 is to use Hardware profiles. A Hardware profile is the combination of drivers that Windows loads. The trick is to have one profile in which the standard Windows parallel port driver is available. Another Hardware profile loads the WdmIo driver. When Windows starts, select the profile that you want to use.
In the Control Panel System applet, select the Hardware profiles tab. Copy the "Original Configuration" profile into a new profile called "WdmIo". Reboot into this new profile. In the Device Manager, select the Parallel port (LPT1) driver. First, remember which resources the LPT1 driver uses. Now remove the Parallel port driver from this profile. Simply disabling the driver is not good enough, as a disabled driver still keeps its resource assignments. Do not restart yet.
Now install a WdmIo device in the Other devices category as usual. You will have to reboot for the WdmIo device to be activated. I found that removing the Parallel port driver may mean that the parallel port was disabled in the BIOS setup. As you do the last reboot, you should go into your BIOS setup and reenable the LPT1 port again.
If you ever reboot and choose the "Original Configuration" hardware profile, you will find that the WdmIo device has been removed from the WdmIo profile. You will have to reinstall it and reboot. Sigh.
If you use WdmIo to talk to some of your own custom hardware, you will almost certainly not need to go through all of the previous shenanigans. Similarly, for a one off test of WdmIo talking to a parallel port, you should be able to install it without using different hardware profiles.
In Windows 2000 beta 2, I found that it was impossible to install a WdmIo device in place of the standard parallel port driver. If a parallel port driver was removed to make the resources available, Windows 2000 always reinstalled the driver on reboot before the WdmIo device could be loaded.
The WdmIo INF files must be set up for each device to which it will talk. A LogConfig section or sections must be added to give the resource assignments. Listing 15.5 shows the amendments that have been made to the WdmIoLpt1Checked.inf and WdmIoLpt1Free.inf installation files.
Both the Windows 98 WdmIo.Install and the Windows 2000 WdmIo.Install .NTx86 sections have a LogConfig directive that refers to the three LogConfig sections, each of which have different possible resource assignments. These eventually appear to users in the Device properties Resources tab as "Basic configuration 0", "Basic configuration 1", and "Basic configuration 2". Each section has a configuration priority. WdmIo.LogConfig1 has priority DESIRED that is higher than the NORMAL priority of the other LogConfig sections.
The WdmIo. LogConfig1 resource assignments are chosen by preference. There is only one I/O port range, 0x378 to 0x37A, so this address range is used. However, two IRQ numbers are listed, 5 and 7. Only one of these is used. In some computers, IRQ5 will already be used, so IRQ7 will be selected automatically. If both IRQs are available, the user will be prompted to make a choice.
The user, therefore, has to confirm which resource assignments to make. For the WdmIo device, Windows does not detect the parallel port and load the appropriate drivers. It is up to the device installer to select the correct resource assignments. The WdmIo driver does not care which port address or IRQ it is assigned. To do a useful job, the resources must correspond to a real device, and the controlling Win32 application must use the registers and interrupts in the correct way.
If you try to install a second WdmIo device for a parallel port, Windows should choose one of the remaining configurations. Windows 98 displays the chosen resource assignments and asks the user to confirm that these are satisfactory. It then tells you to insert the card. If the system's resource assignments have to be juggled to accommodate the new device, you may have to reboot.
Listing 15.5 WdmIoLpt1Xxx.INF LogConfig sections
[WdmIo.Install]
CopyFiles=WdmIo.Files.Driver
AddReg=WdmIo.AddReg
LogConfig=WdmlIo.LogConfig1,WdmIo.LogConfig2,WdmIo.LogConfig3
[WdmIo.LogConfig1]
ConfigPriority=DESIRED
IOConfig=378-37a
IRQConfig=7,5
[WdmIo.LogConfig2]
ConfigPriority=NORMAL
IOConfig=278-27a
IRQConfig=7,5
[WdmIo.LogConfig3]
ConfigPriority=NORMAL
IOConfig=3bc-3be
IRQConfig=7,5
[WdmIo.Install.NTx86]
CopyFiles=WdmIo.Files.Driver.NTx86
LogConfig=WdmIo.LogConfig1.WdmIo.LogConfig2,WdmIo.LogConfig3
A large number of entries can be put in Logconfig sections. For a start, further IOConfig and IRQConfig entries can be given if a device can have more than one of these resources simultaneously. The IOConfig and IRQConfig entries can be specified in a variety of ways. DMAConfig, PcCardConfig, MemConfig, and MfCardConfig entries can also be included. I found that the Windows 2000 DDK was the best source of information on all these entries.
With a WdmIo device finally installed for one of your parallel ports, it is now possible to run the WdmIoTest program. Do not forget to plug in a printer. I found that a clattery old dot matrix printer was best for testing, as it produced audible output straightaway. With a page-oriented ink jet or laser printer, you might have to press Formfeed to see the printed information.
When you run WdmIoTest, the printer should initialize itself and print out a two-line short message.
If the printer is switched off, WdmIoTest will keep checking to see if it is ready for 20 seconds. It will give up if it is not available by then. To test the time-out behavior of WriteFile, you must comment out the busy check code. If you now run WdmIoTest again, it will attempt to write information to the printer. It should time-out after 10 seconds with error 21 (ERROR_ NOT_READY).
The WdmIo driver handles the user pressing Ctrl+C correctly. I shall show in the next chapter how this IRP cancelling and cleanup occurs.
The PHDIo driver was initially aimed just at the NT and Windows 2000 platforms. However, I found that it works fine in Windows 98. These installation instructions are for both types of platform. You do not need to remove or disable existing drivers yet. Installing and running PHDIo does not make it use any resources. It only asks for resources when a Win32 program, such as PHDIoTest, runs.
These instructions describe how to install PHDIo by hand, by copying files and making registry entries. See Chapter 11 for details of how to automate this process.
Start by copying the driver executable PHDIo.sys to the Windows System32\Drivers directory. Use the checked build version of PHDIo.sys if you want to view its DebugPrint output. The version of DebugPrint supplied with this book does not run under Windows NT 3.51 or NT 4.
Make a registry key for PHDIo at HKLM\SYSTEM\CurrentControlSet\Services\PHDIo. Make the values specified in Table 15.6. The Start value of 2 indicates that the PHDIo driver should be loaded once the system is up and running. In Windows 98, also add an ImagePath REG_SZ value with \SystemRoot\System32\Drivers\PHDIo.sys.
Table 15.6 PHDIo driver registry values
Name | Type | Contents |
---|---|---|
Type | DWORD | 1 |
Start | DWORD | 2 |
DisplayName | REG_SZ | "PHDIo" |
ErrorControl | DWORD | 1 |
Now reboot your system. The PHDIo driver should now be running. Check the DebugPrint output to see that it has generated a few start-up trace messages.
If you change the driver, the actions you must take to use the new version vary from platform to platform. In Windows 98, you must reboot the system. In NT 3.51 and NT 4, use the Control Panel Devices applet to stop the old driver and start the new one. In Windows 2000, use the book software Servicer utility to do the same job.
The PHDIo driver is started in all Hardware profiles.
The PHDIoTest software is in subdirectory PHDIo\Exe of the book software. As mentioned previously, PHDIoTest opens the PHDIo device with this filename: \\.\PHDIo\isa\io378,3\irq7\override. This filename tells PHDIo which resources to use. In this case, an I/O port of 0x0378-0x037A (length 3) and IRQ7 are specified. Change these values if your parallel port is configured differently. The \override specifier means that PHDIo should go ahead and use these resources even if they conflict with another device.
In a similar way to WdmIoTest, you must disable any existing devices that talk to the parallel port. In Windows 98, use the WdmIo hardware profile, with the WdmIo driver removed. In W2000, NT 3.51, and NT 4, you must change "parport" driver startup option to Manual (3) and reboot. The \override specifier is usually needed to make PHDIoTest work in Windows 2000.
You should now be able to run PHDIoTest successfully on all these Windows platforms.
As with WdmIo, if you use PHDIo to talk to some of your own custom hardware, you should not need to do any clever hardware configuration, as only you will know where your device lives.
From the user perspective, WdmIo and PHDIo differ in these respects: the platforms on which they run, the installation method, the method by which their resources are specified, and the number of devices that can be controlled.
WdmIo runs in Windows 98 and Windows 2000. PHDIo also runs in NT 3.51 and NT 4.
WdmIo devices can be installed by the user without a special installation program. PHDIo needs an installation program, but this should be a much easier option for users.
WdmIo receives its resources from the installation INF file. Different configurations can be chosen by the user in the Device Manager. The PHDIo resources are specified by the program that uses it. I can imagine situations in which each technique has its advantages.
Finally, more than one WdmIo device can be installed and they can all be used simultaneously by different Win32 applications[35]. Only one PHDIo device can be installed. This device can be used to control different hardware at different times, but not by two Win32 applications simultaneously. PHDIo could be enhanced fairly easily to make a series of devices (e.g., called \\.\PHDIol, \\.\PHDIo2, etc.). If an application found that \\.\PHDIol was busy, it could try \\.\PHDIo2, etc.
In most cases, PHDIo is the better bet, as it covers more platforms and can be installed fairly easily.
As you might have guessed by now, WdmIo and PHDIo do have some deficiencies. However, they do let you do simple I/O.
The most fundamental criticism of WdmIo and PHDIo is that they are too dangerous. A device driver should not let mere Win32 programmers control what happens in the kernel, even in the limited way that WdmIo and PHDIo allow. Use these drivers at your own risk.
The command set deliberately does not include conditional or loop commands. Such functionality can easily be implemented in user mode. The commands that are run to handle an interrupt could possibly benefit from such commands. However, a desire for simplicity rules these commands out for now. If you need to add such facilities for your drivers, do so.
The WdmIo and PHDIo drivers will correctly refuse to read or write to ports that are outside the range of allocated addresses. However, in most cases they will not report a command error if an out-of-range read or write is attempted. Only the PHDIO_IRQ_CONNECT command checks its register parameter.
Another possible improvement is to implement shadow output registers. Quite often, hardware registers are write-only. If you want to set a bit in an output port, reading the register, setting the bit, and writing out the register will not work. A shadow output register is a memory copy of the value that was last written. If the shadow value is read, the set bit operation will work successfully.
Both WdmIo and PHDIo disconnect from their interrupt when a file handle is closed. Make sure that your device does not generate any further interrupts.
The WdmIo driver copes fairly well if a WdmIo device's resources are reassigned. It will not allow resources to be reassigned if there are any open handles. When it receives a Plug and Play stop device message, it waits until all IRPs have completed.
As it stands, each WdmIo device is created as being shareable. It is probably a good idea to alter this characteristic to require exclusive access, as competing IRPs from different processes may result in some confusing results. The PHDIo driver's one \\.\PHDIo device does require exclusive access, as only one open handle can be allowed to specify the resources.
I am sure that you can come up with other ideas for ways in which WdmIo and PHDIo can be enhanced. Go for it.
This chapter has introduced the WdmIo and PHDIo drivers. It has described the facilities that they make available to Win32 applications. WdmIo has its resources specified in its INF file. The resources are given to PHDIo in the Win32 CreateFile call. The WdmIoTest example user mode application uses WdmIo to drive a parallel port printer and output a short message. The similar PHDIoTest application does similar tests using the PHDIo driver.
The next chapters look at the construction of these drivers, how they queue IRPs to serialize hardware access and they perform interrupt driven I/O.
See Chapter 19 for the best way to talk to a parallel port in NT and W2000.
You might want to add another IOCTL that tells you which resources a device is using, so applications know which WdmIo device to use.