PHDIo Generic device driver PHDIo  Generic Device Driver
for Windows 98, Me, NT, 2000 and XP

Device control from Win32

Product information
   Installation instructions
   PHDIo header file
   Opening a handle
parallel ports
   Issuing commands
   Interrupt driven I/O
Example application
Using from Visual Basic
Version info Known bugs

All source provided
Full C++ example provided
Visual Basic example included

Redistribute the PHDIo driver in your products

USE OF PHDIO IS ENTIRELY AT YOUR OWN RISK. Incorrect handling of interrupt-driven I/O could slow or crash a computer.

PHD Director, Chris Cant's book on WDM Device Drivers is available now.
Easy to use PHDIo is a general purpose device driver for simple hardware devices.  PHDIo lets you access ports and perform basic interrupt-driven reads and writes.
Easy to use Use a Win32 program to access your device through the PHDIo device.  Use IOCTLs to run commands in the PHDIo driver.
Easy to use Includes an example of how to use PHDIo from Visual Basic.

Version 1.1 lets you open multiple handles at once.

Last modified: 4 July 2001.

Version 1.1.0  Read me
© Copyright 1999-2002 PHD Computer Consultants Ltd

There is no evaluation version of PHDIo.

PHDIo is no longer for sale (20 February 2003)

  • PHDIo generic driver
  • Basic I/O to ISA ports
  • Interrupt-driven reads and writes
  • Controlled by a Win32 program
    • CreateFile and CloseHandle
    • 5 IOCTLs (DeviceIoControl)
    • ReadFile and WriteFile
  • CreateFile call specifies:
    • ISA I/O Port address range
    • IRQ number
    • Override resource check
  • 12 simple but powerful commands
    • Read and Write a byte
    • Delay
    • Or, And and Xor
    • Connect to interrupt

  • PHDIoInstall installation program

  • Operating systems
  • Windows 98/Me
  • Windows 2000 x86
  • Windows NT 4 x86
  • Windows NT 3.51 x86
  • Windows XP x86

  • Note:
  • NOT Windows 95
  • Only 8 bit R/W at the moment,
    ie 16 and 32 bit not possible.

Product Information

PHDIo is a general purpose device driver, designed to let Win32 programs talk to simple hardware devices.  It runs in Windows 98, Windows Me, Windows NT 3.51, Windows NT 4, Windows 2000 and Windows XP.  Only an x86 platform version is available.  If you want to use PHDIo to talk to a parallel port, please read the notes below.

This documentation primarily describes how to use PHDIo from C or C++ Win32 programs.  However, a Visual Basic example is given below.  Other non-C or C++ programs should also be able to access the PHDIo device.  However, the language must be able to issue IOCTLs using the DeviceIoControl Win32 function.

