HomeUncategorizedSTM32 UART using LL Drivers (Part 2): Transmit using Interrupt & DMA

STM32 UART Transmit using LL Drivers (Interrupt & DMA)

UART is one of the most widely used communication interfaces in STM32 microcontrollers. It is commonly used for debugging, logging, and data exchange. In Part 1 of this series, we learned how to transmit UART data using polling mode with STM32 LL drivers. That tutorial is a prerequisite for this post, as it introduces the basic UART setup and LL driver usage.

In this tutorial, we move to more efficient methods. We will transmit UART data using interrupt and DMA with STM32 LL drivers. These methods allow the CPU to perform other tasks while data is being sent. We will first understand why polling is not enough, then learn how UART interrupts and DMA work, and finally implement both methods using simple code examples and full working code.

STM32 UART Transmit using LL Drivers (Interrupt & DMA)

Why Polling Is Not Enough for UART Transmission

Polling is the simplest way to use UART. The CPU continuously checks the UART status flags and sends data when the hardware is ready. This method works well for learning and small tests. However, as applications grow, polling starts to create serious limitations.

Why Polling is an issue

In polling mode, the CPU waits in a loop until the UART is ready to send the next byte. It keeps checking flags like TXE and TC again and again. While the CPU is polling, it cannot do any other useful work.
Even simple tasks like reading sensors, updating displays, or handling timers get delayed. This wastes CPU time and reduces overall system performance.

Polling may look harmless in small examples, but it creates problems in real applications. As the project grows, multiple peripherals need attention at the same time.

Using polling:

  • The CPU stays busy unnecessarily
  • Timing becomes unpredictable
  • Power consumption increases
  • The code becomes harder to scale

When Polling Can Still Be Used

Polling is not always bad. It is still useful in very simple applications where UART data is sent rarely.

Polling works well for:

  • Early learning and testing
  • Quick debug prints during bring-up
  • Very small programs with no timing constraints

Once the project becomes more complex, polling should be replaced with interrupt or DMA-based UART transmission.

Why We Need UART Interrupts in STM32

Polling keeps the CPU busy and limits performance. UART interrupts solve this problem by allowing the hardware to notify the CPU only when attention is needed. Instead of waiting in a loop, the CPU continues executing other code. When the UART is ready to send data or has fully completed a transmission, an interrupt is triggered. This makes UART communication more efficient and suitable for real applications.

An interrupt is a hardware signal that temporarily stops normal program execution and moves the CPU to a special function called an interrupt handler. In UART transmission, interrupts are generated when the transmit data register becomes empty or when the entire transmission is fully completed. The CPU reacts only when these events occur, which removes the need for continuous flag checking.

TXE and TC Flags Explained

UART transmission mainly uses two important flags. Both flags play a key role in interrupt-based UART transmission.

  • The TXE (Transmit Data Register Empty) flag indicates that the UART is ready to accept a new byte.
    When TXE becomes active, the next data byte can be written safely.
  • The TC (Transmission Complete) flag indicates that the last byte has fully left the shift register.
    This means all bits, including the stop bit, are sent on the UART line.

UART Transmission flow Using Interrupts

The interrupt-based UART transmission follows a simple flow.

Image shows the interrupt-based UART transmission flow in STM32 using LL drivers.
  • First, the application starts the transmission and enables the TXE interrupt.
    Each time TXE occurs, one byte is sent to the UART data register.
  • After the last byte is loaded, the TXE interrupt is disabled.
    Then the TC interrupt is enabled to detect when transmission is fully complete.
  • Once TC is triggered, the UART transmission ends.
    The application is notified that sending data is finished.

STM32 UART Transmission Using Interrupt (LL Drivers)

Now that we understand why UART interrupts are needed, we can move to the implementation. In this section, we will transmit UART data using TXE and TC interrupts with STM32 LL drivers.

We will first look at the variables required for interrupt-based transmission. Then we will understand how the transmit function and interrupt handler work together.

Global Variables Used for Interrupt Transmission

Interrupt-based UART transmission needs some global variables to track the data and its progress.

