2.2. UART & printf()
1. Debug Console
When you open the Windows peripheral manager, you can see that connecting the Nucleo board to the computer with USB adds 2 devices to the peripheral list.
- The ST-Link Debug is the device we already use to program and debug the MCU. Communication between the ST-Link and the MCU is done via the Serial Wire Debug (SWD) protocol.
- The second device is a virtual COM Port (COM5 in the example below). Such COM port can be used to exchange user data between the computer and the MCU. Typically, this link is used to implement console functions such as printf(). The link between the ST-Link and the MCU is a Universal Asynchronous Receiver Transmitter interface (UART).
You can use any serial terminal program to emulate the console. Here we will use PuTTY (www.putty.org).
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).
- 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):
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 function
int main()
{
uint8_t 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 COM5 (your COM number is probably different) at 115200 bauds. Below example illustrate the configuration of PuTTY:
Then play with the Nucleo user-button (blue). You should see a ‘#’ being printed each time the button is pressed:
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:
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 printchar() 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);
}
When you call the printf() function with some string to format and print, the first task that printf() does is to build the final string into a memory buffer replacing along the way wildcards (such as %d, %c, %s...) by numbers converted to ASCII formats. Once the string is ready, characters are streamed one by one to the output destination.
Here, we want the output destination to be our serial console. The printchar() function is the one that receives individual characters (the c argument) and is in charge to redirecting the printout to the desired output via a user-defined putchar() function. Instead of writing a putchar() function, we will put the two required lines of code directly in place. Those simply:
Wait until USART2 TX register (TDR) is available
Put c in the TDR, which instantly produce the output of the c character toward the console.
#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;
}
}
Note that because we are using device aliases such as USART2->ISR, USART_ISR_TC, or USART2->TDR, we need to include the device header on top the printf-stdarg.c file.
#include "stm32f0xx.h"
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…
...
#include "stm32f0xx.h"
#include "main.h"
#include "bsp.h"
...
// Main function
int 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.
- 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).
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. 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.
- STM32CubeMonitor can help you spy variables, in real-time with no effect on execution performance.