Installing the PHDIo development kit installs the PHDIo driver.  The driver creates one device called "\\.\PHDIo" that can be opened by a Win32 program.  (Note that the PHDIoInstall program can be used to install the PHDIo driver on your users' computers.)

A Win32 program specifies the hardware resources for its device when it opens a handle to the PHDIo device.  Details of how to specify the resources are given below.  As an example, if the filename given is "\\.\PHDIo\isa\io378,3\irq7" then PHDIo tries to use the 3 I/O port addresses starting at 0x378, ie 0x0378-0x037A, and IRQ7.

The controlling Win32 program can then issue 'commands' using IOCTLs.  There are commands to read and write I/O ports.  For full details, see below.

The controlling application can also issue read and write requests that process a whole buffer of data in an interrupt-driven operation.  Only simple hardware such as a standard parallel port can be used.  Full details of how to set up these transfers is given below.


Development Kit

The PHDIo development kit is supplied as an executable, named phdio110.exe (or similar).  Run phdio110.exe to install the PHDIo development kit and install the PHDIo driver.  If you are running Windows 98/Me, you will need to restart your computer before the PHDIo driver is loaded.  You can uninstall the development kit and the PHDIo driver using Add/Remove Programs in the Control Panel.

Note that the development kit cannot be installed in Windows NT 3.51 or earlier, as NT 3.51 uses the old Program Manager shell.  However, the PHDIoInstall program should run OK in NT 3.51.


The PHDIoInstall program in the development kit can be used to install the PHDIo.sys driver on your users' computers.  PHDIoInstall does not install the development kit.  The PHDIo.sys driver file must be in the same directory as PHDIoInstall (eg on a floppy).

Running PHDIoInstall also checks whether the PHDIo driver has been installed correctly.

See the redistributables section below for full details.

NT 4, W2000 and XP

You need to have Administrator privileges if you want to install the development kit or the PHDIo.sys driver in NT 4, Windows 2000 or Windows XP.  If you do not have sufficient privilege, the dev kit install fails with Internal Error 53.

If you are running the PHDIo example programs or using the parallel port in W2000 and XP, then you will probably need to allocate an interrupt resource - see below.

Trying PHDIo

If you are using W2000 or XP, you will probably need to allocate an interrupt resource - see below.
  1. For the first example program you must have a printer attached to a parallel port at I/O address 0x378 using IRQ7.  This usually corresponds to LPT1.

    Run the PHDIoTest example (Start+Programs+PHDIo+Run printer example).  This runs a console window application.  It resets the printer and outputs three lines of text and a form feed.  See the full description below.  Select Start+Programs+PHDIo+Printer example project to open a Visual Studio workspace for this C++ project.

  2. The second example program is a Visual Basic program You must have Visual Basic installed for this example to run.  This program has a user interface that lets you alter the parallel port address and IRQ.

    Select Start+Programs+PHDIo+Visual Basic printer example project to open this VB4 project.  Screenshot and full details below.

PHDIo Header file

To use the PHDIo driver, you must include the PHDIo PHDIoctl.h header file.  This defines the PHDIo IOCTLs, commands and status codes. For example:
#include "PHDIoctl.h"

If you are using Visual Basic, the file PHDIo.bas in the vb directory defines the PHDIo commands, etc.  If you are using another language, you must translate this header into a suitable format for your language.

Opening a handle to PHDIo

Table 1.   PHDIo CreateFile filename resource elements
Element Required Description
\isa Mandatory Initial string: ISA bus
\io<base>,<length> Mandatory IO ports <base> and <length> in hex
\irq<number> Optional IRQ<number> in decimal
\override Optional Use these resources even if they cannot be allocated
One (or more) Win32 applications can open a handle to the PHDIo device.  When the application opens a handle it must use a filename that specifies the hardware resources that should be used.  An ISA I/O port address range must be given.  Optionally, an interrupt IRQ number can be given.

After the basic device name \\.\PHDIo you must specify \isa to indicate that you are using standard ISA devices.  Then specify the I/O port address range that you want to use, \io followed by the starting port address in hexadecimal, a comma, then the address range length in hex.  For example use \io378,3 to specify that you want to use the three I/O ports, 0x0378 to 0x037A.

If you are going to perform interrupt-driven reads or writes, you must specify an interrupt using \irq followed the IRQ number in decimal, eg \irq7.  The IRQ number must be in the range 0 to 15.  Finally, you can specify \override if you want to override a resource conflict, see below.

Here is an example of a C call to the Win32 CreateFile function to open a handle to the PHDIo driver.  Note that C strings represent a single backslash character with a double backslash character.

HANDLE hPhdIo = CreateFile("\\\\.\\PHDIo\\isa\\io378,3\\irq7",

If the open fails for some reason, CreateFile returns INVALID_HANDLE_VALUE.  Calling GetLastError may track down the source of the problem.
If an invalid filename was given then GetLastError returns ERROR_INVALID_PARAMETER (87).  If the I/O Ports or IRQ number are not available then ERROR_NOT_ENOUGH_MEMORY (8) is returned.

If the I/O Ports or IRQ number are not available, you can specify \override as a part of the filename.  This ignores any resource conflict.  While this may be a useful development technique, \override should not be used in a commercial release.

Closing the PHDIo handle

When you have finished using your device, close your file handle using CloseHandle.  For example:
CloseHandle( hPhdIo);
hPhdIo = NULL;

Parallel Ports

PHDIo is intended as simple interface to new ISA cards, which do not have a device driver to allocate the hardware resources (ie I/O address and IRQ).  However a lot of users want to use PHDIo to access a parallel port.  Unless you disable the standard drivers, the standard drivers will have reserved the parallel port hardware resources.  Therefore, you will need to use \override if you want to access a parallel port. 

Interrupt resources
Note that - by default - the standard parallel port printer driver does not use interrupts to send data to the parallel port.  Instead it polls for hardware register changes.  This frees up an interrupt which could be used more productively elsewhere.  The polling technique could be used with PHDIo; as well as being hard to do, this technique will not be very efficient because the polling will be done from user mode.

The PHDIo example program uses interrupts so you must ensure that the interrupt resource is allocated to the LPT1 printer port.

In W98, WMe and NT4 the parallel port drivers have the interrupt allocated, even if they do not use it.

In W2000 and XP, by default the port drivers do not even reserve the interrupt.  You can change this property in the Device Manager (Control Panel+System+Hardware tab).  Find LPT1.  Right click and select properties.  In Port settings, change to "Use any interrupt assigned to the port".  In Resources, IRQ7 should now appear.  Check that no resource conflicts are shown.

There is a complicated way of enabling this interrupt programmatically.  However it is probably best to provide instructions for your users to do it manually using the procedure above.

Multiple PHDIo handles

You can call CreateFile more than once to open several handles to the PHDIo device.  These open calls can be made in the same or different Win32 processes.  These open calls would usually specify different I/O Port addresses and IRQ numbers.

Note carefully that all PHDIo requests are currently serialised.  For example, if there is a write in progress on one open handle, then all other requests are delayed until this write completes.

Issuing commands to PHDIo

Five IOCTLs can be used with the PHDIo driver, as listed in Table 2.  An IOCTL is an operation that is run using the DeviceIoControl Win32 function.  An IOCTL is identified by an IOCTL code, and can have both input and output parameters.

Table 2.   PHDIo IOCTLs
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 byte and store in the read buffer
IOCTL_PHDIO_CMDS_FOR_READ_START Yes No Store the commands that start the read 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

The first IOCTL, IOCTL_PHDIO_RUN_CMDS, is used to run PHDIo commands straight away.

IOCTL_PHDIO_RUN_CMDS is issued using the Win32 DeviceIoControl function call.  The lpInBuffer parameter must point to the input buffer.  The input buffer is a BYTE array with the PHDIo commands and the command parameters.  The nInBufferSize parameter must specify the size of the input buffer in bytes.

The DeviceIoControl call can optionally specify an output buffer to receive the results of the command run.  lpOutBuffer must point to the output buffer, or be NULL if there is none.  nOutBufferSize is the size of the output buffer in bytes.  Using the output buffer is covered later.

The lpBytesReturned parameter to DeviceIoControl must point to a DWORD.  This will receive the output byte count.  This parameter must be valid even if lpOutBuffer is NULL.

Table 3 shows all the available PHDIo commands.  The last two commands (PHDIO_WRITE_NEXT and PHDIO_READ_NEXT) can only be used in commands that handle interrupt-driven writes and reads.

All commands and parameters are bytes.  In all cases, the reg parameter is the offset into the I/O port address range.  For example, if the I/O port start address is 0x0378, a write to reg 2 will write the value to I/O port 0x037A.

Table 3.   PHDIo commands
Command Input parameters Output Description
PHDIO_WRITEreg,ValueWrite value to a register
PHDIO_READregValueRead value from a register
PHDIO_DELAYdelayDelay for given microseconds.
Delay must be 60Ás or less
PHDIO_WRITESreg,count,Values,delayWrite values to same register with delay (<=60Ás)
PHDIO_READSreg,count,delayValuesRead values from same register with delay (<=60Ás)
PHDIO_ORreg,ValueRead register, OR with value and write back.
Use to set bit(s)
PHDIO_ANDreg,ValueRead register, AND with value and write back.
Use to clear bit(s)
PHDIO_XORreg,ValueRead register, XOR with value and write back.
Use to toggle bit(s)
PHDIO_IRQ_CONNECTreg,mask,ValueConnect to interrupt
PHDIO_TIMEOUTsecondsSpecify time-out for reads and writes
PHDIO_WRITE_NEXTregWrite next value from write buffer
PHDIO_READ_NEXTregStore next value in read buffer

Table 4.   Parallel port registers
1Read onlyStatus
Bit 6ACK#
Bit 7BUSY#
Bit 2INIT#
Bit 61
Bit 71
Listing 1 shows an example of how to use two of the common commands, Write Byte (PHDIO_WRITE) and Delay (PHDIO_DELAY).  The example is in C.

In this case, the code is resetting a printer on a standard parallel port.  Table 4 defines the registers and bits in a parallel port.  The code in the listing first defines the register offsets.  The Data register is at offset zero, the Status register is at offset 1 and the Control register at offset 2.

The InitPrinter BYTE array contains the commands to initialise the printer, ie set the INIT# line low for 20 microseconds.  INIT# is then brought high again, the printer is selected (SELECT set high) and interrupts are enabled (ENABLE_INT set high).

The Write Byte command (PHDIO_WRITE) takes two parameters from the following bytes.  The first parameter is the register to write to.  The second parameter is the value to write to the register.

The Delay command (PHDIO_DELAY) takes one parameter, the time to delay in microseconds.  The delay must be less than or equal to 60Ás.  Microsoft recommend that you do not hog the processor for more than 50Ás in total at a time.  Therefore, keep your total delay in any set of commands to less than 50Ás.

The listing then shows how to issue the DeviceIoControl Win32 call.  First, the BytesReturned DWORD variable is declared; this will receive the count of output bytes returned.  The rv WORD array will receive the output data.  More on this later.

DeviceIoControl is then called, passing the file handle, the IOCTL_PHDIO_RUN_CMDS control code, the input buffer, the output buffer and a pointer to BytesReturned.  The last parameter is NULL because this is not an overlapped asynchronous transfer.

DeviceIoControl returns a non-zero value if it succeeded.  Otherwise it returns zero; the code calls GetLastError to see what went wrong.

DeviceIoControl only returns zero if there is a serious error with your request.  For example, GetLastError returns ERROR_INVALID_FUNCTION (1) if an invalid IOCTL was used.
Listing 1.   Issuing IOCTL_PHDIO_RUN_CMDS to run commands to reset a printer

const BYTE PARPORT_DATA    = 0;

BYTE InitPrinter[] =
    PHDIO_DELAY, 20,                     // Delay 20us
    PHDIO_WRITE, PARPORT_CONTROL, 0xDC,  // INIT# high, select printer, enable interrupts
    PHDIO_DELAY, 20,                     // Delay 20us

    DWORD BytesReturned;
    WORD rv[2];

    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[1]);
        printf("XXX  InitPrinter failed %d\n",GetLastError());
        goto fail;

Status codes

It is useful to know whether your commands were processed correctly.  If there is an error in the commands, DeviceIoControl still returns successfully.

You must provide an output buffer to receive a Status Code and an Error Index, two 16 bit words.  No status information is returned if the output buffer is less than 4 bytes long.

Table 5 lists all the possible Status Code words.  If the Status Code is PHDIO_OK (0) then no error occurred.  The Error Index is the zero-based index into the input buffer indicating which byte in the command buffer caused the problem.

As an example, if you specified an invalid command in the first byte of your input buffer then the Status Code would be PHDIO_UNRECOGNISED_CMD and the Error Index would be zero.

In Listing 1, above, if DeviceIoControl returns non-zero the code simply prints out the Status Code and Error Index values in rv[0] and rv[1] respectively.

( Note that if you specify a register offset that is not available in the requested I/O port range, then the register will not be read or written.  However no error is returned. )
Table 5.   PHDIo Status Codes
Status codeDescription
PHDIO_UNRECOGNISED_CMDUnrecognised command
PHDIO_NO_CMD_PARAMSCommand does not have required number of parameters
PHDIO_NO_OUTPUT_ROOMNo room in output buffer
PHDIO_NO_INTERRUPTPHDIO_IRQ_CONNECT: No interrupt resource given
PHDIO_NOT_IN_RANGEPHDIO_IRQ_CONNECT: Interrupt register not in range
PHDIO_BAD_INTERRUPT_VALUEPHDIO_IRQ_CONNECT: Impossible to get interrupt value with specified mask
PHDIO_CANNOT_CONNECT_TO_INTERRUPTPHDIO_IRQ_CONNECT: cannot connect to the given interrupt
PHDIO_DELAY_TOO_LONGDelay must be 60Ás or smaller
PHDIO_CANCELLEDCommand processing stopped as IRP cancelled
PHDIO_BYTE_CMDS_ONLYOnly BYTE/UCHAR size commands are currently supported

Reading data

If you use the PHDIO_READ or PHDIO_READS commands then you are reading data.  In this case you must provide a larger output buffer to DeviceIoControl to receive the data.  Remember that the first two words (4 bytes) are reserved for the Status Code and Error Index.

Listing 2 shows how the parallel port Status register is read.  ReadStatus has one command that reads the Status register.  As one byte is being read, the output buffer must be at least 5 bytes long.  In the code, the rv WORD array is defined as having 3 elements which means it is 6 bytes long.

If DeviceIoControl succeeds, the fifth byte of the output buffer is printed out.  The code then goes on to check if the BUSY# and ONLINE bits are set high.  If they are, the printer is ready to receive data.

Listing 2.   Issuing IOCTL_PHDIO_RUN_CMDS to read the port status

BYTE ReadStatus[] =
    PHDIO_READ, PARPORT_STATUS,  // Read status

    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[1], *pbrv);
        if( (*pbrv&0x88)==0x88)
            // Printer ready...