uint8_t *tx_buf = 0;
volatile uint16_t tx_len;
volatile uint16_t tx_index;
volatile int tx_done = 0;
  • Here, tx_buf stores the pointer to the transmit buffer.
  • tx_len holds the total number of bytes to send.
  • tx_index keeps track of the current byte being transmitted.
  • The tx_done flag is used to indicate when transmission is complete.

These variables are shared between the main code and the interrupt handler.


UART_Send_IT Function Explained

This function starts the UART transmission using interrupts.

void UART_Send_IT(uint8_t *buf, uint16_t len)
{
    tx_buf = buf;
    tx_len = len;
    tx_index = 0;
    tx_done = 0;

    LL_USART_ClearFlag_TC(USART2);   // VERY IMPORTANT
    LL_USART_EnableIT_TXE(USART2);
}
  • First, the transmit buffer and length are stored in global variables. The index is reset, and the completion flag is cleared.
  • The TC flag is cleared before enabling interrupts. This step is very important, as TC might still be set from a previous transmission.
  • Finally, the TXE interrupt is enabled. This allows the UART to request the CPU whenever it is ready to accept a new byte.

USART Interrupt Handler Logic

The actual data transmission happens inside the UART interrupt handler. You must define this function in the stm32f4xx_it.c file, inside the USART2_IRQHandler(). When the interrupt is triggered, the USART2_IRQHandler() is called, and here we will call USART2_Handler() to handle this interrupt ourselves.

void USART2_Handler(void)
{
    if (LL_USART_IsActiveFlag_TXE(USART2) && LL_USART_IsEnabledIT_TXE(USART2))
    {
        if (tx_index < tx_len)
        {
            LL_USART_TransmitData8(USART2, tx_buf[tx_index++]);

            if (tx_index == tx_len)
            {
                LL_USART_DisableIT_TXE(USART2);
                LL_USART_EnableIT_TC(USART2);
            }
        }
    }

    if (LL_USART_IsActiveFlag_TC(USART2) && LL_USART_IsEnabledIT_TC(USART2))
    {
        LL_USART_ClearFlag_TC(USART2);
        LL_USART_DisableIT_TC(USART2);
        tx_done = 1;
    }
}

When the TXE interrupt occurs, one byte is written to the UART data register. This continues until all bytes are loaded. After the last byte is sent, the TXE interrupt is disabled an then the TC interrupt is enabled to detect when the final bit is transmitted.

When the TC interrupt occurs, the transmission is fully complete, hence the tx_done flag is set to notify the application.


Sending Data from main() Using Interrupt

In the main loop, we can now send data using the interrupt-based function.

int len = sprintf((char *)buffer, "Hello %d\r\n", count++);
UART_Send_IT(buffer, len);

Here, sprintf() formats the message into a buffer. The buffer is then passed to UART_Send_IT() to start transmission. The CPU remains free while the data is being transmitted in the background.


Complete Interrupt-Based UART Transmission Code

Below is the full combined code for UART transmission using interrupts and STM32 LL drivers.

uint8_t *tx_buf = 0;
volatile uint16_t tx_len;
volatile uint16_t tx_index;
volatile int tx_done = 0;

void UART_Send_IT(uint8_t *buf, uint16_t len)
{
    tx_buf = buf;
    tx_len = len;
    tx_index = 0;
    tx_done = 0;

    LL_USART_ClearFlag_TC(USART2);
    LL_USART_EnableIT_TXE(USART2);
}

void USART2_Handler(void)
{
    if (LL_USART_IsActiveFlag_TXE(USART2) && LL_USART_IsEnabledIT_TXE(USART2))
    {
        if (tx_index < tx_len)
        {
            LL_USART_TransmitData8(USART2, tx_buf[tx_index++]);

            if (tx_index == tx_len)
            {
                LL_USART_DisableIT_TXE(USART2);
                LL_USART_EnableIT_TC(USART2);
            }
        }
    }

    if (LL_USART_IsActiveFlag_TC(USART2) && LL_USART_IsEnabledIT_TC(USART2))
    {
        LL_USART_ClearFlag_TC(USART2);
        LL_USART_DisableIT_TC(USART2);
        tx_done = 1;
    }
}

int count = 0;
uint8_t buffer[64];

