How to use DMA in circular mode on STM32?

 Using DMA in circular mode on STM32 basically means:

“Keep filling the same buffer in RAM over and over, wrapping around when you reach the end.”

This is perfect for continuous ADC sampling, UART RX streams, audio, etc. Here’s how to do it, in practical steps.




1. Basic idea

  • Peripheral data register → DMA → RAM buffer.

  • DMA writes:

    • buf[0] … buf[N-1]

    • then automatically wraps back to buf[0] and continues.

  • Your code never restarts the DMA; it just reads from the buffer while DMA keeps filling it.


2. Create the buffer in RAM

Example for an ADC (16-bit samples):

#define ADC_BUF_LEN 256 uint16_t adc_buf[ADC_BUF_LEN];

Example for UART RX (8-bit):

#define UART_RX_BUF_LEN 128 uint8_t uart_rx_buf[UART_RX_BUF_LEN];

Make sure the buffer is global or static so it stays allocated.


3. Configure DMA in circular mode (HAL example)

Let’s say you’re using STM32Cube HAL.

3.1. In CubeMX / code, set:

  • Direction: Peripheral → Memory

  • Mode: DMA_CIRCULAR

  • PeriphInc: DMA_PINC_DISABLE (peripheral addr fixed)

  • MemInc: DMA_MINC_ENABLE (walk through buffer)

  • Data width:

    • If ADC 12-bit → usually use 16-bit (DMA_PDATAALIGN_HALFWORD, DMA_MDATAALIGN_HALFWORD)

    • If UART → 8-bit (BYTE)

Example manual init (F4 style):

hdma_adc1.Instance = DMA2_Stream0; hdma_adc1.Init.Channel = DMA_CHANNEL_0; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // <- key line hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc1); // Link to peripheral __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

For UART RX it’s the same idea, but Direction = DMA_PERIPH_TO_MEMORY with 8-bit alignment and linked to huartX.


4. Start DMA in circular mode

ADC example

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);

UART RX example

HAL_UART_Receive_DMA(&huart2, uart_rx_buf, UART_RX_BUF_LEN);

Because the DMA is in circular mode, it will never “finish” – it just keeps overwriting the buffer.


5. How to process data from the circular buffer

Two common strategies:

5.1. Half-transfer + transfer-complete (ping-pong style)

Enable DMA interrupts for:

  • Half transfer

  • Transfer complete

HAL lets you implement callbacks:

  • HAL_ADC_ConvHalfCpltCallback() / HAL_DMA_XferHalfCpltCallback()

  • HAL_ADC_ConvCpltCallback() / HAL_DMA_XferCpltCallback()

Concept:

  • Half-transfer interrupt
    → first half of the buffer [0 … N/2 - 1] is full → process that chunk.

  • Transfer-complete interrupt
    → second half [N/2 … N - 1] is full → process that chunk.

Example:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { // Process first half: adc_buf[0 .. ADC_BUF_LEN/2 - 1] } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // Process second half: adc_buf[ADC_BUF_LEN/2 .. ADC_BUF_LEN-1] }

Or, for a generic DMA stream:

void HAL_DMA_XferHalfCpltCallback(DMA_HandleTypeDef *hdma) { // First half ready } void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma) { // Second half ready }

This ping-pong approach is simple and robust for streaming data.


5.2. True “ring buffer” with head/tail pointers

If you don’t want fixed half-chunks, you can treat the DMA buffer as a ring and track where the DMA is currently writing.

For STM32, the DMA has a “number of data to transfer” (NDTR) register.
For a circular buffer:

uint16_t dma_get_head(void) { // How many elements have been written so far: uint16_t remaining = __HAL_DMA_GET_COUNTER(&hdma_adc1); // NDTR return (ADC_BUF_LEN - remaining) % ADC_BUF_LEN; }

Then you maintain your own tail index of “data already processed”:

static uint16_t tail = 0; void process_adc_ring(void) { uint16_t head = dma_get_head(); while (tail != head) { uint16_t sample = adc_buf[tail]; // TODO: process sample here tail++; if (tail >= ADC_BUF_LEN) tail = 0; } }

Call process_adc_ring() from your main loop or from a timer/IRQ when you know new data is available.

Tip: briefly disable interrupts while reading NDTR + updating tail if you need absolute safety against race conditions.


6. Circular DMA for UART RX with idle-line detection (very common)

Typical pattern to receive variable-length messages:

  1. Circular DMA on UART RX:

    HAL_UART_Receive_DMA(&huart2, uart_rx_buf, UART_RX_BUF_LEN);
  2. Enable IDLE line interrupt on the UART:

    __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
  3. In the UART IRQ handler or callback:

    void HAL_UART_IDLE_Callback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // Clear IDLE flag by reading SR then DR __HAL_UART_CLEAR_IDLEFLAG(huart); uint16_t head = UART_RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart->hdmarx); // Process data between tail and head (ring buffer logic like above) // ... } }

Now the DMA keeps filling the buffer; when the line goes idle, you know a frame ended and can process all bytes since the last time.


7. Things to watch out for

  • Buffer size:
    Must fit in RAM; big audio/video buffers can eat memory quickly.

  • Alignment:
    Data width (byte/halfword/word) must match peripheral and buffer type.

  • Caches (F7/H7 etc.):
    If your MCU has D-Cache, DMA buffers must be in non-cached region or you must clean/invalidate cache around DMA.

  • Overruns:
    If your processing is too slow and head overtakes tail, you’ll lose data. Make sure your processing loop is fast enough.


TL;DR

  1. Create a global buffer in RAM.

  2. Configure DMA channel/stream:

    • Mode = DMA_CIRCULAR

    • MemInc = ENABLE, PeriphInc = DISABLE

  3. Start DMA from peripheral to that buffer (e.g. HAL_ADC_Start_DMA, HAL_UART_Receive_DMA).

  4. Use half/full transfer callbacks or head/tail + NDTR to know which part of the circular buffer to process.

评论

此博客中的热门博文

Detailed Explanation of STM32 HAL Library Clock System

How To Connect Stm32 To PC?

How do you set up ADC (Analog-to-Digital Converter) in STM32?