Input and Output in C¶
Learning Objectives: After reading this material, you will be able to produce formatted output and understand its usefulness in embedded systems.
In previous material, we have already succeeded in printing text to the computer screen using the C language’s
printf
function. Next, we will delve a bit deeper into the topic and see how to produce formatted output. In this context, we will introduce the C language's standard library stdio functions made for formatted output and reading input. Since these functions are part of the standard, they should work the same way in all systems.In embedded systems, it is common to encounter various implementations of standard libraries, where not all functionalities are included. Due to resource constraints, for example, floating-point handling, screen output, keyboard input, etc., may be omitted. Instead of the standard library, custom libraries for peripherals are often provided, implementing similar functionality, such as functions for printing a string to an LCD screen.
At this stage, we do not know all the peculiarities of the C language, but we can manage quite well with the functions covered in this material.
Formatted Output¶
In C, there are several functions for producing formatted output for slightly different purposes. Here, we will cover two common cases, the latter of which is important to know for efficient embedded programming.
The printf Function¶
The
printf
function outputs a string to a selected peripheral device (screen) in workstation environments to its output stream, from which the operating system then picks up our output and delivers it to the peripheral device.Let's look at an example of using printf...
uint8_t a=97;
printf("The number is %d and plus one is %d\n", a, a+1);
// This prints to the screen:
The number is 97 and plus one is 98
More specifically, the syntax of the
printf
function is:printf("format string", argument1, argument2, ..., argumentN);
The format string specifies what kind of string we want to print. The string can contain plain text, control characters (newline, etc.), and variable values (and of course also constant values). For variables, the format string indicates how the variable values are formatted as part of the string output.
In the example above, the format string is
"The number is %d and plus one is %d\n"
. It mixes text with the values of the variables (here a
and a+1
). The placeholder %
indicates where the variable's value will be printed, and the additional characters after the percentage sign specify how the value will be formatted. In the example, the placeholder %d
tells us that we want to print the variable's value as an integer.There is a whole set of these placeholders (format specifiers) in C, with the most common listed below.
Placeholder | Variable Type |
d,i | integer (int) |
u | unsigned integer (uint) |
f | floating-point number (float, double) |
x | hexadecimal number |
c | character (according to ASCII code) |
s | string (char array) |
p | pointer, memory address (we'll return to this) |
% | percentage sign |
Note! There is no standard formatting for printing a binary number. This is a great exercise for self-study...
Example.
uint8_t x=97;
printf("x as an integer is %d\n",x);
printf("x as a hexadecimal is %x\n",x);
printf("x as a floating-point number is %f\n",(float)x);
float y = 97.126;
printf("y as a floating-point number is %.2f\n",x);
// This prints to the screen:
x as an integer is 97
x as a hexadecimal is 61
x as a floating-point number is 97.000000
y as a floating-point number is 97.13
It is important to note that
printf
actually performs a type conversion from the variable type to the formatting variable type if necessary! Well, to add some complexity to the matter, the printf
function performs the conversion according to its own rules, and the result is not always what is desired. Therefore, it is better to enforce type conversion yourself. In the example above, the floating-point output was achieved by converting x
to the desired variable type using the (float)
operator.And when using derived variable types, the compiler may sometimes require type conversion, or a warning will be issued during the compilation phase.
Format Specification can be refined with additional information:
Specifier | Additional Formatting |
n | minimum width of the integer field (including decimal part and point), right-aligned |
-n | minimum width of the integer field (including decimal part and point), left-aligned |
0n | minimum width of the integer field (including decimal part and point), filled with leading zeros |
.n | minimum width of the decimal part |
l | length of the integer (long) |
h | length of the integer (short) |
+ | display with a sign (+ or -) |
Example.
uint8_t x=97;
printf("x as an integer is %05d\n",x);
printf("x as a floating-point number is %.2f\n",x);
// This prints to the screen:
x as an integer is 00097
x as a floating-point number is 97.00
Character constants can be used to control the output. Like placeholders, constants can be placed anywhere in the output. Below are some common character constants, but this is not an exhaustive list.
Character Constant | Purpose |
\n | newline |
\t | tab |
\\ | backslash |
\? | question mark |
\" | quotation mark |
The sprintf Function¶
At the beginning, we promised to cover two different versions of the standard library’s output functions, so now we introduce the second one: the
sprintf
function. This function has extremely useful applications when programming embedded systems.This function works the same way as
printf
, but with the additional argument of a string where the output is stored. The function formats the text into another string... okay, but why?In embedded systems, outputs are directed to a peripheral device through its own (driver) library. Often, these driver libraries do not implement nearly all the output features of the standard library, even though they might have a function named printf. As mentioned earlier, for example, floating-point output functionality might be completely missing. Therefore,
sprintf
is very handy because it allows us to pre-format a string for output to the peripheral device. The peripheral's output function then receives the formatted string as an argument, and it doesn't need to modify the output. Easy and convenient!In the syntax,
sprintf
differs in that it takes as an argument the string where the output is stored, before the format string.Example: Printing the value of pi to an LCD screen. Assume the LCD library has a
lcd_printf
function for printing a string. Also, let's define the screen width in characters as the constant LCD_MAX_WIDTH
, so now the variable lcd_str
can hold exactly one line of text for the display.#define LCD_MAX_WIDTH 16
float pi = 3.14159;
char lcd_str[LCD_MAX_WIDTH];
sprintf(lcd_str,"%f\n",pi);
lcd_printf(lcd_str);
This example is worth understanding because this is exactly how we will work later in the course. More on this later...
Note! You must be particularly careful that the length of the output string does not exceed the allocated space when placeholders are replaced with argument values. For example, the placeholder
%08d
outputs eight characters, even though the placeholder itself is only four characters long. Such an overflow (writing past the end of the string) can be a difficult-to-trace bug because the function might overwrite another variable's value in memory, which can then disrupt the program's operation and, in the worst case, the device itself.Dealing with Overflow: snprintf¶
While
sprintf
is very useful, it comes with a significant risk in embedded systems: the potential for buffer overflows. To mitigate this risk, we can use the safer alternative, snprintf
. This function allows us to specify the maximum number of characters to write to the buffer, including the null terminator, preventing the possibility of writing past the end of the buffer.Let's revisit our previous example where we formatted the value of pi for an LCD screen:
#define LCD_MAX_WIDTH 16
float pi = 3.14159;
char lcd_str[LCD_MAX_WIDTH];
snprintf(lcd_str, sizeof(lcd_str), "%f\n", pi);
lcd_printf(lcd_str);
In this example, the second argument to
snprintf
is sizeof(lcd_str)
, which ensures that the output does not exceed the size of lcd_str
. If the formatted string (including the null terminator) is too long, snprintf
will truncate it to fit within the buffer, while still ensuring that the string is properly null-terminated.Hox! It is crucial to use
sizeof
when passing the buffer size to snprintf
, as hardcoding the size can lead to errors if the buffer size changes in the future. This practice helps prevent tricky bugs related to buffer overflows, which can be especially challenging to debug in embedded systems.Embedded Matters¶
Let's now look at what else, and very importantly, makes formatted output useful in embedded systems programming.
Program Debugging¶
In addition to the above-mentioned use, the
sprintf
function is an indispensable tool for debugging the operation of an embedded system, that is, monitoring its behavior during runtime and investigating error situations.Recall from the first lecture that the execution of an embedded program on a device can be controlled using a debugger device. Compared to workstation programming, it is difficult to monitor the execution of an embedded program and the inner workings of the physical device "remotely" from a workstation. Therefore, debuggers usually offer (various) ways to send messages from the device to the programming environment through the debugger. For example, strings can be printed from the running program on the device to the console window of the programming environment.
Example: Now, for instance, in a
for
loop, we can examine the sensor data values we have collected in almost real-time. Suppose the values for different axes of the accelerometer have already been retrieved into the variables acc_x
, acc_y
, and acc_z
, and they are printed into the string debug_str
. The System_printf
function prints from the SensorTag to the console window of the CCS development environment, as we will see in later material.char debug_str[80];
...
sprintf(debug_str,"%.2f %.2f %.2f",acc_x,acc_y,acc_z);
System_printf(debug_str);
...
In Pico SDK, the same result can be achieved by using the function
printf
in place of System_printf
. Well, in this case the function sprintf
is not really needed, as printf
can also handle the printing of numeric values into a string, but we are doing it like this here just for comparison.char debug_str[80];
...
sprintf(debug_str,"%.2f %.2f %.2f",acc_x,acc_y,acc_z);
printf(debug_str);
...
Note! You wouldn’t believe how many bugs in exercises and projects over the years have been solved using debug intermediate prints. Often, a beginner programmer might think, "nope, I'll figure this out in my head," which then takes time. You can certainly do this in the course, but in the working world, even professionals prefer to rely on logging the program’s behavior, from which the problem can be much easier to spot. In larger software projects in the working world, logging is often the only way to debug, especially if the program consists of multiple modules running simultaneously.
The Comma is Important¶
In the embedded world (and elsewhere), related information, such as the values of different axes of an accelerometer at the same moment in time, is useful to handle as a single package (data structure) in programs. We will go into data structures in more detail in later material, but let’s see how the
sprintf
function relates to this.In general, to transfer related information, the computer science field has introduced the Comma Separated Value (CSV) format, where a comma separates the different fields of the data structure. CSV has been proposed for various uses, but you don't need to know the standards in this course. Clear uses for the CSV format in embedded systems include string-based communication with peripherals and wireless communication.
Example: The values of the different axes of the accelerometer mentioned above could be represented in CSV format as follows, where the comma-separated numeric values correspond to the axis values.
0.342,0.665,0.734
It can be observed that implementing such CSV-formatted data transmission is easy in C using the
sprintf
function.Example: Again, the values of the 3 axes of the accelerometer have been stored in the variables
acc_x
, acc_y
, and acc_z
, and let’s create a CSV-formatted string from them according to the example above.char message[80];
sprintf(message,"%.3f,%.3f,%.3f\n",acc_x,acc_y,acc_z);
Note! You will definitely need the
sprintf
function for this purpose in the course project...Handling Input¶
Well, moving on to another topic, the
stdio
library functions can also be used to request input from the keyboard (or, in fact, from any other peripheral device). In low-level programming, these functions are very rarely, if ever, used, so this material is just for informational purposes.The
getchar
function provides a way to request input from the user one character at a time.Example: With the following
do-while
structure, the characters entered by the user are stored in the c
variable one at a time until the user presses the Enter key (Finnish: palautusnäppäin).uint8_t c = 0;
do {
c = getchar();
} while (c != '\n');
...so the
do-while
structure has its convenient sides. If we wanted to capture the entire input here, it should be stored in a string instead of a single character variable.Note that when we test the program on a workstation, the keyboard echoes the keystrokes onto the screen. In different operating systems, we can control how the console window input is displayed on the screen, and the echo can be turned off if necessary, for example, when asking the user for a password.
Formatted Input¶
The
scanf
function is a standard library function that can be used to request formatted input from the user and store it in variables in the desired format. This is just for your information; we will not be using this function in the course.The function's syntax is similar to
printf
, with small but essential differences when using the same formatting options. Additionally, the &
operator must be placed before the variables in the function call, but we'll come back to that later.scanf(format_string, &variable1, &variable2, ...);
Example: Ask the user for an integer on a workstation and print it to the screen.
uint32_t number = 0;
scanf("%ld", &number);
printf("%ld\n", number);
The
scanf
function can be used to request formatted inputs as well, but the formatting is easy to break because the function itself cannot control what the user actually types. Below, if the user does not follow the additional specifications given to the function in the input, for example, the number's length being greater than two characters, the function will get confused.uint8_t day = 0;
char month[12];
uint16_t year = 0;
printf("Enter the date in the format: 24.December 2017? ");
scanf("%2d.%s %4d", &day, month, &year);
Not surprisingly, there is also a function
sscanf
for storing the input in a string.In Conclusion¶
For those more interested in the secrets of
printf
's formatting, additional information can be found in the document Secrets of printf.Give feedback on this content
Comments about this material