int main()
{
  ....
  ....
  while (1)
  {
      int len = sprintf((char *)buffer, "Hello %d\r\n", count++);
      UART_Send_IT(buffer, len);
      LL_mDelay(1000);
  }
}

Output Using UART Interrupt Mode

The image below shows the UART output printed repeatedly on a serial terminal. The counter value increases every second, confirming that data is transmitted correctly using UART interrupt mode.

image shows the UART output printed repeatedly on a serial terminal. The counter value increases every second, confirming that data is transmitted correctly using UART interrupt mode.

Why We Need DMA for UART Transmission

Interrupt-based UART transmission is efficient, but it still requires CPU involvement for every byte. When the data size increases, interrupt overhead also increases. Interrupt-based UART works well for small and medium data sizes. However, it still interrupts the CPU for every transmitted byte.

This causes:

  • Reduced efficiency for large buffers
  • High interrupt frequency
  • Increased CPU load

This is where DMA becomes important. DMA allows data to move from memory to UART without CPU intervention. The CPU only configures DMA once and gets notified when the transfer is complete.

What Is DMA and How It Works

DMA stands for Direct Memory Access. It is a hardware block that transfers data between memory and peripherals.

For UART transmission, DMA:

  • Reads data directly from memory
  • Writes it to the UART data register
  • Works in the background without CPU involvement

The CPU only starts the DMA transfer and waits for a completion interrupt.


Flow of UART Transmission Using DMA

The DMA-based UART transmission follows a simple flow.

Image shows the DMA-based UART transmission flow in STM32 using LL drivers.
  • First, the application configures the DMA stream with memory and UART addresses.
    Then the UART DMA request is enabled.
  • DMA starts transferring data from memory to the UART data register.
    The CPU is not involved during the transfer.
  • Once all data is transmitted, a DMA transfer complete interrupt occurs.
    The application is notified that UART transmission has finished.

STM32 UART Transmission Using DMA (LL Drivers)

DMA makes UART transmission much more efficient by transferring data directly from memory to the UART peripheral. In this section, we will implement UART transmission using DMA with STM32 LL drivers.

UART_Send_DMA Function Explained

The UART_Send_DMA function starts the UART transmission using DMA.

void UART_Send_DMA(uint8_t *buf, uint16_t len)
{
    tx_done = 0;

    LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_6);

    LL_DMA_SetPeriphAddress(DMA1, LL_DMA_STREAM_6, LL_USART_DMA_GetRegAddr(USART2));
    LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_6, (uint32_t)buf);
    LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_6, len);

    LL_USART_ClearFlag_TC(USART2);
    LL_USART_EnableDMAReq_TX(USART2);

    LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_6);
    LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_6);
}

Here’s what happens step by step:

  1. Disable DMA stream to ensure it is ready for a new transfer.
  2. Set the peripheral (UART) and memory addresses and the data length.
  3. Clear the TC flag in the UART to avoid conflicts.
  4. Enable the UART DMA request so that UART can request data from DMA.
  5. Enable DMA transfer complete interrupt and start the DMA stream.

Once started, the DMA controller automatically moves data from memory to UART without CPU involvement.


DMA Interrupt Handler for UART Transmission

The DMA interrupt handler detects when the transfer is complete and notifies the application. You must define this function in the stm32f4xx_it.c file, inside the DMA1_Stream6_IRQHandler(). When the DMA finishes the transfer, an interrupt is triggered and the DMA1_Stream6_IRQHandler() is called. Here we will call USART_DMA_IRQHandler() to handle this interrupt ourselves.

void USART_DMA_IRQHandler(void)
{
    if (LL_DMA_IsActiveFlag_TC6(DMA1))
    {
        LL_DMA_ClearFlag_TC6(DMA1);
        LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_6);

        tx_done = 1;
    }
}

When the DMA transfer complete flag is set, the stream is disabled and tx_done is set to 1. This informs the main application that the transmission is finished.


Sending Data from main() Using DMA

Using DMA is simple in the main loop:

int len = sprintf((char *)buffer, "Hello %d\r\n", count++);
UART_Send_DMA(buffer, len);

