How to use Interrupt & DMA to Transmit Data
This is the 2nd 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 this tutorial, we will see how to use the interrupt and DMA to send the data over the UART. We have already covered the configuration and sending data using the blocking mode in the previous tutorial. We will also understand why it is not convenient to send large data in the blocking mode and how interrupt or DMA makes it easy.
Sending Large data in blocking mode
Let’s assume a case where we want to send 10 kilobytes of data continuously via the UART. We will also blink a LED periodically indicating a process that needs to run at a fixed interval. Below is the code to send the data in the blocking mode.
uint8_t TxData[10240];
int main()
{
...
...
/* Fill array with some data */
for (uint32_t i=0; i<10240; i++)
{
TxData[i] = i&(0xff);
}
while (1)
{
HAL_UART_Transmit(&huart2, TxData, 10240, HAL_MAX_DELAY);
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
}
}
Here we defined a 10 KB array. Then filled the array with some data in the main function. The data is sent to the UART in the while loop and the timeout is set to HAL_MAX_DELAY. This basically means the function will never timeout and it can take as much time as it wants to send the data. After the data is sent, the LED will toggle and the loop repeats every 500ms.
As I mentioned the LED blinking can be assumed as a process which runs periodically. The data transmission should not affect the rate of this process.
But this does not takes pace as expected. Below is the gif showing the LED blinking rate.
You can see the LED is taking more than 500 ms to blink. This is because the UART transfer is taking significant amount of time and it blocks the CPU while transmitting the data. This affects other processes as they have to wait for the CPU.
This issue generally arise when transferring large amount of data. Small data is transferred in an instant, so we don’t need to worry about that.
To solve this issue, the data needs to be transmitted in the background while the CPU can process other tasks. This is why we use interrupt or DMA in the first place.
Using Interrupt
Interrupt can be used to send the data to the UART in the background. While the transmission is taking place, the CPU can process other tasks as well. Note that even the data transmission takes place in the background, the data is still transmitted by the CPU, so it still puts the load on the CPU.
Below is the cubeMX configuration for the interrupt.
I am keeping the same configuration as used in the previous tutorial. Here we will just enable the UART interrupt in the NVIC tab.
The data sent using interrupt is transferred in the background. Once the data is sent completely, an interrupt will trigger and the transfer complete callback is called.
int isSent = 1;
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
isSent = 1;
countinterrupt++;
}
Inside the callback we will increment the interrupt counter. This keeps track of how many times the callback was called. We also set the variable isSent indicating that the data has been transferred. This is to make sure that CPU sends the data again only after the previous data has been transferred.
To do so, we will guard the Transmit function with the condition as shown below.
int main()
{
...
...
while (1)
{
if (isSent == 1)
{
HAL_UART_Transmit_IT(&huart2, TxData, 10240);
isSent = 0;
}
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
countloop++;
}
}
The HAL_UART_Transmit_IT is used to transfer the data in the interrupt mode. The function only runs if the previous data has been transmitted. We toggle the LED at the normal rate of 500ms and also increment the loop counter. This keeps track of how many times the while loop runs.
Below is the image showing the loop counters after running the code for few seconds.
As you can see the interrupt counter is half of that of the loop counter. Actually the loop is running every 500ms and the interrupt is only triggered once the data is sent completely, which is taking around 1 second.
Below is the gif showing the LED blinking.
The LED blinks every 500ms. This means the CPU is not blocked for the transmission and it can process the rest of the tasks while the data is being transmitted in the background.
So the Interrupt can be used to transfer large data and the CPU can still run other tasks as usual. But the data transfer is still done by the CPU so it increases the load. If you want to transfer data once in a while, it is fine to use the interrupt, but if the data is being transferred continuously, it is better to use the DMA instead.
Using DMA
The Direct memory access (DMA) is used to provide high-speed data transfers between peripherals and memory and between memory and memory. Data can be quickly moved by the DMA without any CPU action. This keeps CPU resources free for other operations.
Below is the cubeMX configuration for the DMA mode.
- The DMA Request is set to USART_TX as we want to use DMA to send the data.
- The DMA Stream is selected automatically by the cubeMX. It is basically the DMA channel that we are going to use for the data transmission.
- The direction is Memory to Peripheral as we are sending the data from the MCU memory to the UART peripheral.
- The Data width is set to Byte as we are sending one byte data to the UART.
- After sending 1 byte, the memory address will increment so it can send the next data byte.
The DMA can work in 2 modes, Normal and Circular. In Normal mode, the DMA sends the data once and then stops automatically. This is just like how we used the interrupt. In circular mode, The data flow is continuous. The source address, the destination address and the number of data to be transferred are automatically reloaded after the transfer completes. This allows DMA to continuously transmit the same data until we issue the stop command manually.
Sending data in normal mode
In Normal mode, the DMA stops after sending the data completely. An interrupt will trigger and the transfer complete callback is called. We will send data the same way we did in case of the interrupt.
int isSent = 1;
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
isSent = 1;
countinterrupt++;
}
int main()
{
...
...
while (1)
{
if (isSent == 1)
{
HAL_UART_Transmit_DMA(&huart2, TxData, 10240);
isSent = 0;
}
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
countloop++;
}
}
The HAL_UART_Transmit_DMA is used to transfer the data in the DMA mode. The function only runs if the previous data has been transmitted. We toggle the LED at the normal rate of 500ms and also increment the loop counter. This keeps track of how many times the while loop runs.
Below is the image showing the loop counters after running the code for few seconds.
As you can see the interrupt counter is half of that of the loop counter. Actually the loop is running every 500ms and the interrupt is only triggered once the data is sent completely, which is taking around 1 second.
Below is the gif showing the LED blinking.
The LED blinks every 500ms. This means the CPU is not blocked for the transmission and it can process the rest of the tasks while the data is being transmitted in the background. Since DMA is being used to transfer the data, the CPU load is also reduced.
DMA should be used when we want to transfer a very large data or if the data is being transmitted continuously. For the continuous transfer of data, we use the circular mode.
Sending data in circular mode
We first need to select the Circular mode in the cubeMX configuration as shown below.
In circular mode the Data is transmitted continuously. Once all the data has been sent, the DMA reloads the same data again and sends it. We can also update the data in the runtime and use the DMA more efficiently.
Actually the 2 interrupts gets triggered while transmitting the data via the DMA. The Half Transmit Complete callback is called when the DMA finished transferring half the data and the Transmit complete callback is called when the DMA transferred all the data. For example, if we are transferring 10KB data, the HT Callback will be called when DMA finished transferring 5KB and the TC Callback will be called when DMA finished transferring 10KB.
When the HT callback is called, the DMA is still transferring the 2nd half of the buffer. In that time, we can load a new data to the first half of the TX buffer.
int indx = 49; // char '1'
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
{
for (uint32_t i=0; i<5120; i++)
{
TxData[i] = indx;
}
indx++;
}
Similarly, when the TC callback is called, the DMA is start transferring the 1st half of the buffer. In that time, we can load a new data to the 2nd half of the TX buffer.
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
for (uint32_t i=5120; i<10240; i++)
{
TxData[i] = indx;
}
indx++;
if (indx>=60)
{
HAL_UART_DMAStop(&huart2);
}
isSent = 1;
countinterrupt++;
}
This way we are loading a new data to the DMA in parallel to the previous data transfer.
This loop keeps running till we issue the stop command. Here we will set a condition and stop the DMA when the condition is satisfied.
We still need to call the DMA once in the main function, so that everything can start from there.
int main ()
{
/* Fill array with some data */
for (uint32_t i=0; i<10240; i++)
{
TxData[i] = i&(0xff);
}
HAL_UART_Transmit_DMA(&huart2, TxData, 10240);
while (1)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
countloop++;
}
}
You can check the result better in the video attached below.
So the Interrupt and DMA can be used to send large data continuously. This allows the CPU to handle other tasks in a proper way.