Easily Use scanf on STM32

While there are many approaches to interfacing with embedded systems, one of the simplest and most versatile is with user input from a serial console. Rather than manually parse the incoming ASCII characters and convert them to the appropriate data format, the scanf() function is often used to do so automatically. However, like the printf() function, some additional code must be supplied to utilize this stdio library function. Intended as a companion to Easily Use printf on STM32, this article goes a step further by also mapping the UART peripheral to the scanf() function.

Procedure

The following steps can be directly applied to any STM32CubeIDE project (version 1.9.0 at the time of writing). If another development environment is preferred, this procedure may have to be modified slightly to accommodate any differences.

0. Establish a UART Instance

As a preliminary step, ensure that a UART (or USART) peripheral has been properly configured for sending and receiving data to a virtual COM port. On ST development boards, this likely means choosing the UART RX and TX lines connected to the ST-LINK programmer/debugger. See the corresponding step in Easily Use printf on STM32 for complete details.

1. Redirect scanf() to UART Instance

a. In the Private Includes section of the main.c file, add an include directive for the stdio library, as shown in Figure 1.

Include the stdio library
Figure 1: Including the stdio library

b. To redirect the scanf() function only, copy and paste the code provided in Listing 1 into the Private Function Prototypes section of main.c. To redirect both the scanf() and prinf() functions (as most applications will require) copy the code provided in Listing 2 instead.

Listing 1: Simple code for redirecting scanf()

#ifdef __GNUC__
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif

GETCHAR_PROTOTYPE
{
  uint8_t ch = 0;

  /* Clear the Overrun flag just before receiving the first character */
  __HAL_UART_CLEAR_OREFLAG(&huart?);

  /* Wait for reception of a character on the USART RX line and echo this
   * character on console */
  HAL_UART_Receive(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

Listing 2: Simple code for redirecting both printf() and scanf()

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

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

GETCHAR_PROTOTYPE
{
  uint8_t ch = 0;

  /* Clear the Overrun flag just before receiving the first character */
  __HAL_UART_CLEAR_OREFLAG(&huart?);

  /* Wait for reception of a character on the USART RX line and echo this
   * character on console */
  HAL_UART_Receive(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart?, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

Be sure to change the dummy UART handles used in the Listings above to that of your desired UART peripheral. For example, Figure 2 shows huart2 being utilized as the target UART instance.


Figure 2: Adding retargeting code to main.c file

c. Unfortunately, the default syscalls.c file automatically generated by STM32CubeIDE results in unexpected behavior when internal buffering of the input stream is enabled. The simplest solution to this issue is to disable buffering before any call to scanf() is made. Copy and paste the line of code provided in Listing 3 into the initialization section of the main() function.

Listing 3: Disable internal buffering for the input stream

setvbuf(stdin, NULL, _IONBF, 0);

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

2. Enable Floating Point Support (Optional)

Similar to the printf() function, floating point support must be explicitly enabled for scanf() if the application demands that floating point numbers be read from the serial input. Otherwise, any call to scanf() with floating point specifiers in the format string will behave unexpectedly.

a. Right-click on the project name in the Project Explorer and choose Properties. Select Settings in the “C/C++ Build” category and choose MCU Settings under the Tool Settings tab. Check the box next to “Use float with scanf from newlib-nano” as shown in Figure 3 below. Click Apply and Close.


Figure 3: Enabling float formatting support

Results

Using the example code from the scanf() reference page (see Listing 4), we see in Figure 4 that the user input is properly read and formatted.

Listing 4: Example code for testing

char str[80];
int i;

printf("Enter your family name: ");
scanf("%79s", str);
printf("Enter your age: ");
scanf("%d", &i);
printf("Mr. %s, %d years old.\n", str, i);
printf("Enter a hexadecimal number: ");
scanf("%x", &i);
printf("You have entered %#x (%d).\n", i, i);

example code output
Figure 4: Example code output