Contact Info

Crumbtrail

ActiveXperts.com » Serial Port Component » Tutorials » UART

Basics of programming a UART

The UART (Universal Asynchronous Receiver/Transmitter) chip is responsible
for just what its name implies; transfering data, to and from the serial
port. The 8250 is quite old, and has been almost entirely replaced (the
8250 UART was shipped WITH the original IBM PC--and I mean the original.)
Its first replacement was the 16540 UART, which had the same general
architecture, but was somewhat faster and supported higher baud rates for
data transfer. The 16540 was replaced by the 16550, a UART which featured a
16-bit wide receive buffer for characters and a built-in FIFO buffer. A
close cousin to the 16550 is the 16560, a chip which sports a 32-bit wide
receive buffer.

Nevertheless, modern serial controllers are backward compatible, so what
you learn about the 8250 can still be applied on today's machines. With
that bit of background covered, we can begin studying the 8250.

Where to start? For software engineers, a register listing is the most
direct and intimate way to get to know a piece of hardware. I've provided
you with one for the 8250 below.

 If you haven't worked with hardware much, you're probably not used to
 register listings. Register listings give the addresses of registers that
 are used to program a chip and list the manner in which the register
 affects the behavior of the chip. You can use this information to program
 the chip to perform tasks.

[note: I didn't piece this together entirely from memory. A lot of the
details came from http://www.byterunner.com/16550.html.]

                           8250 REGISTER LISTING
 To write to an 8250 register, you write to the base address of the chip
 plus an offset. The base address is 2e8h for COM1 and 3e8h for COM2.
 Register 0:     RHR (Receive Holding Register; Receive Buffer
                 Register in some literature).  Doubles as the THR
                 (Transmitter Holding Register).  Is also the LSB
                 of the DLR (Divisor Latch Register) occasionally;
                 don't worry about that yet, but remember it.

 Purpose:        This register is where you both read and write data
                 for the serial port.

 Bits:           Bits 0-4 contain data bits 0-4.
                 Bits 5-7 may or may not be defined, depending upon
                 whether the UART has been instructed to use 5, 6, 7,
                 or 8 bit words.
 Register 1:     IER (Interrupt Enable Register).  Also the MSB of
                 the DLR (Divisor Latch Register) occasionally; don't
                 worry about that yet, but remember it.

 Purpose:        Tells the UART to generate an interrupt when different
                 things occur.

 Bits:           Bit 0: RHRI (Receive Holding Register Interrupt; RxRDY
                 in some literature).  The UART will generate an
                 interrupt when a character is received in the RHR if
                 this bit is set.
                 Bit 1: THRI (Transmit Holding Register Interrupt; TxRDY).
                 If set, the UART generates an interrupt when a
                 character is moved from the THR to the Internal Shift
                 Register.
                 Bit 2: RLSI (Receive Line Status Interrupt; ERROR).
                 If set, the UART interrupts when a parity or overrun
                 error occurs, or when a break condition is encountered.
                 Bit 3: MSI (Modem Status Interrupt; DELTA).  If set,
                 the UART interrupts whenever an RS-232 line changes
                 state.
                 Bits 4-7: Unused

 Register 2:     ISR (Interrupt Status Register; also refered to as
                 the Interrupt Identification Register).

 Purpose:        Tells what event caused a UART interrupt.

 Bits:           Bit 0: Flags if an interrupt has occurred
                 Bits 1-2: Indicates what caused interrupt:
                           00 -> RS-232 line change
                           01 -> THR emptied
                           10 -> RHR contains character
                           11 -> Error condition
                 Bits 3-7: Unused

 Register 3:     LCR (Line Control Register).

 Purpose:        Configures the UART.  Also  flags the use of
                 registers 0 and 1 for the DLR (Divisor Latch
                 Register).  More about that shortly.

 Bits:           Bits 0-1: Sets the number of data bits in a
                 serial word:
                           00 -> 5-bit data
                           01 -> 6-bit data
                           10 -> 7-bit data
                           11 -> 8-bit data
                 Bit 2: Stop bits; 0 flags 1 stop bit per word,
                 1 flags 2 stop bits per word.
                 Bits 3-5: Sets the parity
                          000 -> No parity
                          001 -> Odd
                          011 -> Even
                          101 -> Mark
                          111 -> Space
                 Bit 6: Break control; sends the receiver a break
                 condition.
                 Bit 7: DLR access enable; if set, registers 0
                 and 1 become one big word register (the DLR)
                 that stores the baud rate divisor for calculating
                 the baud rate of the UART.

 Register 4:     MCR (Modem Control Register).

 Purpose:        Controls the lines on the RS-232 interface.

 Bits:           Bit 0: Is reflected on RS-232 DTR (Data
                 Terminal Ready) line.
                 Bit 1: Reflected on RS-232 RTS (Request to
                 Send) line.
                 Bit 2: GPO1 (General Purpose Output 1).
                 Bit 3: GPO2 (General Purpose Output 2).
                 Enables interrupts to be sent from the UART
                 to the PIC.
                 Bit 4: Echo (loop back) test.  All characters
                 sent will be echoed if set.
                 Bits 5-7: Unused.

 Register 5:     LSR (Line Status Register).

 Purpose:        Stores general status information about the UART.

 Bits:           Bit 0: Set if RHR contains a character (called
                 RxRDY or RDR, depending on literature).
                 Bit 1: Overrun error (character overwrote the last
                 in the RHR)
                 Bit 2: Parity error
                 Bit 3: Framing error (stop bit was set to 0 instead
                 of 1).
                 Bit 4: Break condition
                 Bit 5: THE (or TBE).  Transmit Buffer Empty.  If
                 set, the UART sent data from the THR to the OSR
                 (Output Shift Register) and data can be safely
                 written without overwriting anything.
                 Bit 6: Transmitter empty; both the THR and shift
                 register are empty if this is set.
                 Bit 7: Unused on the 8250.

 Register 6:     MSR (Modem Status Register).

 Purpose:        Displays the status of the modem control lines.
                 After bits 0-3 are read they are reset.

 Bits:           Bit 0: CTS (Clear To Send) line has changed
                 (since last read of MSR).
                 Bit 1: DSR (Data Set Ready) has changed.
                 Bit 2: RI (Ring Indicator) has been set since
                 the last time the MSR was read.
                 Bit 3: CD (Carrier Detect) has changed.
                 Bit 4: Value of CTS
                 Bit 5: Value of DSR
                 Bit 6: Value of RI
                 Bit 7: Value of CD

 Register 7:     SPR (Scratch Pad Register)

 Purpose:        Just what the name implies; a scratch pad, for
                 nothing.

 Bits:           Bit 0-7: As you will

The registers listed above are pretty much all that there is to programming
the 8250; you access them by doing an out to the base address of the UART
for a given COM port (it's got a set of registers for every COM port) plus
the offset of the register that you want to work with. The base addresses
of the COM ports are as follows:

#define PORT_COM1       0x3f8
#define PORT_COM2       0x2f8
#define PORT_COM3       0x3e8
#define PORT_COM4       0x2e8

In C, that means

void Write_UART (int COM_port, int reg, int data) {
     outp((COM_port + reg), data);
}

int Read_UART (int COM_port, int reg) {
    return(_inp(COM_port + reg));
}

Easy enough. These functions will allow you to program the UART registers,
and they're all you need to construct a full serial communications library.
Most things that you can program the UART to do are self-explanatory. As a
for-instance, let's try to figure out how to send and receive characters
from the serial port. To do this, first we have to figure out how to set it
up.

Setting up the UART:

Refering to the table above, you'll see that the LCR (register 3) allows us
to establish essential aspects of a serial communications session via the
UART. We need to set up the format of the characters to be sent or
received, namely how many bits each character is to have (5-8), the parity
of the connection (something like a method of error checking), the number
of stop-bits (don't worry about them), and the baud rate at which the
connection is to take place (the number of bits per second to transfer).

Let's go with 8-bit serial words, because that comes to exactly a byte, the
size of the type unsigned char in ANSI C. This makes things a lot easier.
It takes care of the parity issue--there's no room for it with 8-bit serial
words, so forget it (parity=NONE). As for stop bits, we'll just set the
UART to send one of them. That leaves us the issue of setting the baud
rate. Be sure you're sitting down for this next one.

To set the baud rate:

You set bit 7 of register 3, which makes registers 0 and 1 one big
word-sized register that is used to hold the baud-rate divisor. This
divisor is calculated as 0x1C200 / baud_rate and stored in a word-sized
(two-byte) variable, like int in a 16-bit compiler or short in a 32-bit
compiler. Then to set the baud rate for the UART, you do a word out (I
think the ANSI C standard library has a word-out function--like, say,
outpw()--for your convenience) to register 0 on the UART, which fills
registers 0 and 1 with the divisor.

 Don't ask me why it's designed like that, I'm not a hardware engineer.
 Let's just say that some Roman god decreed it two thousand years ago and
 your PC will get hit by lightning if you contradict it--some things are
 just plain wierd.

So let's throw together a function that sets up the serial port. Skip the
box if you know about bit flags.

 Bit flags for the beginner: It's often desirable to cram a lot of
 information about something into a small space. One way to achieve this
 is by using bit flags, by which each bit of a byte represents the state
 of something. Bit-flags work by using the logical operators & and |, AND
 and OR. OR on a bit level compares two bits and produces one output bit:
 If either one of the input bits is set (=1), then the resulting bit is 1,
 else it isn't. AND on a bit level also compares two bits and produces one
 output bit: If both input bits are set to 1, then the resulting bit is 1,
 else it isn't. When used on a byte level, AND and OR work the same way as
 they do on the bit-level, only on every bit of a byte.

 Bit "flags" are defined as numbers that have only one bit set in them,
 such as 128, 64, 32, etc. Individual flags can then be combined to form a
 flag that records the information stored in both of them, such as (128 |
 4) = (01000000 | 00000100) = 01000100. Then you check the state of each
 bit in the byte using & to "mask" all bits in the byte except the one
 that you're interested in. If the bit that you're interested in is set,
 the result of & will be non-zero.

typedef short word
//typedef int word

//SOME CONSTANTS FOR PROGRAMMING THE UART

#define REG_RHR         0
#define REG_THR         0
#define REG_IER         1
#define REG_IIR         2
#define REG_LCR         3
#define REG_MCR         4
#define REG_LSR         5
#define REG_MSR         6
#define REG_SCRATCH     7

//LCR-related constants
#define PARITY_NONE     0
#define PARITY_ODD      8
#define PARITY_EVEN     24
#define PARITY_MARK     20
#define PARITY_SPACE    28

#define STOP_ONE        0
#define STOP_TWO        4

#define BITS_5          0
#define BITS_6          1
#define BITS_7          2
#define BITS_8          3

#define DLR_ON          128

int port_in_use=0;

int Setup_Serial (int COM_port, int baud, unsigned char misc) {
    word divisor;

    if(port_in_use)
      return(port_in_use);

    port_in_use = COM_port;

    Write_UART(COM_port, REG_LCR, (int)DLR_ON);
    divisor = 0x1c200 / baud;
    outpw(COM_port, divisor);

    Write_UART(COM_port, REG_LCR, (int)misc);
    return 1;
}

Here's a demonstration:

Setup_Serial(PORT_COM1, 2400, BITS_8 | PARITY_NONE | STOP_ONE);

Now we'd like it if we could actually use the serial port. Let's send and
receive characters.

Sending characters:

To send a character out the serial port, you write it to the THR (register
0). What if there's another character in the THR waiting to be sent? On the
8250, it'll be overwritten. However, you can wait for it to be sent. Test
bit 3 of the LSR to determine if the last character in the THR was shifted
out of the buffer before writing a character. Like this:

void Serial_Write (unsigned char ch) {
     while (!((unsigned char)Read_UART(port_in_use, REG_LSR) & 0x20)) {}

     //clear interrupts
     _asm cli

     Write_UART(port_in_use, REG_THR, (int)ch);

     //set interrupts
     _asm sti
}

Note that software interrupts are turned off using _asm cli. This makes
sure that interrupts using the COM port set up by other programs don't
interfere with the operation of our application. Always be sure to turn
interrupts back on.

Receiving characters:

To determine if a character is in the THR, you read bit 0 of the LSR. When
this bit is set, a character is in the THR, and you can retrieve it by
reading register 0 of the UART.

unsigned char Simple_Serial_Read (void) {
              while (!((unsigned char)Read_UART(port_in_use, REG_LSR) << 7)) {}
              return(Read_UART(port_in_use, REG_RHR));
}

Programming the UART to generate interrupts:

It is often desirable to have the UART tell you when an event occurs,
rather than having to poll its registers to find out. You can do this by
programming it to generate interrupts on certain events.

Before setting up any interrupts, you must set bit 3 of the MCR (UART
register 4) to 1. This toggles the GPO2, which puts the UART out of
tri-state, and allows it to service interrupts. [don't ask; I didn't design
the thing] Then, set the bits of the IER (register 1) that represent the
interrupts you want the UART to generate.

Next, you set up the PIC (Programmable Interrupt Controller) to allow
interrupts from the COM ports. Many of you aren't familiar with this chip
[the PIC], but there's not enough space to give a tutorial on it, so you'll
just have to believe me. COM1 and COM3 are on IRQ4 and COM2 and COM4 are on
IRQ3, so you enable the interrupt by zeroing the 3rd (IRQ3) or 4th (IRQ4)
bit of the PIC's Interrupt Mask Register (be sure to keep the other bits
intact!).

The serial interrupt vector is 0Bh (for COM1/COM3) or 0Ch (for COM2/COM4).
Write an ISR that latches onto the appropriate one. Your ISR should check
the LSR (register 5) to determine what caused the interrupt, then handle
it. Was that fun or what? Some sample code follows, hopefully you can
follow it (DJGPP). [warning: not tested, compiled, or warranted in any way;
uses functions developed earlier in this article.]

#include 
#include 
#include 

#define ON_RHRI 1
#define ON_THRI 2
#define ON_RLSI 4
#define ON_MSI  8

_go32_segment_info old_ISR, new_ISR;

void my_ISR(void) {
     //does nothing, and does it well
     asm("cli;pusha");
     asm("popa;sti");
}
void end_my_ISR(void) {}

int serial_interrupt(int COM_port, unsigned char conditions) {
     unsigned char data;

     Write_UART(COM_port, REG_MCR, 0x08);
     Write_UART(COM_port, REG_IER, (int)conditions);

     _go3d_dpmi_lock_code(my_ISR,(unsigned long)(end_my_ISR-my_ISR));

     //WARNING:  You should also lock any data accessed from within
     //an interrupt handler using _go32_dpmi_lock_data();

     new_ISR.pm_offset = (int)my_ISR;
     new_ISR.pm_selector = _go32_my_cs();

     switch(COM_port) {
           case PORT_COM1:
                _go32_dpmi_get_protected_mode_interrupt_vector(0x0B, &old_ISR);
                _go32_dpmi_allocate_iret_wrapper(&new_ISR);
                _go32_dpmi_set_protected_mode_interrupt_vector(0x0B, &new_ISR);

                data= inportb(0x21); //get PIC IMR
                data&=0xF7;       //zero bit 4
                outportb(0x21,data); //write PIC IMR
                break;
           case PORT_COM2:
                _go32_dpmi_get_protected_mode_interrupt_vector(0x0C, &old_ISR);
                _go32_dpmi_allocate_iret_wrapper(&new_ISR);
                _go32_dpmi_set_protected_mode_interrupt_vector(0x0C, &new_ISR);

                data= inportb(0x21); //get PIC IMR
                data&=0xFB;       //zero bit 3
                outportb(0x21,data); //write PIC IMR
                break;
           default:
                return 0;
     }

     return 1;
}

void set_irpt_conditions (int COM_port, unsigned char conditions) {
     Write_UART(COM_port, REG_IER, (int)conditions);
}

int stop_serial_irpt (int COM_port) {
    unsigned char data;

    //toggle GPO2
    data = Read_UART(COM_port, REG_MCR);
    data&=0x08;
    Write_UART(COM_port, REG_MCR, (int)data);

    //disable interrupts
    Write_UART(COM_port, REG_IER, 0x00);

    if (COM_port==PORT_COM1) {
       _go32_dpmi_set_protected_mode_interrupt_vector(0x0B, &old_ISR);
       _go32_dpmi_free_iret_wrapper(&new_ISR);
       return 1;
    }
    if (COM_port==PORT_COM2) {
       _go32_dpmi_set_protected_mode_interrupt_vector(0x0C, &old_ISR);
       _go32_dpmi_free_iret_wrapper(&new_ISR);
       return 1;
    }

    return 0;
}