I sometimes have projects using AVR microcontrollers – specifically, I use the ATTiny24 quite a lot since it has a fair number of IO lines and has most of the basic features you would expect from an AVR. The only problem I have with this chip is that it doesn’t seem to have a full UART on it. I’ve looked through the datasheet at the universal serial IO it has, but it doesn’t strike me as being useful for RS-232 communications. Besides that, if you have the code I’m going to show you in this post, you can do RS-232 output on any unused pin without the need for a UART, which in my opinion, is a more flexible solution.
(Yes, you can use JTAG if you have the tools for it, but it’s not ideal when all you really need is simple feedback to know what/how your program is behaving. I actually have the JTAG ICE mkII and I mostly just use it as a programmer if I’m writing my program in C.)
To do this, you need to know about two basic approaches to structuring a program for an embedded system. First, we’ll be implementing an interrupt driven scheduler that uses a timer. The timer helps to ensure that the serial output line is changing at the correct rate so that the remote system can understand the data being sent. In this example, we’ll be trying to communicate at 9600 baud, so we’ll need to have the timer interrupt occur every 0.104mS (1/9600 = 0.104mS). Each time the interrupt occurs, we’ll change the state of the output pin to signal the next bit to be transmitted.
The second concept we’ll need to implement is a state machine. Basically, we need to keep track of where we are in the current byte to be sent as well as where we are in the output string. As each bit is sent, we need to determine if the current byte is just beginning and we need to send a space (start) bit or if the current byte is ended and we need to send a mark (stop) bit. (See Wikipedia for an in-depth discussion of the RS-232 protocol.) The state machine is built around the use of a few variables and control logic so that the information sent complies with the protocol. Here is the code:
/* Headers needed for this module */
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
volatile char msg[32]; // message buffer
//Sample message - note how the string is null terminated so we can use strcpy()
const char testmsg PROGMEM = "Testing Serial IO!\r\n";
// This line is our output for serial data
#define SEROUT PB2
// Scheduler interrupt frequency. This directly affects the baud rate
// for serial communications. Do not change this.
// This was approximately calculated as follows (some tweaking was needed):
// ((0xFFFF) - ((1 / BAUD) / (1 / F_CPU)))
#define TCNT1_CNT 0xFD00
void init_timer(void)
{
TCCR1A &= ~(1<<COM1A1) & ~(1<<COM1A0) & ~(1<<COM1B1) & ~(1<<COM1B0) &
~(1<<WGM11) & ~(1<<WGM10);
TCCR1B &= ~(1<<ICNC1) & ~(1<<WGM13) & ~(1<<WGM12) &
~(1<<CS12) & ~(1<<CS11);
TCCR1B |= (1<<CS10);
// WGM13:0 = 0 for normal mode
TIMSK1 |= (1<<TOIE1); // interrupt on overflow
TCNT1 = TCNT1_CNT;
}
ISR(TIM1_OVF_vect) // TimerCounter1 overflow interrupt handler
{
/* Some of the frequency calculations may need to be tweaked to make
it so that the routines execute with an acceptable amount of error.
Since we are bit banging serial communication, the amount of error
we can have before the serial stream turns to garbage is fairly
small */
TCNT1 = TCNT1_CNT; // reset counter for next time
// This must be done first because the timing is critical
do_serial_comm();
// ... do other things if need be....
}
}
void do_serial_comm(void)
{
/* State machine to output serial data. This function must be
called with the proper frequency to output serial data at the correct
rate. If the rate varies slightly, the baud rate won't be stable
enough to ensure the data can be understood by the remote host.
*/
static volatile uint8_t bitPos = 8;
static volatile uint8_t charPos = 0;
static volatile uint8_t needStopStart = 0;
static volatile char curChar;
/* The order of the following blocks is critical. For this
state machine to work correctly, we have to do something
with the transmit line every time this function is called.
The operation to be performed depends on where we are in
current character and the message itself.
*/
// send serial data
if (needStopStart > 1)
{
PORTB |= (1<<SEROUT); // logical mark output
needStopStart--;
}
else if (needStopStart > 0)
{
PORTB &= ~(1<<SEROUT); // output a start bit
needStopStart--;
}
else if (bitPos < 8) // LSB first
{
if ((curChar & (1<<bitPos++)) == 0) // bit is 0
PORTB &= ~(1<<SEROUT);
else // bit is 1
PORTB |= (1<<SEROUT);
}
else if (charPos < strlen(msg))
{ // move to the next byte
curChar = msg[charPos++];
bitPos = 0;
needStopStart = 3; // Sends 2 idle bits + 1 start bit
PORTB |= (1<<SEROUT); // Idle the line
}
else // only get here if bitpos is 0 and curPos = strlen(msg)
{ // end of string
strcpy(msg,""); // blank the message
bitPos = 8;
charPos = 0;
needStopStart = 0;
PORTB |= (1<<SEROUT); // Idle the line
}
}
/* Code */
int main(void)
{
init_timer();
strcpy(msg, testmsg); // initialize output message
sei(); // enable interrupts
for (;;) {} // loop
}
Some of the above code may look confusing, but it works quite well. Our cpu is configured to run at 8MHz from the internal oscillator. Since that timing source is going to drift from chip to chip, some tweaking of the counter overflow value may be needed to get the timing of the serial line changes to work correctly.
In the do_serial_comm() call, the needStopStart value is used to output the necessary stop, idle, and start bits that have to occur in between each byte. Since the sequence in between each character is always the same – a stop bit, idle bit, and a start bit – we can simply decrement needStopStart each time the function is called to get the correct output. When we are sending character bits, we simply test the current bit to see if it’s a 0 or a 1 and then send the proper logic level on the output pin.
When the message buffer is exhausted the message buffer is set to a blank string and the remaining state machine variables are reset. The remaining program code can then test the length of the message buffer to figure out if the current message has been completely sent before placing a new message into the buffer. Testing for a blank message buffer has to be done each time the program wants to send a new message. If a new message is put into the buffer before the previous message has been completely sent, the output may be corrupted.
This technique works best if your program has a lot of time in between status updates. In my case, I used this code in a NiCD battery charger I built and I wanted the charger to be able to log the battery voltage on the computer once per second. So long as the message is short and the check for the battery voltage isn’t happening too quickly this method is very reliable. If on the other hand, you’re doing something 1000 times per second, you’ll find this to be very limiting because you simply can’t send messages that fast at this baud rate. Also the baud rate is also a limiting factor since the amount of acceptable timing error is reduced as you try to communicate at faster speeds.
Anyway – I hope this information was helpful. If you spot an error, please let me know in the comments.