Identifying Serial Port IRQs

Detecting how many ports are installed and which IRQ they're set to

John Ridley

John is a programmer in Ypsilanti, Michigan. He can be contacted at john.ridley@hal9k.com.


While developing installation programs for a hardware vendor, I collected code to identify equipment in the PC. I was fascinated by programs, such as Microsoft Diagnostics, which are able to detect most of the standard hardware in a machine. Eventually, I had a collection of code for detecting everything from the type of CPU to the kind of soundboard.

My employer had decided that it was worth his money to have good installation software for even fairly minor things, particularly serial ports, because although they are among the simplest elements in a PC, serial ports were eating up a lot of tech-support time. They are also one of the most common add-ins. If we could mail a disk that costs 75 cents and save an 800 tech-support call, the company would save money and the tech-support people would have more hair left on their heads. (Try explaining to a stockbroker over the phone which of those 12 jumpers to move where--then discovering that you first have to explain what a jumper is.)

I soon had software that would detect existing serial ports in the machine and, after prompting the user for which of the remaining ports he wanted, draw a picture of how to set the jumpers. What I didn't have, however, were code fragments to detect which interrupt request (IRQ) the ports were using.

It used to be okay to assume that "standard" interrupts were being used--for serial ports, ports 1 and 3 on IRQ4, and 2 and 4 on IRQ3. But now that nearly every new PC is running Windows or some other multitasker, sharing interrupts is no longer acceptable. If you want to run a mouse and a modem under Windows, for instance, they had better not be using the same interrupt.

