STM32 UART: Polling and DMA modes using HAL API

STM32 UART: Polling and DMA modes using HAL API


9 minute read

1. UART Introduction

UART, an acronym for Universal Synchronous/Asynchronous Receiver Transmitter, stands as one of the oldest communication protocols. Besides that, in contemporary applications, UART plays an important role in facilitating communication between microcontrollers and computers. Renowned for its simplicity and relatively high speed, UART involves only a few essential steps for data transmission between a PC and UART-enabled devices. This tutorial is dedicated to guiding you through the process of configuring the STM32 UART Peripheral, enabling you to seamlessly send and receive data with a computer.

In this tutorial, we will be working with the Nucleo-L476RG Microcontroller board. However, feel free to adapt the steps outlined here to any other MCU board you have on hand. It's important to note that for debugging and demonstration purposes, we'll be utilizing the printf function. If you haven't configured the printf function for your STM32 microcontroller yet, you can refer to [this TUTORIAL] for detailed instructions. While you can still follow this article without configuring the printf function, setting it up will significantly ease the configuration and debugging processes.

Nucleo-l476RG tutorial

2. UART details

Along with conventional power pins, UART has RX and TX lines. Also, the clock line is added in the synchronous version of UART (USART). However, we focus on the asynchronous protocol version omitting the clock line. Due to the lack of a clock line, devices that are communicating through UART have to operate at the same baud rate (operating frequency).

Commonly used baud rates are 4800 bps, 9600 bps, 19200 bps, 115200 bps, etc. Let's delve into the low-level operation of UART for a clearer understanding. In the idle state, the TX line is held high. The transmitter initiates communication by pulling the line low for a one-bit duration, signaling the start of the transaction. Subsequently, the transmitter transmits data, typically 7, 8, or 9 bits, followed by a stop bit that keeps the line high for one bit to indicate the end of the transaction. Additionally, an optional parity bit can be employed for error detection. The illustration below provides a visual representation of the operating principle of UART.

UART data structure

3.  STM32 UART Polling Mode

Fortunately, STMicroelectronics simplifies the process of configuring UART through its HAL API, allowing us to establish data transmission with just a few lines of code. Let's begin by configuring STM32 UART using the STM32CubeMx software:

  1. Create a project, open STM32CubeMx and navigate to Connectivity -> USART2.
  2. Choose the Asynchronous mode, and retain the default parameters, including a baud rate of 115200 bits/s and a word length of 8 bits. For pinout, note that UART2 pins are typically connected to the ST-LINK, enabling data transmission to the computer without the need for a separate UART-to-USB adapter. However, if you opt for a different UART, using an adapter becomes necessary.
  3. We keep the default parameters of the UART, including the baudrate (115200) and word length (8 bits)

STM32 UART CubeMx Configuration

After configuring the UART settings, save the file and generate the code for your project using STM32CubeMx. To initiate the transmission of data through UART, we will utilize the HAL_UART_Transmit function, which requires four arguments:

  1. huart: UART handle. pData:
  2. Pointer to the data buffer.
  3. Size: Number of data elements to be sent. 
  4. Timeout: Duration for the operation to wait for completion. 

Now, within the main while loop of your code, call this function at a regular interval, for instance, every 1000 milliseconds, to ensure a steady data transmission:

uint8_t uart_data[] = "Just do It! \n";                                                   
while (1)
{
    // Your application logic goes here.

    // Transmit data through UART every 1000 milliseconds.
    HAL_UART_Transmit(&huart2, uart_data, sizeof(uart_data), 1000);
    HAL_Delay(1000);  // Introduce a delay to control transmission frequency.
}

This loop structure allows for periodic transmission of data through UART, providing a practical example of how to integrate UART communication into your STM32 microcontroller project

4. Putty Software to read UART data from the Computer side

After successfully configuring and initiating data transmission, the next crucial step is to test whether the computer is receiving the data. Begin by identifying the COMPORT number

  1. In Windows, open the Device Manager.
  2. Check the list of devices on your PC to find the COMPORT number assigned to your STM32 microcontroller. For example, in my case, it's COMPORT 8.

STM32 UART Comport number

With the COMPORT number identified, proceed to install software for reading data from the computer side. Numerous tools are available online, and one popular choice is PUTTY:

  1. Run PuTTY.
  2. Choose the serial port associated with your COMPORT number.
  3. Set the baud rate to match the configured rate in your STM32 microcontroller (e.g., 115200 bps). Start the communication by pressing the open button.
  4. Upon successful setup, you should observe your transmitted message on the PuTTY software screen, confirming the data reception process.

Stm32 Uart Putty data reception

5.  STM32 UART Receiving data:

To receive data through UART, we employ HAL_UART_Receive function, which requires four arguments:

