Vince Scott has a B.S.E.E.T. and is currently working for Sandia National Laboratories in Albuquerque, New Mexico. He has been programming in C for two years and also enjoys programming as a hobby.
Introduction
The 8250, a popular UART, can be found in millions of IBM PCs and compatibles. If you program on PC compatibles chances are that you will or already have encountered one of these devices. It may be that you need to talk with a printer, a plotter, a modem, or even another computer using serial communications. I found that getting the information to accomplish this task was not always as straightforward as I would have liked, so the code in this article evolved. It can be configured for both polled and interrupt I/O as well as access to all four COM ports at baud rates up to 115200 (if your PC can handle the speed).I designed this code to be readable and understandable instead of using the old one-line, hard-to-decode syntax that C does so well. This code is very powerful and can be modified to accomplish any serial communication task on the PC.
Implementation
The heart of this article can be found in Listing 1, sereal.h. It describes each of the ten user registers on the 8250. Each register can be accessed through seven port addresses relative to the COM port's base address. I will not try to cover every register in detail in this article. I will only refer to those that are important to supporting the functions in the other listings.
Parameter Settings
Listing 2 sets up the communication parameters: COM port, baud rate, parity, data bits, and stops bits. SetSerial must be called first to set up the global variable portbase as well as error check the parameter list. It calls three functions that are used to accomplish this task. Note that each of these functions return a --1 on error.SetPort passes the COM port parameter (COM1 through COM4) defined in serial.h and sets the global variable portbase.
SetBaud sets the baud rate. The baud parameter can be any value from 1 to 115200. The first line of code in this function flags for an invalid baud rate. If it is a valid rate then it is divided into 115,200L to create a divisor that is used by the 8250 to set the appropriate baud rate. The value 115200 is derived by dividing the reference clock frequency (1.8432 MHz) by 16. This value is called the master data clock. It is then divided by the desired baud rate value to create the divisor.
The next line of code reads the current value in the Line Control Register (LCR). The value is then bitwise ORed with 0x80 to set the Data Latch Address Bit (DLAB). Make a note here of the gimmick used to set the baud rate. This device only has seven port addresses, but accesses 10 registers. To access registers 8 (DLL) and 9 (DLH) you must first set the Data Latch Address Bit bit in the Line Control Register high and then access each register at an offset of 0 and 1, respectively, to the portbase address. Register 8 latches the low byte and register 9 latches the high byte of the divisor. Once this has been accomplished you will need to turn the Data Latch Address Bit off by writing the previous value stored in the variable Current_Value back out to the Line Control Register.
The last function, SetOthers, sets the parity bit, data bits, and stop bits parameters. The first few lines of code flag for invalid parameters. The rest of the code creates a byte value that will be written to the Line Control Register. You may want to study the bit patterns described in Listing 1 for the Line Control Register.
Interrupt Vectors
Listing 3 redirects the interrupt vector and sets the enable interrupt bits in the Modem Control Register (MCR) and the Interrupt Enable Register (IER). Init-Serial initializes the current portbase address for interrupt I/O. Setvects switches the port parameter to the correct interrupt vector. getvect saves the old vector value and setvect redirects the interrupt to the ReceiveData function.EnableInt enables the 8250 for interrupt communications. Lines one and two are used to set the General Purpose Output (GPO2) of the Modem Control Register. This enables the 8250 to interrupt your computer's CPU.
The next line of code describes the type of interrupt to be enabled. The first bit of the IER (RX_INT) is enabled to generate an interrupt when a byte is received.
The last few lines of code are use to select the correct port IRQ value and set the mask bits in the 8259 Programmable Interrupt Controller (PIC) (see Listing 1) . Also note that COM 1 and COM 3 typically share IRQ4 and COM 2 and COM 4 share IRQ3.
CloseSerial resets the interrupt vectors when you are through using the serial port. This is accomplished by the functions DisableInt and Resetvects. These two functions are the reverse logic of their counterparts EnableInt and Setvects.
Grabbing the Interrupt Byte
Listing 4 shows the functions that perform the interrupt service routines. ReceiveData is the interrupt function where the data is stored into a global ring buffer that is read by the function IntSerialIn. The first line of code determines if the interrupt was activated by the data-received bit. (See the Interrupt Identification Register in Listing 1) . If so, the byte is then stored in the ring buffer and the bufptr is incremented. Note that if the pointer reaches the end of the buffer, it is reset to zero and will begin overwriting the buffer. After this is accomplished an end-of-interrupt is sent to the programmable interrupt controller.IntSerialIn reads data from the ring buffer. The first few lines of code compare the byteptr to the bufptr. They should be equal only when the buffer is empty or the number of bytes read equals the number of bytes received. If they are equal a --1 is returned. If not, then the byte is read and byteptr is incremented. If the pointer reaches the end of the array, it is reset to zero and starts reading from the beginning of the buffer.
Polling the Port
Listing 5 is the polling function SerialIn. Note that when using polled I/O, the functions in Listing 3 are not needed. In this code I've used the clock function to flag for a timeout. In the first version of this code a counter was decremented, but when I switched from an 8086 to the 80386, the 386 ate up the timeout. I modified the code to use the clock function. It seems to work well this way. The time difference may be changed if you wish a longer timeout. I have used this code on everything from an 8086 to an 80486 with no problems.The next line of code looks for the Data Ready bit of the Line Status Register (LSR) to be set. Or if a timeout, it returns a -1. If the DR bit is set then read the Receive Register and return the Char_Value.
Sending Data
Listing 6 is used in both polled and interrupt I/O. Serial-Out is used to send a byte of data out the current portbase address. Again, I used the clock function to flag for a timeout. The next line looks for the Transmitter Buffer Empty (TBE) bit of the Line Status Register to set. If it finds a timeout, it returns a -1. If the Transmitter Buffer Empty bit is set, then it writes Char_ Value to the Transmit Register and returns zero.
Modem Control
If you wish to talk with a modem, you will need to use the functions in Listing 7. You activate the modem by setting the DTR bits and deactivate it by toggling the DTR bit to off. I also have ran into a few other devices that want to see the DTR bit set high before they would allow any communication. Assert allows you to set any of the bits in the Modem Control Register by passing the correct bit value. They can then be turned off by the Assert_Off function. Status is used to read any of the bit values in the Modem Status Register. These functions are useful for full handshaking with CTS and RTS.
Putting It All Together
If you're like me, anytime I see functions that are written by other programmers, I would like to see just how they are implemented. In addition to keeping this code as straightforward as possible, I have put the functions together in a simple terminal program that you can use. This allows you to see how the functions work together and to help complete the serial communications picture.You can use Listing 8, the terminal program, to talk with about anything you want. I use this program to do most of my initial test work when trying to develop a program that involves serial communications. It allows both polled and interrupt modes. Keep in mind that sometimes interrupts on serial cards are not configured properly or may not even work, so try both modes. This will also let you know if your interrupts are working correctly.
The first if statement in Listing 8 flags for no parameters passed and prints the correct syntax to the screen. If it finds parameters, the parameter list is set to the correct data types. SetSerial is the first serial function called. It not only sets up the COM port, but also checks for bad parameter values. If no errors occurred during the set up, I tell the user that the terminal is established. Assert then sets the DTR bit. I flag to see if interrupt mode is selected. If so, I call InitSerial and pass the current COM port (COM1 through COM4). I then go into a do/while loop that looks for either keyboard activity or activity on the serial port and displays it to the screen. If data is received I AND it with 0x7f to convert the int value to an ASCII value. This can be changed to 0xff if you wish to work with binary values. Both of these values have been defined at the top of the program. If the ESC key is pressed, the program exits. Before I quit, I again check the flag for interrupt mode. If the flag is true, CloseSerial is called to reset the old vectors and exit gracefully. Assert_Off turns the DTR bit off to release communication ties with external devices if so required.
Closing Notes
This code was written using Turbo C and the ANSI standard. It can be easily ported to Microsoft C by changing getvect and setvect to _dos_getvect and _dos_setvect.
Suggested Reading
Campbell, Joe. 1989. The C Programmer's Guide to Serial Communications. Howard W. Sams & Co.: Carmel, IN.