Interrupt driven reads and writes

The PHDIo driver can perform some types of interrupt-driven reads and writes.  PHDIo commands are used to transfer each byte.  To do interrupt-driven transfers, you must have specified an interrupt IRQ number in the filename when you opened a handle to the PHDIo device.


To do interrupt-driven writes, you must do the following steps.
  2. Use IOCTL_PHDIO_CMDS_FOR_WRITE to tell PHDIo what commands to run to output one byte from the write buffer.
  3. Use WriteFile to pass the data to write.
  4. Use IOCTL_PHDIO_GET_RW_RESULTS to obtain the write results
For subsequent writes, you need only repeat the last two steps.

Connecting to interrupt

You must use the PHDIO_IRQ_CONNECT command to connect to the interrupt.  This installs the PHDIo Interrupt Service Routine (ISR).  PHDIO_IRQ_CONNECT must be the last command in a IOCTL_PHDIO_RUN_CMDS command set.

As an interrupt line might be shared, the PHDIo ISR must determine whether your device caused the interrupt.  It does this by reading a register and seeing if some bits in the register are set correctly.

The PHDIO_IRQ_CONNECT command has reg, mask and Value parameters.  The ISR reads the reg register.  It then ANDs the read value with the mask.  If the resultant value is equal to the Value then your device did cause the interrupt.