1. huart - UART handle

2. pData - pointer to data buffer

3. Size - amount of data elements to be received

4. Timeout - timeout duration

Within the main while loop of your code, ensure to call this function to facilitate the reception of data. Don't forget to define an array that will act as the data buffer. This array should be one of the arguments of the function, providing a location to store the received data. To detect when the microcontroller receives data, utilize an if statement checking whether the HAL_UART_Receive function returns HAL_OK. This return value indicates successful data reception:

uint8_t received_data[10];
while(1)                                                          
{                                                                 
    if(HAL_UART_Receive(&huart2, received_data, sizeof(received_data), 1000) == HAL_OK)        
    {                                                                            
        printf("%s \n", received_data);                                                       
    }                                                                                        }

We use the if statement to detect when the MCU receives data. The function should return HAL_OK when the microcontroller receives data. 

We again use Putty to check that our STM32 MCU receives data. But, besides choosing the baud rate and serial port, we have to do additional configurations as shown in the picture below. Within the Terminal, choose 'force on' for both local echo and local line ending.  

Putty serial allow sending data

Then, write any message on the screen and you should see the same message on the console of the microcontroller:

STM32 uart data reception

6. STM32 UART DMA Data Transmission

While we learned how to use UART in polling mode, which is simple and easy to use, it comes with serious drawbacks.  In polling mode, the MCU must continually read the UART line to receive data, potentially leading to inefficiency and a tendency for the STM32 to become stuck in the polling functions. To overcome these limitations, we can leverage STM32 UART in DMA (Direct Memory Access) mode. This mode enables us to use the MCU's computation time more efficiently and intelligently. By incorporating DMA, we can enhance the performance and responsiveness of our STM32 microcontroller when dealing with UART communication. To configure DMA, revisit the STM32CubeMX file and enable DMA for both RX and TX, along with global interrupts, as illustrated in the image below.

STM32 UART DMA Configuration

With DMA configured, we can now use the HAL_UART_TRANSMIT_DMA function. The primary distinction here is that we won't define a timeout. This means that the microcontroller won't wait for the transaction to complete; instead, it will promptly move on to the next line of code. This shift in behavior allows for more efficient use of the STM32 MCU's processing time.

uint8_t uart_data[] = "Just do It! \n";                                                   
while (1)
{
    // Transmit data through UART every 1000 milliseconds.
    HAL_UART_Transmit_DMA(&huart2, uart_data, sizeof(uart_data));
    HAL_Delay(1000);  // Introduce a delay to control transmission frequency.
}

7. STM32 UART DMA Data Reception

When incorporating DMA for data reception, utilize the HAL_UARTEx_Receive_DMA function. The code structure remains unchanged from our previous implementation, with the only modification being the removal of the timeout argument.

   // inside of main function after all the configuration functions: 
HAL_UART_Receive_DMA(&huart2, received_data, sizeof(received_data));
while(1){}

When the STM32 MCU successfully receives data through DMA, it triggers an interrupt, leading to the automatic invocation of the HAL_UART_RxCpltCallback function. At this point, you can utilize this callback function to print the received data and initiate another data reception cycle by calling HAL_UART_Receive_DMA once again. This seamless workflow ensures continuous and efficient handling of incoming data in your STM32 MCU application

// outside of main function:                                                         
uint8_t received_data[10];
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	printf("%s \n", received_data);
	HAL_UART_Receive_DMA(&huart2, received_data, sizeof(received_data));
}                                                                                                                                                                                 
 

8. STM32 UART DMA Variable length Data Reception

In the preceding sections, we employed the HAL_UART_Receive_DMA function for data reception, which, unfortunately, posed a limitation in handling varying data lengths. If a fixed length, say 10 bytes, is defined, the function reads only that specific amount of data. To overcome this limitation, we'll transition to using HAL_UARTEx_ReceiveToIdle_DMA, a function designed to receive complete messages regardless of their length.

Upon successful reception, an interrupt is triggered, automatically invoking the HAL_UARTEx_RxEventCallback function. In response to this callback, you can print the received data and initiate another data reception cycle by calling HAL_UARTEx_ReceiveToIdle_DMA again. It's worth noting that one of the callback function's arguments indicates the number of bytes received. For flexibility, it is recommended to define a buffer array size larger than the expected maximum length. In the example below, the array size is set to 40

// outside of main function:                                                         
uint8_t received_data[40];
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	printf("%.*s\n", Size, received_data);
	HAL_UARTEx_ReceiveToIdle_DMA(&huart2, received_data, sizeof(received_data));
}
   // inside of main function after all the configuration functions: 
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, received_data, sizeof(received_data));
while(1){}

Finally, let's see how our code receives messages with various lengths:

STM32 UART receive until Idle

« Back to Blog