...

STM32 UART DMA Guide: Non-Blocking Serial Communication with Circular Buffers

Blocking UART reads stall your main loop and drop bytes at high baud rates. This guide shows how to receive variable-length UART data on STM32 using DMA in circular mode with idle-line detection, so your CPU stays free for everything else.

If you have ever polled a UART with HAL_UART_Receive in a loop, you already know the problem: the CPU sits there waiting for bytes that may never come, or arrive faster than you can read them. DMA solves this by letting the peripheral move bytes into memory on its own, freeing the CPU to do everything else. This guide covers the pattern most production STM32 firmware actually uses: UART RX in circular DMA mode combined with idle-line detection.

Why Circular DMA Instead of Normal Mode

Normal DMA mode fills a fixed-size buffer once and then stops, which is fine if you know exactly how many bytes are coming. Real serial data rarely works that way — GPS strings, AT command responses, and sensor packets all vary in length. Circular mode keeps DMA running indefinitely, wrapping back to the start of the buffer once it reaches the end, so the peripheral never stalls waiting for you to reset it.

Adding Idle-Line Detection

Circular DMA alone does not tell you when a message ends, since it just keeps filling the buffer. Idle-line detection solves this: the UART peripheral raises an interrupt when the line goes quiet for one frame period after receiving data, which is a reliable signal that a message is complete.

huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart1);

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);

Handling the Idle Interrupt

In the interrupt handler, clear the idle flag and calculate how many bytes actually arrived by comparing the DMA’s remaining-count register against the buffer size. This tells you the message length without needing a length prefix or terminator byte.

void USART1_IRQHandler(void) {
  if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
    __HAL_UART_CLEAR_IDLEFLAG(&huart1);
    uint16_t remaining = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
    uint16_t received_len = RX_BUFFER_SIZE - remaining;
    process_message(rx_buffer, received_len);
  }
  HAL_UART_IRQHandler(&huart1);
}

Keep the Interrupt Handler Short

Do not parse the message inside the interrupt handler itself. Copy it into a queue or set a flag and handle the actual parsing in your main loop or an RTOS task. This keeps interrupt latency low, which matters if you have other time-critical peripherals like PWM or ADC running on the same chip.

TX Side: DMA Out Too

The same principle applies to transmission. Use HAL_UART_Transmit_DMA for anything longer than a few bytes, and check HAL_UART_GetState or wait for the TX complete callback before queuing the next transmission, rather than blocking with HAL_UART_Transmit.

Where This Pattern Breaks Down

Circular DMA with idle detection works well for message-based protocols, but if your data has no natural pause between frames — continuous streaming sensor data, for example — you will need a different strategy, typically DMA half-transfer and full-transfer interrupts to process the buffer in two halves while the other half keeps filling. That is worth its own guide, but the setup above will cover the vast majority of UART use cases on STM32.

Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.