You must issue the PHDIO_IRQ_CONNECT command using IOCTL_PHDIO_RUN_CMDS in a DeviceIoControl call.  The DeviceIoControl output buffer Status Code is set to PHDIO_NOT_IN_RANGE if the reg parameter indicates a register that is not in the I/O Port range.  Other error Status Codes are listed in Table 5 above.

The read or write must have a time-out in case no interrupts are received.  The default time-out is 10 seconds.  You can override this value using the PHDIO_TIMEOUT command.  It takes one parameter that specifies the time-out in seconds.  A time-out of zero will result in the subsequent reads or writes terminating almost straight away.

The ConnectToInterrupts example shows how to connect to an interrupt.  The Status register is read when an interrupt arrives.  The mask and Value parameters are both 0x04.  If the Status register third bit is set then it means that the interrupt was from this device.

BYTE ConnectToInterrupts[] =
    PHDIO_WRITE, PARPORT_CONTROL, 0xCC,            // Disable interrupts
    PHDIO_TIMEOUT, 10,                             // Write time-out in seconds
    PHDIO_IRQ_CONNECT, PARPORT_STATUS, 0x04, 0x04, // Connect to interrupt

Commands for Writes

Before you issue your WriteFile call, you must set up the commands that are used to process a byte in the write buffer.  IOCTL_PHDIO_CMDS_FOR_WRITE is used to tell PHDIo the commands.

