Steve Rothkin is a member of the TelDEAR research and development staff at Granada Systems Design where he has written several communications drivers and emulations. He holds a BS in computer science from Stevens Institute of Technology. He can often be found roaming the Tele-comm echo on Fidonet.
Most IBM compatible PCs currently in use have at least one (if not more) serial ports for data communications. At the heart of the standard serial port is the National Semiconductor 8250 Universal Asynchronous Receiver Transmitter (UART). More recent systems have replaced the 8250 with a faster NS16450 or NS16550 UART, both of which are backwards compatible with the 8250 (the NS16550 also adds 16 byte transmit and receive FIFO buffers to aid in higher speed communications).
Although DOS includes standard drivers for COM1 and COM2, these are not adequate for high speed communications, and will not recognize additional serial ports. This article presents an interrupt driven library that will allow you to handle communication via one or more serial ports simultaneously, up to the maximum speed of the UART/computer (the NS16450 and 16550 can handle speeds up to 115,200 bps, but many slower computers may not be able to service interrupts fast enough to keep up). The library includes a simple API (Application Programming Interface) to allow your program to use it.
Overall Design
The main interrupt driver is designed with emphasis on the execution speed. This is necessary to prevent data overruns, and to allow other system critical functions (such as timer interrupts) to continue to run. Speed is not such an overriding concern for the remainder of the API, but it is still designed with an eye toward efficiency.The basic operation of the library revolves around two circular buffers for each port, one for transmitting data, and the other for receiving data. The size of these buffers (in characters) is controlled by the macro BUFFER_SIZE (see Listing 4, UART.H) which I have set to 2048. The transmit buffer has one byte per character, while the receive buffer has two bytes per character (the low byte is the actual data character received, and the high byte is a bitmapped flag indicating the presence/absence of several error conditions for each received character). A head pointer marks the current start of each buffer, and a tail pointer marks the current end of each buffer.
The driver and API functions use an array of port information structures (struct t_port_info, one entry per port) which is defined in UART.H. This structure contains the circular buffers for the port and some status information for the port. It also contains some configuration information that must be set prior to initializing the driver.
UART Initialization
The function Init_UART (see Listing 1, UARTAPI.C) must be called to make the driver and API functional. Prior to calling Init_UART, the main program must allocate an array of port info structures (gp_port_info) and set the communications parameters in each entry (base I/O address, baud rate, data bits, stop bits, parity, RTS/CTS flow control). The number of ports to be handled (g_num_ports) and the interrupt request number (g_uart_irq) must also be set. (An example of how to set all this up can be found in Listing 6, TERM.C, and is discussed at the end of this article).Init_UART attempts to initialize each port described in gp_port_info one at a time. Before actually initializing the UART, the function attempts to verify the port's existence. This is done using an undocumented, but reliable (and useful) trick: if the THRE interrupt (see Sidebar 1 Explanation Of UART Registers) is first disabled, and then reenabled, the UART will generate a THRE interrupt, immediately if the THR is currently empty, or when the character is finished transmitting if it is not. I call this technique 'bouncing THRE'. (Remember it. I use it later to tell the interrupt driver to start transmitting data.) To verify the presence of a UART at a given base I/O address, and on a given IRQ line, I set up a dummy interrupt handler (see Listing 2, TESTASY.C and Listing 5, UARTMACS.C) to catch the interrupt from the UART, and bounce THRE. When this dummy handler is called, it checks the status registers on the UART to see if a THRE interrupt is being generated, and if the THR is actually empty. If THR is empty, the function sets a flag. If this flag is not set within three seconds, then the check fails.
Note: If a properly functioning UART is located at the indicated base I/O address and on the designated IRQ, this check will definitely succeed. It will fail if the port is at the designated I/O address, but on a different IRQ. If another device (other than a UART) is located at I/O addresses being written for the test, it is remotely possible for the check to erroneously succeed (depending on the device's characteristics); crashing the system by writing incorrect values to that device is also possible.
Once the verification of the UART's presence has been completed, it is safe to set the UART's operating parameters. The UART uses a 16-bit divisor to generate the data rate (from its hardware clock input signal). A lookup table translates the baud rate (supplied by the main program) to an appropriate divisor value, which is then written to the UART. The parity, stop bits, and data bits settings are combined into a bitmapped word and written to the UART's line control register (see Sidebar 1, Explanation Of UART Registers, for further explanation). The OUT2 signal pin serves as an interrupt mask on IBM-compatible serial adapters it must be activated (via the MCR) for interrupts to be generated (if you do not do this, you will not get interrupts from the UART even though they are enabled through the IER).
The presence of a FIFO buffer (NS16550) is automatically detected by enabling the FIFO, and then checking bits in the IER to see if it was actually enabled. On older, non-FIFO UARTs, this sequence will do nothing, and the bits will not be set. On UARTs with the FIFO, I am programming the UART to issue a data received interrupt only when there are at least eight characters in the receive FIFO (if more time than is needed for one character to be received elapses without receiving any data, the UART will issue a timeout interrupt, which when masked in the interrupt driver, looks like an ordinary receive data interrupt). This reduces the frequency of receive-data interrupts to the driver, and gives it breathing space equivalent to eight character times before it must read the received data. I also set a variable indicating that the driver will transmit up to 16 characters on each THRE interrupt (the size of the FIFO).
If RTS/CTS handshaking (discussed later as part of data transmission) is not enabled, the RTS signal is raised at this time since some modems require the presence of this signal to transmit data.
Once this base initialization has been completed, I proceed to latch the interrupt driver into the vector table, saving the original interrupt vector so that it can be restored later. Then, I enable recognition of the interrupt by the CPU by setting the appropriate bit on the 8259 interrupt controller chip. I register the function Exit_UART to be called at program exit to guarantee that the UARTs will be turned off and the interrupt vectors restored. If this were not done, the interrupt driver would continue to execute even after the program had exited, and loading other programs could cause the system to crash.
Having setup my routine to catch interrupts from the UARTs, I now enable each UART to generate all of the types of interrupts. After doing this, it is necessary to read the IER, RBR, MSR, and LSR registers to clear any interrupt pending conditions that may have existed prior to setting up our interrupt handler. I also keep flags in the port information entry that track the modem's CTS, DSR, and DCD signals. These are loaded from the LSR at this time. I flush the transmit and receive FIFOs to clear any extraneous data, and raise the DTR modem signal to indicate to any attached device that the driver is ready to carry on data communication on this port.
Processing Interrupts From The UART
Listing 3 (UARTLOW.C) contains the interrupt driver. When the UART raises an interrupt, it sets bits in the IIR to indicate what type of interrupt it is raising (and if it is indeed raising an interrupt). Since there may be multiple UARTs hanging on the same interrupt, anytime an interrupt is received the driver must check all of the UARTs to see if any of them have generated interrupts. This must be done in a round-robin fashion to guarantee that all UARTs are processed in a timely fashion even at high data rates. If one UART has an interrupt serviced, then it is necessary to make a repeat pass through all of the UARTs. The interrupt routine can safely exit only after it has made one complete pass over all of the UARTs without detecting any interrupts pending.Note: Because of this design, the driver can easily handle UARTs on multiple interrupt levels any of the interrupts will cause all UARTs to be checked. To handle multiple interrupt levels, Init_UART and Exit_UART have to be modified to latch the driver into all of the desired IRQs. To use multiple IRQs between 3 and 7, this would be the only change required. To Use IRQs 2 or 8-15, the driver has to be modified to properly reset both 8259 interrupt controllers (not a trivial task if it is not known which controller has generated the interrupt!). Use of IRQs 0 and 1 is not recommended because of the system tasks they are normally reserved for.
On NS16550 UARTs (with FIFO), a timeout interrupt is generated when there is data in the receive FIFO, and more than one character time has elapsed without receiving data. I always mask the IIR value in such a way as to make this interrupt look like an ordinary receive data interrupt.
Monitoring Modem Status Changes
The UART constantly monitors four key signals from the modem DCD, DSR, RI, and CTS. When any of these change, a modem status interrupt is generated. Reading the MSR identifies which of these signals have changed, and clears the interrupt condition. The lower four bits of MSR indicate which signals have changed, the upper four bits give the actual signal levels. Multiple signals can change at the same time so all four signals must be checked!For changes in DCD, DSR, and CTS, the corresponding flag in the port information entry is updated so the main program can know these signal states. In addition, if the CTS signal comes on, I bounce THRE so that transmission can begin if RTS/CTS handshaking is enabled (if RTS/CTS handshaking is not enabled, bouncing THRE at this point has no effect). If the RI signal comes on, a ring indication is placed in the circular buffer.
Receiving Data
When a receive-data interrupt occurs, I check the LSR to verify that data is actually available. This is also necessary to properly clear the receive-data interrupt. If the LSR indicates that data is not available, I continue on to the next UART without reading the RBR.Reading the LSR and RBR clears the receive interrupt. I then add the received character to the circular receive buffer. If the circular buffer is already full, the buffer overrun flag for the character (remember the receive circular buffer is composed of paired words, the low bytes are the actual data and the high bytes are bitmapped flags) is set, and the oldest character in the buffer is lost.
Making effective use of the receive FIFO on NS16550 UARTs requires multiple characters to be read off of a single receive-data-ready interrupt. This is handled by checking the LSR to see if data is ready any time the IIR indicates that no interrupt is pending. If data is ready, I jump to the read data section of the code (even though there is not an interrupt). A counter is kept of the number of times this is done for each port. This counter is reset whenever an actual receive data interrupt occurs. If this counter exceeds a predefined threshold (currently set at 12), the driver will not check for data until the receive interrupt occurs. This is necessary to guarantee that the driver gives control of the CPU back to the main program for at least a few moments. The way this is coded, it will still operate correctly on older UARTs that don't have a FIFO.
Transmitting Data
Whenever the UART is ready to transmit another character (or when THRE is bounced), a THRE interrupt is generated. Upon receipt of this interrupt, I check to see if there is any data to transmit. If there is no data to transmit and RTS/CTS handshaking is enabled, then I drop the RTS signal to indicate to the modem that I don't need to transmit anymore. If there is data to transmit, RTS/CTS handshaking is enabled, and CTS is not on, I raise the RTS signal to the modem, and exit (waiting for the CTS signal to come on before trying again).Before actually transmitting data, I check the LSR to verify that the TBR is actually empty (if not, then I don't transmit). If it is empty, I load as many characters into the TBR as indicated by the variable set in Init_UART (16 characters for FIFO chips since the THRE interrupt will only come when the FIFO is empty, one otherwise) or until there is nothing left to transmit.
Processing Modem Errors
The UART generates receiver-line-status interrupts for framing, parity, and overrun errors, and for the start of a break signal. Reading the LSR clears the interrupt condition, and tells the driver which conditions are present. As with the modem status interrupts, multiple conditons can be present at the same time so all four must be checked! I also read the RBR to clear the character associated with the condition. Not doing so may inhibit future interrupt generation. This character is then added to the receive circular buffer along with flags indicating which error conditions were present.
Shutdown
As mentioned earlier, Init_UART adds the shutdown routine Exit_UART to the list of routines to be executed at program termination, causing Exit_UART to be called automatically when the main program calls exit().This function restores the interrupt vector table and 8259 interrupt controller to their states prior to UART initialization. For each UART it disables interrupt generation, flushes the FIFOs, and drops the RTS, DTR, and OUT2 signals. This allows for an orderly shutdown of the interrupt driver and notifies any attached devices that the UARTs are no longer prepared to communicate.
API Send And Receive Functions
For demonstration purposes, I have written very simple API functions to send and receive characters.Send_char adds one character to the transmit circular buffer, and bounces THRE to cause the driver to start transmitting. Note: To make effective use of the transmit FIFO, a function that would add a block of characters to the circular buffer before bouncing THRE would be desirable.
Read_char returns the next character from the circular buffer (including the bitmapped flag). It returns -2 if no data is available. Since the high bit of the flags is never used, the calling program can assume that a return code < 0 indicates that nothing was available on the circular buffer.
Simple Terminal Emulation Program
TERM.C (Listing 6) is a simple terminal emulation program that demonstrates the use of the API library that I have presented. It initializes COM1 to 2400bps, eight data bits, no parity, one stop bit, and RTS/CTS handshaking. It can be modified to use different ports, different parameters, or even to use multiple ports (which the driver and API support).After initializing the UARTs / API, TERM sits in a loop alternating between checking for received characters on the current terminal (which are displayed to the screen) and checking the keyboard. Normal keys cause the corresponding characters to be transmitted (and displayed if local echo is enabled). Function keys allow you to display help, sequence through the available UARTs, view the current port's status, toggle local echoing of transmitted data, and exit the program.
Wrapping Up
Through this article, I have described the general operation of the UART and presented a C library to drive multiple UARTs. I have included support for all of the typical UART functions (although I have omitted description of a small handful of out-of-the-way features that wouldn't normally be used). Although considerable space has been devoted to this endeavor, you will see that programming the UART is fairly straightforward. This library provides building blocks that you can use to implement more sophisticated communications software and protocols (in my view this is where the challenge lies). In addition, this library actually works, and is living proof that device drivers can be written in C.
Sidebar: Compiling Program With Microsoft C Version 5.1
Use Of I/O Delays