In the mini and microcomputer world, RS-232 links provide what is likely the most heavily used form of communications. RS-232 is versatile and relatively inexpensive. It is possible to connect computers that are considered otherwise to be completely incompatible. RS-232 is also used by computers to talk to devices such as modems, dumb terminals, bar code readers, printers, lab equipment, and more. So it made sense that IBM defined a standard RS-232 interface card for their PC. Up to two of the cards could be installed in a PC, and were referred to as COM1 and COM2.

Unfortunately, each COM card installed in a PC needed its own interrupt line. The original IBM PC had only eight interrupt lines, of which just two were available for use by RS-232 ports. Some third party COM cards could be configured to operate at other addresses on other interrupt lines, so it was in theory possible to operate three ports at once, but this required using interrupt lines that were allocated for other devices. And a user who wanted to operate as many as 8 RS-232 ports simultaneously was essentially out of luck.

The solution to this problem comes in the form of the Multiple Port Shared Interrupt (Multiport) board. These are boards that are set up so that multiple communication ports can share a single interrupt on the PC bus. This article discusses how the programmer can use these boards in MS-DOS applications.
What is a Multiple Port Shared Interrupt Board?

There is no such thing as a standard Multiport RS-232 board. There are many different manufacturers of these boards, and each of them has implemented their board in a different fashion. However, most of the boards have enough in common so that the same programming techniques, and even the same code, can be used to operate them.

The boards discussed in this article all have several things in common. First of all, each of them uses a Universal Asynchronous Receiver/Transmitter (UART) for the 8250 family. The 8250 family of UARTs include the 8250, 16450, and 16550 manufactured by National Semiconductor. These are the same UARTs used by IBM in the standard COM1 and COM2. This means that the actual programming of the registers on the UART of a Multiport board can be done using essentially the same code as already exists for standard RS-232 ports. Most of the boards support 4 or 8 UARTs on a single board, although there are some 16 port boards available.

In addition, the manufacturers of multiport boards all support the same I/O pins used on the IBM PC COM1 and COM2. These are:

TX RS-232 transmission
RX RS-232 reception
RTS Request to Send
CTS Clear To Send
DTR Data Terminal Ready
DSR Data Set Ready
CD Carrier Detect
RI Ring Indicator

The Multiport board designers use various techniques to bring these signal lines out to multiple 9 or 25 pin D connectors. The DB-25 connector is an industry standard, but IBM began using a nine pin connector as an alternative when the AT series was introduced. For the most part, the board manufacturers seem to be adhering to the 25 pin standard. The nine pin connector used by IBM may have been implemented to save space on a card edge for more connectors, but the multiport vendors don’t attempt to mount their connectors on the card edge anyway, so geographical considerations are of less importance.

What makes a Multiport board work on a PC is the shared interrupt logic. This is special hardware that takes the interrupts from all 4, 8 or 16 UARTs on the board and manages them to produce interrupts on a single line on the PC bus. This is not as simple as just ORing them all together to produce an interrupt signal. The IBM PC bus interrupt lines are edge-triggered, which means that a low to high transition needs to take place in order to create a new interrupt. This causes a problem for the hardware designer. In an ORed logic scheme with two UARTS, the following sequence of events could hang the board up:

  1. UART 2 generates an interrupt, bringing PC IRQ high
  2. The interrupt service routine (ISR) is invoked
  3. The ISR checks UART 1, which is idle
  4. The ISR checks UART 2, sees that it is active
  5. UART 1 receives a character, generates an interrupt
  6. The ISR services UART 2
  7. UART 2 drops its interrupt line, PC IRQ remains high because of UART 1
  8. The ISR exits

At this point, the IRQ line on the PC is still high, but a new interrupt will not be generated. This is because it did not go low after UART 2 was serviced, so an edge was not generated.

The Multiport boards have to intelligently manage the interrupt output going on to the PC bus. The exact method varies from board to board, but the final outcome is the same: an edge needs to be generated for each interrupt that needs to be serviced by the CPU. This implies circuitry somewhat more sophisticated than just a giant OR gate, but doesn’t require revolutionary new designs. This allows the board designers to keep the prices at a reasonable level.

All the boards that will be discussed in this article manage the interrupts in this manner. Each of the boards also incorporates an additional feature that helps the programmer: a status register. When the board generates an interrupt, there is a special register on the board that can be read by the interrupt service routine. This register indicates which port(s) are presently in need of servicing. Without this register, the interrupt service routine would have to poll each UART on the board for its current interrupt activity. The status register makes that job a little easier.