These Write commands are run in two circumstances:

Note carefully that your device must generate an interrupt after each byte has been processed when it is ready to work on the next byte.  Therefore it must generate an interrupt for each byte that is sent, including the last byte of the write buffer.

Listing 3.   Interrupt-driven Write byte commands

BYTE WriteByte[] =
    PHDIO_WRITE_NEXT, PARPORT_DATA,      // Write next byte
    PHDIO_DELAY, 1,                      // Delay 1us
    PHDIO_DELAY, 1,                      // Delay 1us
    PHDIO_DELAY, 1,                      // Delay 1us

    PHDIO_READ, PARPORT_STATUS,          // Read status
The WriteByte commands in Listing 3 show the write commands used to send a byte to a printer port.

  1. Set the Control port STROBE bit low.
  2. Write the next byte to the Data port.
  3. Wait for 1Ás for the data lines to settle.
  4. Set the Control port STROBE bit high.
  5. Wait for 1Ás for the STROBE bit to settle.
  6. Set the Control port STROBE bit low.
  7. Wait for 1Ás again.
  8. Just for information, read the Status port.
The crucial command is PHDIO_WRITE_NEXT.  This gets the next byte from the write buffer and writes it to the specified register.  Note carefully that there should only be one PHDIO_WRITE_NEXT command in the Write commands.

Writing data

You are now finally ready to write the data.  Use the WriteFile Win32 call to pass the write buffer.

Listing 4 shows how to write a string.  WriteFile has parameters for the file handle, the write buffer, its length and a pointer to a DWORD to receive the number of bytes transferred.  The last parameter is NULL as this is not an overlapped asynchronous transfer.

If WriteFile returns zero then the transfer failed seriously.  If it succeeded then BytesReturned is filled with the number of bytes that were written; this should should be the same length as the write buffer.

Listing 4.   Writing data

DWORD BytesReturned;
char* Msg = "Hello from PHDIo\r\nChris Cant, PHD Computer Consultants Ltd\r\n";
DWORD len = strlen(Msg);
if( !WriteFile( hWdmIo, Msg, len, &BytesReturned, NULL))
    printf("XXX  Could not write message %d\n",GetLastError());
else if( BytesReturned==len)
    printf("     Write succeeded\n");
    printf("XXX  Wrong number of bytes written: %d\n",BytesReturned);

Getting Write results

You must check if the write worked.  IOCTL_PHDIO_GET_RW_RESULTS fills a 6 byte output buffer.  The contents of this buffer is shown in Table 6.

The first two words contain the results from the last run of the Write commands, ie the Status Code and the Error Index.  The fifth byte of the output buffer contains any output data from the last run of the Write commands.  The Write commands shown in Listing 3, above, read the Status register.  The last value read from the Status register is contained in the fifth results byte.  The sixth byte in the output buffer contains the last value that the ISR read from its register.

