C Programming: The printf and scanf Family of Functions
In the course of computer programming, the idea of formatting a string comes up very often. When writing in any programming language, whether it be for a computer or a microcontroller, you are always storing data in specialized data structures (int, double, float, lists) that are not very readable for humans. This means that whenever you are taking input from a user, or displaying data for the user, you need to be able to format a string in different ways to represent your data. Converting to a human-readable format can also be useful when passing information between devices, such as sending a sensor reading from a microcontroller to a PC.
The printf() and scanf() family of functions are part of the standard C library, and make it easy to take input from the user, and display data to the user. Learning how to effectively use these functions can open up a whole world of possibilities when building new projects.
We make extensive use of the printf and scanf functions in many of our other tutorials, and we highly suggest that you look at those for further examples.
These are some projects which highlight microcontroller output (printf_P and fprintf_P):
And here are some projects which highlight microcontroller input (scanf_P):
printf Examples
The printf function together with the idea of a format string is a very versatile combination, and can be used to display data in pretty much every way you might ever want to. Here, we present some simple examples of the ways printf is often used:
Variable Initialization | printf Example | Output | Description |
---|---|---|---|
int8_t i = 42; | printf_P(PSTR("My Integer: %d"), i); | My Integer: 42 | Printing a standard signed integer |
uint8_t u = 7; | printf_P(PSTR("My unsigned integer: %u"), u); | My unsigned integer: 7 | Printing a standard unsigned integer |
int16_t i = 42; | printf_P(PSTR("My Integer: %d"), i); | My Integer: 42 | Printing a standard 16-bit signed integer |
int32_t i = 42; | printf_P(PSTR("My Integer: %ld"), i); | My Integer: 42 | Printing a standard 32-bit ("long") signed integer |
double pi = 3.14159; | printf_P(PSTR("I like %lf"),pi); | I like 3.14159 | Printing a double precision floating point number |
double pi = 3.14159; | printf_P(PSTR("I like %.2lf"),pi); | I like 3.14 | Printing a double precision floating point number limited to two digits after the decimal point |
uint8_t prime = 7; double pi = 3.14159; |
printf_P(PSTR("%u %.2lf"),prime,pi); | 7 3.14 | Using more than one variable in a single format string |
The real magic of the printf and scanf functions can be unleashed with effective use of the format string, and all it takes is a little basic understanding of how these special strings work. A format string is basically just a string with placeholders where data should be subsituted in. These placeholders contain all the information necesary about how to display the data. Here is the basic anatomy of a placeholder:
"%[flags][width][.precision][length]type"
All placeholders start with a % sign, they may then be followed by special flags, then a number representing the width then a . then a number representing the length and then finally the type of value you are representing.
The only required parts are the % and the type, and most of the time those are the only ones you will see used. For example "%d" means decimal integer, and "%f" means floating point number. (NOTE: to read in a double with scanf, you must use "%lf" to indicate that it's a long float. This is a common mistake for new microcontroller programmers -- don't get caught!) One or more of the other parameters can be specified if you want to have more fine tuned control over how the string looks. The tables below show the possible values of the parameters and what they do.
Type | Use | Example |
---|---|---|
d | Signed decimal integer (int8_t, int16_t) | -7 |
u | Unsigned decimal integer (uint8_t, uint16_t) | 7 |
f | Floating point number (float) | 7.4 |
c | Character (char) | n |
x or X | Hexadecimal integer | beef or BEEF |
o | Octal | 777 |
e or E | Scientific notation | 6.022e23 or 2.99E9 |
Flag | Description |
---|---|
# | Forces "alternate form" of conversion. For %x and %X, it puts 0x or 0X before the hex output. It does not affect most of the other types. |
0 | Pads the conversion on the left with zeros (instead of spaces as usual) |
- | The conversion is left-aligned within the display width (instead of right-aligned as usual) |
space | Puts a space in front of signed conversions (%d) if the number is positive (where a - sign would appear for a negative number). |
+ | Forces the conversion to display a sign (+ or -) |
* Note that more than one flag can be used, but some do override others. Check the avr-libc documentation (linked below) for these special cases.
The width parameter specifies the minimum character width that conversion will take up. By default the conversion will be as short as it can be to still accommodate the precision required. For example "%d" with a 7 passed in will simply become "7", however "%3d" specifies a minimum width of 3 so passing it 7 will result in " 7" (two spaces followed by a 7). Flags can be used to specify how the number is adjusted and padded.
The precision specifies the number of digits after the decimal in a floating point number. A "." must precede the precision specifier: for example "%.2f" will force any decimal passed to it to have two digits after the decimal point.
Escape Sequences
Sometimes it is necessary to use special characters in your format string to get the characters you want past the compiler. For example, to put a " double-quote in your string, you can't just write it directly in your code, because the " character is also used to denote the start and end of a string itself! So to get around this issue, we have to "escape" that character with a backslash. Here is a table of commonly used escape sequences:
escape code | output |
---|---|
\" | " |
\\ | \ |
\t | [tab] |
\r\n | [new line] |
(Although technically not an escape character, %% in a format string will output a %, since a single % will indicate the start of a substitution placeholder.)
Newlines are different depending on the operating system. \r\n is the newline sequence on Windows files as well as the standard on serial connections. \n is used as the newline character on Unix and OSX.
As we explain in the video, there is a slight difference between using printf in "normal" C programming for a computer, and in microcontroller programming. That has to do with the fact that strings are typically stored in program (flash) memory on a microcontroller, so we have to use a little bit of notation to indicate that. For example, on a normal PC you might write:
printf("Hello, world!\r\n");
But if you did this on your microcontroller, you'd probably be disappointed to see that it didn't work as you expected. That's because RAM is a precious resource on a microcontroller, and we would prefer to put something constant like that in our flash memory, instead of replicating it both in flash and in RAM each run. (A more detailed technical explanation can be found on our forum.) So to do the same on the microcontroller, you would write:
printf_P(PSTR("Hello, world!\r\n"));
We made two changes: first, by surrounding the string with PSTR(...), we indicate to the compiler that we want that string stored only in program memory. Second, we changed printf to printf_P, which indicates that we want a variant of the printf function that accepts a string stored in program memory instead of RAM like the "normal" printf function expects.
Trying printf on your PC
If you've got a C compiler such as gcc on your computer (Windows users -- grab Cygwin, Mac OS X and Linux users are probably ready to go), you can make a simple C file and try writing little programs to test your printf/scanf skills. For example, just make a file called "test1.c" and enter these contents:
#include <stdio.h> #include <string.h> int main() { char x[32]; int y = 0; double z = 0.0; printf("What's your name? "); // note the use of fgets instead of scanf("%s",...) // to prevent buffer overflows by specifying a maximum length! fgets(x, 32, stdin); // remove newline x[strlen(x)-1] = '\0'; printf("Hi, %s! How old are you? ", x); scanf("%d", &y); printf("What's your favorite number? "); scanf("%lf", &z); printf("Nice to meet you, %s! You are %d years old and like the number %f.\n", x, y, z); return 0; }
Then, open a console, find that directory, and run:
gcc -o test1 test1.c; ./test1
to compile and run your program. This is an easy way to experiment with C programming even if you don't have one of our educational microcontroller kits yet.
Source Code
You can download the source code here. Start with a standard NerdKits project and Makefile (included with the kit) and plug in this source code.
More Documentation
You can also refer to the avr-libc documentation for these functions, specifically the vfprintf documentation for printf_P and fprintf_P, and the vfscanf documentation for scanf_P.
More Videos and Projects!
Take a look at more videos and microcontroller projects!
Comments
Did you know that negative numbers are represented in two's complement notation in binary? Learn more...
|