The multiport boards discussed in this article differ somewhat from RS-232 interface boards that readers may have seen on various other system. First of all, the 8250 family of UARTs are not considered to be high performance parts. Boards designed for systems such as DEC’s VAX line of minicomputers can select the optimum UART for the job, since all system access goes through a device driver. PC board designers don’t have this luxury, and must stick to the compatible family of parts, for better or worse.

An even more important distinction between PC multiport boards and those found on other systems is the requirement of processor intervention on every single character event. Higher powered boards relieve the system CPU of “character juggling” through many different mechanisms.

Generally these boards will have a dedicated processor that takes care of processing individual interrupts, buffering data, and transferring it to the main CPU memory via DMA or shared memory. Even if a slave processor is not available, many UARTs have FIFOs that can drastically reduce the number of interrupts required when processing data.

These intelligent boards suffer from only two problems: they cost an arm and a leg, and they are for the most part incompatible with the existing PC code base. So when an application requires support for 4 or more RS-232 lines, it makes sense to look at the lower cost, dumb-but-compatible multi-port boards.

When to Select an 8250 Family Multiport Board

When you have an application that needs to be able to service 4 or more RS-232 ports, you have several choices, including intelligent boards, networked systems, and multiport boards. The decision making process on which board to choose is fairly simple. As long as cost is an important factor, the multiport board board is your first choice. The end user price of under $100/port is well below the price of an intelligent board.

The primary drawback to the multiport board is one of throughput. As was discussed earlier, the CPU in your PC will have to individually transfer each byte from the UART to PC memory, or vice versa. By adding up the number of cycles needed to process each character, you can quickly get an idea of the maximum throughput on a PC. Even with carefully crafted assembly code, a 10 Mhz 80286 machine will only be able to keep up with perhaps 4-6 ports running continuously at 9600 baud. At that rate the CPU will be receiving a new interrupt about once every 250 microseconds, which is barely enough time to save and restore all the registers.

Fortunately, RS-232 throughput for most applications tends to be “bursty”. An AT machine will have no trouble managing 8 or even 16 ports in an application where data is being sent only periodically. Think of a typical Point-of-Sale terminal application. Most terminals attached to the PC will be idle most of the time. As long as the processor can send the 2000 or so characters needed to redraw a screen in a second or so, it will have plenty of breathing room to catch up while the clerk runs the customer’s Visa card through the imprinting machine.

One additional advantage to consider when selecting a type of board is the availability of library software. Most of the multiport boards discussed in this article have support available from several C and Pascal library vendors. The intelligent boards for the most part are limited to device drivers supplied by the vendor. These device drivers generally implement a basic feature set, but are short on bells and whistles. The competitive environment in the third-party library arena has created a buyer’s market, with extensive feature lists available for library purchasers. Sources for these libraries are discussed at the end of this article.

The Software

Given that you now have an application that needs one of these multiport boards, and you have purchased one and installed it in your computer, what do you do next? Just having the board itself is not enough, you now need an interface to control and communicate with the RS-232 ports resident on it. The code to do this breaks down into three major sections:

  • The control interface, used to open and close ports, and set and change parameters, such as the baud rate.
  • The Input/Output interface, used by the programmer to read and write single characters, or blocks of characters.
  • The device interface code, used to handle the physical level interface with the multiport board.

The conventional way to talk to a piece of hardware in the computer world is through a device driver, which is either part of or closely linked to the operating system of the computer. In the MS-DOS world, if you are talking to a device driver you can generally use the standard I/O routines that are supplied with your programming language.

Unfortunately, for various reasons, device drivers on MS-DOS machines will frequently not perform as well as we would like. This leads programmers to use shortcuts that talk directly to the hardware. Serial communications software is no exception to this.

The device driver built into the IBM PC BIOS for the RS-232 ports is of very little value. Since it is a polled mode interface, about the only thing it can reasonably be used for is an output-only interface, such as to a printer. In addition, multiport boards are unknown to the BIOS, so any device driver to support these boards must be added in as a separate program. Most manufacturers of multiport boards do in fact supply device drivers for their boards. Some are true device drivers which are installed via modification of the MS-DOS CONFIG.SYS configuration files. Others are TSR programs which can be installed and removed at any time on a running system.

Using the vendor supplied software can be a good or bad idea. The true MS-DOS device drivers allow you to open ports in C with standard fopen() commands, and read and write with putc() and getc(). Other vendors require you to learn new commands and link in their library routines. At the most inconvenient end of the scale, some vendors emulate the INT 14 BIOS service calls for their COM ports, requiring you to learn the arcane syntax of your particular compiler’s version of the int86() function.

All of these are reasonable solutions, but they generally suffer from one or more disadvantages. First of all, the device drivers tend to be somewhat inflexible. MS-DOS does not support loading or unloading device drivers, so they usually have to be left in at all times, imposing an unwelcome burden on applications that don’t need them.

Communicating with devices via system interrupts is usually slower than direct function calls. And most importantly, there are a lot of times when you want to have the source to your device interface code, and you don’t usually get that from a board vendor.

Fortunately, it is not that difficult to implement your own code to talk to a multiport board. Most C compilers being sold today have the capability to implement a complete interface without having to resort to any assembly code, and still achieve satisfactory performance. As an example, I have supplied some sample code with this article that does just that, and all in only a little more than a hundred lines of C code.

The Sample Device Interface

The code for interfacing to the multiport board is contained in the file called COM.C. For the most part, this code glosses over the details of programming the 8250 UART. Various computer magazines have published many good articles over the past few years detailing how this part works, and the reader unfamiliar with the part can refer to these. In addition, I would recommend ordering the data sheets from National Semiconductor.

The code in COM.C is compatible with Microsoft C, Quick C, and Turbo C. The major difference to be handled between the two dialects of C is their implementation of the I/O port input and output routines, and the get and set interrupt routines. These differences are managed through the use of macros conditionally defined in the COM.H header file.

In order to understand how to use this code, you need to look at the procedural interface to the code. The data structures remain mostly hidden from view. In order to understand how the code works, you need to look at those data structures, and examine how they interface with the code.
Data Structures

There are two major types of data structures in this program, and once you understand how they work, the rest of the program’s operation quickly makes sense. The two data structures are the BOARD structure and the PORT structure. The PORT structure is very similar to the structure you might use in a conventional single port program. The definition of type PORT in COM.H is shown below:

typedef struct {
    unsigned int   address;     /* Address of the 8250                */
    char           buffer[256]; /* The receive buffer.                */
    unsigned char  head;        /* Offset for insertion into the buff.*/
    unsigned char  tail;        /* Offset for removal from the buffer.*/
    unsigned char  match;       /* The status register match value    */
} PORT ;

The structure elements are as follows:

address: This is the base address of the 8250 UART. It is needed to read and write characters, as well as to control the UART, set operational parameters, etc.
buffer[256]: This is a simple 256 byte receive circular buffer. The arbitrary size of 256 was chosen to simplify the put and get routines.
head: This is the buffer offset used by the interrupt service routine to add characters to the circular buffer.
tail: This is the buffer offset used to remove characters from the receive buffer.
match: This byte is used to show what bits need to be set in the board status register to indicate that this port has an interrupt needing servicing.

The only structure element that wouldn’t normally be used in a single port operation is the “match” element. Depending on the board type, the match can either indicate which bit is set for the given port in the board status register, or what value in the status register will indicate the port is active. (Digiboard is the only board using the latter algorithm).

The BOARD data structure is a little different. A BOARD structure is what is attached to a particular interrupt line, and it in turn has to keep track of the PORT structures that are owned by a particular board.

The structure elements for BOARD are described here:

status_address: This is the address of the status register for the particular board.
irq_mask: This is the mask that has to be applied to the system 8259 interrupt controller in order to enable and disable interrupts. Note that this structure element is redundant, and could be calculated given the interrupt number. Storing it in the structure just means it doesn’t have to be recalculated every time interrupts are enabled and disabled.
int_number: This is the CPU interrupt number that the board is attached to. On the PC, this will typically be 11 or 12 for IRQ3 and IRQ4 respectively.
old_vector: When the board is opened, the old interrupt vector is stored here. When the board is closed, that interrupt vector is replaced.
ports[4]: This array holds pointers to the four ports that can be attached to this board. Note that for other types of boards, this array could be 8 or 16 elements long.
port_count: This value indicates how many elements of the ports[] array are presently installed.

The Code

There are only eight routines in the COM.C file, and they implement a complete interface to a multiport board. In this case, the code was written to support a Stargate Technologies Plus-8 board, but it only requires changing a line or two in the interrupt service routine to support other boards. The routines are described below:

board_open( address, number ) :

This routine is called to perform the logical connection between a multiport board and an interrupt line on the PC. The address parameter is the address of the status register on the board. The number is the interrupt number to be used on the PC. For the first 8 interrupts on the ISA bus, the interrupt number is 8+IRQn, so IRQ3 and IRQ4 will have interrupt number 11 and 12. This routine initializes the BOARD data structure and returns a pointer to that structure. It saves the old interrupt vector, sets up a new interrupt vector, and enables interrupts on the specified line.

