Serial Communications
One of the easiest ways of getting information in and out of
your microcontroller, including giving it commands, is to use the built in
serial module.
There are a number of different types of serial
communication methods (that is, transmitting one bit after another), but what
we’re talking about here is asynchronous serial communication. That means that we agree beforehand what
speed we will communicate at and then we wiggle the output at the correct
rate. There are a few more things to
agree on though – asynchronous serial communications use a dummy bit at the
start of a byte to signify the fact that something is coming, and a bit at the
end to say that the byte has finished.
Why do we need this if there is always 8 bits in a byte? In asynchronous communications, the byte can
start at any time and indeed, we may have started listening half way through a
byte. Start and stop bits allow the
other end to understand what’s happening on the line.
The challenge of course is timing everything. Transmitting is no problem, you can “bit
bang” a serial output just by changing the output at the correct time. It’s more challenging to receive, since you
need to sample the input at the right time (each bit).
The serial module in the PIC microcontroller takes care of
the hard part of receiving bytes, simply telling you when a complete byte has
arrived. It also handles the byte
transmission by allowing you to give the module an entire byte and then you can
sit back while the hardware takes care
of sending the right bit at the right time.
The PicPack library adds to the PIC hardware by giving you a
buffer for transmitting and receiving.
This means you can send a bunch of bytes at once, and the circular
buffer will pump out each byte one after another automatically. It’s interrupt driven, so your program will
continue at full speed and the serial port will continue to transmit in the
background. It also provides a buffer
for receiving, so if your program doesn’t get to service the serial port in
time, the bytes are not lost. So this
gives you the ability to print out strings and do an incredible amount of
debugging, simply by being able to display information on a serial terminal.
Building on that, the PicPack library also gives you some
simple terminal functions that make it easy to build programs that respond to
user commands. That gives you the ability to provide a menu over the serial
terminal whereby you can ask the PIC to execute particular pieces of code.
So, onto using the serial port. Firstly, it’s a TTL serial port – meaning
that you can’t whack the transmit (tx) and receive (rx) pins of the PIC straight
into serial port of your PC. “TTL”
serial ports, or UART ports as they are also known, use a high signal (Vcc) to
signify a ‘1’ bit and a low signal to signify a ‘0’ bit. True RS-232, on the other hand uses -12V for
‘1’ and +12V for ‘0’. Modern RS-232
ports often skimp by using +5v for ‘0’ and 0v for ‘1’.
In either case, you can see immediately that
we need to adapt the polarity and voltage levels for a UART serial port to talk
to an RS-232 serial port. There’s a
couple of easy ways to do that. Sparkfun
have a serial module - I’ve used the RS232 Shifter SMD
module, but the other rs232 modules will work just as well. These are great for
breadboards. Alternatively, you can use
the Olimex boards that have a proper MAX232
chip on board with all the associated capacitors. These are used to bump up the serial port
voltages to +/- 12v. On the Olimex board
you’ll need to wire the serial port output / input to the TX / RX pins on the
pic. Don’t worry about the RTS/DTR pin,
it’s not necessary.
You should have no problems with any of these serial port
connections with USB serial port
adapters. So on to the software.
The serial port on PICs, like most microcontrollers, is
driven by the clock that the pic is running at.
As such, there’s not really the concept of “baud rate” or “bits per
second”, or bps, at least, at the hardware level. Everything is divided down from the clock
rate. The 16f range of pics have an 8
bit clock divider, and a “fast” and “not so fast” setting. Have a look at the datasheet for your PIC –
the section is titled “Addressable universal synchronous asynchronous receiver
transmitter”. And you wonder why we
stick to calling it “serial port”. We
don’t care about synchronous serial ports, they require a clock, and serial
ports on PCs are asynchronous anyway.
Notice that there’s a setting for High speed and Low speed, based on the
BRGH bit of the TXSTA register. Hunt
down the baud rate formula. You’ll see
that you can calculate the baud rate based on the BRGH setting, the SPBRG divider
and the clock frequency.
When BRGH = 0 (Low Speed) the baud rate = FOSC/(64(SPBRG +
1))
When BRGH = 1 (High Speed) the baud rate = FOSC/(16(SPBRG +
1))
FOSC is the frequency of the clock running your PIC.
Thankfully the PicPack serial port routines take away a lot
of the pain. Who wants to calculate
these values anyway? For all the popular
serial port values and clock frequencies, the work has been done for you.
Load up the serial_demo project and have a look at the
config.h file. You’ll start to see that
most things you configure in a PicPack project are done through the config.h
file.
// - - - - - - - - - - - - - - - - - - - -
// pic_serial defines
// - - - - - - - - - - - - - - - - - - - -
#define SERIAL_TX_BUFFER_SIZE 20
#define SERIAL_RX_BUFFER_SIZE 4
// Use this define if you want fine-grained control of what happens in the serial port
//#define SERIAL_DEBUG_ON
// Use this define if you are debugging in the IDE simulator and don't want it to hang
// waiting for serial interrupts that will never come...
//#define SERIAL_IDE_DEBUG
// Use this define if you want to drop a character if the TX buffer is full,
// rather than the default behaviour, which is to wait until the TX buffer has
// a spare spot.
//#define SERIAL_DISCARD_ON_TX_FULL_DURING_INT
// - - - - - - - - - - - - - - -
// General platform definitions
#define PLATFORM_TYPE breadboard
#define PLATFORM_CLOCK 12000000
The pic_serial defines are pretty obvious – you set the buffer
sizes for transmitted and received characters with the SERIAL_TX_BUFFER_SIZE
and the SERIAL_RX_BUFFER_SIZE. Since the
PicPack serial routines are completely interrupt driven, we need a buffer to
store things on the way in or way out, until your routines get around the
handling them. Remember you need a TX
buffer big enough that everything can sit there until it gets transmitted –
which only occurs while interrupts are running.
If you print out lots of stuff while in an interrupt service
routine (and this means interrupts are off), the library will wait until
there’s a spot in the buffer if the buffer is full. Nothing will be lost, but everything will
slow down while it waits for the serial port to flush characters from the
buffer. The alternative here is to increase
your buffer size, or uncomment the line:
#define SERIAL_DISCARD_ON_TX_FULL_DURING_INT
…in which case if the buffer is full, and you’re trying to
transmit while in an interrupt service routine, the byte will be
discarded. This can be useful if you
don’t want to slow down the proceedings to wait for the serial port, but this
will be at the expense of lost characters.
Set your platform type and clock speed in cycles per second
– the example here is 12Mhz, but if you’re using a 16f88 with internal clock,
for example, you’ll need to set this to 8000000, of course.
Have a look at serial_demo.c
// configure_system
//
// Get the pic ready for everything we want to do
void configure_system() {
kill_interrupts();
turn_analog_inputs_off();
serial_setup(SPBRG_9600);
term_init();
turn_peripheral_ints_on();
turn_global_ints_on();
}
In the configure_system routine, we set things up. Generally, when you start projects, it can be
handy to turn off interrupts at the start, just in case, along with turning
analog inputs off (that is, making sure all pins are acting as digital I/O). In this demo, it’s not strictly necessary,
but always good practice unless you need them.
See the serial_setup routine? It
makes it easy. Just pop in the baud
rate, and if you’ve set your PLATFORM_CLOCK rate correctly, you’re all set to
go without requiring you to do any calculations.
After that, we turn on peripheral interrupts (which includes
the serial port module), and global interrupts (which is the overarching
control on whether interrupts are on or off).
Our interrupt routine is as easy as this:
void interrupt() {
serial_handle_tx_isr();
serial_handle_rx_isr();
}
PicPack does all the hard work for you.
Now we’re ready to start getting some serial port action.
serial_print_str("\n\nPIC terminal\n");
That’s as easy as it gets for printing out strings. Use serial_print_int for 16 bit unsigned
integers, and serial_print_int_hex for printing 8 bit hex numbers.
This demo also uses the pic_term library, which makes it
easier to do serial debugging on the PIC.
All you need is a:
term_init();
in your configure_system() routine, some parameters in
config.h:
// - - - - - - - - - - - - - - - - - - - -
// pic_term defines
// - - - - - - - - - - - - - - - - - - - -
#define TERM_BUFFER_SIZE 10
// Reset (go to bootloader) if magic character received
#define TERM_ALLOW_BOOSTBLOADER
// Echo typing so user can see what they're doing
#define TERM_ECHO_INPUT
And in your main program loop, a routine that allows you to
respond to the user while in the main program, as opposed to an interrupt:
for(;;) {
term_process();
}
The routine term_entry_callback will be called when the user
presses enter. It will be passed the string of characters the user has entered
(if any). From there you can choose how
you respond to the user. You can print
out variable values, execute commands and so on. In this example you can set a variable to a
value by typing:
s7e
to set “my_var” to 7e”, and then you can print the value you
have set it to by typing
a
to get the result in decimal or
x
to get the result in hex.
So,
compile the program, bootload it into your pic and start trying out some of
those commands.
|