How to Receive UART Data using DMA
Learn how to configure STM32 UART with HAL and DMA (normal & circular modes) for efficient reception of large data packets and continuous streams. The project is available for download at the end of the post.

This is the 4th tutorial in the series on the UART peripheral of STM32 Microcontrollers. Today we will use the DMA in normal mode and also in the circular mode. I will demonstrate some scenarios under which we will use these different modes to receive a large data via the UART.
Recommended Resources:
You must check out the previous tutorials to understand the data reception via the UART.
Introducing STM32 UART
The UART (Universal Asynchronous Receiver/Transmitter) peripheral in STM32 microcontrollers provides a simple and efficient way to perform serial communication. It allows data exchange between the microcontroller and other devices such as PCs, GPS modules, Bluetooth modules, or other microcontrollers using standard asynchronous protocols.
UART communication is asynchronous, meaning it doesn’t require a separate clock line—only TX (Transmit) and RX (Receive) lines are used. STM32 MCUs often provide multiple UART/USART interfaces, and they can be configured using STM32CubeMX and HAL libraries to suit different baud rates, word lengths, parity, and stop bits.
STM32 supports both UART and USART peripherals, where USART adds synchronous mode (with a clock line) in addition to asynchronous UART mode.
Features of STM32 UART:
- Configurable Baud Rate
Supports a wide range of baud rates (e.g., 9600 to 1+ Mbps), allowing flexible communication speed based on the application. - Full-Duplex Communication
Simultaneously sends and receives data through separate TX and RX lines. - Interrupt and DMA Support
UART can be operated in polling, interrupt-driven, or DMA mode for efficient, low-latency data handling. - Flexible Frame Format
Supports custom configurations like 8/9 data bits, 1/2 stop bits, and optional parity for error checking.
DMA in Normal mode
Let’s assume a case where we want to receive a large amount of data, and our MCU has enough RAM to store that data into a buffer. We can use the DMA in NORMAL mode to receive this data over the UART and then store the data into the buffer.
A situation like this can work for few kilobytes of data as most of the STM32 MCUs has RAM in few kilobytes. But if you want to store an audio file or a video file, then you can’t afford to use a single buffer.
I will use 2 kilobytes of data to demonstrate how to receive the data using the DMA in normal mode and store the data in a buffer.
CubeMX Configuration
Below is the cubeMX configuration to enable the UART DMA in normal mode.
The DMA request is set for USART2_RX as we are receiving the data via the DMA. The data width is Byte as the UART transfers the data in bytes. The DMA mode is set as Normal.
The rest of the UART configuration is same as the previous tutorials with Baud Rate of 115200 with 8 data bits, 1 stop bit and no parity.
The Code
We need to know the size of the incoming data. So the sender should first send 4 bytes of the size data followed by the data itself. If you are receiving larger data, you can change the length of the size data bytes.
In the main function, we will set the DMA to receive 4 data bytes for the size.
uint8_t RxData[4096]
int main()
{
....
HAL_UART_Receive_DMA(&huart2, RxData, 4);
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(1000);
}
}
The function HAL_UART_Receive_DMA
is used to receive 4 data bytes. Once all the 4 bytes has been received, the interrupt will trigger and the UART Receive Complete Callback will be called.
int isSizeRxed = 0;
uint16_t size = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (isSizeRxed == 0)
{
size = ((RxData[0]-48)*1000)+((RxData[1]-48)*100)+((RxData[2]-48)*10)+((RxData[3]-48));
isSizeRxed = 1;
HAL_UART_Receive_DMA(&huart2, RxData, size);
}
else if (isSizeRxed == 1)
{
isSizeRxed = 0;
HAL_UART_Receive_DMA(&huart2, RxData, 4);
}
}
The callback will be first called when the 4 size bytes are received. The isSizeRxed variable was 0 in the beginning so we calculate the size using the 4 bytes of the RxData buffer.
The size bytes were transferred in the Ascii form and hence we need to subtract 48 to convert them to integer equivalent.
After calculating the size, we will set the variable isSizeRxed to 1 so that we don’t enter this loop again. Then we will call the function HAL_UART_Receive_DMA to receive the required number of data bytes as calculated by the size variable.
Once all the required number of data bytes has been received, we will enter this function again. This time the variable isSizeRxed is set to 1, so the else condition will execute. Here we will reset the variable isSizeRxed to 0 and receive the 4 size bytes. This will make this entire loop to run forever.
Now we can receive the large data of any size, but it should be less than 4KB. This is because we have defined a buffer of 4KB to store the data. Although we need to send the size first, followed by the data itself.
Result
Below are the images showing the data sent by the serial console and the data stored in the RxData buffer.
- We are going to send a file of size 2176 bytes, so we need to send the size first.
- We send the size data.
- select the file that contains the data.
- send the file.
- The MCU has extracted the size data, and it is expecting 2176 bytes to be received.
The RxData buffer has the data. To make sure we received the entire data, we will cross check the start and end part with the actual data. The images below shows the comparison between the actual data, and the data stored in the RxData buffer.
You can see the actual data in the file and the data stored in the RxData buffer have the same content in the beginning and in the end. This means we have received entire data from the file.
DMA in Circular Mode
Let’s assume another case where we want to receive an audio or a video file from the UART and then store it in the SD card or a flash memory connected to the MCU. These types of files can be of few megabytes in size, so we can’t store them in a buffer. Instead we can receive a portion of the file and write it to SD card, then receive another portion and write it. This way we can transfer the entire file to the SD card without even storing it to the buffer in the MCU Ram.
Although I don’t want to involve the SD card related functions in this tutorial, so I will just use a buffer to store the data. If you are using an actual SD card or flash storage, you can use the same code, just instead of writing to buffer, write the data to the SD card. The process remains the same, so there aren’t many changes from the writing prospective.
CubeMX Configuration
Below is the cubeMX configuration to enable the UART DMA in circular mode.
The DMA request is set for USART2_RX as we are receiving the data via the DMA. The data width is Byte as the UART transfers the data in bytes. The DMA mode is set as Circular.
The rest of the UART configuration is same as the previous tutorials with Baud Rate of 115200 with 8 data bits, 1 stop bit and no parity.
In Circular mode, the DMA never stops automatically, it is always in the receiving mode. Once all the required number of data bytes has been received, it automatically reset the receive counter to 0 and hence starts receiving again.
We still need to know the size of the incoming data. So the sender should first send 4 bytes of the size data followed by the data itself. If you are receiving larger data, you can change the length of the size data bytes, the rest of the code will change accordingly.
The Code
In the main function, we will set the DMA to receive 256 bytes of data. This data will contain the size bytes as well as the actual data itself.
uint8_t RxData[256];
uint8_t FinalBuf[4096];
int main()
{
....
HAL_UART_Receive_DMA(&huart2, RxData, 256);
....
}
The 256 bytes we requested contains the size data as well as the actual data. Once 128 bytes are received, the half received complete callback will be called. We can handle the received data inside this callback, while the DMA continues to receive the second half. Once all the 256 bytes are received, the receive complete callback will be called. Here we will process the data received in the second half of the buffer, while the DMA continues to receive the 3rd half. This process keep going on until the sender stops sending the data.
int HTC = 0, FTC = 0;
uint32_t indx=0;
int isSizeRxed = 0;
uint32_t size=0;
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
if (isSizeRxed == 0)
{
size = ((RxData[0]-48)*1000)+((RxData[1]-48)*100)+((RxData[2]-48)*10)+((RxData[3]-48)); // extract the size
indx = 0;
memcpy(FinalBuf+indx, RxData+4, 124); // copy the data into the main buffer/file
memset(RxData, '\0', 128); // clear the RxData buffer
indx += 124; // update the indx variable
isSizeRxed = 1; // set the variable to 1 so that this loop does not enter again
}
else
{
memcpy(FinalBuf+indx, RxData, 128);
memset(RxData, '\0', 128);
indx += 128;
}
HTC=1; // half transfer complete callback was called
FTC=0;
}
The size data bytes are sent first so they are received in the first half of the received data. We will extract the size data, and write the rest of the data in the buffer/file inside the half received complete callback.
we are using 4 bytes for the size, so the remaining 124 bytes will be written to the buffer/file. We will also update the indx variable, which keep track of how many data bytes has been written to the buffer/file.
This callback will be called several times during the transfer, depending on how large data the sender is sending. We only need to extract the size data in the first call, and for the rest, we will simply copy the 128 bytes to the buffer/file.
Similarly, the receive complete callback is called whenever all 256 bytes are received.
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
memcpy(FinalBuf+indx, RxData+128, 128);
memset(RxData+128, '\0', 128);
indx+=128;
HTC=0;
FTC=1;
}
Here we will simply copy the 128 bytes, from the second half of the buffer into the final buffer/file. Then clear the RxData buffer and update the indx variable.
Copying data using the half and complete callback is fine as long as the received data is in the multiple of 256. If not, then we have an issue. For example, if we are receiving 260 bytes, the half callback will be called first, then the Rx complete callback will be called, and the remaining 4 bytes will be stored in the beginning if the RxData buffer. Since we did not received the 128 bytes, the half received callback will not be called and hence we might loose these 4 bytes.
To avoid this, we will manually check the received size with the size mentioned by the sender. If they are not equal, we will look where the remaining data is stored and then copy the data to our buffer/file.
Below is the code showing it in the while loop.
while (1)
{
if (((size-indx)>0) && ((size-indx)<128))
{
if (HTC==1)
{
strcpy((char *)FinalBuf+indx, (char *)RxData+128); // memcpy (FinalBuf+indx, RxData+128, (size-indx));
indx = size;
isSizeRxed = 0;
HTC = 0;
HAL_UART_DMAStop(&huart2);
HAL_UART_Receive_DMA(&huart2, RxData, 256);
}
else if (FTC==1)
{
strcpy((char *)FinalBuf+indx, (char *)RxData); // memcpy (FinalBuf+indx, RxData, (size-indx));
indx = size;
isSizeRxed = 0;
FTC = 0;
HAL_UART_DMAStop(&huart2);
HAL_UART_Receive_DMA(&huart2, RxData, 256);
}
We basically check if the difference between the size and indx variable is more than 0 and less than 128. This is necessary as the size variable is calculated in the beginning, so it have a large value. Also the indx variable increases as more data bytes are received. The value 128 is chosen because if we have more than 128 bytes remaining, either half or complete callback will trigger eventually.
So if we do enter inside this condition, it means that neither of the callbacks are being called. The sender have stopped sending the data, and we have some extra data in either the first half or the second half of the RxData buffer.
We will verify which half contains the data by checking the HTC and FTC variables. If the HTC variable is set, it means that half receive callback was called, and hence the data is in the second half of the RxData buffer. Similarly, if the FTC variable is set, it means that receive complete callback was called, and hence the data is in the first half of the RxData buffer.
We will simply copy the remaining data (size-indx) from the RxData buffer into the Final buffer/file. Then update the indx variable and reset the HTC/FTC variable. Also reset the isSizeRxed variable, so that the size of the new upcoming data can be processed.
Now we need to start storing the received data from the beginning of the RxData buffer. But the DMA in circular mode will just store the data at the very next position. So we need to manually stop the DMA and call the function again to receive 256 bytes of data.
We have discussed the case when we have received some extra bytes, which might be stored in either the first half or the second half of the RxData buffer. But we could also receive data in the multiples of 128, so there will be no extra byte at all.
We also need to handle this scenario.
else if ((indx == size) && ((HTC==1)||(FTC==1)))
{
isSizeRxed = 0;
HTC = 0;
FTC = 0;
HAL_UART_DMAStop(&huart2);
HAL_UART_Receive_DMA(&huart2, RxData, 256);
}
}
Here we will check if the size variable is equal to the indx variable. This could also be possible in the beginning when both of them are 0 and also when the previous if loop was executed. So we will add one more check to this condition. We will check if eiher of the HTC or FTC variables are set. This will confirm that the indx variable and the size variable are equal only after receiving all the data.
Inside this condition, we don’t need to copy any data since all the data has already been handled. We will simply reset the variables and start the DMA again.
Now we can receive the large data of any size and store in the buffer/file. Although we need to send the size first, followed by the data itself.
Result
Below are the images showing the data sent by the serial console and the data stored in the FinalBuf buffer.
As shown in the image above:
- We are going to send a file of size 2200 bytes, so we need to send the size first.
- We send the size data.
- select the file that contains the data.
- send the file.
- The MCU has extracted the size data, and it is expecting 2200 bytes to be received.
- The indx variable is 2200, which means that the MCU has received 2200 bytes.
VIDEO TUTORIAL
Watch the video below to see complete working.
Check out the Video Below
PROJECT DOWNLOAD
Info
You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.
🙏 Support Us by Disabling Adblock
We rely on ad revenue to keep Controllerstech free and regularly updated. If you enjoy the content and find it helpful, please consider whitelisting our website in your ad blocker.
We promise to keep ads minimal and non-intrusive.
Thank you for your support! 💙