Getting printf to work on an AVR can be confusing. We'll show you exactly how to do it.
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:
- usart_init
- usart_getchar
- usart_purchar
- usart_pstr
- usart_kbhit
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:
- stdout
- stdin
- stderr
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
- printf has a huge overhead if you use it even just once. If you decide to use it, go ahead and use it often.
- You must include stdio.h for any of this to work.
- There are 3 steps to getting printf to work:
- Include stdio.h
- Create a global stream variable that references a stream handling function.
- Assign stdout to your stream variable
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.