How to Use printf on STM32

By default, most microcontrollers have no concept of a console, so you have to help them out a bit. When it comes to debugging, outputting information to some kind of console can be extremely useful. One option is to use semihosting with STM32CubeIDE.

However, semihosting can be extremely slow. Another good option is to output debug information over the serial port (UART). We can call the STM32 HAL functions (e.g. HAL_UART_Transmit), but sometimes it’s easier to use the standard C library functions printf, scanf, and so on. To do that, we need to re-write the underlying functions.

Note: the code for this section is taken from Carmine Noviello’s Mastering STM32 book. I am simply updating the process for how to get retargeting working in STM32CubeIDE. All credit goes to Carmine Noviello for his code. You are welcome to write your own retarget.h and retarget.c files, if you wish.

Start a new STM32 project with all the defaults. I am using a Nucleo-L476RG for this example. Enable UART, if needed. For almost all the Nucleo boards, UART2 is tied to the ST-LINK microcontroller, which gives us a virtual COM port over USB. Save and generate code.

The first thing we need to do is disable syscalls.c. This file defines many of the same functions that we are trying to make. If you do not disable it, you will likely get a “multiple definition” error when you try to compile and link. For example:

multiple definition of `_isatty'

Right-click on the syscalls.c file and select Properties. Under C/C++ Build > Settings, check Exclude resource from build.

Exclude files from STM32CubeIDE build

Click Apply and Close. The file name in the project browser should now be grayed out.

This will allow us to not include a file (or folder) when we compile and link in STM32CubeIDE. The process is similar for most other Eclipse-based IDEs.

Create a new file in the Inc directory called retarget.h. Copy the following code into this file:

// All credit to Carmine Noviello for this code
// https://github.com/cnoviello/mastering-stm32/blob/master/nucleo-f030R8/system/include/retarget/retarget.h

#ifndef _RETARGET_H__
#define _RETARGET_H__

#include "stm32l4xx_hal.h"
#include <sys/stat.h>

void RetargetInit(UART_HandleTypeDef *huart);
int _isatty(int fd);
int _write(int fd, char* ptr, int len);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _read(int fd, char* ptr, int len);
int _fstat(int fd, struct stat* st);

#endif //#ifndef _RETARGET_H__

Save this file.

Create a new file in the Src directory called retarget.c. Copy the following code into this file:

// All credit to Carmine Noviello for this code
// https://github.com/cnoviello/mastering-stm32/blob/master/nucleo-f030R8/system/src/retarget/retarget.c

#include <_ansi.h>
#include <_syslist.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/times.h>
#include <limits.h>
#include <signal.h>
#include <../Inc/retarget.h>
#include <stdint.h>
#include <stdio.h>

#if !defined(OS_USE_SEMIHOSTING)

#define STDIN_FILENO  0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2

UART_HandleTypeDef *gHuart;

void RetargetInit(UART_HandleTypeDef *huart) {
  gHuart = huart;

  /* Disable I/O buffering for STDOUT stream, so that
   * chars are sent out as soon as they are printed. */
  setvbuf(stdout, NULL, _IONBF, 0);
}

int _isatty(int fd) {
  if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
    return 1;

  errno = EBADF;
  return 0;
}

int _write(int fd, char* ptr, int len) {
  HAL_StatusTypeDef hstatus;

  if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
    hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
    if (hstatus == HAL_OK)
      return len;
    else
      return EIO;
  }
  errno = EBADF;
  return -1;
}

int _close(int fd) {
  if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
    return 0;

  errno = EBADF;
  return -1;
}

int _lseek(int fd, int ptr, int dir) {
  (void) fd;
  (void) ptr;
  (void) dir;

  errno = EBADF;
  return -1;
}

int _read(int fd, char* ptr, int len) {
  HAL_StatusTypeDef hstatus;

  if (fd == STDIN_FILENO) {
    hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
    if (hstatus == HAL_OK)
      return 1;
    else
      return EIO;
  }
  errno = EBADF;
  return -1;
}

int _fstat(int fd, struct stat* st) {
  if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
    st->st_mode = S_IFCHR;
    return 0;
  }

  errno = EBADF;
  return 0;
}

#endif //#if !defined(OS_USE_SEMIHOSTING)

Save this file.

Your project directory structure should look like the following:

Adding files to STM32CubeIDE directory structureNotice that we’ve added retarget.h and retarget.c. We also removed syscalls.c from the build, but the file still exists in our project directory.

Include stdio.h and retarget.h in your main.c file. You can now use printf and scanf, as shown here:

/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "retarget.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart2;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  char buf[100];
  /* 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_USART2_UART_Init();
  /* USER CODE BEGIN 2 */
  RetargetInit(&huart2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    printf("\r\nYour name: ");
    scanf("%s", buf);
    printf("\r\nHello, %s!\r\n", buf);
    /* USER CODE END WHILE */

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

// ***REST OF INITIALIZATION CODE NOT SHOWN***

Build this project and open a debugging session in STM32CubeIDE. Open your favorite serial terminal program and connect to the Nucleo’s COM port (baud rate of 115200, 8-N-1). You should be greeted by a query. Type out some answer and press enter. Note that you will not be able to see what you typed. However, the program should append your entered text to a “Hello” message.

Using printf and scanf on STM32

Try not to overflow your assigned character buffer. Bad things happen if you do. Or you could write some kind of bounds checking in your code. Anyway, this is just a starting point to hopefully help you with debugging on your STM32 project.

 

 

35 thoughts on “How to Use printf on STM32

Leave a Reply

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