Parallel ports

Parallel switchboxes
Don't use a parallel switchbox if you want to do high speed transfers. I have not yet found one, manual or automatic type, that is reliable at high byte rates.

PS/2 (or Extended) mode takes the standard parallel port (SPP) and introduces an output latch and direction control for bidirectional port operation. Contrary to ECP/EPP mode, the behavior of the nACK interrupt is also changed so that the IRQ becomes active on the trailing edge instead of mirroring the pin; also, bit 2 of the status register reflects the status of the ACK interrupt (latched when IRQ generated, cleared on read) – a violation of ECP spec.

The difference between EPP 1.7 and EPP 1.9 as set in a system BIOS is one trivial difference in the EPP handshake. EPP 1.9 is equivalent to IEEE 1284. The only purpose for the EPP 1.7 setting is for any particular EPP devices built before IEEE 1284 that malfunction with the IEEE 1284/EPP 1.9 handshake. Note: IEEE 1284 defines the electrical characteristics and handshake protocols of an EPP port, not the register definition.

An ECP-capable port is a functional superset of IEEE 1284. An ECP-capable port in ECP mode is incompatible with non-ECP devices, however.

Many ECP ports do not implement the full ECP specification. Common elements to leave out are:
– nFault IRQ generation (full/empty FIFO can still generate IRQ though!)
– Hardware RLE compression (not required by spec)
– DMA (PCI cards cannot implement ECP DMA and generally do not need to since PCI write buffers provide sufficient speed)
– IRQ/DMA resource configuration (PCI cards cannot implement this)

Handling a parallel port interrupt
By definition, when sharing interrupts it is necessary for your device driver to be able to determine whether your device is the source of the interrupt or not (so you can pass the interrupt on unclaimed to other drivers if it is not). This is exceedingly difficult to do in a generic fashion for PCI parallel cards. Whether or not an interrupt is delivered in a particular operating mode, and where the status of that interrupt is reflected, is highly implementation dependent.

There are three places where an interrupt can be enabled:
– Control register bit 4 (~ACK interrupt)
– ECP Extended Control register (ECR) bit 4 (~ERR interrupt)
– ECP Extended Control register (ECR) bit 3 (DMA interrupt)
– ECP Extended Control register (ECR) bit 2 (FIFO interrupts)

There are five places where an interrupt can be generated:
– ~ACK transition
– ECP ~ERR transition
– ECP DMA completion
– ECP read FIFO filling
– ECP write FIFO emptying
– Some devices (NS) generate an interrupt on an unexpected EPP read

There are at least three places where an interrupt can be detected:
– Status register bit 2 (latched after ~ACK transition)
– ECP Config B register bit 6 (follows interrupt pin on bus)
– ECP Extended Control register (ECR) bit 2 (check for 0->1 transition)

PCI multifunction cards usually also have a global control register, which has some location outside of the usual parallel port register set that reflects the status of a parallel interrupt.

We don't really care what in particular caused the interrupt, but we do need to find some proof somewhere in the registers that this card was the one responsible for the interrupt, or things will go horribly wrong.

– ~ACK transition is only latched to Status[2] in PS/2 mode by many cards. In SPP and other modes, it either reads 1 or follows the IRQ pin. Since a spec-conforming PCI card will use a level triggered interrupt, we can in theory use this to test for the interrupt (but only on PCI cards!)
– ECP Config B register can be used, but first the port has to be switched into Test mode to read it, which means the ECP FIFOs must be flushed and current ECP transaction terminated, possibly too high a cost for interrupt handling.
– There is no way to determine whether a ~ERR transition caused the interrupt or not. On an ISA card or one without a shared interrupt, it can be determined by a process of elimination (since a spec-conforming driver disables the ~ACK interrupt when in ECP mode), but on a shared interrupt it is impossible.
– ECR bit 2 is only useful if in ECP mode and FIFOs are being used.

Basically, the most useful parallel interrupts (those generated by external events) give us no reliable way to determine which card owns the interrupt. The ~ACK interrupt could be probed, had the PC parallel port's designers thought to put in a loop-back test, but they did not.