board_close( board ) :

This routine is called to shut down a given board, specified by the “board” parameter. board_close() takes care of disabling interrupts on the correct interrupt line, then restoring the interrupt vector that was in place before the board was opened. It also takes care of closing any of the ports attached to it that have not been closed with an explicit call to port_close().

port_open( board, address, match ) :

This routine is called to open a single port up, and attach it to the board it belongs with. The board parameter is a pointer to the BOARD structure, which should have already been opened. The address parameter is the base address of the UART, and the match parameter is used in the interrupt service routine to check for the status register bit indicating that this particular UART has an interrupt pending.

port_set( port, speed, parity, data, stopbits ) :

This routine is called after the port has been opened. It sets up the baud rate, parity, number of data bits, and number of stop bits for the port specified in the port parameter. After the parameters are set up for the port, receiver interrupts on that port are enabled.

port_close( port ) :

port_close is called to shut down the UART. All it does is disable interrupts on the selected UART, then free up the space that had been allocated for the port structure. The way the routines are presently written, board_close() will call this routine for each open port, so it doesn’t need to be called by the application.

port_putc( port, c ) :

This routine is analagous to the fputc() routine in the C standard i/o run time library. It is passed a character and a pointer to a port data structure. It sits in a loop and waits for the UART to have space available in its transmit holding register. When the UART is ready to send another character, this routine stuffs it into the holding register and returns.

port_getc( port ) :

port_getc() is analagous to the fgetc() routine in the standard i/o library. It doesn’t have to talk directly to the UART, since incoming characters are being stored in the circular buffer. It checks to see if there are any characters in the buffer, and extracts and returns one if there are. If no characters are available, an error code is returned.

interrupt_service_routine() :

The final routine found in COM.C is interrupt_service_routine(), which is not called by any routines outside of the COM.C file. It is the routine that is branched to when an interrupt is generated by the board. Its job is to service any and all interrupts pending on the multiport board. The way the routine determines which ports need servicing is by reading the status register. Once the status register has been read in, the ISR can loop through the list of open ports, looking for ports that have bits set in the status register.

The device dependent portion of the code is found in the interrupt service routine. The way the ISR decides whether to service a port on the Stargate board is by executing the following lines:

