Easily Use printf on STM32

Aside from blinking an LED, printing informative messages to a serial console is perhaps the simplest, most straightforward, and most common technique employed when debugging embedded projects. While most platforms have their own APIs capable transmitting data over a UART bus, they all lack the power and popularity of the printf() function. Unfortunately, to utilize this function in an embedded C application, simply including the stdio library is usually insufficient. In the case of an STM32 project, a few extra lines of code are required.

For those wishing to both send and receive formatted data over a UART bus, check out the companion article Easily Use scanf on STM32 as well!

Procedure

This procedure covers the preliminary step of configuring and enabling the appropriate UART peripheral, the primary step of mapping that UART instance to the printf() function, and an optional secondary step for those wishing to print floating point numbers. It was written for projects developed with STM32CubeIDE (version 1.9.0), but should be extendable to other environments as well.

0. Establish a UART Instance

To properly include the printf() function from the stdio library as described in the next step, a UART (or USART) peripheral must be configured to transmit the formatted string. Typically when working on an ST development board, the UART peripheral with RX and TX lines connected to the ST-LINK programmer/debugger is chosen for this purpose. This allows strings to be sent to a serial console thanks to the ST-LINK’s USB Virtual COM port bridge. Luckily, when starting a new project in STM32CubeIDE or STM32CubeMX, this UART instance is configured by default! You as the programmer just have to take note of which UART was configured and move on to the next step.

For those with an existing project and no UART configured, simply open the project’s .ioc file and make the modification outlined in Figure 1. In particular,

  • Select the appropriate peripheral instance
  • Set the mode and configuration parameters (the settings shown in Figure 1 are most commonly used)
  • Ensure the appropriate GPIO pins are configured as UART RX and TX.

To finish, save the .ioc file to generate code for the project.


Figure 1: An example of enabling and configuring the desired U(S)ART peripheral.

1. Redirect printf() to UART Instance

With the UART ready to go, adding printf() is just a matter of adding a few lines of code.

a. Add #include <stdio.h> to the Includes section at the top of the main.c file (Figure 2).

include
Figure 2: Including the stdio library

b. Copy and paste the following code into the main.c file before the main() function but after the UART handle declaration. A sensible option is in the Private Function Prototypes section, as shown in Figure 3 below. Note that the UART handle must be changed from &huart? to the handle of the desired UART peripheral (e.g., &huart2).

#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

putchar_prototype
Figure 3: Adding retargeting code to main.c file.

The printf() function should now behave as expected for all but the floating point format specifiers. To enable those, continue on to the next step.

2. Enable Floating Point Support (Optional)

To illustrate the issue with floating point formatting, we can use the example code provided on the printf() reference page.

printf("Characters: %c %c\n", 'a', 65);
printf("Decimals: %d %ld\n", 1977, 650000L);
printf("Preceding with blanks: %10d\n", 1977);
printf("Preceding with zeros: %010d\n", 1977);
printf("Some different radices: %d %x %o %#x %#o\n", 100, 100, 100, 100, 100);
printf("floats: %4.2f %+.0e %E\n", 3.1416, 3.1416, 3.1416);
printf("Width trick: %*d\n", 5, 10);
printf("%s\n", "A string");

After adding this code to our project, a warning appears explaining that “the float formatting support is not enabled”. If we continue to build and run the project, the output in the serial console confirms that the floating point numbers will not be printed (Figure 4).

ex_output_noFloat
Figure 4: Example output without float formatting support enabled.

This may not be a problem if floating point numbers are not utilized in your application. However, if they are and you would like to enable them, simply right-click on the project name in the Project Explorer and choose Properties. Choose Settings in the “C/C++ Build” category and select MCU Settings under the Tool Settings tab. Check the box next to “Use float with printf from newlib-nano”, as shown in Figure 5 below. Click Apply and Close.

Note that enabling float formatting support for printf() will consume a fair amount of additional memory. Specifically, it will consume approximately 0.35 KB of RAM and 10.30 KB of Flash on top of what the printf() function consumes without floating point support. This may be an issue for lower-end devices.


Figure 5: Enabling float formatting support.

Now, after re-building the project and running the code, the floating point values are properly formatted and displayed as Figure 6 demonstrates.

ex_output_float
Figure 6: Example output with float formatting support enabled.