Skip to content

Hussam Talks Tech

Welcome to my Electronics Blog!

Menu
  • Home
  • STM32
  • KiCad Tutorial
  • ESP32
  • Raspberry Pi
  • About
Menu

Redirecting printf to the serial port on the STM32 with Newlib-Nano

Posted on 10.10.202610.10.2026 by halherta

In this blog entry, standard printf redirection to the serial port on STM32 microcontrollers (STM32G474RE specifically) will be covered via the standard Newlib-nano.

Whether you create an new STM32 project with Visual Studio Code (VSC) + STM32Cube plugin or the STM32CubeIDE, both project setups will automatically add newlib-nano and syscalls.c and sysmem.c to the project. This applies when creating an empty project or when using STM32CubeMX to generate an eclipse-based or CMake project.

The syscalls.c file defines a weak _write function:

__attribute__((weak)) int _write(int file, char *ptr, int len)
{
  (void)file;
  int DataIdx;

  for (DataIdx = 0; DataIdx < len; DataIdx++)
  {
    __io_putchar(*ptr++);
  }
  return len;
}

And declares a weak __io_putchar function:

extern int __io_putchar(int ch) __attribute__((weak));

To redirect printf to the UART (serial port), all we need to do is to either re-implement the _write function or implement the __io_putchar. Add a new source file called retarget.c to the project. It’s contents are provided below:

#include <stdint.h>
#include "usart.h"
extern UART_HandleTypeDef hlpuart1;

//uncomment only one of these sections

//Section 1
// here we define __io_putchar which is then called by the 'weak' _write in syscalls.c
// one function call per char in string/buffer 

/*
int __io_putchar(int ch)
{
    uint8_t c = (uint8_t)ch;
    HAL_UART_Transmit(&hlpuart1, &c, 1, HAL_MAX_DELAY);
    return ch;
}
*/

// Section 2
// Here we re-define the whole write function. This definition runs
// instead of the one in syscalls.c because that one is 'weak'
// One function call for the entire string/buffer
int _write(int fd, char *ptr, int len)
{
    (void)fd;
    HAL_UART_Transmit(&hlpuart1, (uint8_t *)ptr, (uint16_t)len, HAL_MAX_DELAY);
    return len;
}

The main difference between both approaches is that __io_putchar will call HAL_UART_Transmit once for every character transmission, whereas the _write function as implemented above will call HAL_UART_Transmit once for every buffer transmission. My personal preference is to re-implement the _write function as it makes significantly less calls to HAL_UART_Transmit.

In addition to that we’ll need to make sure that the retarget.c source file is added to the project’s CMakeLists.txt file for a STM32CubeMX CMake-based project (VSC + STM32Cube plugin), or add the source file correctly in the eclipse-based build system of the STM32CubeIDE.

When using the STM32CubeIDE, also make sure to check the Use float with printf from newlib-nano (-u_printf_float) checkbox is checked. It can be found by going to Project->Properties via the top menu. In the Properties dialog, go to C/C++ Build->Settings in the right list view. Under the Tool Settings tab in its list view, select MCU/MPU Settings. You’ll find the check box there.

When using a CMake based project created via STM32CubeMX, simple add the following to the main CMakeLists.txt:

# add this to enable newlib-nano's float printing
target_link_options(${PROJECT_NAME} PRIVATE   
    -u _printf_float
)

And that’s it! Here’s a sample STM32CubeMX CMake-based project with this configuration.

Separating generated and user code (app_main.c)

The github repo linked above contains an CMake-based project where the STM32CubeMX auto generated initialization code in main.c is separated from the application / user code in app_main.c. This approach allows the user to reconfigure the project with STM32CubeMX, without overwriting the user’s code. As an added bonus,the user/programmer will not have to navigate the code wizard’s ‘guard rail’ comment sections. i.e. main.c mostly contains initialization code generated by STM32CubeMX. It looks like:

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_LPUART1_UART_Init();
  /* USER CODE BEGIN 2 */
  app_main();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Notice how the fake main function app_main is called in the main function. app_main contains user code (application logic) and is implemented in app_main.c:

#include "app_main.h"
#include "stm32g4xx_hal.h"
#include <stdio.h>

void app_main(void){
    setvbuf(stdout, NULL, _IONBF, 0);
    char *str = "A dog and a lazy fox";
    char ch = 'a';
    int  whole_num = 420000;
    float decimal_num = 123.4567;
    printf ("Printf tutorial");
    while(1){
        printf("string: %s\r\nchar: %c\r\nint:  %d\r\ndouble: %3.3f\r\n", str,ch,whole_num,decimal_num);
        HAL_Delay(500);
    }

}

You’ll notice that I called the setvbuf(stdout, NULL, _IONBF, 0) function at the beginning of app_main. This function is implemented in stdio.h and ensures that no stream buffers are allocated for printf. It reduces the memory footprint of printf and forces printf’s formatted string to print straight out to the UART.

Here’s the memory usage (release build) when building the above project code for the Nucleo-G474RE (STM32G474RE):

[build] Memory region         Used Size  Region Size  %age Used
[build]              RAM:        3032 B       128 KB      2.31%
[build]            FLASH:       22344 B       512 KB      4.26%

Newlib-nano UART redirection with FreeRTOS

While there’s a way to make printf re-entrant when using FreeRTOS, it’s a lot easier to implement a different logging function entirely using newlib-nano’s vsnprintf function in a log.c file:

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

extern UART_HandleTypeDef hlpuart1;

#define LOG_BUF_SIZE 128

static SemaphoreHandle_t log_mtx;
static char log_buf[LOG_BUF_SIZE];   /* shared static buffer, so no big task stacks needed */

void log_init(void)
{
    log_mtx = xSemaphoreCreateMutex();
    configASSERT(log_mtx);
}

int log_printf(const char *fmt, ...)
{
    int running = (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED);
    if (running) xSemaphoreTake(log_mtx, portMAX_DELAY);

    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(log_buf, sizeof log_buf, fmt, ap);
    va_end(ap);

    int len = n;
    if (n >= (int)sizeof log_buf) {          /* truncated */
        len = sizeof log_buf - 1;            /* 127 chars + NUL */
        log_buf[len - 1] = '~';              /* replace the last char */
    }

    if (len > 0) {
        HAL_UART_Transmit(&huart2, (uint8_t *)log_buf, (uint16_t)len, HAL_MAX_DELAY);
    }

    if (running) xSemaphoreGive(log_mtx);
    return n;    /* still returns the untruncated length, like printf */
}

The log_init function initializes the mutex used to synchronize access to the UART via HAL_UART_Transmit.

In log_printf, the vsnprintf function is then used along with variadic functions to create a formatted string in the log_buf buffer and sends it to the uart with HAL_UART_Transmit. If the original fmt string is too long, the code truncates it and prints a ~ at the end of the string to indicate that truncation has happened.

In future entries the nanoprintf library will be examined for retargeting printf to UART as an alternative to newlib-nano. It’s use for deferred logging in FreeRTOS will also be covered.

Category: C and C++, STM32

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

October 2026
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« Sep    
  • C and C++
  • Electronics
  • ESP32
  • KiCad
  • KiCad Tutorial
  • Linux
  • Micropython
  • Python
  • Raspberry Pi
  • STM32

Archives

  • October 2026
  • September 2026
  • August 2026
  • January 2026
  • November 2025
  • October 2025
  • September 2025
  • August 2025
  • July 2025
© 2026 Hussam Talks Tech | Powered by Minimalist Blog WordPress Theme