for ( i = 0 ; i < board.port_count ; i++ )
    if ( board.ports[i]->match & status_reg) {

The match value in each port structure has been set up in my sample program for the Stargate status register, where each port has a single bit in the status register. Other boards may expect the bit to be cleared for the given port. The Digiboard products don’t use a bit pattern in the status register, they actually put a single port number in the register. These differing cases would only require a minor change to if line of code in the ISR.

The Test Program

In order to test this interface code, I wrote a short program called MULTTERM. This program creates four windows on the screen which each have an independent terminal session taking place concurrently. The way I wrote the program allows the user to switch the keyboard between the four windows by pressing Function keys 1 through 4. Even though keyboard input can only be directed to one port at a time, all four ports are actively receiving input characters at all times, and displaying them on the screen as they arrive.

The program consists of four modules that can be compiled separately and linked together. There are also three header files. The files are listed below:

MULTTERM.C: This is the file that contains main(), and drives the test program. All of the other files contain the support and driver routines.
COM.C: This is the file that has all of the routines that interface to the multiport board.
KEYS.C: This contains the routine to get keys from the keyboard.
VIDEO.C: This contains a “quick and dirty” windowed interface to the display. It is hard coded for CGA/EGA/VGA displays.
COM.H: The function prototypes and structure definitions needed to use the communications functions.
VIDEO.H: The function prototypes and constants needed to use the display interface functions.
KEYS.H: Function prototypes and key definitions for the keyboard interface.

The operation of MULTTERM.C is relatively simple. The program consists of only three sections. In the first section, four non-overlapping windows are opened up. In the second section of code, the board and the four ports are opened up. My test board was set up with a status register at address 0×580, and the UARTs spaced 8 addresses apart starting at 0×180. The Stargate Plus board uses a single bit in the status register to indicate that a port needs servicing, so the port parameters look like this:

Status Register Bit
UART Base Address

Finally, after the ports are opened, the main terminal emulation loop is entered. This main loop has two sections. In the first section, the input buffers for all four ports are emptied out, with all the characters being written out to the appropriate display window. Once this is done, the second section is entered. This is where the keyboard is read. An Escape key causes an immediate exit, a Function key causes a switch to a particular window, and any other key is output to the appropriate communications port.

To test the operation of this system, I ran it on a standard 10Mhz XT, and connected it to Xenix system on four different ports. A sample screen capture is shown here:

|                                    ||23%                                 |
|17%                                 ||23% w -t                            |
|17% tty                             ||  3:24pm  up 1 day, 21:08,  5 users,|
|/dev/ttyi1a                         || load average: 0.00, 0.00, 0.00     |
|18% cat > /dev/ttyi1b               ||24%                                 |
|Message...                          ||24% tty                             |
|Message line two...                 ||/dev/ttyi1b                         |
|19%                                 ||25% Message...                      |
|                                    ||Message line two...                 |
|                                    ||                                    |
|Message  1:                         ||                                    |
|From nelson Sun Mar 18 15:22:48 1990||4% mail nelson                      |
|To: nelson                          ||Subject: Stargate                   |
|Subject: Stargate                   ||This message is being sent          |
|Date: Sun Mar 18 15:22:48 1990      ||through a Stargate board.           |
|                                    ||(end of message)                    |
|This message is being sent          ||5%                                  |
|through a Stargate board.           ||                                    |
|                                    ||                                    |
|d                                   ||                                    |


The sample program was built using the following QuickC 2.0 command line:


If you are using Borland’s Turbo C 2.0, use this line:


The only differences between the two versions of the program are the names of the routines to get and set interrupt vectors, and the routines to input and output bytes to I/O ports. These differences are handled by #define statements in COM.H. Note that this program can be ported easily to any MS- DOS compiler that supports interrupt service routines written in C.

The test program operated fine on the XT, although I didn’t subject it to heavy data traffic. It was able to receive data simultaneously on all four windows without losing any characters. However, if I want to keep up with continuous data on multiple ports I would either need to upgrade to a faster machine or optimize both my Com port and display I/O routines.

Software Improvements

If you wanted to develop the routines shown here into a production quality library, there are quite a few areas that would benefit from attention. Most of the refinements can be done one at a time, allowing for a steady evolutionary improvement in your library. Some of the most beneficial improvements would be:

  • Allowing for adjustable buffer sizes. 256 bytes may prove to be far to small for some applications.
  • Interrupt driven transmission, as well as reception. This requires modification of the ISR to check for the type of interrupt.
  • Hardware and software handshaking, including XON/XOFF, RTS/CTS, and DTR/DSR.
  • Support of more than one board operating at a time.
  • Hand coding of the interrupt service routine in assembly language for maximum optimization.
  • Break detection on the incoming line.
  • Adding lines to check the status of the incoming modem status lines.
  • Detection of parity errors, overrun errors, etc.
  • Protection from uncontrolled exit of your program without disabling interrupts.

Hardware Sources

There are quite a few different companies making multiport boards for the PC bus family of machines. A few of the names and phone numbers of companies whose hardware I have used are shown below. The biggest demand for these types of boards has in the past come from users of multitasking operating systems, so the manufacturers have tended to advertise in Unix oriented magazines. If you want to do more research, this would be a good place to look. This is not intended to be a comprehensive list, and any time you spend doing research in area will probably be worthwhile.

Commtech, Inc.
Wichita, KS

Contec U.S.A
San Jose, CA

Digiboard, Inc.
St. Louis Park, MN

Norcross, GA

Quatech, Inc.
Akron, Ohio

Stargate Technologies, Inc.
Solon, Ohio
Software Sources

It is relatively easy to go into business selling C libraries. All you need is a few months of time to develop a product, and a little seed money for advertising. This fact, combined with the high demand for quality libraries, has resulted in there being at least a dozen companies selling C libraries for serial communications. With this many entrants in the market, a “features war” has developed, resulting in a good buyers market. For less than $300, you can buy libraries that have an enormous amount of functionality. Most of these library vendors have ads in the pages of this magazine, so the research job is relatively simple. Armed with the list of functions you need, you should be able to estimate the amount of effort required to write your own code. Given this, you are ready to make a build or buy decision.

In addition to the commercial market, there are a few public domain and shareware serial communications libraries available. However, I am not aware of any that offer support for multiport boards. Of course, it could be practical to modify one of these libraries to suit your particular needs.


In order to use more than two RS-232 ports on a PC, you need to purchase a multiport board. While this does mean you have to either build or buy some non-standard device interface software, this can be done without too much new code. So if you have an application that demands communication with multiple source of information in the real world, this can provide an economical means of accomplishing your goal.