2.2. UART & printf()


1. Debug Console

When you open the Windows® peripheral manager, you can see that connecting the board to the computer through USB cable adds 2 devices to the peripheral list. The ST-Link dongle is the device we already use to program and debug the MCU. The second device is a virtual COM port (COM3 in the example below). Such COM port can be used to exchange user data between the computer and the MCU. You can use any serial terminal program. Here we will use putty (www.putty.org).

image011.png
 

This virtual COM port is connected to UART (or USART) pins on the MCU. Looking at the schematics, it appears that PA2 is used for TX (transmission) and PA3 is used for RX (reception).

 

image013.png

 

  • UART     stands for Universal Asynchronous Receiver Transmitter
  • USART   stands for Universal Asynchronous Synchronous Receiver Transmitter

In a Synchronous communication scheme, there is one additional wire that carries a clock signal. This is optional, and we will only use the Asynchronous protocol (UART). STM32 peripherals are mostly USART that can work in both modes. In the following USART or UART words are used with no difference in mind.

 

2. USART setup

 

Opening the STM32F072 datasheet, we can find that PA2 and PA3 are associated to USART2 peripheral, in the AF1 mode (Alternate Function 1):

image017.png
 

We will first implement a driver function that initializes USART2 to work with the virtual COM port.

Open my_project and add the function prototype in bsp.h:

/*
 * Debug Console init
 */

void	BSP_Console_Init	(void);

 

And then, the implementation code in bsp.c. You need to refer to the reference manual to understand the code below. Basically, you need to configure 2 peripherals:
-    GPIOA →Set PA2 and PA3 in AF1 mode (USART2)
-    USART2 → Communications settings (such as the Baud Rate)

/*
 * BSP_Console_Init()
 * USART2 @ 115200 Full Duplex
 * 1 start - 8-bit - 1 stop
 * TX -> PA2 (AF1)
 * RX -> PA3 (AF1)
 */

void BSP_Console_Init()
{
	// Enable GPIOA clock
	RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

	// Configure PA2 and PA3 as Alternate function
	GPIOA->MODER &= ~(GPIO_MODER_MODER2_Msk | GPIO_MODER_MODER3_Msk);
	GPIOA->MODER |= (0x02 <<GPIO_MODER_MODER2_Pos) | (0x02 <<GPIO_MODER_MODER3_Pos);

	// Set PA2 and PA3 to AF1 (USART2)
	GPIOA->AFR[0] &= ~(0x0000FF00);
	GPIOA->AFR[0] |=  (0x00001100);

	// Enable USART2 clock
	RCC -> APB1ENR |= RCC_APB1ENR_USART2EN;

	// Clear USART2 configuration (reset state)
	// 8-bit, 1 start, 1 stop, CTS/RTS disabled
	USART2->CR1 = 0x00000000;
	USART2->CR2 = 0x00000000;
	USART2->CR3 = 0x00000000;

	// Select PCLK (APB1) as clock source
	// PCLK -> 48 MHz
	RCC->CFGR3 &= ~RCC_CFGR3_USART2SW_Msk;

	// Baud Rate = 115200
	// With OVER8=0 and Fck=48MHz, USARTDIV =   48E6/115200 = 416.6666
	// BRR = 417 -> Baud Rate = 115107.9137 -> 0.08% error
	//
	// With OVER8=1 and Fck=48MHz, USARTDIV = 2*48E6/115200 = 833.3333
	// BRR = 833 -> Baud Rate = 115246.0984 -> 0.04% error (better)

	USART2->CR1 |= USART_CR1_OVER8;
	USART2->BRR = 833;

	// Enable both Transmitter and Receiver
	USART2->CR1 |= USART_CR1_TE | USART_CR1_RE;

	// Enable USART2
	USART2->CR1 |= USART_CR1_UE;
}

Now we can try something in main.c:

// Main program

void main()
{
	uint8_t	i, sent;

	// Configure System Clock
	SystemClock_Config();

	// Initialize LED & Button pin
	BSP_LED_Init();
	BSP_PB_Init();

	// Initialize Debug Console
	BSP_Console_Init();

	// Main loop
	while(1)
	{
		// If User-Button is pushed down
		if (BSP_PB_GetState() == 1)
		{
			BSP_LED_On();	// Keep LED On

			// Send '#' only once
			if (sent == 0)
			{
				while ((USART2->ISR & USART_ISR_TC) != USART_ISR_TC);
				USART2->TDR = '#';
				sent = 1;
			}
		}

		// If User-Button is released
		else
		{
			BSP_LED_Off();	// Keep LED Off
			sent = 0;
		}
	}
}

The code above calls the initialization function. Then, every time you press the user button the character ‘#’ is put into the Transmit Data Register (TDR) of USART2, after making sure that USART2 is not busy by checking state of its Transfer Complete (TC) flag.

Save all, build project and run.

 

Launch a COM terminal program on COM3 (your COM number is probably different) at 115200 bauds. Below example illustrate the configuration of putty:

image021.png
 

Then play with the Nucleo user-button (blue). You should see a ‘#’ being printed each time the button is pressed:

