Firmware 101: STM32 UART Hands-on
Getting Started with Serial Communication on STM32 Ecosystem
In this article, we’ll walk you through the process of configuring and using the UART peripheral on STM32 microcontrollers. UART is a powerful and widely used communication protocol for sending and receiving data over a serial connection, and mastering it is essential for firmware development.
What is UART and Why It Matters
UART (Universal Asynchronous Receiver/Transmitter) is a communication protocol used to send and receive data between two devices asynchronously. Unlike other communication protocols, it doesn’t require a clock signal, making it simpler to implement.
Communication is based on the use of just two lines:
- TX (Transmit): It is used to send data from the transmitting device to the receiving device.
- RX (Receive): It is used to receive data from the transmitting device.
The baud rate defines the speed of communication, i.e., the number of bits transmitted per second. Both devices must be configured to use the same baud rate for successful communication.
UART vs USART
While both UART and USART (Universal Synchronous/Asynchronous Receiver/Transmitter) perform similar functions, USART can operate in both asynchronous and synchronous modes. UART, on the other hand, typically only supports asynchronous communication.
Typical Use Cases
- Debugging: Sending real-time status and error information to a terminal.
- Peripheral Control: Communicating with peripherals like Sensors, Bluetooth modules, etc.
- Configuration: Sending configuration commands from a PC to a microcontroller.
Project Setup
The Development Board: NUCLEO-G431RB
For this hands-on project, we’re using the STM32 NUCLEO-G431RB, a development board built around the STM32G431RBT6 microcontroller. Part of the NUCLEO family by STMicroelectronics, it offers a rich set of peripherals and Arduino Uno compatible headers, making it easy to prototype and interface with existing hardware.
What is STM32CubeMX?
STM32CubeMX is a graphical tool by STMicroelectronics that helps configure STM32 microcontrollers. It lets you set up pins, peripherals (like UART, SPI, ADC), and generates initialization code for use in STM32CubeIDE or other IDEs.
Configuration with STM32CubeMX
We’ll use USART1, routed to PC4 (TX) and PC5 (RX). These pins are connected to the D1 (TX) and D0 (RX) pins on the Arduino-compatible header, allowing easy serial communication with external shields or USB-to-serial adapters via those pins.
- Open STM32CubeMX, go to File → New Project, and select the NUCLEO-G431RB board from the Board Selector.
- In the Pinout & Configuration tab, open the Connectivity section and enable USART1 in Asynchronous mode.
- Keep the default settings: baud rate (115200), word length (8 bits), stop bits (1), and parity (None).
- Generate the code and open the project in STM32CubeIDE to begin development.
Generated Code — UART Initialization
The generated code demonstrates how to initialize and configure the USART1 peripheral. The configuration sets up the UART interface for asynchronous serial communication with the following parameters:
- Baud rate: 115200
- Word length: 8 bits
- Stop bits: 1
- Parity: None
- Mode: Transmit and receive
- Hardware flow control: Disabled
#include "main.h"
UART_HandleTypeDef huart1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
int main(void) {
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1) { }
}
static void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart1.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart1, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart1, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&huart1) != HAL_OK)
{
Error_Handler();
}
}The main() function performs the standard STM32 initialization sequence:
HAL_Init()resets peripherals and initializes the HAL library.SystemClock_Config()configures the system clock.MX_GPIO_Init()initializes GPIO pins.MX_USART1_UART_Init()sets up the USART1 peripheral with the defined parameters.
The main loop (while(1)) is empty, but the system is fully initialized and ready to perform UART communication.
Using a USB-to-Serial Adapter
To connect and interact with the UART of your STM32 board, you’ll need a USB-to-Serial adapter. The adapter connects the STM32’s TX (PC4) and RX (PC5) pins to the adapter’s RX and TX pins, respectively. It converts the UART signals to USB, enabling communication between the board and your computer via a serial terminal.
One of the most common adapters is the FTDI TTL-232R (based on the FT232 chip). When using it, make sure to connect the correct power supply voltage: the board operates at 3.3 V, so you must connect the VCC pin of the adapter to the 3.3 V pin on the board. Don’t forget to connect the GND between the board and the adapter to ensure a common reference. See the figure below for proper wiring.
To read data from the UART on your computer, you’ll also need serial terminal software such as PuTTY, Tera Term, or minicom. These programs allow you to open the corresponding COM port and communicate with your STM32 board, sending and receiving data via the serial connection.
Sending Data Over Serial
In this section, we’ll write a simple program to send data over UART. The goal is to loop a message every second, sending a “Hello World” message to a connected terminal.
The program uses HAL_UART_Transmit() to transmit the message and introduces a 1-second delay between transmissions using HAL_Delay().
#include "main.h"
UART_HandleTypeDef huart1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
int main(void) {
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
char msg[] = "Hello World\n";
while (1) {
HAL_UART_Transmit(&huart1, (uint8_t *)msg, sizeof(msg) - 1, HAL_MAX_DELAY);
HAL_Delay(1000); // Send message every second
}
}Receiving Data from the Serial
To receive data, you can either use polling or interrupts methods. We’ll go over both methods, starting with a simple polling example that reads one character at a time.
In polling, the program repeatedly checks the status of the UART receiver to see if new data is available. In contrast, interrupts allow the microcontroller to automatically respond to an event (such as data being received) without continuously checking for it.
Polling-Based
Here’s how you can implement a blocking read, which waits for data to arrive.
The program waits for a character to be received via UART. Once a character is received, it is sent back to the UART port using HAL_UART_Transmit, creating an echo effect.
Example: Echo with Polling
The HAL_UART_Receive function is used to receive data from the UART peripheral. It is a blocking function, meaning it waits until the requested number of bytes is received.
int main(void) {
// ...
while (1) {
/* Polling to receive one character at a time */
if (HAL_UART_Receive(&huart1, &receivedChar, 1, HAL_MAX_DELAY) == HAL_OK) {
/* Send back the received character */
HAL_UART_Transmit(&huart1, &receivedChar, 1, HAL_MAX_DELAY);
}
}
}Interrupt-Based
Interrupts allow the MCU to react to incoming UART data without continuously checking for it. With interrupts, when data arrives, an interrupt is triggered and the corresponding callback function is executed.
CubeMX Setup for Interrupt
- Enable USART1 Global Interrupt option in the NVIC Settings of the USART1 peripheral.
- Regenerate the code.
Example: Echo with Interrupt
This example performs a simple echo of received characters. Every time a character is received, it is immediately sent back through UART.
int main(void) {
// ...
// Start receiving data via interrupt
HAL_UART_Receive_IT(&huart1, &received_char, 1);
while (1) {
// Main loop can handle other tasks
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// Echo received character
HAL_UART_Transmit(&huart1, &received_char, 1, HAL_MAX_DELAY);
// Restart reception
HAL_UART_Receive_IT(&huart1, &received_char, 1);
}
}HAL_UART_Receive_IT: Starts interrupt-based reception of one byte.HAL_UART_RxCpltCallback: This callback is triggered when data is received. The received byte is echoed back, and the reception is restarted.
Use DMA for more efficient communication
When you’re dealing with larger amounts of data, polling or interrupts can be inefficient. By using DMA (Direct Memory Access), you can offload the data transfer task to hardware, improving performance.
What is DMA?
DMA is a microcontroller feature that enables peripherals like UART to read from or write to memory automatically, without CPU intervention for each data unit. This is especially useful for continuous or large data transfers, such as sending or receiving buffers of data via UART.
Example: Using DMA for UART Transmission
To enable DMA for UART transmission, open the USART1 Mode and Configuration panel and navigate to the DMA Settings tab. Here, you can add a DMA request for USART1_TX.
Remember to re-generate the code!
const char msg[] = "Hello from DMA UART!\r\n";
int main(void) {
// ...
// Transmit the message via UART using DMA
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)msg, sizeof(msg) - 1);
while (1) {
// Main loop can perform other tasks while DMA handles transmission
}
}The HAL_UART_Transmit_DMA function takes care of sending the entire message buffer via DMA without blocking the execution, freeing the CPU to run other tasks concurrently.
In contrast, as seen before, HAL_UART_Transmit blocks execution until the transmission is finished.
Common Pitfalls and Debug Tips
If nothing is printing or you see “garbage” characters in the terminal:
- The first thing to check is the baud rate. Ensure that the baud rate settings on both the board and your terminal match exactly.
- Another common issue could be an incorrect connection: make sure the ground (GND) of the board is properly connected to the ground of the serial interface.
What’s Next?
Now that you’ve learned the basics of UART communication on STM32, you can take it further by implementing a simple command parser. For example, a parser that allows you to send specific commands from the terminal to control your board, such as turning on/off a LED, read sensor values, or toggling different functionalities.
👀 If you’re looking to dive deeper, we can explore USART in a future article. USART supports both asynchronous and synchronous communication, offering more advanced options for serial communication.
