|
|
Serial Port I/O, With Interrupts & Buffering
Don't like reading docs, why not just
Skip Down to the Code?
Overview
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.
Configuration
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.
- rx_buf_head
- rx_buf_tail
- tx_buf_head
- tx_buf_tail
- 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.
- rx_buf
- tx_buf
- 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.
- rx_buf_size
- tx_buf_size
- 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.
Routines
- uart_intr
- 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).
- init_uart_intr
-
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.
- num_recv
- How many bytes are in the receive buffer? Value returned in Acc
- num_xmit
- How many bytes are in the transmit buffer? Value returned in Acc
- cin
- Get a character from the receive buffer. If nothing is in the
buffer, wait for something to appear.
- cout
- Put the character in Acc into the transmit buffer. If the buffer is
full, wait until there is a space to put it.
This code is available as plain text or in a
zip file.
|