AVR and printf

Thumbnail image of Jason Bauer
Jason Bauer
December 01, 2015 (Last Updated: ) | Reading Time: 7 minutes

Getting printf to work on an AVR can be confusing. We'll show you exactly how to do it.

AVR microcontroler with printf function
AVR microcontroler with printf function

In the picture above the text was sent to the LCD display using printf.

STK500 Setup

You should leave your STK500 set up exactly the same as it was set up for a previous guide, AVR USART Serial Communications.

Required Functions

You should keep all of the functions that you need from the previous guide:

What printf does

printf stands for "print formatted". printf can take variables from memory and print them to the serial port so that they show up on screen with more formatting options than just using usart_pstr. For instance, what if you have the value 65 in a variable called myvalue and you wanted to print that value out to screen? There is no way to accomplish this with usart_pstr since the value in myvalue will be interpreted as ASCII and will show up as a capital A. With printf you can print the numeric value stored in a variable to screen. You simply tell printf how to interpret the variable that you are passing it, and then pass it the variable to be printed. Here is an example:

printf("The decimal value in myvalue is: %d", myvalue);

See the %d in the string? printf will replace %d with the decimal value stored in myvalue. Why decimal? Because you asked for %d, which means decimal to printf. You could ask for hex if you like as well:

printf("The hex value in myvalue is: %X", myvalue);

When printf sees the %X (that's a capitol X), it will replace it with the value stored in myvalue and it will represent the value in hex notation.

printf Format Specifiers

There are many printf format specifiers, and learning how to use them will take a little bit of work. You can specify the number of digits of precision, the number of leading zeros, and all sorts of other tricks. Here are the basic printf format specifiers:

Code Format
%c character
%d signed integers
%i signed integers
%e scientific notation, with a lowercase 'e'
%E scientific notation, with an uppercase 'E'
%f floating point
%g use %e or %f, whichever is shorter
%G use %E or %f, whichever is shorter
%o octal
%s a string of characters
%u unsigned integer
%x unsigned hexadecimal, with lowercase letters
%X unsigned hexadecimal, with uppercase letters

The Problem With printf

printf is an excellent function that adds lots of utility to your programs. The big problem with it is that it is pretty large. When you use printf in your program, your code size automatically jumps by about 1,444 bytes to your code. This is a one-time penalty, so if you are going to use printf once, you might as well use it as often as you want.

Streams and stdio.h

In order to use printf you need to include stdio.h in your project. This step is easy.

#include stdio.h

You also need to define a stream for printf to work. Streams are a part of C, and are a way to move data around. In this case, we are going to move character data around in a stream. To define a stream for printf, make sure you are in the global section of your code, which means outside of main, preferably near the top, and put this line in:

static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar_printf, NULL, _FDEV_SETUP_WRITE);

Ok, that's a complicated line. You do not have to understand it to use it. Maybe later you can come back to this line, but for now, just accept the fact that a variable calledmystdout gets created as a stream. Notice that this line references usart_putchar_printf. This is a function that we have to write. stdio streams are done in a way that you can do anything with the stream of data that you want. You can send the stream over a wireless link, you can print the stream to an LCD, or you can send the stream out the serial port like we are going to do. What happens with the stream is defined in usart_putchar_printf. Here is the version that we will use:

int usart_putchar_printf(char var, FILE *stream) {
    if (var == '\n') usart_putchar('\r');
    usart_putchar(var);
    return 0;
}

This function first replaces any newline characters with return characters. You may want to comment that line out depending on the terminal program that you are using. For thebr@y++ terminal, this line is needed. Then each character that is received is handed off to our homemade usart_putchar function, which sends the character out the serial port. When you pass a string to printf, each and every character of that string will pass through this function one character at a time, and this function will send those characters out of the port for us.

Bind The Stream

The last thing that you have to do is bind mystdout to the stream that you want to use. There are 3 streams available:

We are only going to use stdout at this point in time. In your main code, near the top before the main loop, but this line:

stdout = &mystdout;

This last step is what connects the stdio stream called stdout to our function called usart_putchar_printf.

Putting it All Together

Here is a program listing that uses the above concepts to send formatted output to the terminal using printf.

