skip navigational linksPJRC
Shopping Cart Download Website
Home Products Teensy Blog Forum
You are here: 8051 Tools Code Library Serial I/O, Intr.

PJRC Store
8051 Board, $79
LCD 128x64 Pixel, $29
LCD 16x2 Char, $14
Serial Cable, $5
9 Volt Power, $6
More Components...
8051 Tools
Main Page
Development Board
Code Library
89C2051 Programmer
Other Resources

Serial Port I/O, With Interrupts & Buffering

Don't like reading docs, why not just Skip Down to the Code?


These routines provide a interrupt driven serial input and output, which is intended to replace CIN and COUT in the Serial I/O Routines. This code uses separate transmit and receive buffers in Internal RAM, so that no external chips are required.

The purpose of this code is to allow your application code to be able to send and receive data via the serial port, without having to wait for each byte. This is done with buffers (internal ram memory), one for transmitting and one for receiving. When your program needs to transmit data, it will call cout. Using the simple polled serial I/O routines, cout would check if the 8051's UART is still sending a previous byte and wait until it's done, then put the byte it was given when called into SBUF to send it. This code does not wait. Instead, the byte is written into a buffer in memory, and a pointer is changed which tracks what portion of the buffer is holding data. If there is room available in the buffer, this cout routine returns to your program immediately. At some point later in time, when the 8051's UART is ready to transmit another byte, it will generate an interrupt and the uart_intr will transfer one of the bytes from the buffer to the SBUF register to send it and update the pointers to reflect that a byte was removed from the buffer. This process allows your program, for example, to send a large message "all at once". Repetitive calls to cout place all the bytes into the buffer, and then they are later sent by the UART while your program is free to do other tasks. As long as the transmit buffer is not full, your program may call cout and not have to wait for the time it takes the UART to send the bits.

Similarily, for reception of data, the 8051's UART generates an interrupt when it has received a byte. Again, uart_intr is run and it removes the byte from the UART by reading SBUF, and it writes the byte into a receive buffer (which is a separate from the transmit buffer). Later, when your program needs to receive data, it calls cin to fetch a byte from the receive buffer. If there is no data in the receive buffer, cin will wait until a byte is received and placed there. Your program may call num_recv to find out how many bytes are currently waiting in the receive buffer. Usually this is done to verify that there is at least one byte, as your application may have other tasks to perform and would skip calling cin and processing received data when there hasn't actually been anything received. For example, your program may have some lengthy computation task to perform, and during this time data may arrive at the UART. The uart_intr will take care of receiving it, so that your computational code need not be written to also handle data reception. When the computation is done, your program may call num_recv to see if any data has been received, and cin to retrieve the bytes. As long as the receive buffer has space, bytes may be properly received and later fetched with cin.

The advantage of using this code is that it allows your program send a group of bytes, without waiting for each one to be sent, and to receive one or many bytes, without having to regularily poll the RI bit. Many applications can not tollerate waiting while bytes are sent, or must be able to receive bytes while the code is busy doing some computation, and later read them from the buffer.

There is a price to be paid. The code is more complex, though it's already written and tested, so with some luck you can use it as-is. If your application needs to perform tasks while data is being sent or received, using these routines is usually much simpler than trying to make your application check the serial port as it does its work. On the other hand, if your program doesn't do much while using the serial port, the simpler polled I/O routines may be a better choice.

Approximately 30-40 instructions are executed for each byte sent or received, which consumes some CPU time, particularily at fast baud rates. At the 8051's fastest possible baud rate (timer1 auto-reload set to 0xFF, which is 57600 baud at 11.0592 MHz), there are 160 instruction cycles per byte transmitted by the UART, so in this extreem case, CPU usage can be substantial.

Of course, you must allocate memory in the 8051's internal ram. The two buffers may be any size (3 bytes or more each), and they may be located anywhere, even in the indirect-only area (0x80 to 0xFF) in 8052 chips with 256 bytes of internal ram. You must also choose four bytes of ram to hold the pointers that track how much data is stored in each buffer. These four pointers much be in the directly addressable internal ram (0x00 to 0x7F).

