How to Monitor Interrupts on the Parallel Port

This tutorial shows you how to control a Neutrino process using interrupts via the parallel port.

The base address of the parallel port (often referred to as device LTP1:) is detected by BIOS, and stored at 0x0040:0008. The base address of the second parallel port LPT2: (if it exists) is stored at 0040:000a, and that of LPT3 at 0040:000c. If the BIOS could not detect a parallel port, 0 is stored at the corresponding location.

A parallel port is three bytes wide. The first byte (located at the base address) is the data register, the second byte is the status register, and the third byte is the control register. Typically, the base address of LTP1: is 0x378, so the data register is located at 0x378, the status register at 0x379, and the control register at 0x37a.

The pinout of the status register is as follows:

BIT # FUNCTION
0 Timeout
1 Unused
2 Unused
3 IO error
4 Printer online
5 Out of paper
6 Acknowledge
7 Printer busy

The pinout of the control register is as follows:

BIT # FUNCTION
0 Strobe (should be 1)
1 Auto linefeed (doesn't matter)
2 Printer initialisation (should be 1)
3 Printer selected (should be 1)
4 Hardware IRQ (1 = enabled)
5-7 Unused (should be 1)

The Acknowledge bit of the Status register is an input (to the computer), and is used to allow the printer to signal to the computer that it is ready for more information. Typically, this bit is connected to hardware interrupt 7, though this can be changed in the CMOS settings on some computers. Before a signal to the Acknowledge bit will generate an interrupt, however, the Interrupt Enable bit of the Control register must be set high.

Thus to interrupt the CPU with our own hardware signal source, we first enable bit 4 of the Control register, then send a TTL signal to pin 10 of the parallel port.

The following program makes the appropriate initialisations to enable and monitor interrupts occurring on the parallel port. It then waits for a given number (MAX_COUNT) of separate interrupt signals. The current number of elapsed CPU clock cycles is recorded into an array, upon receipt of each interrupt. After the required number of interrupts have been received, the program displays a table of results showing the number of CPU clock cycles at each interrupt, the number since the previous interrupt, and the time-difference between interrupts in milliseconds.

Note that the difference between interrupts, in time or in cycles, would be identical in an ideal world. Inconsistencies between the differences would be due to the natural inaccuracies in the signal generator, and (more importantly) latencies in the operating system. This is where a realtime operating system excels -- the latencies and inconsistencies will be tiny and will have an upper limit.

This program does not use an interrupt handler; instead, it uses an interrupt signal that wakes this thread when an interrupt has been received. After the processing of each interrupt has been completed, the thread puts itself to sleep until the next interrupt is received (i.e. the kernel wakes it with the interrupt signal).

It is necessary to slay Neutrino's parallel port driver (devc-par) for this program to work properly. Otherwise, two pieces of software will be fighting over the same piece of hardware. Note that slaying devc-par from the command line was not enough to get this program to work for me. I had to slay it from /etc/rc.d/rc.local to get the program to detect any interrupts.

The program was verified by feeding a signal generator's TTL output into pin 10 of the parallel port. I monitored the IRQ 7 interrupt line by placing an oscilloscope probe on pin B21 on an empty ISA slot on the motherboard. Using this setup, I found that devc-par prevented interrupts occurring (even though the Acknowledge line was being signalled correctly) unless it had been slain in rc.local. Remember that slaying it from the command line is too late.

You will have to run this program as root so that it can obtain the necessary permissions to control the hardware. Also, the thread that calls InterruptWait() must be the same thread that called InterruptAttachEvent().

The key functions in this program are ThreadCtl(), mmap_device_io(), out8(), InterruptAttachEvent(), InterruptWait(), ClockCycles, InterruptUnmask(), and InterruptDetach().



#include <stdio.h>
#include <stdlib.h>       /* for EXIT_* */
#include <stdint.h>       /* for uintptr_t */
#include <hw/inout.h>     /* for in*() and out*() functions */
#include <sys/neutrino.h> /* for ThreadCtl() */
#include <sys/syspage.h>  /* for for cycles_per_second */
#include <sys/mman.h>     /* for mmap_device_io() */

/* The Neutrino IO port used here corresponds to a single register, which is
 * one byte long */
#define PORT_LENGTH 1 

/* The first parallel port usually starts at 0x378. Each parallel port is
 * three bytes wide. The first byte is the Data register, the second byte is
 * the Status register, the third byte is the Control register. */
#define CTRL_ADDRESS 0x37a
 /* bit 2 = printer initialisation (high to initialise)
  * bit 4 = hardware IRQ (high to enable) */
#define INIT_BIT 0x04
#define INTR_BIT 0x10

#define PARALLEL_IRQ 0x07
#define MAX_COUNT 1000

/* ______________________________________________________________________ */
int
main( void )
{
   int privity_err;
   uintptr_t ctrl_handle;
   
   uint64_t clock_cycles[MAX_COUNT];
   uint64_t cps;
   uint64_t diff;
   float diff_ms;
   struct sigevent event;
   int intr_id;
   int count;

   /* Give this thread root permissions to access the hardware */
   privity_err = ThreadCtl( _NTO_TCTL_IO, NULL );
   if ( privity_err == -1 )
   {
      printf( "Can't get root permissions\n" );
      return -1;
   }

   /* Get a handle to the parallel port's control register */
   ctrl_handle = mmap_device_io( PORT_LENGTH, CTRL_ADDRESS );
   /* Initialise the parallel port */
   out8( ctrl_handle, INIT_BIT );
   /* Enable interrupts on the parallel port */
   out8( ctrl_handle, INTR_BIT );

   /* Tell the kernel to attach an interrupt signal event to this thread */
   event.sigev_notify = SIGEV_INTR;
   intr_id = InterruptAttachEvent( PARALLEL_IRQ, &event, 0 );
   if ( intr_id == -1 )
   {
      printf( "Couldn't attach event to IRQ %d\n", PARALLEL_IRQ );
      return EXIT_FAILURE;
   }
   
   for ( count = 0; count < MAX_COUNT; count++ )
   {
      /* Sleep until the next interrupt */
      InterruptWait( 0, NULL );
      /* Record the time in elapsed CPU cycles */
      clock_cycles[count] = ClockCycles();
      /* Reenable this interrupt */
      InterruptUnmask( PARALLEL_IRQ, intr_id );
   }

   /* Find out how many CPU cycles there are per second */
   cps = SYSPAGE_ENTRY(qtime)->cycles_per_sec;
   printf( "cps = %lld\n", cps );
   printf( "Count\tCPU Cycles\tDiff (Cycles)\tDiff (msec)\n");

   /* Don't start from the first event, since the delay since the previous
    * event cannot be calculated. */
   for ( count = 1; count < MAX_COUNT; count++ )
   {
      diff = clock_cycles[count] - clock_cycles[count-1];
      diff_ms = (float)diff / (float)cps * 1000;
      printf( "%d\t%lld\t%lld\t\t%f\n", count, clock_cycles[count], 
            diff, diff_ms);
   }

   /* Remove the attachment from this thread */
   InterruptDetach( intr_id );

   return EXIT_SUCCESS;
}

A sample output obtained on a 500MHz Intel Pentium 3 with QNX 6.2.0, with a 10kHz input signal shows that Neutrino handles this speed confortably.

Some excellent sources of useful information on the parallel port are Interfacing the IBM PC Parallel Printer Port, Version 0.96, 9/1/94, by Zhahai Stewart and Your PC's Printer Port: a Window on the Real World, Version 1.5, 12/95, by Tom Dickinson


Home About Me
Copyright © Neil Carter

Content last updated: 2003-11-24