Hello! Welcome to the next lesson in our journey to becoming a professional embedded systems engineer.
In the last session, you built an impressive command-line interface (CLI) that can parse commands and arguments. You used your custom UART functions to send feedback to the user, which works, but can be a bit rigid. For example, printing a variable's value requires manually converting it to a string. What if you could use a familiar, powerful tool for this instead?
This lesson directly addresses the learning outcome: Redirect printf output to UART for streamlined debugging. We will connect the standard C library's printf function to your STM32's UART peripheral. This is a fundamental technique used across the industry to create flexible debug logs and sophisticated user interfaces with minimal effort.
1. Why printf Doesn't Just Work
In a typical desktop C program, printf "just works" because the operating system provides the underlying functions to write characters to the console. In a bare-metal embedded system, there is no OS. The C standard library is present, but its I/O functions like printf have no underlying hardware driver to talk to.
printf itself is a high-level function responsible for formatting a string (e.g., converting %d to a number's decimal representation). Once formatted, it passes the final string to a lower-level function to be physically sent somewhere. Our job is to implement that low-level function and tell it to use our UART peripheral.
2. The Bridge to Hardware: Overriding _write
In the GCC toolchain used by STM32CubeIDE, the key low-level function is _write. You can find it defined in a file called syscalls.c within your project's Core/Src directory. If you open it, you'll see a function signature like this:int _write(int file, char *ptr, int len)
Notice the __attribute__((weak)) keyword before the function. This weak attribute tells the linker that this implementation is a placeholder. If the linker finds another function with the exact same name without the weak attribute (a "strong" implementation), it will discard the weak one and use the strong one instead.
This is our entry point. We will provide our own strong implementation of _write in main.c, and inside it, we will write the code to transmit the character data over UART.
3. Implementing _write for UART
Let's watch a video that walks through this exact process. The presenter will show you how to find the weak _write function, copy its prototype, and implement a strong version that sends data using the UART transmit register, just as you've done before.
Redirect Printf To UART And Send Serial Data Via On-Board USB Port (STM32, STM32CubeIDE)
This video from Z-The-Programmer provides a clear, step-by-step guide to redirecting printf to UART in a bare-metal style. It explains the role of syscalls.c and demonstrates the implementation.
Watch the first 2 minutes and 15 seconds (00:00 - 02:15). Focus on these key steps: Including stdio.h. Locating the weak _write function in syscalls.c. Copying its signature into main.c and creating a new implementation. Inside the new function, looping through the data and writing each byte to the UART transmit register (USARTx->DR or USARTx->TDR) after checking the Transmit Data Register Empty (TXE) flag.
Based on the video, add the following code to your main.c. Place it in the /* USER CODE BEGIN 4 */ section to keep your code organized.
/* USER CODE BEGIN PFP */
#include <stdio.h> // Make sure this is included at the top
/* USER CODE END PFP */
// ... other code ...
/* USER CODE BEGIN 4 */
/**
* @brief Redirects the C library printf function to the USART.
* @param file Standard file descriptor.
* @param ptr Pointer to the data to be written.
* @param len Length of the data.
* @retval The number of characters written.
*/
int _write(int file, char *ptr, int len)
{
(void)file; // Unused parameter
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
// Wait until the transmit data register is empty
while (!(USART2->SR & USART_SR_TXE));
// Write the character to the data register
USART2->DR = (*ptr++);
}
return len;
}
/* USER CODE END 4 */
4. The Buffering "Gotcha"
Now, try replacing a uart_send_non_blocking call in your main loop's CLI logic with a printf call. For example: printf("System OK");.
When you run the code, you might be surprised to see that nothing appears on your terminal! This isn't a bug in your _write function. It's due to a standard C library feature called line buffering. By default, the standard output stream (stdout) is line-buffered, which means it collects characters in a temporary buffer and only sends them out when it encounters a newline character (\n) or the buffer becomes full.
You have two ways to solve this:
-
Always add a newline: Change your call to
printf("System OK\r\n");. The\nwill flush the buffer, and the string will be sent. -
Disable buffering: At the beginning of your
mainfunction, add the following line. This tells the C library not to use any buffer forstdout, so characters are sent immediately.// Add this after your HAL_Init() and SystemClock_Config() calls setvbuf(stdout, NULL, _IONBF, 0);
For most interactive CLIs, disabling buffering is the preferred approach. The video you watched discusses this exact issue.
Redirect Printf To UART And Send Serial Data Via On-Board USB Port (STM32, STM32CubeIDE)
Let's continue with the same video to see the explanation and solution for the line buffering issue.
Watch from 02:15 to 03:18. The presenter first demonstrates the problem, then shows both solutions: adding a newline character and using the setvbuf function to disable buffering entirely.
5. Enabling Floating-Point Support
Another common issue is that, by default, the printf implementation included in embedded projects is a "minimal" version that does not support printing floating-point numbers (%f, %g, etc.). This is done to save Flash memory space.
If you try to printf("%f", 3.14);, you will likely get a question mark, a build error, or a hardfault. To enable full printf support, you need to adjust a linker setting.
This process is straightforward in STM32CubeIDE. Both of the following videos demonstrate how to do it.
Redirect Printf To UART And Send Serial Data Via On-Board USB Port (STM32, STM32CubeIDE)
This clip from the first video shows how to enable floating-point printing in the project settings.
Watch from 03:18 to 04:07. The presenter shows you how to navigate to Project Properties > C/C++ Build > Settings > MCU GCC Linker > Miscellaneous and add the linker flag -u _printf_float.
For a slightly different perspective, here is another video showing the same setting. This one uses the HAL and a different printf implementation method, but the linker setting for floats is identical.
printf() over the UART using STM32
This second video, from AI2MCU EMBEDDED ACADEMY, also covers enabling float support. It's good to see the same solution in a different context.
Watch from 09:58 to 11:25. The presenter demonstrates the issue and then navigates to the linker settings to add the -u _printf_float flag.
To enable float support:
- Right-click your project in the Project Explorer and go to Properties.
- Navigate to C/C++ Build -> Settings.
- Under the Tool Settings tab, select MCU GCC Linker -> Miscellaneous.
- In the "Other flags" box, add
-u _printf_float. - Click "Apply and Close" and rebuild your project.
6. An Alternative Method: __io_putchar
It's worth knowing that there is another common way to retarget printf, especially in projects that heavily use the STM32 HAL. This method involves implementing a function called __io_putchar.
How to redirect the printf function to a UART for debug messages
This official ST Community article explains how to redirect printf by implementing __io_putchar. You don't need to change your code, but it's important to recognize this alternative approach as you will likely encounter it in example projects.
Quickly read through section 7, which provides the __io_putchar implementation using HAL_UART_Transmit. Then, look at the first comment in the comments section. Notice how the commenter suggests that overwriting _write is sometimes a more robust solution, which validates the approach we've taken.
The _write method you implemented is generally more portable and fundamental, as it's part of the standard system call interface. The __io_putchar method is more specific to the ARM Compiler/GCC toolchain's C library implementation.
7. Hands-On: Upgrading Your CLI
Now, let's apply what you've learned.
- Go back to the
process_commandfunction you created in the last lesson. - Replace all of your
uart_send_non_blockingcalls with equivalentprintfstatements.- Example:
uart_send_non_blocking((uint8_t*)"OK\r\n", 4);becomesprintf("OK\r\n");
- Example:
- Add a new command to your CLI that takes a numeric argument or prints a system variable to see formatted output in action. For example, a command
repeat <number>that prints a message that many times, or a commanduptimethat prints how many seconds the system has been running.
This will make your code significantly cleaner, more readable, and easier to maintain.
Conclusion
You have now mastered a critical skill for professional embedded development. By redirecting printf, you've unlocked a powerful and convenient tool for debugging and creating interactive command-line interfaces.
Key Takeaways:
printfrelies on a lower-level function,_write, to interact with hardware.- By providing a "strong" implementation of
_write, we can override the defaultweakversion and direct its output to our UART peripheral. - The
stdoutstream is often line-buffered by default, which can be disabled usingsetvbuffor immediate output. - To use
printfwith floating-point numbers, you must enable support via the-u _printf_floatlinker flag.
Preview of the Next Lesson:
We have thoroughly covered asynchronous serial communication with UART. In the next module, we will explore a different class of peripherals: synchronous serial protocols. We'll begin with the SPI (Serial Peripheral Interface) protocol, learning how to configure it in master mode and communicate with external devices like memory chips or sensors.