STM32 UART #4 || Receive Data using DMA
This is the 4th tutorial in the series on the UART peripheral of STM32 Microcontrollers. In this series we will cover different ways of transmitting and receiving data over the UART protocol. We will also see different UART modes available in the STM32 microcontrollers and how to use them.
In the previous tutorial we saw how to receive the data over the UART in blocking mode and using the interrupt. Today we will continue receiving the data, but we will use the DMA to do so. DMA is generally used when we need to receive a large amount of data. The DMA bus is a separate unit and hence the data transfer does not utilize the CPU. Instead the CPU can be used to handle the other tasks.
In today’s tutorial, 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.
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.
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.
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. 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.
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.
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. 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.