image022.png
 

Good! But we still don’t have a printf() function available. We can only send character one by one. Don’t be afraid, you will not have to code your own printf() function (but we're not too far from this...)

 

  Commit name "Console driver"
  Push onto Gitlab

 

3. Getting printf() to work

One solution would be to use the printf() function that comes with GCC newlib-nano and redirect its output to USART2. That’s not what you’ll do.

Another one is to get a lightweight version of printf() function here:
https://www.menie.org/georges/embedded/printf-stdarg.c

Download the printf-stdarg.c source file from the above link. Copy it into your /app/src folder and Refresh the Project Explorer view in Eclipse:

 

image024.png
 

In order to make this printf() version working, you need to provide your own implementation of a putchar() function. This implementation is simple, and that’s basically what we’ve done just before.

Open printf-stdarg.c and locate the princhar() function on top of the code:

#include <stdarg.h>

static void printchar(char **str, int c)
{
	extern int putchar(int c);

	if (str) {
		**str = c;
		++(*str);
	}
	else (void)putchar(c);
}

Instead of writing a putchar() function, we will put the two required lines of code directly in place:

#include <stdarg.h>
#include "stm32f0xx.h"

static void printchar(char **str, int c)
{
	if (str) {
		**str = c;
		++(*str);
	}
	else
	{
		while ( (USART2->ISR & USART_ISR_TC) != USART_ISR_TC);
		USART2->TDR = c;	
	}
}

Now locate printf() and sprintf() functions at the bottom of the code, and change the function names into something you like:

int my_printf(const char *format, ...)
{
        va_list args;

        va_start( args, format );
        return print( 0, format, args );
}

int my_sprintf(char *out, const char *format, ...)
{
        va_list args;

        va_start( args, format );
        return print( &out, format, args );
}

Why do we change these functions names?
Because printf() and sprintf() are known functions for the linker. Keeping these names produces linker errors if standard libraries such as <stdio.h> are not included.

By using printf-stdarg.c, we have a self-sufficient my_printf() function that does not require any linked library such as <stdio.h>.

We still need to declare function prototypes somewhere. Let us do this in main.h:

/*
 * main.h
 *
 *  Created on: 5 août 2017
 *      Author: Laurent
 */

#ifndef APP_INC_MAIN_H_
#define APP_INC_MAIN_H_

/*
 * printf() and sprintf() from printf-stdarg.c
 */

int my_printf	(const char *format, ...);
int my_sprintf	(char *out, const char *format, ...);


#endif /* APP_INC_MAIN_H_ */

Ready for a test?
Don’t forget to include main.h in main.c now…
 

// Main program

void main()
{
	uint8_t	i, sent;

	// Configure System Clock
	SystemClock_Config();

	// Initialize LED & Button pin
	BSP_LED_Init();
	BSP_PB_Init();

	// Initialize Debug Console
	BSP_Console_Init();
	my_printf("Console ready!\r\n");

	sent = 0;
	i = 0;

	// Main loop
	while(1)
	{
		// If User-Button is pushed down
		if (BSP_PB_GetState() == 1)
		{
			BSP_LED_On();	// Keep LED On

			// Send '#i' only once
			if (sent == 0)
			{
				my_printf("#%d\r\n", i);
				sent = 1;
				i++;
			}
		}

		// If User-Button is released
		else
		{
			BSP_LED_Off();	// Keep LED Off
			sent = 0;
		}
	}
}

Save, build, run and open a COM terminal.

image030.png

 

  Commit name "Console printf()"
  Push onto Gitlab

 

4. Summary

 

Now you have a working printf().

You should bear in mind that using printf() takes time because it relies on a slow serial interface. For instance, the message “Console Ready!\r\n” takes about 1.4ms to print at 115200 bauds. It is quite long considering what CPU would be able to do in such a long time.

Screenshot below shows the USART signal corresponding the “Console Ready!\r\n” message. You can probe it using the RX pin header available on the ST-Link dongle (PA2 and PA3 pin headers are actually disconnected from the MCU. Refer to board schematics).

image031.png

 

During the printf() process, CPU is spending most time waiting for USART to be available for the next character to be sent. Although using printf() is of great help while debugging, you should be aware that it will dramatically  slow down code execution.

Here are some hints to make things better:

  • Use of Semihosting feature instead of USART. You can redirect the printf() function to the debugger console thru OpenOCD. It only works under debugger control, whereas our printf() function works even in standalone running applications. Nevertheless, Semihosting can be slower than using the USART.
  • Avoid long messages. Ultimately, you can send only one character by directly loading USART TDR register. This process does not involve any waiting time since the USART sending process is parallel to CPU execution. It is not applicable to strings but might be really helpful. Use it when you want to real-time monitor code checkpoints.
  • You can fully parallelize the USART sending process of strings by using DMA (more on this later).
  • The baud rate may be increased way past standard RS232 baud rates since actual signal is carried thru USB which is a fast serial interface. Baud rates up to 1M bauds work fine.
  • STM-Studio® can help you spy variables, in real-time with no effect on execution performance.