Brian is a programmer at the USDA developing data-acquisition and analysis software. He can be reached on the Internet at bwh@cis.ufl.edu or on CompuServe at 72144,3662. Dennis, a research scientist at the USDA, develops electronic/accoustic systems to detect insect pests in agricultural commodities. He can be reached at USDA, ARS, 1700 SW 23rd Dr., Gainesville, FL 32608.
Data-acquisition and analysis is often performed with dedicated, proprietary, and expensive laboratory instruments. However, the PC's open architecture makes it a cost-effective alternative for many data-acquisition and analysis projects. One particular project we developed at the Acoustic/Electronic Insect Detection Laboratory at the United States Department of Agriculture, Agricultural Research Service facility in Gainesville, Florida required just such a system. The system is an integrated hardware/software setup that allows for digital input and output in a low-end PC configuration. This article describes what we learned while implementing digital I/O via the PC's parallel port.
The system, known as "EGPIC" (Electronic Grain Probe Insect Counter), checks for insects in stored-grain bins and elevators. It does this by electronically sensing insects that crawl into specially designed probes placed at a number of locations in the grain mass. The hardware side of the EGPIC is responsible for detecting an insect and generating the appropriate signal for some digital-input computer interface. The software is responsible for reading, analyzing, displaying, and storing the collected data. We selected the PC as the host system for the software because of its wide, low-cost availability and profusion of development tools.
Given these design criteria and the cost and compatibility constraints, the final specification sheet for our digital input and output (DIO) interface was as follows:
The PC architecture has a wide variety of input and output techniques available to it, from specialized DIO boards to the relatively crude game port. Each has some advantages and disadvantages for this type of system.
Specialized DIO boards are available for the PC, typically as 8- or 16-bit ISA boards with 48 I/O lines, configurable to generate interrupts on one of several different IRQs. While nearly ideal feature-wise, these boards are relatively expensive--from $40 or $50 to more than $1000. Even with the inexpensive boards, this cost becomes significant in high volumes. In applications using from 9 to 96 probes, EGPIC has been configured for use with these DIO boards. However, when only one to eight probes are needed, the DIO board is unnecessary. Also, since these boards require a free bus slot, some systems, such as laptops (a likely target platform), would be excluded from using EGPIC.
The PC's standard RS-232 serial port is suitable for this type of application, but the external EGPIC hardware would require an extra translation layer to generate RS-232-compatible bit streams from the eight digital inputs. This method is suitable for very large systems requiring hundreds or thousands of probes, but is unnecessarily complex for smaller-scale systems.
Among other deficiencies, neither the keyboard input nor the game port offer output capabilities, ruling out their consideration as suitable I/O interfaces for EGPIC.
This leaves the PC's printer parallel port. Like a dedicated DIO card, it offers digital output lines and interrupt-on-input capability (using either IRQ 5 or 7). Unlike DIO cards, it's available for all PC platforms and is relatively inexpensive. The parallel port's only shortcomings are that not all implementations have input capability and the port may already be in use by another device, likely a printer. However, many systems have two parallel ports; if not, a second parallel port is an inexpensive addition. And as for the lack of input capability, after some software tricks, a 100 percent compatible PC parallel port can, in fact, be used for up to 8 bits of digital input.
To illustrate the programming techniques discussed in this article, I've written the Parallel Port Digital Input Output (PPDIO) package, a rudimentary set of C functions that allow for reading and writing to the parallel port and installing an interrupt-service routine (ISR) to handle incoming data on the parallel port. Listing One (page 103) is PPDIO.H; Listing Two (page 103) is PPDIO.C.
The parallel port is programmed via three separate I/O registers: the input-only data register, the output-only status register, and the input/output control register. The addresses of these register ports differ depending on the machine, but they are usually offset from 0x378, 0x278, or 0x3BC. The base address for a particular LPT port is stored in the BIOS data area. The PPDIO_Get- LptAddress() routine shows how to retrieve this information.
The data register (see Figure 1), located at the parallel port's base address, takes a standard bit mask that indicates which pins should be sent high and low. Sending information out the parallel port is accomplished with a simple OUT instruction. PPDIO_SendByte() handles this. The parallel port transmits this byte until told to transmit a different one, making digital output a trivial task. Note that while we can theoretically read the data register with an IN instruction, the byte read won't be incoming data--it will be the most recent data transmitted.
The data register can't be used for input, so we must use both the status and control registers; see Figure 2 and Figure 3 for their respective layouts. Reading the status register is very straightforward, but keep in mind that the logic of pin 11 is inverted.
The control register is nominally an output-only register, but by taking advantage of the four output lines driven with open-collector drivers, we can force the control register into giving us input. If we produce a high TTL logic level at the control register's corresponding pins, we can drive the pins low via incoming signals. Thus, by setting the appropriate bits of the control register, the pins can be used as input. This is handled transparently when PPDIO_InstallISR() is called.
Reading the control and status registers is accomplished by an IN instruction at the port's base address and relevant offset. The routines PPDIO_
ReadStatusRaw() and PPDIO_ReadControlRaw() illustrate how to accomplish this. Because several of the input lines have negative active logic placed upon them by the parallel port, helper functions that translate negative logic would be useful. The routines PPDIO_
ReadStatusCooked() and PPDIO_ReadControlCooked() provide this functionality, along with converting reserved and unused bits to 0.
Now that input and output have been addressed, all that's left is making the communications interrupt-driven. The parallel port's input lines could be polled; however, this would be cumbersome, time consuming, and error-prone. Having an interrupt generated whenever a digital line is sent high is a far more elegant means of input detection.
Assuming the target system supports it, interrupt-driven input on the parallel port is actually not very complicated to achieve. First, the control register must have its interrupt-enable bit set. Next, an ISR must be installed in the DOS interrupt vector table for the appropriate IRQ. Finally, the port's IRQ must be unmasked from the 8259 Programmable Interrupt Controller's interrupt-enable register. All of this is demonstrated in PPDIO_InstallISR().
While programming in an interrupt-driven manner is theoretically simple, hardware support can be shaky. The printer port has traditionally utilized IRQ 7, but IRQ 5 isn't uncommon either. Even worse, some machines either have the parallel-port interrupt disabled altogether or have an alternate device (such as a network or sound card) using its IRQ. To compound matters, there's no easy way to detect which IRQ a given base address or LPT port corresponds to--this must either be known by the user or determined empirically by the program.
To solve this problem, EGPIC uses a simple call-and-acknowledgment method of IRQ determination. This involves installing ISRs at IRQs 5 and 7, "calling" the EGPIC hardware (which acknowledges the call by generating an interrupt), then seeing which ISR is called. If no ISR is called, either another IRQ is in use (doubtful, since the PC industry has fairly well standardized on IRQs 5 and 7 for the parallel port), or no IRQs are being used for the parallel port. It's simpler to have the user input which IRQ to use, but this demands a higher level of user knowledge than the application may reasonably assume. An interesting secondary use of this call-and-response procedure is for hardware testing--if a probe is known to be installed and fails to generate a response when requested, then that probe must be malfunctioning.
Interrupts are generated via pin 10, normally a printer's ACK line. A high-line level sent to pin 10 results in an interrupt being generated, assuming that all other relevant setup has been done.
During the development of EGPIC we found that both cable length (from the probe to the computer) and interrupt latency played a role in determining whether a signal actually existed at the inputs when the ISR was called. With long cable lengths and a fast computer, it was possible for some inputs not to be updated by the time the ISR was executed. Conversely, with a slow computer it was possible for the signal to have come and gone (depending on the length of the generated input) by the time the ISR was called. These timing problems can be compensated for in hardware, but not knowing of their existence can lead to some rather irksome bugs.
Two particularly bothersome problems came to light while developing the EGPIC system.
First, if the interrupt lines were allowed to float while the system was collecting data, the PC would likely lock up. This is because a floating line will often fluctuate between TTL TRUE and FALSE, causing thousands of interrupts to be generated every second, freezing up the system. Something as innocuous as accidentally pulling a cable loose or turning off the input hardware may cause a system lockup.
The second problem is that not all PC parallel ports are identical. Some parallel ports deviate considerably from the original PC's design, rendering this type of specialized input and output--which assumes 100 percent hardware compatibility--impossible. Unfortunately, only trial and error will determine which systems are nonstandard.
Listing Three (page 103) is DIO.C, a program that implements simple interrupt-driven DIO with the parallel port. By itself, DIO.C is fairly useless--consider it more of a basic framework to draw upon than a real program. Any program that would use these routines would require some amount of custom hardware and software design.
At first glance, the antiquated design of the PC parallel port seems highly limited, but it can quite easily be configured as an inexpensive digital input/
output interface. The hardware required is minimal: a one-shot circuit per channel and a single interrupt line for the logical OR of all the channels. The software, as demonstrated in this article, is quite simple and easily customized for specific applications. In our case, the parallel port satisfied all of our requirements superbly, enabling the EGPIC project to be both completed in a timely and cost-effective manner and distributed across a wide range of systems.
We are grateful to Hok Chia and Sergey Kruss (University of Florida) for electronic technical assistance.
Eggebrecht, Lew. Interfacing to the IBM Personal Computer, Second Edition. Carmel, IN: SAMS, 1992.
Bit Pin/Function Logic
0 0 1=TRUE
1 1 1=TRUE
2 2 1=TRUE
3 3 1=TRUE
4 4 1=TRUE
5 5 1=TRUE
6 6 1=TRUE
7 7 1=TRUE
Bit Pin/Function Logic
0 (reserved) --
1 (reserved) --
2 (reserved) --
3 15 1=TRUE
4 13 1=TRUE
5 12 1=TRUE
6 10 1=generate interrupt
7 11 0=TRUE
Bit Pin/Function Logic
0 1 0=TRUE
1 14 0=TRUE
2 16 1=TRUE
3 17 0=TRUE
4 IRQ enable 1=enabled
5 (reserved) --
6 (reserved) --
7 (reserved) --
//----------------------------------------------------------------- // PPDIO Parallel Port Digital IO routines // Version 1.0 Copyright 1993 by Brian Hook. All Rights Reserved. // File: PPDIO.H -- header file for the PPDIO library // Compile with Borland C++ 3.1 -- porting to another compiler // should be extremely trivial. //----------------------------------------------------------------- #ifndef __PPDIO_H #define __PPDIO_H //--- Pin definitions for control register ------------------------ #define PIN_1 0x01 #define PIN_14 0x02 #define PIN_16 0x04 #define PIN_17 0x08 //--- Pin definitions for status register ------------------------- #define PIN_15 0x08 #define PIN_13 0x10 #define PIN_12 0x20 #define PIN_10 0x40 #define PIN_11 0x80 //--- Interrupt enable bit definition ----------------------------- #define PTR_ENABLE_INT_BIT 0x10 //--- Function prototypes ----------------------------------------- unsigned PPDIO_GetLptAddress( int lpt_port ); void PPDIO_InstallISR( void interrupt (*fnc)(), int irq ); unsigned char PPDIO_ReadControlRaw( void ); unsigned char PPDIO_ReadStatusRaw( void ); unsigned char PPDIO_ReadControlCooked( void ); unsigned char PPDIO_ReadStatusRaw( void ); void PPDIO_RemoveISR( void ); void PPDIO_SendByte( unsigned char data ); void PPDIO_SetBaseAddress( unsigned base_address ); void PPDIO_SetLptPort( int lpt_port ); #endif
//-----------------------------------------------------------------
// PPDIO Parallel Port Digital IO routines
// Version 1.0 Copyright 1993 by Brian Hook. All Rights Reserved.
// File: PPDIO.C -- code and variables for the PPDIO library
// Compile with Borland C++ 3.1 -- porting to another compiler
// should be extremely trivial.
//-----------------------------------------------------------------
#include <dos.h>
#include "ppdio.h"
static unsigned ppdio_data_register;
static unsigned ppdio_control_register;
static unsigned ppdio_status_register;
static unsigned ppdio_interrupt_no;
static unsigned ppdio_irq;
static unsigned char ppdio_old_control_value;
static unsigned char ppdio_old_8259_mask;
static void interrupt (*ppdio_old_intvec)();
unsigned PPDIO_GetLptAddress( int lpt_no )
{
unsigned far *pp = ( unsigned far * ) MK_FP( 0x40, 8 );
//--- Assumes values of 1, 2, or 3 -----------------------------
return ( pp[lpt_no-1] );
}
void PPDIO_InstallISR( void interrupt (*fnc)(), int irq_no )
{
static char mask[] = { 0xfa, 0xf7, 0xef, 0xdf, 0xaf, 0x7f };
unsigned char temp;
//--- Interrupt number = IRQ no + 8 ----------------------------
ppdio_interrupt_no = irq_no + 8;
//--- Save original interrupt vector ---------------------------
ppdio_old_intvec = getvect( ppdio_interrupt_no );
//--- Install new ISR ------------------------------------------
setvect( ppdio_interrupt_no, fnc );
//--- Enable interrupts by setting the PTR_ENABLE_INT_BIT in
//--- the control register. Also, OR it by 0x04 to send pin
//--- 16 high then write out a 0 to pins 1, 14, and 17 so
//--- that we can use the control register for input.
ppdio_old_control_value = inportb( ppdio_control_register );
temp = ppdio_old_control_value | PTR_ENABLE_INT_BIT | PIN_16;
temp &= ~ ( PIN_17 | PIN_14 | PIN_1 );
outportb( ppdio_control_register, temp );
//--- Unmask our IRQ in the interrupt controller ---------------
ppdio_old_8259_mask = inportb( 0x21 );
temp = ppdio_old_8259_mask & mask[ppdio_interrupt_no-10];
outportb( 0x21, temp );
//--- Clear pending interrupts ---------------------------------
outportb( 0x20, 0x20 );
}
unsigned char PPDIO_ReadControlCooked( void )
{
unsigned char raw_control;
unsigned char cooked_control = 0;
raw_control = PPDIO_ReadControlRaw();
//--- Return a control register mask that compensates for the inverse logic
//--- of pins 1, 14, and 17, and with 0s where bits are reserved or unused.
if ( !( raw_control & PIN_1 ) )
cooked_control |= PIN_1;
if ( !( raw_control & PIN_14 ) )
cooked_control |= PIN_14;
if ( raw_control & PIN_16 )
cooked_control |= PIN_16;
if ( !( raw_control & PIN_17 ) )
cooked_control |= PIN_17;
return ( cooked_control );
}
unsigned char PPDIO_ReadControlRaw( void )
{
return ( inportb( ppdio_control_register ) );
}
unsigned char PPDIO_ReadStatusCooked( void )
{
unsigned char raw_status;
unsigned char cooked_status = 0;
raw_status = PPDIO_ReadStatusRaw();
//--- Return a status register mask that compensates for the
//--- inverse logic of pin 11, and with 0s for any reserved or unused bits.
if ( raw_status & PIN_15 )
cooked_status |= PIN_15;
if ( raw_status & PIN_13 )
cooked_status |= PIN_13;
if ( raw_status & PIN_12 )
cooked_status |= PIN_12;
if ( !( raw_status & PIN_11 ) )
cooked_status |= PIN_11;
return ( cooked_status );
}
unsigned char PPDIO_ReadStatusRaw( void )
{
return ( inportb( ppdio_status_register ) );
}
void PPDIO_RemoveISR( void )
{
//--- Restore the interrupt controller's previous state --------
outportb( 0x21, ppdio_old_8259_mask );
//--- Restore the original interrupt vector --------------------
setvect( ppdio_interrupt_no, ppdio_old_intvec );
//--- Restore the printer control register ---------------------
outportb( ppdio_control_register, ppdio_old_control_value );
}
void PPDIO_SendByte( unsigned char data )
{
outportb( ppdio_data_register, data );
}
void PPDIO_SetBaseAddress( unsigned base_address )
{
ppdio_data_register = base_address;
ppdio_status_register = base_address + 1;
ppdio_control_register = base_address + 2;
}
void PPDIO_SetLptPort( int lpt_port )
{
PPDIO_SetBaseAddress( PPDIO_GetLptAddress( lpt_port ) );
}
//-----------------------------------------------------------------
// PPDIO Parallel Port Digital IO routines
// Version 1.0 Copyright 1993 by Brian Hook. All Rights Reserved.
// File: DIO.C -- this is an example how you could use the PPDIO
// routines. This could be used as a framework upon which you
// could build real applications.
// Compile with Borland C++ 3.1 -- porting to another compiler
// should be extremely trivial.
//-----------------------------------------------------------------
#include <conio.h>
#include "ppdio.h"
volatile int isr_called = 0;
void huge interrupt MyISR( void )
{
isr_called = 1;
//--- Normally you would read the input pins here and do something important
//--- Signal end of interrupt to the interrupt controller ------
outportb( 0x20, 0x20 );
}
void main( void )
{
//--- Use LPT1 -------------------------------------------------
PPDIO_SetLptPort( 1 );
//--- Install our ISR on IRQ 5 ---------------------------------
PPDIO_InstallISR( MyISR, 5 );
//--- Run until either a key is pressed or interrupt is generated on IRQ 5
while ( !kbhit() && !isr_called ) {
}
PPDIO_RemoveISR();
}
Copyright © 1994, Dr. Dobb's Journal