For these routines to work, the UART interrupt much remain enabled. Calling to init_uart_intr will enable it. Some operations performed by your application may not be able to tollerate an interruption. For example, when driving a motor with a push/pull circuit, it may be very undesirable if the UART causes an interupt between an instruction that turns off the pullup and the next instruction which turns on the pulldown device. Another example is reading a Dallas one-wire device, where a bit must be read within 2 to 15 µs. The interrupt routines temporarily stop your program when the UART needs to transfer a byte. During timing critical portions of your code (if there are any), you will need to disable interrupts ("CLR EA") before beginning the timing critical code, and then re-enable interrupts afterwards ("SETB EA"). If interrupts are disabled for too long, it is possible that the transmission will be paused or a received byte could be lost.


Eight variables must be defined to configure the internal ram usage. These should be edited carefully to assure that memory for the buffers and their pointers is not overwritten by the program or stack. These eight variables are constants which are defined to the assembler using EQU statements. The first 6 much be defined at the memory locations, in the 8051's internal ram, where these routines will store their data. The first four are single-byte pointers, which keep track of what portion of each buffer is in use. The other two are the locations where the buffers will exist in memory. The two "_size" variables define how large each buffer will be.
These pointers keep track of the data in the buffers. Each must be set to the location of one byte. These bytes must reside in memory between 0 to 7F.
Buffer starting locations. The tx buffer must not contain location 00. Because the code only accesses these buffers with indirect addressing, they may be located anywhere in internal ram, including 80 to FF in the 8052.
These define the buffer sizes. One byte of each buffer is never used (to speed up checks for empty/full conditions). A size of 23 would actually provide a buffer which can contain 22 bytes, though it will use 23 bytes of internal memory. These should never be set to 0 or 1. Using 2 offers no advantage over polled serial I/O, though it does work.
It is your job, when integrating this code into your project, to define these 8 variables. You must choose values which cause these routines to use memory that will not be overwritten by your other code's variables, registers, and the stack. It is also your decision to select the size of both buffers to fit the needs to your program.

There are two general techniques to choose buffer sizes. The first way is to consider how frequently the application sends and attempts to receive data, compared with the actual time taken to send and receive the bytes at the configured baud rate. For example, at 9600 baud (approx 1ms per byte), if the longest task the program performs without checking for reception (cin or num_recv) is 12 ms, then a buffer size of at least 13 would be needed (one byte of the buffer is not used).

A second approach is to ignore timing and consider the content of messages sent and received. For example, if the application receives messages from a user, and the longest valid message is 9 bytes, then a receive buffer of 10 bytes may be acceptable. This assumes that it is illegal for the user to send a second message until having received a response from the first one.

Every application is different, and these routines have been designed to be configurable to the buffer sizes required. It is critically important to choose large enough buffers, and to set these eight constants to properly allocate the memory needed by these routines.


Service interrupts generated by the uart... put a received character into the receive buffer or get a character from the transmit buffer and send it. This should not be called directly from the main program. It is only run is response to an 8051 UART interrupt. Usually a "LJMP UART_INTR" instruction is placed at location 0x0023 (or at a similar location such as 0x2023 if a monitor program is used).
One of the most common problems encounted is properly initializing the 8051 serial port. With these interrupt driven serial port routines, the four head/tail pointers must be initialized, and the SCON register must be initialized with RI and TI cleared (for polled I/O, they are usually initialized as ones). Another common problem is properly initializing TMOD, which controls both timer1 (baud rate generation) and timer0. Timer1 should have both TH1 and TL1 initialized to the baud rate constant with is it not running. This code demonstrates a correct and complete initialization of the 8051 UART that will work properly with these serial port interrupt routines. This code uses a fixed (hard coded) baud rate. If you'd like to automatically detect your user's baud rate, you'll find the code on the Automatic Baud Rate Detection page.
How many bytes are in the receive buffer? Value returned in Acc
How many bytes are in the transmit buffer? Value returned in Acc
Get a character from the receive buffer. If nothing is in the buffer, wait for something to appear.
Put the character in Acc into the transmit buffer. If the buffer is full, wait until there is a space to put it.

The Code

This code is available as plain text or in a zip file.

Paul's 8051 Code Library, Paul Stoffregen
Last updated: February 24, 2005
Status: complete
Suggestions, comments, criticisms: <>