AVR UART Transmit and stdio

By | 2012-09-04

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.


  1. The _BV() macro is another handy one provided by avr-libc, it’s simply

    #define _BV(bit) (1 << (bit))

    ↩

Leave a Reply