Here, sprintf() formats the message into a buffer. UART_Send_DMA() then triggers the DMA transfer, sending the data in the background.
The CPU can continue executing other tasks while the transfer is in progress.


Full STM32 UART DMA Example Code

uint8_t buffer[64];
int count = 0;
volatile int tx_done = 0;

void UART_Send_DMA(uint8_t *buf, uint16_t len)
{
    tx_done = 0;

    LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_6);

    LL_DMA_SetPeriphAddress(DMA1, LL_DMA_STREAM_6, LL_USART_DMA_GetRegAddr(USART2));
    LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_6, (uint32_t)buf);
    LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_6, len);

    LL_USART_ClearFlag_TC(USART2);
    LL_USART_EnableDMAReq_TX(USART2);

    LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_6);
    LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_6);
}

void USART_DMA_IRQHandler(void)
{
    if (LL_DMA_IsActiveFlag_TC6(DMA1))
    {
        LL_DMA_ClearFlag_TC6(DMA1);
        LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_6);

        tx_done = 1;
    }
}

int main()
{
  ....
  ....
  
  while (1)
  {
      int len = sprintf((char *)buffer, "Hello %d\r\n", count++);
      UART_Send_DMA(buffer, len);
      LL_mDelay(1000);
  }
}  

This code demonstrates efficient UART transmission using DMA with STM32 LL drivers. It allows large data transfers while keeping the CPU free for other tasks.


Output Using UART DMA Mode

The UART data appears on the serial terminal just like with interrupts. The counter increments every second, confirming that the DMA transfer is working correctly.

image shows the UART output printed repeatedly on a serial terminal. The counter value increases every second, confirming that data is transmitted correctly using UART interrupt mode.

Compared to interrupts, CPU usage is lower, and the application can perform other tasks seamlessly.

Interrupt vs DMA UART Transmission in STM32

In STM32 projects, both interrupt-based and DMA-based UART transmission are widely used. Each method has advantages and limitations. Understanding the differences helps you choose the right approach for your application.

CPU Usage Comparison

Interrupt-based UART transmits one byte at a time. Each byte triggers an interrupt, which consumes CPU cycles.
For small messages, this is fine. But as data size grows, CPU usage increases significantly.

DMA, on the other hand, transfers the entire buffer in the background. The CPU only configures DMA and gets notified when the transfer is complete.
This frees the CPU to run other tasks during transmission.


Data Size and Speed Considerations

FeatureInterrupt ModeDMA Mode
CPU InvolvementHigh (every byte triggers an interrupt)Low (CPU not involved during transfer)
Best Data SizeSmall to mediumMedium to large
Maximum SpeedLimited by interrupt latencyHigher throughput, efficient for long buffers
ComplexitySimple to implementSlightly more setup (DMA config)
Power EfficiencyLower, more CPU cyclesHigher, CPU idle during transfer
Use CaseOccasional debug prints, small messagesContinuous data streaming, large data transfers

Which Method Should You Use

  • Interrupt Mode:
    Use it for small messages, debug prints, or when you want simple implementation.
    Ideal for projects where timing is not critical and CPU load is not a concern.
  • DMA Mode:
    Best for large buffers, high-speed UART, or RTOS-based applications.
    Reduces CPU load and allows the microcontroller to handle multiple tasks efficiently.

In most real-world applications, DMA is preferred for heavy UART traffic, while interrupts are enough for lightweight communication.

Conclusion

In this tutorial, we learned how to transmit UART data on STM32 using interrupts and DMA with LL drivers. We started by understanding why polling is not efficient, then explored how TXE and TC interrupts allow the CPU to transmit data without blocking. Next, we saw how DMA further improves efficiency by transferring large buffers directly from memory to the UART peripheral while freeing the CPU. We also compared interrupt and DMA methods, highlighting their differences in CPU usage, data size handling, and real-world applications.

These methods are highly useful for building efficient and responsive STM32 applications. Interrupt-based UART is great for small messages and debug prints, while DMA-based transmission is ideal for high-speed communication and large data transfers. By implementing these techniques, your projects can handle UART communication smoothly, keep the CPU free for other tasks, and improve overall system performance.

STM32 LL UART Project Download

Info

You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.

STM32 LL UART FAQs

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments