The setup for a UART on AVR is such common code that I wonder if possibly it’s just assumed to be understood and explained. This is a quick article to fill in the gaps. First let’s talk initialisation. This is often the most important part of using any hardware peripheral on an embedded processor. In the case of the AVR UARTs, avr-libc
includes the very convenient setbaud.h
to help us (I’m using ‘atmega8’ register names here, but the principle is the same whatever member of the family you use)1:
static void initUART()
{
// UCSRA RXC TXC UDRE FE DOR PE U2X MPCM
// UCSRB RXCIE TXCIE UDRIE RXEN TXEN UCSZ2 RXB8 TXB8
// UCSRC URSEL UMSEL UPM1 UPM0 USBS UCSZ1 UCSZ0 UCPOL
// UDR ------------ UART Data Register -------------
// UBRRH URSEL - - - ----- UBRR[11:8] -----
// UBRRL ---------------- UBRR[7:0] ------------------
//
// Assume F_CPU has been set to CPU frequency
// Use setbaud.h to calculate the registers for us
# define BAUD 9600
# include <util/setbaud.h>
// Set baud rate before enabling the UART
UBRRH = UBRRH_VALUE;
UBRRL = UBRRL_VALUE;
# if USE_2X
UCSRA |= _BV(U2X);
# else
UCSRA &= ~_BV(U2X);
# endif
// Enable receiver and transmitter
UCSRB |= _BV(TXEN) | _BV(RXEN);
// Set frame format: 8 data; 0 stop bits
// Note: UCSRC and UBRRH share the same I/O address. The URSEL bit
// is used to choose which of the registers the lower 7 bits are
// written to. For URSRC, include URSEL in the set, otherwise don't
UCSRC |= _BV(URSEL) | (0 << USBS) | (3 << UCSZ0);
}
Outputting a byte is then very simple. We need only trigger the UART peripheral.
UDR = '\n';
We should be a little careful though. The UDR
register might be in use sending an earlier byte. We should check for that before overwriting it.
static void uart_putchar( char c )
{
// Wait for data register empty flag
while( !( UCSRA & _BV(UDRE) ) )
;
// Begin next transmit
UDR = c;
}
Now you want to be able to use printf()
. What do you do?
Conveniently, avr-libc
is pretty excellent. It includes a stdio
implementation that is retargettable. Here’s the magic line…
static FILE UARTstdout = FDEV_SETUP_STREAM(
uart_putchar,
NULL,
_FDEV_SETUP_WRITE );
Then to use this structure in printf()
and all the other stdio
functions, we simply add the following to our initialisation:
stdout = &UARTstdout;
The _FDEV_SETUP_WRITE
flag makes UARTstdout
a write-only handle, which is what we’d expect for stdout
. We need to slightly modify our uart_putchar()
to match what FDEV_SETUP_STREAM()
expects:
static int uart_putchar( char c, FILE *stream )
{
// Wait for data register empty flag
while( !( UCSRA & _BV(UDRE) ) )
;
// Begin next transmit
UDR = c;
}
Now we’ve got a FILE
variable, we need to tell stdio
to use it.
int main(void)
{
initUART();
stdout = &UARTstdout;
// ... other initialisation
printf( "Hello, World!\r\n" );
while(1)
;
return 0;
}
It’s pretty wasteful to force the inclusion of printf()
just to write an unformatted string of course; but I presume you’ll have better uses for your printf()
.
We’ve not discussed interrupts at all here; and for good reason: interrupts often complicate matters considerably, but once under your control they make your program much more reactive and responsive. They’re also vital for power control, since you can put your processor to sleep and only wake up when an event occurs. I’ll cover interrupt handling in embedded systems in a future article.
-
The
_BV()
macro is another handy one provided byavr-libc
, it’s simply#define _BV(bit) (1 << (bit))