Squeeze extra features out of Microchip's popular PIC microcontroller chips. The tricks, such as making extra serial ports and performing 32-bit math on a 8-bit chip, are all here and can be used on other microcontrollers as well.
Although Microchip keeps adding capabilities to its ever-growing line of PIC microcontrollers, sometimes you need just a little more than what's already there. Perhaps you need a combination of features that doesn't exist yet, or you inherited a design and can't upgrade, or maybe you just like to experiment and push things beyond their traditional limits. This article will show you ways to work around the limitations of various PICs. Some of these tricks may also work with other microcontrollers, but source code and specific examples will be for PICs. We'll examine ways to add another asynchronous serial port, easily handle extended-precision (32 bits or more) arithmetic, enhance the parallel slave port, and use some oft-neglected features of the asynchronous serial port.
Many PICs include one or two asynchronous serial ports, but what if they're not enough? Bit-banging spare I/O pins is one obvious solution, and it works with the entire PIC family, but it's software intensive and it's tricky to do anything else while maintaining the critical timing needed for reliable communication. Another option would be to detect the leading edge of the start bit and use a timer interrupt to read the data. This is a big improvement over bit-banging but still requires significant software overhead to process an interrupt for each bit. Any delay servicing any interrupt could lead to synchronization problems.
Most PICs also offer an SPI (Serial Peripheral Interface) port. With minimal overhead, the normally synchronous SPI port can be tricked into receiving a standard asynchronous transmission.
A typical asynchronous data stream consists of a start bit (always 0), eight data bits (least-significant bit first), and a stop bit (always 1). Figure 1 shows an example, receiving the ASCII letter "Q" (hexadecimal 51). Variants may use fewer data bits or add a parity bit or additional stop bits.
Figure 1: ASCII "Q" (0x51) as asynchronous serial data
The SPI port also uses 8-bit data, but simultaneously sends and receives a byte on two different pins. A data clock eliminates the need for a start or stop bit, and the most-significant bit is first. Figure 2 shows the same data as sent by an SPI port.
Figure 2; Letter "Q" (0x51) as synchronous serial data
As Figure 2 shows, the SPI port receives a data bit on each falling edge of the clock and shifts outgoing data on the rising edge.
A push to get started
Once received, the data must be reversed, but if the falling edge of the SPI clock were synchronized to the center of each asynchronous data bit, it would receive just fine. The falling edge of the start bit provides the initial synchronization cue. The rest takes advantage of one of the PIC's SPI options. There are several timing options, including using timer TMR2. TMR2 increments until its value equals the contents of special function register PR2. TMR2 then toggles the SPI clock, resets to zero, and continues counting. If TMR2 starts with a value greater than PR2, the first interval would be longer than the usual clock time as it first wraps to zero as shown in Figure 3.
Figure 3: TMR2 initialized to - PR2 at the leading edge of the start bit
Once the SPI port takes over, it clocks in all eight data bits with no extra overhead. Now I know you're all thinking, "what about interrupt latency?" After all, it takes time from the leading edge of the start bit until proper initialization of TMR2 and the SPI port. If a higher priority interrupt is already active, there could be an even longer delay. Well, not to worry. The PIC provides another secret weapon. Many PICs have two or more capture/compare/PWM modules. An I/O pin can capture the value of a timer on a falling edge. Now the leading edge of the start bit stores the value of TMR1 in a CCPRx special function register and generates an interrupt. The interrupt service routine initializes TMR2 with the value of TMR1-CCPRx-PR2, which cancels out any latency. Listing 1 shows a typical interrupt routine.
Listing 1: A CCPR1 interrupt routine
// PIC clock = 16MHz
// Baud rate is 9600
// TMR2 prescaler is 4
// Asynchronous serial data on CCPR1 and SDI
// CCPR1 set to capture every falling edge and interrupt is enabled
// Calculate PR2 value
#define dTim2PR2 16000000/9600/4/4/2
TMR2 = -dTim2PR2; // Extra delay skips start bit
TMR2 += (TMR1L - CCPR1L); // Adjust for elapsed time since falling edge
CCP1IF = 0; // Clear interrupt
if (!CCP1) // False start-bit if CCP1 high
// OK if CCP1 still low
CCP1IE = 0; // No more edge interrupts until SPI done
SSPEN = 1; // Enable SPI port
SSPIE = 1; // Enable SPI interrupt
SSPBUF = 0; // Write dummy data to start
The optional "if (!CCP1)" line confirms the input pin is still low, which could prevent reading a quick glitch as serial data. Because (unsigned)- PR2 must be greater than PR2, choose the TMR2 prescaler value carefully and use the same prescaler for TMR1. In the above example, dTim2PR2 is 52. Your worst-case interrupt latency should be less than the serial data rate. For example, at 9600 baud, this would be about 104 microseconds or 416 instructions on a 16-MHz PIC. The SPI interrupt can just stash the data and enable the CCP1 interrupt for the next byte. Just remember to reverse the data bits at some point.
So much for receivingwhat about sending? Although you often have no control over when incoming data arrives and both receivers must be ever vigilant, you can usually alternate between two transmitters. There are many ways to handle the routing in hardware, and a few leftover logic gates or transistors, together with an output bit, can make the selection. Figure 4 shows one way to use a 74HC00 quad NAND. "Select" high transmits on "Serial1," "Select" low transmits on "Serial2." Either way, "DataIn" comes from your TX pin.
Figure 4: Using a quad NAND to control two serial ports
Secret serial stuff
The PIC's asynchronous serial port's status bits include the FERR (framing error) flag. FERR indicates the stop bit was low rather than high as required. This could indicate that the sender's baud rate is lower than the receiver's and the anticipated stop bit was really a data bit. You could also use FERR to detect an RS-232 "break" condition. A break consists of the usual low start bit, all-zero data, and a zero stop bit. If FERR is set and the data is 0, you may have a break but you normally should watch the input a little longer to confirm it stays low. The "break" condition is one way of sending your system the secret "bat signal" and activating a special configuration or test mode. Some terminals can send a break of various lengths, or you can short the RX input of your serial port to a positive voltage. The exact level depends on your hardware, but it can be as little as 3V. If you have an RS-232 status output such as DTR, its active level is positive and should therefore provide the voltage your receiver would need.
TX9 and TX9D are two more special function register bits that often get no respect. Setting TX9 sends TX9D as a ninth data bit. One traditional use for the extra data bit is as a parity bit for error checking. If error-detection is critical for your application, simple parity is limited as the error detection bit could be the one that's wrong. True error correction might be more appropriate, but that is a substantial subject in itself.
I can't stop
Once upon a time there was a system that used RS-485 to communicate over wires that could be hundreds of feet long. The system used fail-safe RS-485 chips that guaranteed a valid output even if the wires were open, which is often the case with half-duplex communications because data travels in both directions over the same pair of wires. Only one side can transmit at a time; in the interim both sides are listening, leaving the wires with no driving signal. The system worked reliably for years until the customer wanted to attach other "foreign" RS-485 equipment.
The foreign units didn't use fail-safe parts. The system released the line right after the PIC's TRMT status bit indicated that all bits were sent. TRMT doesn't count the stop bit, so the PIC released the line before the other side received the stop bit. A hardware change wasn't an option but configuring the PIC to transmit nine data bits and leaving TX9D high sent an extra data bit that appeared as a stop bit at the other end. And the two systems lived happily ever after.
If you're designing a new system, appropriate termination can help, and if your hardware allows it, you could leave the receiver active while transmitting. Once you hear your own transmission, you'll know that all bits, including the stop bit, are safe at the other end.
Do you know where your data is?
While writing some graphics functions for an 8-bit microprocessor many years ago, I had a need for 32-bit fixed-point math. With limited memory, it was tempting to reuse temporary storage for intermediate values during complicated calculations. Knowing when I could safely reuse a particular temporary location created new complications.
Those of you familiar with Forth know it uses a parameter stack and RPN (reverse-Polish notation). Those of you not familiar with Forth now know that it uses a parameter stack and RPN. Calculations pop their parameters from the stack and push the result back onto the stack. Temporary storage is always available at the top of the stack. After popping a value from the stack, that temporary storage is automatically freed and available.
A parameter stack is a convenient way of handling complicated expressions and intermediate results, especially if your compiler doesn't support the optimal data size. Without a stack, an "add" function might take two parameters, add them, and return the sum, which has to go somewhere. With a stack, arithmetic functions require no parameters and return nothing. An Add() function might pop two numbers from the stack, add them, and leave the sum on the stack. Data can be 32 bits, 24 bits, or whatever you need. You seldom have to think about data size unless you're transferring data between the stack and other locations. Using a stack in this way requires rethinking math operators and functions, and RPN offers a simple solution.
The algebraic expression "5+3" would be encoded as "5 3 +" in RPN. We could code this as:
The algebraic expression "(2*3) + (4*7)" would be represented in RPN as "2 3 * 4 7 * +" and could be coded as:
Just as with the Add() function, the Multiply() function pops two parameters from the stack, multiplies them, and pushes their product back onto the stack.
RPN has its fans and it can be very efficient but what if you need to keep a few numbers around for later calculations? The Add() and Multiply() functions send their parameters into the bottomless bit bucket of oblivion, never to be seen again. Fortunately, Forth offers a few more recyclable concepts. Besides the usual support for arithmetic, there are ways to manipulate values on the stack. Dup() makes a copy of the last value pushed onto the top of the stack (TOS). Over() copies the second value on the stack to the top of the stack, and Swap() exchanges the two top values. Drop() drops the value on top of the stack. Pick(n) copies the Nth value on the stack to the top of the stack. Figure 6 shows how Over() can keep two values on the stack and still calculate the sum.
Figure 5: Adding 5+3 on the stack
Figure 6: Over() and Add()
Once you pile your numbers on the stack, you can do complex calculations with just a few function calls without the need for parameters or return values.
I developed an extended-precision math library using these concepts borrowed from Forth and RPN. With PICs, the function calls often compile as a single opcode. You can download PicMath.c from ftp://ftp.embedded.com/pub/2005/04rowe. The version posted is for the CCS PCM compiler. Configuration is minimal:
#define StackDataSize as size of data in bytes (4 for 32-bits, etc.)
Allocate stack storage and Initialize MathPtr to the lowest address of the stack.
The MathCarry data bit stores the PIC's carry flag after calculations.
The MathDouble configuration bit enables double precision multiplies and divides when non-zero
PicMath also provides functions for stackless math, which take pointers to source and destination parameters. The result overwrites the destination parameter.
Parallel slave port
Time was, fast data transmissions were always done in parallel. Now, with serial data rates in megabits per second, serial transmissions can be the better choice but many of the larger PICs provide a parallel slave port (PSP) in addition to various serial port options. When you've used up all the serial ports on other things and still need another communication channel, the PSP can be quite useful. There are control lines for chip-select and reading/writing 8-bit data, but no standard way to know when data is ready to read, or if the PIC processed the last value written and wants more.
Internally, the PIC has IBF and OBF status bits. Input buffer full (IBF) indicates that someone wrote to the parallel port, and output buffer full (OBF) indicates that the last value output by the PIC is still there, waiting to be read. Figure 7 shows how these work.
Figure 7: Internal IBF and OBF signals
Although it's possible to communicate using only the existing capabilities, some firmware, and very rigorous protocols, you might need extra handshaking lines to recreate the equivalent of the internal IBF and OBF status bits. This usually requires at least one extra output pin, and one extra input pin to monitor the signal. A quick pulse can indicate a sender or receiver is ready. You can directly connect a handshaking output to an edge-triggered interrupt pin. Some pins can generate an interrupt on change, but if it's a quick pulse, reading the port pins will just show the current logic level.
Level-activated handshaking risks the two sides getting out of sync. A sender might see the receiver's "READY" handshake line, send a byte, and recheck the "READY" signal before the receiver responds. A receiver might see the sender's "READY" handshake line, read a byte, recheck the "READY" signal before the sender responds and read the same data again.
A PLD or other external logic device can create external handshake signals that mimic the internal IBF and OBF status bits. The sender's /WR signal can set XIBF (external IBF), which would be cleared by the receiver's output handshaking pin. The sender monitors XIBF to determine when the receiver is ready for more. The sender's handshaking pin can set XOBF (external OBF) to signal that data is ready to read. The receiver's /RD signal clears XOBF. The sender doesn't need to monitor XOBF as its internal OBF duplicates the signal and generates an interrupt.
It's always nice when your processor offers built-in hardware support for all your needs. If it doesn't, and your design positively absolutely has to be there yesterday, one of these tricks might be close enough. I've used them for years with various processors, including 16Cxx, 16Fxx, and 18Fxx PICs. As long as you understand the potential benefits and limitations, they can be a useful option in your toolbox.
Don Rowe is a registered Microchip consultant and president-for-life of Canzona Technologies, a consulting firm specializing in embedded controllers, digital and analog design, and reverse-engineering. He often prefers sneaky shortcuts and old Indian tricks to more expensive technology. Although unable to make a fire by rubbing two sticks together, he occasionally gets a spark by rubbing two cats together. Don can be reached at firstname.lastname@example.org.