Servicing COM Port Interrupts
|
This article first appeared in the
May, 1990 issue of Tech Specialist |
The COM ports on an IBM compatible PC provide MS-DOS users with an extremely versatile avenue for communications with other devices. Yet experienced PC programmers find that the operating system and their compiler libraries offer them with very little in the way of useful tools for using these COM ports. Fortunately, C compiler technology has advanced to the point where it is now practical to implement an interrupt driven communications system entirely in C. This article discusses some of the techniques for accomplishing this.
A Brief History of COM
The original design for the IBM PC included provisions in the hardware and the BIOS for the support of RS-232 communications ports. The BIOS supports up to four COM ports, but both MS-DOS and the hardware design reduced the number of ports that could practically be used to two. These ports are referred to by their MS-DOS device names, COM1 and COM2. The designers chose the 8250 series of UARTs from National Semiconductor, and implemented a single port communications card that supported the following items:
- Interrupt or polled input and output.
- Modem control output lines RTS, DTR.
- Modem control input lines CTS, DSR, RI and CD.
- Baud rates up to 115200 bps.
Unfortunately the BIOS implementation for the COM ports restricted these capabilities somewhat. The BIOS functions for the COM port do not support interrupt driven I/O, and only allow eight discrete baud rate settings which range up to 9600 bps. MS-DOS uses the BIOS for all of its COM port functions, so the BIOS provides the limits on what functions programmers have available using the operating system.
Using the COM port for output-only applications, such as for spooling to a printer, works fairly well using this built-in support. The application operates in a somewhat inefficient mode, since it is effectively idled for the duration of its output, but it is able to get maximum throughput out of the port, even at higher baud rates.
But when MS-DOS programmers try to use the COM ports for applications that require both input and output, they are quickly stymied. Even at rates as low as 1200 baud, polled mode software has to sample the input port at rates that create incredible scheduling problems within programs. At the highest BIOS supported rate of 9600 baud, a program would have to sample the UART once every millisecond without fail. This is essentially impossible.
This leaves the programmer with only one option for programs that need to make full use of the COM port: using interrupt driven communications. This allows the program to go off and do some work while characters are being read into a buffer and/or being pulled out of a buffer and passed through the UART. The idea behind this sounds attractive, but most programmers feel that implementing this type of code is extremely difficult.
At one time this was certainly true, but advances in C compiler implementation have reduced the level of effort needed to solve this problem. Unlike four or five years ago, it is now relatively easy to implement an interrupt service routine that manages the UART without ever having to leave the comfort of your C compiler. This is a marked change from the days where this type of code had to be developed from assembly language. This article will demonstrate how to write this type of code using both Microsoft C & Borland’s Turbo C.
The UART
Writing device interface code requires that you understand the hardware. In this case, the hardware to interface with is the 8250/16450/16550 family. The original PC was equipped with 8250 UARTs. The AT began using the 16450 UARTs, and some of the newest machines are now being equipped with 16550s. The two newer members of this UART family are completely downward compatible with the 8250, so code written for it will run without modification on newer machines.
The 8250 has 11 internal registers that the programmer can access. On the IBM compatible PC,
these registers are laid out on the I/O bus, rather than being mapped into memory. This means
that the C programmer needs to access the registers using the inp()
and outp()
library routines.
The register definitions are accessed through eight consecutive port locations. The function of
each of the 11 registers is discussed in detail in this section.the following offsets and
conventional mnemonics
RBR: Receiver Buffer Register (Offset 0)
This is the holding register that receives a character after the UART has finished reading it in from the RS-232 line. Since the UART is double buffered, the receive can be working on reading in the next character while at the same time waiting for the CPU to read the character out of the RBR. This give the programmer approximately one character time to respond the character being read in. This is a read-only register.
THR: Transmit Holding Register (Offset 0)
This is a write-only register that accepts a character to be sent out of the UART. As soon as the UART transmitter is available, the character is moved out of the holding register and into the transmitter and starts going out on the RS-232 line.
IER: Interrupt Enable Register (Offset 1)
This register has four bits that individually enable each of the four different types of interrupts that the 8250 family can generate. The bit masks and the interrupt types are:
1 | The Received Data Available interrupt. This interrupt is generated when a character is transferred out of the receiver and into the receiver buffer, where the CPU can then access it. |
2 | The Transmit Holding Register Empty interrupt. When this interrupt is enabled, an interrupt is generated whenever the Transmit Holding Register is empty. This generally occurs when a new character is moved out of the THR and into the transmitter. |
4 | The Receiver Line Status interrupt. Setting this bit will cause an interrupt to be generated whenever an abnormal line condition occurs. These conditions are read out of the Line Status Register, and are: Overrun Error, Parity Error, Framing Error, and Break Interrupt. While the sample program in this article doesn’t use this interrupt, it is frequently used when these conditions need close monitoring. |
8 | The Modem Status interrupt. Setting this bit will cause an interrupt to be generated whenever one of the four incoming RS-232 Modem Status Lines changes. They are CD, RI, CTS, and DSR. This program doesn’t use this interrupt, but it is useful when using CTS or DSR for handshaking. |
IIR: Interrupt Identification Register (Offset 2)
When an interrupt occurs, the Interrupt Service Routine (ISR) reads this register to determine what caused the interrupt. The register contains one of five values, four for the four different interrupt types, with one more for “no interrupt pending”. The values read out of the IIR are:
1 | No interrupt pending. |
0 | A modem status change interrupt. This means the state of CTS, DSR, RI or CD has changed. |
2 | Transmit Holding Register Empty. This means that a new character can be written to the Transmit Holding Register. |
4 | Received Data Available. A new character has been read in by the UART, and can be read from the Receiver Buffer Register. |
6 | A Line Status Interrupt has taken place. This means an overrun error, Parity Error, Framing Error, or Break Signal has occurred. |
LCR: The Line Control Register (Offset 3)
This register has control over how the UART sends and receives characters. All eight bits of this register are used, with the following definitions and bit masks:
0-3 | These four bits set the word length for input and output. 0=5 bits, 1=6 bits, 2=7 bits, and 3=8 bits. |
4 | If set, two stop bits are generated for each output byte, otherwise one stop bit is generated. |
8 | If set, Parity bits are both checked and generated. If clear, no parity bits are added to output bytes, and none are checked. |
16 | If parity is on, setting this bit selects even parity, clearing selects odd parity. |
32 | If this bit is set, parity is stuck at either a 1 or a 0, depending on the setting of the previous bit, mask 16. If it is clear (as it is normally), parity of output is selected by bits 3 and 4. |
64 | In order to output a break, this bit can be set. As long as it is held, the UART will be kept in a spacing condition, which causes the remote end to receive a break. |
128 | This is the Divisor Latch Access bit. Setting it disables the normal states of registers 0 and 1 and converts them to Divisor Latch registers. This bit needs to be set in order to set or change the baud rate. |
MCR: The Modem Control Register (Offset 4)
The Modem Control Register has five bits that can be set or cleared by a program. They are listed here, along with the bit masks used to set or clear the bits:
1 | Data Terminal Ready is controlled by this bit. The sample program shown here asserts DTR when the port is opened, and leaves it there as long as the port is open. |
2 | Request To Send is controlled by this bit. The terminal emulation program included with this article leaves RTS on as long as the port is opened, but this line is frequently used as a handshaking line when performing RTS/CTS handshaking. In that case it will be dropped low when the input buffer passes a certain high water mark. |
4 | OUT1 is controlled by this bit. OUT1 has no function on the IBM PC COM cards. |
8 | OUT2 is controlled by this bit. On most IBM compatible COM cards, OUT2 is used to gate the interrupt line to the PC bus. If OUT2 is not asserted, no interrupts can occur. So all the code in this article keeps OUT2 set. |
16 | This is the loopback bit. On 8250 UARTs this can be used to loopback all data written to the UART, allowing for on-line diagnostics without a loopback connector. Because of a slight change in the design of the part, loopback tests usually won’t work properly on the 16450. During loopback the 16450 will drop the OUT2 line, effectively turning interrupts off. |
LSR: The Line Status Register (Offset 5)
The Line Status Register has seven bits that contain various status indicators relating to the actual transmission of data. The error bits and the break indicator can be read directly from this register. They will also generate an interrupt when they are set, so they can be read from inside an interrupt service routine to eliminate the need to poll this register. The bits and masks that can be set in this register are:
1 | Data Ready. This bit indicates that there is a byte ready to be read out of the Receiver Buffer Register. In interrupt mode, this bit is of no interest. When in polled mode, this bit is monitored until set, when a character can be read. |
2 | Overrun Error. This bit indicates that a character came in before the last one was read out of the Receiver Buffer. This means the receiver is not being serviced rapidly enough, which can occur for many different reasons. If this bit is set it means that at least one input character was lost. |
4 | Parity Error. This bit indicates that a character came in with a different parity set than what was selected in the Line Control Register. If no parity is being used, this bit will never be set. |
8 | Framing Error. When the receiver is clocking a new character in, it expects to see at least one stop bit after the end of the character. The stop bit provides spacing between characters. If the stop bit is not present, a Framing Error is generated. Framing errors frequently mean either a noisy line or mismatched baud rates. |
16 | Break Interrupt. This line is set when the receiver detects a break. A break is indicated if the RS-232 input line is held low for longer than an entire character time, including the time allocated for a start bit and stop bit. |
32 | Transmitter Holding Register Empty. When operating in polled mode, the CPU waits for this bit to be set before writing another character to the THR. When transmitting in interrupt mode, this bit is not generally useful. |
64 | Transmitter Empty. When this bit is set, it means that not only is the THR empty, but the transmitter is empty as well. This means both buffers in the double buffered UART are empty. One of the times it could be useful to watch this bit is when a process needs to wait until it can be reliably sure that all characters have actually been transmitted before performing some action. |
MSR: The Modem Status Register (Offset 6)
For each of the incoming four modem status lines, there are two bits in this register. One of the bits is used to detect the state of the line. This line can be polled at any time to determine what the state of a particular line is. The other bit is used when a Modem Status interrupt occurs. This bit, called the delta bit, indicates which lines have changed state, causing the interrupt. If modem status interrupts are being used, the delta bits can be examined to determine which line caused the interrupt. The bits in the MSR are defined as follows:
1 | Delta Clear To Send |
2 | Delta Data Set Ready |
4 | Trailing Edge Ring Indicator |
8 | Delta Carrier Detect |
16 | Clear To Send |
32 | Data Set Ready |
64 | Ring Indicator |
128 | Carrier Detect |
SCR: The Scratch Register (Offset 7)
There is a spare register in the 8250 that can be used as a storage area. It is generally not used for anything in the IBM PC compatible development environment.
DLL & DLM: Divisor Latch LSB and MSB (Offset 0 and 1, DLAB=1)
The Baud Rate at which the 8250 transmits and receives data is established by dividing a master clock by a 16 bit divider. The master clock on the IBM PC COM card is operating at an effective rate of 115,200 Hz. Thus, a divider of 12 yields a baud rate of 9600. Since the UART has an 8 bit wide data path, the divider has to be loaded in two registers. On the 8250, the least significant byte of the divisor is loaded into register 0, and the most significant byte is loaded into register 1.
The problem with these two registers is that the designers of the 8250 found that they came up a couple short on registers in their address map. They could either expand the address space to 10 registers, or play some tricks with they space they had. They opted for the tricks. In order to access the baud rate divisor registers, there is a bit in the Line Control Register that must be set. This bit, the Divisor Latch Access Bit, temporarily changes the function of registers 0 and 1 while it is set. So to load the baud rate, the code normally sets DLAB to 1, writes to register 0 and 1, then restores DLAB to its normal setting of 0.
Writing The Interface Code
Despite the fact that there are so many registers, control lines, bits and bytes to keep track of on this UART, it is still relatively easy to develop a fully featured communications interface to it. The sample program developed here only uses 6 routines, none of which take up more than a screenful of code.
The code presented here uses interrupts for both input and output. While this adds some complexity to the software, it should also increase the overall performance of a program by relieving the processor of the dead time incurred during polled output. It also makes the code much easier to modify for support of hardware handshaking, which requires the use of Modem Status Interrupts. An ISR supporting multiple interrupt types will also make it easy to add interrupt based detection of line errors to this code.
The interface to a device of any kind has five basic types of functions that are needed in order to operate the device:
- An open function, to establish a link with the device.
- A control function, to manipulate device parameters.
- An input function, to receive data from the device.
- An output function, to send data to the device.
- A close function, to break the link with the device.
In a comprehensive library, there will be multiple functions in many of the five categories, but the basic five provide a platform on which to start building.
The Port Structure
Before looking at the interface functions to the COM port, it is a good idea to understand what
is stored in the PORT
structure. The PORT
structure is analogous to the FILE
structure used by
the C standard I/O library. When a COM port is opened, the PORT
structure is returned, and all
future access to the COM port is done through the structure. The definition of the PORT
structure, found in Listing 2, COM.H
is:
The buffer structure is defined previously in the COM.H
file as:
Once a COM port is opened, the information in the PORT
structure can be used to perform any of
the functions needed to manipulate it. The structure members have the following definitions:
old_vector : |
This is the interrupt vector that was in place for the port when it was first opened. This is stored in the structure so that it can be restored when the port is closed. |
uart_base : |
This is the base address of the UART on the I/O bus, i.e. the first register. |
irq_mask : |
This is the bit mask that is applied to the 8259 interrupt controller in order to enable or disable interrupts from the device. Turning off interrupts at the 8259 doesn’t actually disable interrupts from the UART, it just ensures that the system will never respond to them. |
interrupt_number : |
This is the software interrupt number for the UART. The first eight interrupts on the PC have software interrupts 8-15. IRQ3 and IRQ4 correspond to interrupt 11 and 12, and this is where that COM ports will typically go. |
in : |
This is the input buffer structure. It is where characters that are received by the UART go. The application program will pull characters out of this buffer using the port_getc() command. |
out : |
This is the output buffer structure. It is where characters go that are going to be transmitted by the UART. The UART takes characters out of this buffer in the Interrupt Service Routine. |
The structure of the input and output buffers in this program is fairly simple. They are
implemented as 256 byte circular buffers with each one having a read index and a write index. For
both the input and the output, only one process is allowed to modify the write_index
, and only
one process is allowed to modify the read_index
. This simplifies the code, since interrupts do
not have to be disabled during the routines that insert and delete characters from the buffer.
For example, on the input buffer, the port_getc()
routine is allowed to increment the read_index
when extracting a character, and the interrupt service routine is allowed to increment the
write_index
when inserting a character. An additional simplifying factor with these buffers is
their size, 256 bytes. This means that the indices for the buffers can be one byte, and the
increment procedure will inherently roll the pointers over when they reach their maximum. In point
of fact, most applications will find 256 bytes to be too small for both the input and output
buffers, and will have to increase the size. The code for managing the indices in this case will
also get a little larger.
The Open Function
The open function needs to establish a logical link with a particular device. In the case of a COM port on a PC, the minimum amount of information to establish the connection would be the COM port number, 1 or 2. In order to provide a little more versatility, I had my routine require as parameters the UART base address and the CPU interrupt number. The function prototype is:
This requires an extra parameter beyond what would be needed for just using COM1 or COM2, but it
does allow the routine the flexibility to deal with non-standard COM port implementations. There
are quite a few variations of COM3 and COM4 that are circulating about, and this routine should
be able to handle all of them. Note that like its counterpart in the C standard I/O library,
fopen()
, port_open()
returns a pointer to a structure. The PORT
structure contains all of the
information needed for future operations on the port.
The actual code for port_open is shown in Listing 3. The space for the PORT
structure is
allocated with a call to malloc()
. The input and output buffer pointers are both initialized.
The UART address and interrupt parameters are initialized. The existing UART interrupt vector is
stored, and a new one is established. The new interrupt vector points to the interrupt handler in
COM.C
. Finally, the 8259 Interrupt Controller on the PC bus is told to start accepting
interrupts from the UART. Note that the UART won’t start generating interrupts yet, because it
has not had its interrupts enabled yet. It would be unwise to enable interrupts before setting
the rest of the operating parameters.
There are two lines in the port_open routine that store the DOS CTRL-BREAK vector off and establish our own private interrupt handler for CTRL-BREAK processing. This is done so that the user can’t break out of the program without giving the code a chance to turn off interrupts and restore the old interrupt vectors. Note that in a communications system that was supporting multiple ports simultaneously, this processing would only be done when the first port was opened, so there would need to be a flag set once the CTRL-BREAK vector was subverted to insure that it only happened once.
The Control Procedure(s)
After a program has opened a COM port, it needs to set the baud rate, number of bits, parity,
etc. for the port. Depending on the number of features available in the package, there may be
many control procedures. Some may act directly on the UART, others will operate on the data
structure associated with the UART. My implementation of a control procedure is a single
function, called port_set()
, which allows the caller to set the baud rate, parity, number of
data bits, and number of stop bits for a COM port. The parameters are the same and in the same
order as the DOS MODE command. The prototype for the port_set() procedure is:
The code for this routine, shown in Listing 3, accomplishes this in a straightforward manner, by updating all the relevant registers in the UART. Before any changes can occur, all interrupts in the UART are disabled. At that point, the registers are all updated. Note that it is imperative to have interrupts disabled while the baud rate is being changed. If an interrupt occurred while DLAB was set to 1, the interrupt service routine would think it was accessing the Receive or Transmit data buffers, when in fact it was modifying the divisor registers.
This routine also turns on DTR, RTS, and OUT2. OUT2 has to be enabled on the IBM PC COM card in order for interrupts to occur. A complete package would allow the user some control over DTR and RTS, but adding those functions to this package should be straightforward. Finally, after all is ready, this routine turns Receive interrupts back on. At that point, any incoming characters will be received by the Interrupt Service Routine and inserted into the input buffer.
The Input Function
Input and output functions take on quite a few different forms, and it is unlikely that any
single function would do. However, I have implemented just a single function here, port_getc()
,
which attempts to get a single character from the input buffer for the port. Understanding how
this routine works would allow you to easily build other functions, such as port_get_buffer()
,
port_getc_with_timeout()
, port_get_string()
, etc.
The input function here, port_getc()
, receives just a single parameter, which is a pointer to the
port data structure. It doesn’t talk to the UART at all. Rather, it just looks to see if there
are any characters in the input buffer, and extracts one if there is. It returns an error
immediately if there are no characters available. An alternative way to implement this function
would be to wait either for a fixed amount of time or indefinitely if no characters are available.
The Output Function
The output function for the sample communications package is called port_putc()
. Its prototype is:
This routine has to first check to see if the output buffer is already full. If it is, an error is returned. (Note that it would be a simple matter to modify the routine so it just waited for there to be room in the buffer). In the event that there is room in the buffer, the single character is inserted. Note that the character must be placed in the buffer before the index is incremented. When writing code like this, you have to consider the ramifications of an interrupt occurring at any point in any operation during the routine.
This routine differs from the port_getc()
routine in one respect: it has to actually communicate
directly with the UART. The reason for directly modifying the UART is that Transmit interrupts
are continually being enabled and disabled on the UART. When no characters are present in the
buffer, it is necessary to turn off the Transmit interrupt, otherwise it would be generated
continuously. This means that after a new character is inserted into the buffer, the port_putc()
routine needs to see if Transmit interrupts are already enabled. If they aren’t, this routine
enables them. This starts the chain of events that leads to a Transmit Interrupt occurring, and
the character that has been placed in the buffer being transmitted.
It is probably more common for programmers to implement this routine as a polled mode function.
The code both in this routine and in the Interrupt Service Routine is shortened somewhat when
this is done, at the expense of a certain amount of efficiency. A polled mode version of
port_putc()
would look like this:
The Close Function
A program that has enabled interrupts on the COM port has to be careful to restore the computer’s configuration back to “normal” before exiting the program. Failure to do so could leave interrupts enabled on the port. After the program exits, the interrupt service routine will no longer be in memory, so if an interrupt occurs, disaster will follow.
The close function here is called port_close()
. All it takes as an argument is a pointer to the
port structure:
The first thing port_close()
does is disable all interrupts on the UART. That by itself is enough
to restore things to a safe state, but it does a couple of more things in the interest of
cooperation with other programs. First, it disables the selected interrupt at the 8259 Interrupt
Controller, and then restores the interrupt vector that was in place before the program was
selected. Finally, it restores the system CTRL-BREAK handler. Note that if a program is written
to use multiple ports, you would only want to restore the system break handler when the last port
is closed, or the program exits.
When the port is closed, this code drops DTR and RTS. This should have the effect of letting any device attached to the PC know that the port is no longer active. For example, if a modem was attached to the COM port, and it had a call in progress, dropping DTR should cause it to hang up.
One way this port_close()
routine could be drastically improved would be by saving the settings
of the COM port when it was opened so it could be completely restored when closed. Right now, the
port is left in an arbitrary state on exit. If a previous program had the port set up with a
certain baud rate and parity settings, these routines would obliterate them. It is not too hard
to add the code to the port_open() routine to read in all the register settings of the UART
before it is opened. Then, those settings can be restored upon exit. This should include the
settings of the 8259 interrupt controller as well. This kind of attention to detail would be
mandatory if, for example, you were writing a COM program that would be a TSR.
The Interrupt Service Routine
The heart of the communications port interface code is the Interrupt Service Routine. This is where all the bytes get moved in and out of the UART. This is also the one part of the interface code that the application program never sees.
If you were going to write an interrupt service routine for a C program 3 or 4 years ago, you would have had to get out the Assembly Language reference books, install a copy of MASM, and start doing some low level bit twiddling. But today, both Microsoft and Borland have built into their compilers the function type modifier “interrupt”. If you declare a C procedure to be type interrupt, the compiler takes care of some important details for you. First of all, it saves all the registers, which a normal C routine doesn’t do. Secondly, it loads the DS segment register with a pointer to DGROUP, the default data segment. And in the exit code for the routine, it restores all the registers, and then returns with an “iret” instruction instead of the normal “ret”.
There are still quite a few precautions you have to take when writing an interrupt service routine in C. You are precluded from using most of the run time library, as the routines are generally not written to be re-entrant. You probably don’t want to call any DOS or BIOS functions, for the same reasons. And you need to be very careful about calling other C routines. For example, if the other C routines have stack overflow checking turned on, they will probably abort when called from inside an interrupt service routine.
With all that in mind, it is still possible to write a fully functional ISR in C. The ISR written for this routine is shown in Listing 3. The 8250 ISR is relatively simple. It sits in a loop, reading in the Interrupt Identification Register over and over, servicing each interrupt type as it appears. When it finally reads the IIR and sees that no more interrupts are pending, it exits the ISR.
The reason the 8250 ISR has to continually read the IIR until all interrupts have been dealt with is due to the way the PC hardware is configured. The 8259 interrupt controller on the PC is configured to be edge sensitive. It will only detect an interrupt on a device when the interrupt line makes an OFF to ON transition. If we were to exit our 8250 Interrupt Service Routine without servicing all of the interrupt types that were pending, the 8250 would keep its interrupt line ON, thinking that this would get the processor’s attention later on. However, the 8259 interrupt controller would assume that since the UART hadn’t brought the line down yet, it was still being serviced from the previous interrupt. So it is necessary to clear out all pending interrupt conditions on the device before exiting.
The Interrupt Service Routine has a switch
statement that examines the IID, and goes to one of
five cases upon reading the result. The first four are for the conventional interrupt sources. In
the case of Modem Status Register and Line Status Register interrupts, the ISR just reads the
appropriate register to clear out the interrupt. Since this program isn’t using those interrupts,
they should never be executed anyway.
The IIR_RECEIVE
case in the ISR is called when there is a new character in the receiver buffer.
The first the ISR does is read the character out of the RBR, which clears the interrupt source.
Then, if there is room in the input buffer, the character it received is queued up in the buffer.
A useful addition here would be to set an error flag when a character is received but can’t be
inserted into the buffer.
The IIR_TRANSMIT
case is branched to when the Transmit Holding Register is empty. This occurs
in one of two cases. When transmit interrupts are first enabled by port_putc()
, an interrupt
occurs immediately, since the THR is empty at that time. The other case is when the Transmitter
Register empties out, and the old character in the THR is moved into the transmitter. At this
point, the UART is ready for another character, and an interrupt is generated.
The IIR_TRANSMIT
code first checks to see if a character is ready in the output buffer. If one
is ready, it is pulled out of the buffer and written out to the THR. If no characters are in the
buffer, it is time to shut down transmitter interrupts, as they are no longer needed. The next
time port_putc()
is called, they will be started back up.
The final case in the IID switch
statement is for when there are no further interrupts pending.
In this case, the EOI indicator is written out to the 8259 interrupt controller so it knows that
the interrupt has been serviced.
One of the alternative ways to handle COM port I/O that was discussed in this article was the idea of only allowing receiver interrupts, and having the transmit done with polled I/O. If this is done, it will greatly simplify the interrupt service routine, since it can assume that every interrupt is a receiver interrupt. In that case, the ISR will look like this:
Putting It All Together
In order to test and demonstrate this code, I created a very short terminal program, which is
found in TERM.C
, in Listing 1. After opening the port and setting up the transmission
parameters, the program just sits in a loop polling the keyboard and COM port for input. Keyboard
characters are sent out through the COM port. Characters read in from the COM port are displayed
on the screen using putc()
. Hitting the Escape key shuts down the port and exits.
While this is not a very useful program, it does provide a good test bed for the program.
However, I was able to use it to verify the most important fact, which was that the interrupt
service routine was able to keep up with a steady stream of incoming data at 9600 baud on a
standard XT without missing any incoming characters. Using putc()
for output, the program wasn’t
able to output the characters to the screen fast enough to keep up, but I was able to get around
that by switching over to direct video access, and less frequent polling of the keyboard.
In order to build this program using QuickC, enter:
qcl /W3 /Ox term.c com.c
Using Turbo C, enter:
tcc -w term.c com.c
Enhancements
The communications interface presented here is more than adequate for many applications. And in the places where it isn’t adequate, it provides a framework for developing enhancements. Some enhancements that immediately come to mind are:
Optimization of the three most heavily used portions of the code: port_putc()
,
port_getc()
, and the interrupt service routine. All three are candidates for rewriting in
assembly code for speed. port_putc()
and port_getc()
are candidates to be rewritten as macros
so they can be called in line, saving some overhead.
Adding hardware and/or software flow control: XON/XOFF handshaking is frequently needed when performing terminal emulation, and RTS/CTS or DSR/DTR handshaking is useful when talking to a variety of devices such as printers and modems.
Modifying the code so it can simultaneously support multiple ports: The PORT
structure
has already encapsulated all of the data for a COM port, so this could be accomplished relatively
easily. The main thing that needs to be done is to provide special handling for things that only
need to be done once, like interception of the CTRL-BREAK function. The atexit()
function would
be a useful way to implement this.
Adding detection for incoming break characters, and other line status errors.
Adding support for the 16 byte receive and transmit FIFOs on the 16550 UARTS: This allows data to be moved in and out with far fewer interrupts occurring, resulting in much more efficient code.
This code can provide a useful added capability to your programming toolkit, and takes care of a noticeable gap in the MS-DOS library of device drivers. Best of all, the code is actually portable across several of the leading MS-DOS C compilers, and doesn’t require you to understand assembly language. So if you have an application that needs to take advantage of RS-232 communications, you can now take the plunge with full confidence that the project can be accomplished.