Note carefully the difference between the fifth and sixth bytes.
When the second to last interrupt is received, the 'interrupt register' is stored in the sixth byte of the results buffer.  The Write commands are then run to output the last byte.  The fifth byte of the results buffer contains the Status register that is read straight after the last byte has been output.
When the final interrupt occurs, the interrupt register is stored in the sixth byte of the results buffer (overwriting the previous value).  This time, the Write commands are not run, as there are no more bytes to write.

Table 6.   IOCTL_PHDIO_GET_RW_RESULTS output buffer
2Command error Status Code
2Command Error Index
1Command output value
1Last interrupt register

Listing 5 shows how to read the write results.  DeviceIoControl is used to issue IOCTL_PHDIO_GET_RW_RESULTS.  No input buffer is specified.  The results are read into the rv word array that is 6 bytes long.  If DeviceIoControl succeeded, the Status Code and Error Index are printed out.  Then the fifth and sixth bytes are printed out.

Listing 5.   Getting write results

WORD rv[3];
DWORD BytesReturned;
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[1]);
    BYTE* pbuf = (BYTE*)(&rv[2]);
    printf("                         cmd status=%02x\n", pbuf[0]);
    printf("                         int status=%02x\n", pbuf[1]);


If the write timed out, the WriteFile call returns zero and GetLastError returns ERROR_NOT_READY (21).


Performing interrupt-driven reads, is a similar job to writes.
  2. Use IOCTL_PHDIO_CMDS_FOR_READ_START to tell PHDIo the commands to start the read process.
  3. Use IOCTL_PHDIO_CMDS_FOR_READ to tell PHDIo the commands to run to read one byte and store it in the read buffer.
  4. Use ReadFile to read the data.
  5. Use IOCTL_PHDIO_GET_RW_RESULTS to obtain the write results
For subsequent reads, you need only repeat the last two steps.

There are two sets of commands that you must pass to PHDIo.

Note carefully that your hardware must only generate read interrupts after ReadFile has been called.  Any interrupts received before then will be ignored: the bytes are NOT buffered up somehow.
Therefore your IOCTL_PHDIO_CMDS_FOR_READ_START commands must tell your hardware to start sending characters.  The IOCTL_PHDIO_CMDS_FOR_READ commands process each read interrupt.

The example parallel port program is probably a bit confusing because it fakes a read.  Instead of telling a printer to start sending a character, the IOCTL_PHDIO_CMDS_FOR_READ_START commands output a character.  This generates an interrupt when sent.  This interrupt triggers the running of the IOCTL_PHDIO_CMDS_FOR_READ commands, which output another character, generating another interrupt, etc.

If trying to read a parallel port, make sure it is configured correctly to do reads in the BIOS.

Advanced topics

You can use overlapped asynchronous I/O requests.  Note that all read, write and IOCTL requests are serialised within PHDIo so that they run one after another.  In Windows 98/Me you must use an event (created with CreateEvent) to wait for an asynchronous request to complete (for example using WaitForSingleObject).

You can spawn one or more threads from the process that opened the handle to PHDIo and pass it the file handle.  These threads can issue read, write and IOCTL requests.  Requests from all threads are handled serially in the same way as overlapped I/O requests.

Finally, note that any abnormal terminations of your program will be handled correctly by PHDIo.  PHDIo survives if your program crashes, exits without closing its handle or exits with asynchronous requests outstanding.

Example application

The software includes a full example in the exe subdirectory called PHDIoTest that talks to a printer on a parallel port.  PHDIoTest is a Win32 console application.  Select Start+Programs+PHDIo+Printer example project to open a Visual Studio workspace for this C++ project.

In Windows 2000 and XP you will probably need change the parallel port properties in the Device Manager so that interrupt IRQ7 can be used - see above.

PHDIoTest does the following jobs.

  1. Opens a handle to the PHDIo device (IRQ378,3 and IRQ7)
  2. Connect to interrupts
  3. Initialise the printer
  4. Store the Write commands
  5. Wait 20 seconds for the printer to become available by reading the Status every second.
  6. Write a two line message to the printer, followed by a form feed.
  7. Read the write results
  8. Store the Read Start commands
  9. Store the Read commands
  10. Do a dummy read of 5 bytes
  11. Read the read results
  12. Write CR-LF-FF to the printer
  13. Close the handle to the PHDIo device

PHDIoTest assumes that the printer is located at I/O address 0x0378 and interrupts on IRQ7.  You will have to amend the source code if your printer port uses different hardware resources.

A basic printer does not provide any read information.  The PHDIoTest example does a dummy read.  Instead of reading data, it actually outputs data (an A character) and reads the status character into the read buffer.  Outputting a character generates an interrupt which is necessary to read the next byte.