// ********************************************************************************
// Includes
// ********************************************************************************
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdbool.h>
// ********************************************************************************
// Macros and Defines
// ********************************************************************************
#define BAUD 19200
#define MYUBRR F_CPU/16/BAUD-1
// ********************************************************************************
// Function Prototypes
// ********************************************************************************
void usart_init(uint16_t ubrr);
char usart_getchar( void );
void usart_putchar( char data );
void usart_pstr (char *s);
unsigned char usart_kbhit(void);
int usart_putchar_printf(char var, FILE *stream)
 
// ********************************************************************************
// Global Variables
// ********************************************************************************
static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar_printf, NULL, _FDEV_SETUP_WRITE);
 
// ********************************************************************************
// Main
// ********************************************************************************
int main( void ) {
    // define some local variables
    uint8_t myvalue;
    
    // setup our stdio stream
    stdout = &mystdout;
    
    // fire up the usart
    usart_init ( MYUBRR );
    
    // dump some strings to the screen at power on
    myvalue = 64;
    printf("Here is myvalue as an unsigned integer: %d\n", myvalue);
    printf("Here is myvalue as a char: %c\n", myvalue);
    printf("Here is myvalue in hex: 0x%X\n", myvalue);
    printf("Here is myvalue in octal: %o\n", myvalue);
    
    // main loop
    while(true) {
        // do nothing
    }
}
 
// ********************************************************************************
// usart Related
// ********************************************************************************
void usart_init( uint16_t ubrr) {
    // Set baud rate
    UBRRH = (uint8_t)(ubrr>>8);
    UBRRL = (uint8_t)ubrr;
    // Enable receiver and transmitter
    UCSRB = (1<<RXEN)|(1<<TXEN);
    // Set frame format: 8data, 1stop bit
    UCSRC = (1<<URSEL)|(3<<UCSZ0);
}
void usart_putchar(char data) {
    // Wait for empty transmit buffer
    while ( !(UCSRA & (_BV(UDRE))) );
    // Start transmission
    UDR = data; 
}
char usart_getchar(void) {
    // Wait for incoming data
    while ( !(UCSRA & (_BV(RXC))) );
    // Return the data
    return UDR;
}
unsigned char usart_kbhit(void) {
    //return nonzero if char waiting polled version
    unsigned char b;
    b=0;
    if(UCSRA & (1<<RXC)) b=1;
    return b;
}
void usart_pstr(char *s) {
    // loop through entire string
    while (*s) { 
        usart_putchar(*s);
        s++;
    }
}
    
// this function is called by printf as a stream handler
int usart_putchar_printf(char var, FILE *stream) {
    // translate \n to \r for br@y++ terminal
    if (var == '\n') usart_putchar('\r');
    usart_putchar(var);
    return 0;
}

You can download the complete source code here. It should compile down to 1924 bytes and be ready to download into your chip. Once it is downloaded, every time you reset your AVR you should see the following printed to screen:

Here is myvalue as an unsigned integer: 64
Here is myvalue as a char: @
Here is myvalue in hex: 40
Here is myvalue in octal notation: 100

Things to Remember

Next Up, Accurate Delays

In our next guide, we teach you how to use some of the built-in functions in WinAVR to implement accurate delays in your project. Or head back to our index of AVR Guides.

Comments

avatar

Kurt - 2016-06-21 20:40:52

reply

This works great - but for one problem. When I compile the code, the hex file is 5111 bytes and the object file is 7696 bytes.

That's a tad bigger than 1924 bytes... and quite cramping for a Mega8.

avatar

jurek - 2016-07-29 21:34:16

Hi Hex format and object format aren't bin format which is put into uC flash memory ;). That form is different.

JM

avatar

lior albaz - 2019-01-28 17:59:39

reply

In addition, look at stdio.h file under "vfprintf()" function. you can choose between light version of vfprintf or one with floating point.

light version: -Wl,-u,vfprintf -lprintf_min (~1.3KB @ ATtiny1634 ) full version: - Wl,-u,vfprintf -lprintf_flt -lm (with floating point) (~3KB @ ATtiny1634 )

Add selection to other linker flags.

Leave a reply

Thank you! Your comment has been successfully submitted. It will be approved as soon as possible.

More from Efundies