The best thing you can do to handle PCI parallel interrupt sharing in a generic fashion is to:
– Disable the ~ERR interrupt.
– The DMA interrupt is not an issue on PCI cards since they don't support it anyway.
– Keep track of the state of the ECR bit 2 when you set it to 0 (unmasks the ECP FIFO interrupt) so that you can check if it changed in your interrupt handler (meaning we generated an interrupt).
– Ensure that your card cannot both have the ~ACK interrupt enabled AND be in a mode that will not latch that interrupt in Status[2] (reflecting the pin state is not enough!). Then you can assume that a Status[2]==0 event means that we generated the interrupt. Note: On most/all PCI cards, the status register must be read in order to clear the level-triggered interrupt.
– Assure yourself to whatever degree of confidence required that your card will not produce ANY other type of interrupt (vendor's logic equation for IRQ event helps)!

If you are lucky enough to have a global interrupt flag for the parallel port on your PCI card, USE THAT INSTEAD! Then you can use ~ERR and ~ACK as external interrupt sources without worries, and you can also handle spurious interrupts with a high degree of confidence! Only use the above “generic” mechanism as a last resort. If someone would look into using the ECP Register B to check for the interrupt and see how well that works, that may be an even better “generic” solution for PCI parallel cards.

Simple PC parallel port detection in DOS

unsigned short lpt_base;
char lpt_irq;
unsigned char lpt_vector;
unsigned char lpt_pic; /* 0 = pic1, 1 = pic2 */
unsigned char lpt_mask; /* bit in PIC OCW to unmask/mask */
unsigned char received; /* The last byte received */
char is_ecp;
void interrupt(*old_lpt_irqhandler)(__CPPARGS);

// The following code should be inserted into a setup function, and allow
// user to override base address and IRQ
	// setup parallel port
	if (lpt_base == 0) {
		// Use BDA to find base address of system's first parallel port
		unsigned short far *bda_lpt = (unsigned short far*)MK_FP(0x40, 8);

		lpt_base = *bda_lpt;
		//printf("lpt_base %0.4x", *bda_lpt);
		assert(lpt_base == 0x3bc || lpt_base == 0x378 || lpt_base == 0x278);

	if (lpt_base == 0x3bc) {
	  // We can assume a port at 0x3BC has IRQ 7 unless we find otherwise
	  lpt_irq = 7;
	// Detect ECP port according to ECP spec p.31
	// ECR is at lpt_base + 0x402
	unsigned char test = inp(lpt_base+0x402);
	if ((test & 1) /* fifo empty */ && !(test & 2) /* fifo not full */) {
		// Attempt to write a read only bit (fifo empty) in ECR
		outp(lpt_base+0x402, 0x34);
		test = inp(lpt_base+0x402);
		if (test == 0x35)
			is_ecp = 1;

	// If ECP port, read cnfgB to find parallel port IRQ number
	if (is_ecp) {
		// Put port into configuration mode
		test = inp(lpt_base+0x402);
		test |= 0xE0;
		outp(lpt_base+0x402, test);
		// Read cnfgB
		unsigned char irq = inp(lpt_base+0x401);
		irq &= 0x38;
		irq >>= 3;
		// irq0 means selected via jumper, user will have to hard code the irq
		if (irq != 0) {
				case 1: lpt_irq = 7; break;
				case 2: lpt_irq = 9; break;
				case 3: lpt_irq = 10; break;
				case 4: lpt_irq = 11; break;
				case 5: lpt_irq = 14; break;
				case 6: lpt_irq = 15; break;
				case 7: lpt_irq = 5; break;
				default: break;
		// Set ECP port mode to PS2
		test = inp(lpt_base+0x402);
		test &= ~0xE0;
		test |= 0x20;
		outp(lpt_base+0x402, test);

	if (lpt_irq == -1) {
		fprintf(stderr, "Couldn't find interrupt for parallel port at 0x%x !\n", lpt_base);

	// Convert IRQ number to interrupt vector
	switch(lpt_irq) {
		case 5: lpt_vector = 0x0d; lpt_mask = (1 << 5); break;
		case 7: lpt_vector = 0x0f; lpt_mask = (1 << 7); break;
		case 9: lpt_vector = 0x71; lpt_pic = 1; lpt_mask = (1 << 1); break;
		case 10: lpt_vector = 0x72; lpt_pic = 1; lpt_mask = (1 << 2); break;
		case 11: lpt_vector = 0x73; lpt_pic = 1; lpt_mask = (1 << 3); break;
		case 14: lpt_vector = 0x76; lpt_pic = 1; lpt_mask = (1 << 6); break;
		case 15: lpt_vector = 0x77; lpt_pic = 1; lpt_mask = (1 << 7); break;
		default: abort();

        fprintf(stderr, "Parallel port at 0x%x, irq %d", lpt_base, lpt_irq);
        if (is_ecp)
                fprintf(stderr, ", ECP");
        fprintf(stderr, "\n");

        // set to data input mode using DCR
        outp(lpt_base+2, inp(lpt_base+2) | 0x20);

        // check that data lines are not driven by us
        int fail = 1;

        for (i = 0; i < 5; i++) {
                outp(lpt_base, 0x5a+i);
                if (inp(lpt_base) != 0x5a+i) {
                        fail = 0;
        if (fail) {
                fprintf(stderr, "Parallel port does not appear to be bidirectional!\n");        
        disable();  // cli()
        // grab IRQ vector
        setvect(lpt_vector, lpt_irqhandler);
        if (lpt_pic > 0) {
                // unmask our IRQ
		outp(PICB_1, inp(PICB_1) & ~lpt_mask);
		// then unmask IRQ2
		outp(PICA_1, inp(PICA_1) & ~0x04);

	else {
		// unmask our IRQ
		outp(PICA_1, inp(PICA_1) & ~lpt_mask);
	// enable parallel port interrupt via ACK line
	outp(lpt_base+2, inp(lpt_base+2) | 0x10);

	enable(); // sti()

Simple bidirectional communication between two PCs with a standard parallel port cable
Swap STROBE and nACK pins on one end of the parallel cable. Ensure that the parallel port nACK interrupt is enabled on both ends (DCR[5] := 1). Then the communication looks like the following:

// Parallel port ISR, Turbo C++ 3.1 DOS code
void interrupt lpt_irqhandler(__CPPARGS)

  received = inp(lpt_base);
  // Interrupt the sender, since STROBE on this end
  // is connected to ACK on the other end
  unsigned char tmp = inp(lpt_base+2);
  outp(lpt_base+2, tmp ^ LPT_STROBE);
  outp(lpt_base+2, tmp);

  old_lpt_irqhandler(); // chain old IRQ handler
  outp(PICA_0, EOI); // EOI
  if (lpt_pic > 0)
	outp(PICB_0, EOI); // also send EOI to PIC2


I have found this to be a sufficient quick & dirty way of transferring bytes from one PC to another in interrupt driven fashion.

Leave a Reply