Select Start+Programs+PHDIo+Run printer example or run exe\Release\PHDIoTest.exe to try out this example.

Figure 1.   Screen shot of LptPHDIo Visual Basic example
This Visual Basic example shows how to initialise the printer and write a test message

Using from Visual Basic

The PHDIo driver can be used from Visual Basic.  The supplied module vb\PHDIo.bas declares all the PHDIo constants and the Win32 functions that you need to use.

In Windows 2000 and XP you will probably need change the parallel port properties in the Device Manager so that interrupt IRQ7 can be used - see above.

The LptPHDIo example described below shows how to use the PHDIo device from Visual Basic.

LptPHDIo VB example

The LptPHDIo is an example Visual Basic program that uses the PHDIo driver.  LptPHDIo is found in the vb subdirectory.

LptPHDIo does exactly the same job as the PHDIoTest, described above.  It performs exactly the same steps to initialise a printer on a parallel port and output a simple string.  It then does a read (that does not return any useful information).

The screen shot on the right shows LptPHDIo in action, just after it has run its tests.  The printer parallel port is at ISA address 0x378.  The list box shows the results of running the 13 tests.  They have all completed successfully.  A message "Hello from PHDIo VB example" has been printed on the printer.  The dummy read produces an output on the printer of "AAAAAA".  The read buffer contains five hex DF values that were read from the status buffer.

To run this example, first install the PHDIo driver, as described above.  Load and run the Visual Basic project.  Connect a printer to the parallel port - an old clattery dot matrix printer will produce output straight away.  If you use a page oriented printer, you may need to press Form Feed to see the output.

LptPHDIo VB Project

The LptPHDIo VB project has one main form.  Pressing the Run Test button attempts to initialise the printer and output a message.  Various progress messages are displayed in the list box below.

The RunTest_Click function performs all the tests.  First, it opens a handle to the PHDIo device.  It then calls helper routines to perform each step.  Finally it closes the handle to the PHDIo device.

Opening a handle in VB

The code in Listing 6 shows how to open a handle to the PHDIo device.  As described earlier, the filename parameter to the CreateFile Win32 function call must define the hardware resources that you want to use.  The CreateFile function and all the other standard constants are defined in the PHDIo.bas module.

Listing 6.  Opening a handle to the PHDIo device in VB

