Êíèãà: Writing Windows WDM Device Drivers

LPT Printer Driver Application

LPT Printer Driver Application

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 WdmIoExe 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.

Parallel Ports

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

WdmIoTest

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

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("\.PHDIoisaio378,3irq7override", 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

Issuing Commands

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 WdmIoSys 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 %dn", rv[0], rv[13);
 } else {
  printf("XXX InitPrinter failed %dn",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=%02Xn", rv[0], rv[l], *pbrv);
  if ( (*pbrv&0x88)==0x88) {
   busy = false;
   break;
  }
 }

Writing Data Using Interrupt Driven I/O

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 examplernChris Cant, PHD Computer Consultants Ltdrn";
DWORD Ten = strlen(Msg);
if (!WriteFile(hWdmIo, Msg, Ten, &BytesReturned, NULL)) printf("XXX Could not write message %dn", GetLastError());
else if (BytesReturned==len) printf(" Write succeededn");
else printf("XXX Wrong number of bytes written: %dn", 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 %dn", rv[0], rv[l]);
 BYTE* pbuf = (BYTE*)(&rv[2]);
 printf(" cmd status=%02xn", pbuf[0]);
 printf(" int status=%02xn", pbuf[l]);
}

WdmIoTest finally disables interrupts by running the DisableInterrupts commands and closes its handle to the WdmIo device.

Reading Data Using Interrupt Driven I/O

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.

Îãëàâëåíèå êíèãè


Ãåíåðàöèÿ: 0.030. Çàïðîñîâ Ê ÁÄ/Cache: 0 / 0
ïîäåëèòüñÿ
Ââåðõ Âíèç