Recently in my mailing group I asked a question about which STM32 topic interests you the most right now. I received lots of different answers, but one topic came up several times. It was receiving messages of arbitrary length from UART via DMA. If the reader wants it – I’m writing it!
Many myths have grown around this topic. Some say it can’t be done. Others claim it’s a huge effort. I have no idea where that came from. Perhaps it’s partly because the HAL libraries don’t take into account the very important UART IDLE interrupt at all, which is essential here. Maybe also because it can be done in several ways and each has its pros and cons.
How to receive UART via DMA?
As you know, or are just finding out – when starting DMA reception, we must declare the number of characters after which a DMA transfer-complete interrupt will be generated. At half of that count, a half-transfer interrupt will also fire.
UART communication is such that device messages often have different lengths. Take for example this GPS I described here recently. If there’s no fix, the messages will be very short with no data between separators in the form of commas. After GPS lock, messages can be several dozen characters long. Additionally, different message types also have different lengths. How are you supposed to know how long the next message will be?
You could simply wait for the fixed amount of data to overflow. Then one, two or more messages might arrive before the DMA interrupt is triggered. But what if no more messages come in and the last one was critical? That’s not the most convenient situation, is it?
UART IDLE LINE interrupt
STM32 microcontrollers have lots of interesting interrupts. One of the more interesting is UART IDLE LINE. It can be used for multi-master communication, but it can also serve us to detect the end of a message.
How does it work? After detecting the first incoming character on the UART, the receiver line is monitored for activity. If there is no activity on this line for a period equal to the length of one character, an interrupt is raised. Do you already see the use case?
Let’s say exactly 20 characters are coming to the microcontroller over UART. After the time of the 21st character we get the IDLE interrupt. End of message detected. What next?
End of message and the DMA matter
Alright, but what does DMA have to do with this? During my research I saw several implementation approaches. One interesting way was to cyclically start DMA for, say, 10 characters. In my opinion it generates a lot of unnecessary work restarting DMA. On the other hand, it needs little RAM and is completely independent of UART message length.
My proposal is a DMA buffer that can hold the longest possible message we can expect. So where do we get the DMA interrupt from? There’s a nice relationship that by stopping the “racing” DMA you generate a transfer-complete interrupt. And that’s the key to success!
It’s enough to stop DMA in the IDLE interrupt to enter the DMA transfer-complete handler. Magic!
DMA interrupt handling
There are two “unfortunatelys.” One is that the HAL libraries don’t handle the IDLE interrupt. You have to write your own and replace the standard HAL interrupt handling. The second unfortunately is that Cube will always try to restore the default interrupt handling when regenerating the project, and you have to remember that.
There’s also a big plus in HAL. It generates separate handlers for each UART and each DMA channel. Replacing the interrupt is therefore harmless to other interrupts.
Okay, enough theorizing. Let me walk you through the implementation.
UART reception via DMA project
I wrote the code for the Nucleo F411RE using STM32CubeIDE 1.0.2 and HAL libraries version 1.24.1. There won’t be a schematic this time. I didn’t connect anything extra to the board.