Dim hPHDIo As Long
hPHDIo = CreateFile("\\.\PHDIo\isa\io378,3\irq7\override", GENERIC_READ Or GENERIC_WRITE, _
                    0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
    Print ("XXX  Could not open PHDIo device")
    Exit Sub
End If

Running commands in VB

Listing 7 shows the InitPrinter function that initialises the printer.  It uses the PHDIoRunCmds function to issue the Run Cmds IOCTL.

The commands to run are defined in a Byte array.  Make sure that the Byte array is exactly the right size for the commands and parameters that you put in it.

You must specify a Byte array to receive the results.  This should be at least four bytes long.  Extend this array if your commands read data (see the GetStatus function for an example of this).

The call to PHDIoRunCmds returns True if the IOCTL was issued successfully and the result data indicated no errors.

Listing 7  Issuing commands using PHDIoRunCmds in VB

Private Function InitPrinter(ByVal hPHDIo As Long) As Boolean

    Dim InitPrinterCmds(10) As Byte
    Dim Results(4) As Byte
    InitPrinterCmds(0) = PHDIO_WRITE        ' Take INIT# low
    InitPrinterCmds(1) = PARPORT_CONTROL
    InitPrinterCmds(2) = &HC8
    InitPrinterCmds(3) = PHDIO_DELAY        ' Delay 20us
    InitPrinterCmds(4) = 20
    InitPrinterCmds(5) = PHDIO_WRITE        ' INIT# high, select printer, enable interrupts
    InitPrinterCmds(6) = PARPORT_CONTROL
    InitPrinterCmds(7) = &HDC
    InitPrinterCmds(8) = PHDIO_DELAY        ' Delay 20us
    InitPrinterCmds(9) = 20

    InitPrinter = PHDIoRunCmds(hPHDIo, InitPrinterCmds(), Results())
    If InitPrinter Then
        PrintMsg ("     InitPrinter OK")
        PrintMsg ("XXX  InitPrinter failed")
    End If

End Function

PHDIoRunCmds General Purpose VB routine

Listing 8 shows the PHDIoRunCmds function.  This is a general purpose routine that is used in many places to run some PHDIo commands.  You may need to remove some of its print output calls.

The first parameter to PHDIoRunCmds is a handle to the PHDIo device.

Its second parameter is a Byte array of commands to be run.  This should have a zero lower bound.  Its size should match the number of commands and parameters exactly.

The third parameter is a Byte array to receive the output results data.  The first two bytes contain any error code.  The second pair of bytes contain the location of the error.  Any remaining bytes are filled with the data that has been read by the commands.
The results Byte array should normally be at least 4 bytes long.

PHDIoRunCmds returns False if the input parameters are not valid.  It also returns False if the DeviceIoControl call fails.  Finally, PHDIoRunCmds also checks the results bytes (if they are available) and returns False if an error is indicated.

Listing 8  Running commands in PHDIo in VB

Private Function PHDIoRunCmds(ByVal hPHDIo As Long, Cmds() As Byte, Results() As Byte) As Boolean
    Dim BytesReturned As Long
    Dim InLength As Long
    Dim OutLength As Long

    ' Check parameters
    PHDIoRunCmds = False
    If (LBound(Cmds) <> 0) Then Exit Function
    If (LBound(Results) <> 0) Then Exit Function
    InLength = UBound(Cmds) - LBound(Cmds)
    OutLength = UBound(Results) - LBound(Cmds)

    ' Issue IOCTL
    PHDIoRunCmds = DeviceIoControl(hPHDIo, IOCTL_PHDIO_RUN_CMDS, Cmds(0), InLength, _
                                   Results(0), OutLength, BytesReturned, 0)
    If OutLength >= 4 And BytesReturned >= 4 Then
        Dim report As String, reporti As Integer
        report = "     Results "
        For reporti = 0 To BytesReturned - 1
            report = report & " " & Hex(Results(reporti))
        Next reporti
        PrintMsg (report)
        If Results(0) <> 0 Then
            PHDIoRunCmds = False
        End If
    End If
End Function


You may redistribute the PHDIo.sys driver file and the PHDIoInstall executable as a part of any number of your products, royalty-free.  You may not redistribute the PHDIo development kit.  Also see the installation section above.

Working with standard installation programs

Most installation programs should allow you to run the PHDIoInstall program to install PHDIo.

If you do not want to use the PHDIoInstall program, then these steps should install PHDIo.

  1. Copy PHDIo.sys to <Windowsdir>\System32\drivers This directory may need to be created in W98/WMe.
  2. In the registry, make a key "PHDIo" in HKEY_LOCAL_MACHINE\System\CurrentControlSet
  3. In this PHDIo key, add the following values:
    • DisplayName, REG_SZ, "PHDIo"
    • ErrorControl, REG_DWORD, 1
    • Start, REG_DWORD, 2
    • Type, REG_DWORD, 1
  4. In W98/WMe add the following value in the PHDIo key:
    • ImagePath, REG_SZ, "\SystemRoot\System32\Drivers\PHDIo.sys"
  5. Reboot the computer

Using PHDIoInstall is recommended, because it can verify that PHDIo has been installed correctly, and because a reboot may not be necessary in NT, W2000 or XP.

Parallel port mode

If you are using a parallel port, make sure that the user configures the port mode in the BIOS if necessary.

W2000 and XP parallel port interrupts

If you are using a parallel port in W2000 and XP, your users may need to allocate an interrupt resource using the procedure described above.

Version info

PHDIo driver

1.1.0 November 24 1999 Allows multiple simultaneous opens
1.04 May 20 1999 Timer now stopped when driver unloaded
1.03 May 14 1999 PHDIO_IRQ_CONNECT must be the last command in a IOCTL_PHDIO_RUN_CMDS command set.
Fixed bug: Now connects to interrupt at PASSIVE_LEVEL
Fixed bug: RUN_CMDS now allocates memory at correct IRQL
Fixed bug: All memory now freed when PHDIO driver unloaded
1.02 April 1 1999 Ignores FilePointer
Fixed bug: ProcessCmds works if no output buffer provided
PHDIoctl.h usable from C
1.01 March 11 1999 Release

Development kit installation program

1.04b June 21 1999 Development kit installation routine works even better in W2000 Beta 3
Uninstall routine also updated for W2000b3
1.04a June 15 1999 Development kit installation routine works better in W2000 Beta 3


1.03 June 16 1999 Works better in W2000 Beta 3
1.02 April 19 1999 Copes in a couple of unlikely situations
1.00 March 8 1999 Release

PHDIoTest C++ example

1.02 April 1 1999 Dummy Read example added
PHDIoctl.h usable from C
1.01 March 11 1999 Release

LptPHDIo VB example

1.00 March 29 1999 Release

Known bugs

Dev kit installation program September 13 1999 The installation fails with Internal Error 53 if you try to install PHDIo without Administrator privileges.

Other great PHD products
PHD Computer Consultants Ltd makes no representations or warranties about the suitability of the software, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose.  PHD shall not be liable for any damages suffered by a user as a result of using, modifying or distributing the software or its derivatives.