A little while later, while working on setup for a communications package, I ran across the same problem. The users didn't know which IRQ their ports were set to, and support was sometimes just having them try all the settings until one worked. Unfortunately, even that didn't always work, because the computer may come from the factory with settings that work only in limited situations (don't use the mouse and modem at the same time) or not at all. So if your spiffy new modem software is being installed in a Windows environment where the modem and the mouse share an interrupt, the mouse is going to stop moving as soon as you grab that interrupt. You want to be able to tell the user why and give them advice on how to fix it.

This problem seemed to me like a gaping chasm in my knowledge. I was soon on a nightly quest to untangle the problem of IRQ identification. I knew it was possible--after all, Microsoft Diagnostics tells you the interrupts for the COM ports, doesn't it?

Well, I thought so. After some experimentation, however, I discovered that it actually does no such thing. You can move the actual IRQ around, and Microsoft Diagnostics will not reflect the change--apparently, it detects that the ports are there, then assumes "standard" IRQ settings. Still, I remained convinced that there was a better solution.

The most accurate solution would have been to backtrace the interrupt vectors to find the attached drivers. However, this not feasible for shared interrupts, and the serial ports usually don't have any drivers attached to them unless a mouse is attached.

Next, I tried to determine if there were any way to tell from the serial-port end which IRQ the port was attached to. A short look through the schematics of a serial port dismissed this option. Serial ports are primitive devices; certainly miles away from being able to read back their configurations.

After about a week of trying different methods, I realized there was no elegant solution. There just isn't any passive way to tell what is attached to an IRQ, nor is there any way to query the port to find the IRQ. The only solution, then, is to cause the serial port to generate an interrupt and watch to see which interrupt happens.

To do this, you have to learn how to deal with the 8257, or PIC (Programmable Interrupt Controller). Since the PIC has no pretty interface, you have to twiddle its bits. Fortunately, Jeff Duntemann's columns ("Structured Programming," DDJ, June--September 1991) on serial-port programming provide a good reference. For the purposes of this article, only a little PIC programming is necessary, and it is commented pretty well.

The IsUART and WhichUART routines in SPINFO.C (see Listing Two) are borrowed almost directly from Jeff's columns. What comes after these routines is the meat of this article. (Listing One is SERPORT.H, the necessary include file, and Listing Three is PORTINFO.C, a demo program to utilize SPINFO.C, which scans normal IBM PC serial-port addresses COM1 though COM4, then determines whether an 8250 or compatible UART is at the location, what kind of UART it is, and which IRQ it is set to.)

It is relatively easy to cause a serial port to generate an interrupt. There are several methods--the trick is to use one that doesn't cause grief to any attached devices. I enabled the "Transmitter Holding Register Empty" interrupt. Assuming the chip is not currently trying to transmit anything, this will cause an interrupt to occur as soon as it is enabled. Also, after you disable the interrupt again, the chip is exactly as it was when you came in.

I first intercepted each interrupt in turn and triggered an interrupt to see if the interrupt-handler function was called. This actually worked; it successfully found the IRQs. Success at last--or so I thought.

About ten seconds later, I went back into the editor and found my mouse had stopped working. By the time the program finished checking all the ports, it had completely confused the mouse, as well as any other similar device drivers. To understand why, consider this example: COM1 has a mouse attached and is set to IRQ4. COM3, the port we're trying to find out about, is also on IRQ4. This is a very common situation. We will quite likely not have IRQ4 intercepted when we cause our first interrupt, so the interrupt will go to the mouse driver, which is first in line for IRQ4. The mouse driver has received this interrupt and has no clue what to do with it. Chaos ensues.

I tried a lot of different methods to avoid stomping on serial device drivers, with varying amounts of success, but none were 100 percent correct. The best one tracked down the mouse driver, identified which port it was using, and avoided stomping on this interrupt. Unfortunately, this only works for mice, and it has trouble with some configurations.

After a day or two, I finally gave in to the inevitable: a straightforward--if not very elegant--solution is to intercept all the interrupts on which you think your port might be, cause the interrupt, and then check to see what happened. This method can also be used to resolve existing conflicts, since it will return correct values even if an interrupt is shared. Though the concept and the code seem pretty simple, the timing was hairy. A few instructions had to be juggled to allow the PIC and UART time to setup and interact properly. Since I'm not much of a cycle counter, this was done by juggling for best results.

At first, I got a lot of false triggering; apparently there were interrupts pending much of the time, and when I opened the PIC floodgates I got several false interrupts. This situation was taken care of by inserting the first enable/disable pair; the serial port in question hasn't had its interrupts enabled yet, so anything that comes in at this time is garbage and is thrown out.

After enabling the UART interrupt, all interrupts are enabled for just a moment, during which all pending interrupts are serviced, hopefully including the one you want. Then the pending interrupts are disabled again. Keeping the time short minimizes false triggering (by the user bumping the mouse, or something). Then the UART status is restored, and the IRQ bitmap returned.

In the real world, the calling program will want to check for multiple returned bits, which normally indicate an IRQ occurring simultaneously on another device, or possibly a hardware fault. If this occurs, the detecting routine can be called several times, to try to get a consistent result.

You can expand this procedure to any device that you can cause to generate an interrupt without endangering its normal operation. Sometimes this is impossible; the printer port, for example, is a really dumb device; the only way to get it to generate an interrupt is to actually send something to the printer and wait for the ACK signal. Luckily this doesn't really matter too much for MS-DOS, since the printer IRQ is almost never used. Note, however, that the code does not always work correctly when running in a DOS window in Windows Enhanced mode, seemingly because Windows sets up "virtual chips" that can get into odd states. If this occurs, the code will not detect an interrupt. This rarely happens, but once it does, only restarting Windows seems to clear it up. I have not found a solution to this (other than not running it under Windows).

You can now detect all relevant information for serial ports. You can tell how many ports are installed, where they are, and which IRQ they are set to. With this information, you can write software to handhold the end user through the installation. Users will get their systems going faster and gain some confidence in the process; you'll have another 100 lines of code in the toolbox.

Perhaps someday, every last PC will be replaced with smart systems such as Plug and Play. Meanwhile, methods such as this one will help you keep the frustration levels down.

Listing One


#ifndef _SERPORT_CONSTANTS_
#define _SERPORT_CONSTANTS_
#define COM1 0
#define COM2 1
#define COM3 2
#define COM4 3
#define NOPORT   0                          /*types of UARTS we recognize   */
#define T8250    1
#define T16450   2
#define T16550   3
#define T16550AF 4
#define COM1port 0x3f8
#define COM2port 0x2f8
#define COM3port 0x3e8
#define COM4port 0x2e8
#define HW_IRQ_OFF 8              /* Add to IRQ# to get software IRQ vector */
#define LOOPBIT 0x10              /* MCR reg loopback bit         */
#define DLAB    0x80              /* DLAB flag position           */

/************************ 8250 family UART registers ************************/
/***************     0      1      2      3      4      5      6      7     */
#define RBR 0   /* ==========Receiver Buffer Register (read only)========== */
#define THR 0   /* ========Transmitter Holding Register (write only)======= */
#define DLL 0   /* Divisor Latch LSB: load these bytes with 115200 / BAUD   */
#define DLM 1   /* Divisor Latch MSB:                                       */
                /* NOTE: Set DLAB in LCR to write DLL,DLM, then clear DLAB  */
#define IER 1   /* ===============Interrupt Enable Register================ */
                /* Rcv    Xmitr  Line   Modem    0      0      0      0     */
                /* Data   Empty  Status Status                              */
#define IIR 2   /* ======Interrupt Identification Register (read only)===== */
                /* 0=IRQ   ---IRQ ID--    0      0      0      0      0     */
                /* pending bit 0  Bit 1                                     */
#define LCR 3   /* ================Line Control Register==========  Div.lat.*/
                /* -Word Length-  Stop  Parity  Even   Stick  Set   AccessBt*/
                /* bit 0   bit 1  Bits  Enable  Parity Parity Break (DLAB)  */
#define MCR 4   /* ================Modem Control Register================== */
                /*  DTR    RTS    Out1   Out2 Loopback  0      0      0     */
                /* NOTE: Out2 must be set (1) on PC's to enable serial port */
#define LSR 5   /* =================Line Status Register=================== */
                /* Data  Overrun Parity Framing Break  Xmit  Xmit     0     */
                /* Ready  Error   Error  Error Interpt Hold  Empty          */
                /*                                     Empty                */
#define MSR 6   /* ===============Modem Status Register==================== */
                /* Delta  Delta TrailEdg Delta  CTS    DSR     RI     CD    */
                /*  CTS    DSR  RingInd.  CD                                */
#define SCR 7   /* Scratch Register: not present in early 8250's (8250A)    */
/****************************************************************************/

/**************************** 8259 control ports ****************************/
/***************     0      1      2      3      4      5      6      7     */
#define OCW1 0x21 /* Operation Control Word 1                               */
                  /* Zero in these bits enables the respective IRQ (0-7)    */
#define OCW2 0x20 /* Operation Control Word 2                               */
                  /* 001xxxxx : Non-Specific End Of Interrupt (EOI)         */
                  /* 011xxxxx : Specific EOI                                */
                  /* 101xxxxx : Rotate on Non-Specific EOI                  */
                  /* 100xxxxx : Rotate in Automatic EOI mode (SET)          */
                  /* 000xxxxx : Rotate in Automatic EOI mode (CLEAR)        */
                  /* 11100vvv : Rotate on Specific EOI, vvv indicates IRQ   */
                  /* 11000vvv : Set Priority Command     "   "         "    */
                  /* 010xxxxx : No Operation                                */
/****************************************************************************/
short IsUART(short which);
short WhichUART(short which);
#endif


Listing Two


#include "serport.h"
/********************************** IsUART **********************************/
short IsUART(short PortAddr)
  {
  short HoldMCR, HoldMSR, Port, retval;
  HoldMCR = _inp(PortAddr+MCR);             /* Get Modem Control contents   */
  _outp(PortAddr+MCR, HoldMCR | LOOPBIT);   /* Turn on loopback             */
  HoldMSR = _inp(PortAddr+MSR);             /* Get Modem Status Register    */
                                            /* so we can restore MCR later  */
  _outp(PortAddr+MCR, 0x0a | LOOPBIT);      /* Turn on RTS                  */
  if ((_inp(PortAddr+MSR) & 0xf0) == 0x90)  /* If CTS is on, there's a UART */
    retval = TRUE;
  else
    retval = FALSE;
  _outp(PortAddr+MSR, HoldMSR);             /* Restore MCR/MSR              */
  _outp(PortAddr+MCR, HoldMCR);             /* Turn off loopback            */
  return retval;  
  }
/******************************** WhichUART *********************************/
short WhichUART(short PortAddr)
  {
  short Port, Temp, retval=-1;
  Temp = _inp(PortAddr+SCR) ^ 0xff;         /* Check scratch register       */
  _outp(PortAddr+SCR, Temp);                /* Output complement            */
  if (_inp(PortAddr+SCR) != Temp)           /* check for same return val    */
    return(T8250);                          /* No scratch reg: 8250         */
  _outp(PortAddr+IIR,7);                    /* Enable FIFO                  */
  switch(_inp(PortAddr+IIR) & 0xc0)         /* Check high bits of IRQ ID reg*/
    {
    case 0:    retval = T16450;   break;
    case 0x80: retval = T16550;   break;
    case 0xc0: retval = T16550AF; break;
    }
  _outp(PortAddr+IIR,0);                    /*turn of FIFO                  */
  return retval;
  }
/************************* IRQ Identification code **************************/
short CurPortBase;                          /* shared info: port base addr  */
void (__interrupt __far *old_irq[4])(void); /* save old IRQ vectors         */
short IRQ_Happened;                         /* bitmap: which IRQ happened?  */
#define IRQbit 0x3c                         /* 8259 enable IRQ 2-5          */
void __interrupt __far our_irq2()
  {
  _enable();                                /* Enable CPU interrupts        */
  IRQ_Happened |= 4;                        /* Flag which IRQ happened      */
  _outp(CurPortBase+1, 0x0);                /* Mom,make him shut up!        */
  _outp(OCW2,0x20);                         /* EOI to 8259                  */
  }
void __interrupt __far our_irq3()           /* Same thing for IRQ3-5        */
  {
  _enable();
  IRQ_Happened |= 8;
  _outp(CurPortBase+1, 0x0);           
  _outp(OCW2,0x20);                    
  }
void __interrupt __far our_irq4()
  {
  _enable();                           
  IRQ_Happened |= 16;
  _outp(CurPortBase+1, 0x0);           
  _outp(OCW2,0x20);                    
  }
void __interrupt __far our_irq5()
  {
  _enable();                           
  IRQ_Happened |= 32;
  _outp(CurPortBase+1, 0x0);           
  _outp(OCW2,0x20);                    
  }
void GrabAllIRQ()
  {
  short IRQ;
  for (IRQ = 2; IRQ <= 5; IRQ++)
#pragma warning(disable:4113)
    old_irq[IRQ-2] = _dos_getvect((unsigned)IRQ+HW_IRQ_OFF);
#pragma warning(default:4113)
  _dos_setvect((unsigned)2+HW_IRQ_OFF,our_irq2);
  _dos_setvect((unsigned)3+HW_IRQ_OFF,our_irq3);
  _dos_setvect((unsigned)4+HW_IRQ_OFF,our_irq4);
  _dos_setvect((unsigned)5+HW_IRQ_OFF,our_irq5);
  }
void ReleaseAllIRQ()
  {
  short IRQ;
  for (IRQ = 2; IRQ <= 5; IRQ++)
    _dos_setvect((unsigned)IRQ+HW_IRQ_OFF,old_irq[IRQ-2]);
  }
unsigned short WhichIRQ(short PortAddr)
{
short IRQ, HoldOCW1, HoldIER, HoldMCR;
  if (!IsUART(PortAddr))                    /* Don't bother testing         */
    return 0;
  CurPortBase = PortAddr;
  HoldOCW1 = _inp(OCW1);                    /* remember status of 8259      */
  HoldIER  = _inp(CurPortBase+IER);         /* and of our UART              */
  HoldMCR  = _inp(CurPortBase+MCR);
  _disable();                               /* Be safe...                   */
  _outp(CurPortBase+MCR, 0x03 | 0x08);      /* enable port                  */
  GrabAllIRQ();                             /* We see all now!              */
  _outp(OCW1, HoldOCW1 & (~IRQbit));        /* enable 8259                  */
  _enable();                                /* Clear pending IRQ's          */
  _disable();                               /* Ready for the real thing now */
  IRQ_Happened = 0;                         /* Clear bitmap                 */
  _outp(CurPortBase+IER, 0x02);             /* enable xmt empty int.        */
  _enable();                                /* BANG!                        */
  _disable();                               /* OK, we're done.              */
  _outp(OCW1, (_inp(OCW1) & (~IRQbit)) |    /* Restore 8259                 */
              (HoldOCW1   & IRQbit));
  _outp(CurPortBase+IER, HoldIER);          /* Restore our UART             */
  _outp(CurPortBase+MCR, HoldMCR);
  ReleaseAllIRQ();                          /* Let go of IRQ vectors        */
  _enable();                                /* and relinquish control       */
  return IRQ_Happened;                      /* Send back bitmap             */
}


Listing Three


/******************************** PORTINFO.C ********************************/

#include <dos.h>
#include <stdlib.h>
#include <stdio.h>
#include <memory.h>
#include <conio.h>

#define TRUE 1
#define FALSE 0

#include "spinfo.c"
short PortBases[]={COM1port,COM2port,COM3port,COM4port};
char *PortType[]={"NO PORT","8250","16450","16550","16550AF"};
void main()
  {
  short Portnum, PortAddr;
  unsigned short IRQ,IRQ_bitmap;
  for (Portnum=COM1; Portnum <=COM4; Portnum++)
    {
    PortAddr = PortBases[Portnum];
    if (IsUART(PortAddr))
      {
      printf("COM%d: ",Portnum+1);
      printf("%s ",PortType[WhichUART(PortAddr)]);
      IRQ_bitmap = WhichIRQ(PortAddr);
      if (IRQ_bitmap == 0)
        printf("Unable to detect IRQ");
      else
        for (IRQ = 0; IRQ < 16; IRQ++)
          if (IRQ_bitmap & (1 << IRQ))
            printf("IRQ%d ",IRQ);
      printf("\n");
      }
    }
}


Copyright © 1995, Dr. Dobb's Journal