Generate the project with the standard peripherals initialized. You can easily do this by selecting the Nucleo board instead of the microcontroller when creating the project.
Since you chose the default peripherals, you already have the UART and the built-in LED enabled. We’ll use both today.
However, you need to further configure the UART. Configure the DMA request for USART2_RX in Normal mode. Also add the USART2 global interrupt in the NVIC tab. See the screenshots below.
And that’s it on the Cube side. Let’s move on to the code. I prepared a library to handle the whole thing. It allows you to use more than one UART in DMA receive mode. However, it requires a few steps and modifications of the generated code to work.
Code
First, a small note. I noticed that the latest CubeMX v5.4.0 swaps the initialization order of DMA and UART2, which results in incorrect DMA operation! Make sure that before the while(1) loop you have it as below.
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init(); // <- IMPORTANT: DMA MUST BE BEFORE USART2!!!
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
I’ll start by discussing the structure that is the core of each UART you want to use for DMA reception.
typedef struct
{
UART_HandleTypeDef* huart; // UART handler
uint8_t DMA_RX_Buffer[DMA_RX_BUFFER_SIZE]; // DMA direct buffer
uint8_t UART_Buffer[UART_BUFFER_SIZE]; // UART working circular buffer
uint16_t UartBufferHead;
uint16_t UartBufferTail;
uint8_t UartBufferLines;
}UARTDMA_HandleTypeDef;
It contains a handle to the UART and two buffers:
- DMA-dedicated buffer for receiving the current message
- UART circular buffer into which subsequent messages from the DMA buffer are placed (after the transfer completes)
as well as a few helper variables for handling the circular buffer and a counter of full lines (the number of messages ending with the ‘\n’ character).
From the point of view of detecting incoming UART messages there are 3 functions:
void UARTDMA_Init(UARTDMA_HandleTypeDef *huartdma, UART_HandleTypeDef *huart); void UARTDMA_UartIrqHandler(UARTDMA_HandleTypeDef *huartdma); void UARTDMA_DmaIrqHandler(UARTDMA_HandleTypeDef *huartdma);
In the initialization I work with interrupts:
- I enable the UART IDLE interrupt
- I enable the DMA TC (Transfer Complete) interrupt
- I disable the DMA HT (Half Transfer) interrupt – it won’t be needed and may even get in the way
- I start UART DMA reception into the dedicated buffer
You have to pass a pointer to the previously created structure to this function (for example in main).
void UARTDMA_UartIrqHandler(UARTDMA_HandleTypeDef *huartdma);
This function handles UART interrupts. I check in it whether the interrupt was triggered by IDLE. If so, I read the appropriate registers to clear the flag and stop DMA. The DMA TC interrupt is raised automatically.
void UARTDMA_DmaIrqHandler(UARTDMA_HandleTypeDef *huartdma);
Now the most important function from the perspective of the entire code. We are in a state where characters have stopped being transmitted on the UART. This is a signal that we probably received the entire message. Probably, because we might be dealing with a software UART that doesn’t keep timing. Unfortunately, the IDLE interrupt is absolute and only considers inactivity for the duration of a single character.
What happens here? First, I read the interrupt registers and check whether we’re dealing with a Transfer Complete interrupt. If so, then:
- I clear the interrupt flag (HAL by default does this for us in its handlers, but this is OUR handler)
- I check how many characters came in via DMA using the NDTR register.
- I copy from the DMA buffer into the next free positions of the UART circular buffer. This is a ring buffer, so written data will wrap around. Along the way I check whether the currently copied character is an end-of-line ‘\n’. If so, I increment the appropriate variable in the structure.
- I clear interrupts
- I set the DMA registers to the beginning of the DMA buffer and again set the number of characters to receive, which equals the DMA buffer size.
- I start DMA again
And it keeps spinning. Subsequent messages land in the free spaces of the UART buffer.
However, to keep it spinning we must replace the interrupts! How to do that? Let’s target the stm32fxx_it.c file
For our configuration we’re looking for two functions related to USART2 and DMA1 channel 5. These are
void USART2_IRQHandler(void); void DMA1_Stream5_IRQHandler(void)
Notice that there is the default HAL interrupt handling HAL_DMA_IRQHandler and HAL_UART_IRQHandler. We don’t want that, so comment them out. Reminder: after regenerating the project they will return! Edit – below I added how to solve this more elegantly. With the new method, the default interrupts won’t interfere even if they come back 😉
Now add our functions in the appropriate places. For me it looks like this
/**
* @brief This function handles DMA1 stream5 global interrupt.
*/
void DMA1_Stream5_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream5_IRQn 0 */
/* USER CODE END DMA1_Stream5_IRQn 0 */
//HAL_DMA_IRQHandler(&hdma_usart2_rx);
/* USER CODE BEGIN DMA1_Stream5_IRQn 1 */
UARTDMA_DmaIrqHandler(&huartdma);
/* USER CODE END DMA1_Stream5_IRQn 1 */
}
/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
/* USER CODE END USART2_IRQn 0 */
//HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
UARTDMA_UartIrqHandler(&huartdma);
/* USER CODE END USART2_IRQn 1 */
}
There’s one more thing. This file won’t know these functions, let alone our structure. You have to provide them in the user section at the very top of the file.
/* USER CODE BEGIN Includes */ #include "UART_DMA.h" /* USER CODE END Includes */ /* USER CODE BEGIN EV */ extern UARTDMA_HandleTypeDef huartdma; /* USER CODE END EV */
And that’s it. It will work 🙂 Now it would be nice to add something in main to react to what arrives on UART. I won’t invent anything fancy here. I’ll receive two messages “ON” and “OFF”, which will turn the built-in LED on the Nucleo on and off respectively.
To check whether there’s something in the queue to parse and to fetch a data line, I’ll use two functions
uint8_t UARTDMA_IsDataReady(UARTDMA_HandleTypeDef *huartdma); int UARTDMA_GetLineFromBuffer(UARTDMA_HandleTypeDef *huartdma, char *OutBuffer);
Their names are pretty self-explanatory. Just a bit more code in main. Remember that you need to create the structure variable for the whole machinery.
/* USER CODE BEGIN PD */ UARTDMA_HandleTypeDef huartdma; /* USER CODE END PD */
A small buffer to parse the fetched lines will also be useful.
/* USER CODE BEGIN PV */ char ParseBuffer[8]; /* USER CODE END PV */
Remember to initialize UART with DMA reception
/* USER CODE BEGIN 2 */ UARTDMA_Init(&huartdma, &huart2); /* USER CODE END 2 */
And finally, we fetch the data and parse it
/* USER CODE BEGIN WHILE */
while (1)
{
if(UARTDMA_IsDataReady(&huartdma))
{
UARTDMA_GetLineFromBuffer(&huartdma, ParseBuffer);
if(strcmp(ParseBuffer, "ON") == 0)
{
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
}
else if(strcmp(ParseBuffer, "OFF") == 0)
{
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
- I check whether there’s a line to parse
- If so, I fetch it in full
- With a simple strcmp I check what the message is and react accordingly
- I enjoy the result
Difficult? Of course not 🙂 Let’s also look at the timing, because that’s always interesting. Reception of two characters:
UART IDLE interrupt handling is 1.437 µs, and DMA TC is 4.125 µs. Blazing fast considering these times include GPIO handling time. By the way, I have to compare handling of some peripherals with HAL and “bare registers.” What do you think?
And what if I send 50 characters?
UART IDLE handling didn’t change – it doesn’t depend on message length. The time needed to handle the DMA TC interrupt changed and is now 33.975 µs. The increase comes from the need to copy a larger number of characters into the UART circular buffer.
I think these times are very good.
EDIT – interrupt handling
I got a tip on Facebook on how to better solve the problem of default HAL interrupt handling. Just put “ours” before the factory one and return immediately 🙂 So simple, and I didn’t think of it while writing the code. I won’t remove my original idea even though it’s not very elegant.
/**
* @brief This function handles DMA1 stream5 global interrupt.
*/
void DMA1_Stream5_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream5_IRQn 0 */
UARTDMA_DmaIrqHandler(&huartdma);
return;
/* USER CODE END DMA1_Stream5_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_usart2_rx);
/* USER CODE BEGIN DMA1_Stream5_IRQn 1 */
/* USER CODE END DMA1_Stream5_IRQn 1 */
}
/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
UARTDMA_UartIrqHandler(&huartdma);
return;
/* USER CODE END USART2_IRQn 0 */
HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
/* USER CODE END USART2_IRQn 1 */
}
Summary
You’ll find lots of ideas on the Internet for implementing UART over DMA. I think what I did is one of the simpler implementations. It requires some RAM for two buffers, but in STM32 we have plenty of it 🙂 Besides, if you expect short messages, you don’t need large buffers.
I hope you enjoyed the article and understood how it works. If you have any doubts or questions, the comments section is yours. I’ll be grateful if you share this article with your friends. Let good content spread as widely as possible.
The UART and DMA topic will also be covered in my STM32 course for beginners (conducted in Polish). Thanks to video, I’ll have better tools there to talk about it in more depth. Of course, there will be more complex applications in an enjoyable form. If you haven’t signed up for my newsletter yet, you’re welcome to do so (the newsletter is in Polish). Click the banner to join the waiting list for the course.
In the emails you’ll receive current information and you’ll have a real impact on what happens not only in the course, but also on the blog (emails/newsletter are in Polish).
The full project along with the library can be found as usual on my GitHub: link
If you noticed any error, disagree with something, would like to add something important, or simply feel like discussing the topic, write a comment. Remember that the discussion should be polite and in accordance with the rules of the Polish language.








0 Comments