Skip to main content

STM32 DMA Circular Mode

Introduction

Direct Memory Access (DMA) is a powerful feature in STM32 microcontrollers that allows data transfers between peripherals and memory without CPU intervention. One particularly useful configuration is Circular Mode, which enables continuous data transfers in a loop. This is especially valuable for applications requiring ongoing sampling, buffering, or streaming of data.

In this tutorial, we'll explore how DMA Circular Mode works, when to use it, and how to implement it in your STM32 projects.

What is DMA Circular Mode?

DMA Circular Mode is a configuration setting that causes the DMA controller to automatically reset to the beginning of the source/destination buffer once it reaches the end. Instead of stopping after transferring a specified number of data items, the DMA controller continues operation by wrapping around to the start address.

Let's visualize how this works:

In normal mode, the DMA stops after reaching the final transfer. In circular mode, it returns to the start address and continues transferring data indefinitely until you explicitly disable it.

When to Use Circular Mode

Circular mode is ideal for:

  1. Continuous ADC sampling - Collecting sensor readings continuously
  2. Audio processing - Streaming audio data from/to peripherals
  3. Communication buffers - Managing UART, SPI, or I2C data streams
  4. Signal processing - Real-time analysis of continuous data streams
  5. Double buffering - Processing one half of a buffer while the other half is being filled

Basic Configuration Steps

To set up DMA Circular Mode, follow these general steps:

  1. Enable the DMA clock
  2. Configure the DMA channel/stream parameters
  3. Enable the circular mode flag
  4. Configure the associated peripheral
  5. Enable the DMA stream/channel
  6. Start the peripheral operation

Let's dive into each step with code examples.

Implementing DMA Circular Mode

Example 1: ADC Continuous Sampling with DMA Circular Mode

This example demonstrates how to configure the DMA to continuously sample from an ADC channel and store the results in a buffer.

c
#include "stm32f4xx_hal.h"

#define ADC_BUFFER_SIZE 256

// Buffer to store ADC values
uint16_t ADC_Buffer[ADC_BUFFER_SIZE];

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);

int main(void)
{
HAL_Init();
SystemClock_Config();

MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();

// Start ADC with DMA
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_Buffer, ADC_BUFFER_SIZE);

while (1)
{
// Your application code here
// ADC_Buffer is continuously updated in the background
}
}

static void MX_DMA_Init(void)
{
// Enable DMA2 clock
__HAL_RCC_DMA2_CLK_ENABLE();

// Configure DMA2 Stream0 for ADC1
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; // Set circular mode
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;

HAL_DMA_Init(&hdma_adc1);

// Link DMA to ADC
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

// Configure NVIC for DMA
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}

static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};

// Configure ADC1
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE; // Continuous conversion
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = ENABLE; // Important for circular mode
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;

HAL_ADC_Init(&hadc1);

// Configure ADC channel
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;

HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

The key configurations for circular mode in this code are:

  • hdma_adc1.Init.Mode = DMA_CIRCULAR - Enables circular mode for the DMA
  • hadc1.Init.ContinuousConvMode = ENABLE - Makes the ADC convert continuously
  • hadc1.Init.DMAContinuousRequests = ENABLE - Ensures DMA requests continue to be generated

Example 2: UART Reception with Circular DMA

This example shows how to set up continuous UART reception using DMA circular mode, which is useful for command-line interfaces or protocol parsers.

c
#include "stm32f4xx_hal.h"

#define RX_BUFFER_SIZE 64

uint8_t RxBuffer[RX_BUFFER_SIZE];
uint16_t RxReadPos = 0;

UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART2_UART_Init(void);
void ProcessData(void);

int main(void)
{
HAL_Init();
SystemClock_Config();

MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();

// Start receiving data in circular mode
HAL_UART_Receive_DMA(&huart2, RxBuffer, RX_BUFFER_SIZE);

while (1)
{
// Process any received data
ProcessData();
}
}

static void MX_DMA_Init(void)
{
// Enable DMA1 clock
__HAL_RCC_DMA1_CLK_ENABLE();

// Configure DMA for USART2 Rx
hdma_usart2_rx.Instance = DMA1_Stream5;
hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; // Set circular mode
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_usart2_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;

HAL_DMA_Init(&hdma_usart2_rx);

// Link DMA to UART
__HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx);

// Configure NVIC for DMA
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}

static void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;

HAL_UART_Init(&huart2);
}

void ProcessData(void)
{
// Calculate current position in buffer
uint16_t rxPos = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);

// Process all new data
while (RxReadPos != rxPos)
{
// Process byte at RxReadPos
uint8_t byte = RxBuffer[RxReadPos];

// Echo back the received character
HAL_UART_Transmit(&huart2, &byte, 1, HAL_MAX_DELAY);

// Advance read position with wrap-around
RxReadPos = (RxReadPos + 1) % RX_BUFFER_SIZE;
}
}

In this example:

  • The DMA continuously receives UART data into a circular buffer
  • We track the position of new data using __HAL_DMA_GET_COUNTER to determine how much data has been received
  • The application processes new data as it arrives while DMA continues filling the buffer

Half-Complete and Complete Callbacks

One of the significant benefits of DMA circular mode is the ability to use half-complete callbacks. This enables double-buffering techniques where you process one half of the buffer while the other half is being filled.

Here's an example showing how to implement callbacks:

c
#include "stm32f4xx_hal.h"

#define BUFFER_SIZE 512
uint16_t ADC_Buffer[BUFFER_SIZE];
volatile uint8_t HalfBufferReady = 0;
volatile uint8_t FullBufferReady = 0;

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

// DMA half transfer complete callback
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
// First half of buffer is ready for processing
HalfBufferReady = 1;
}

// DMA transfer complete callback
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// Second half of buffer is ready for processing
FullBufferReady = 1;
}

int main(void)
{
// ... (Initialize system, peripherals, DMA, ADC as in previous examples)

// Start ADC with DMA in circular mode
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_Buffer, BUFFER_SIZE);

while (1)
{
// Process first half of buffer when ready
if (HalfBufferReady)
{
ProcessData(&ADC_Buffer[0], BUFFER_SIZE/2);
HalfBufferReady = 0;
}

// Process second half of buffer when ready
if (FullBufferReady)
{
ProcessData(&ADC_Buffer[BUFFER_SIZE/2], BUFFER_SIZE/2);
FullBufferReady = 0;
}
}
}

void ProcessData(uint16_t* data, uint16_t size)
{
// Process the data (perform filtering, analysis, etc.)
for (uint16_t i = 0; i < size; i++)
{
// Example: Apply some processing to the data
// processed_data[i] = data[i] * coefficient;
}
}

This double-buffering technique is extremely powerful because it:

  1. Ensures no data is lost during processing
  2. Provides consistent timing for data processing
  3. Maximizes throughput of your application

Advanced Techniques: Stream Processing

A common use case for DMA circular mode is real-time stream processing. Here's an example that performs a moving average filter on continuous ADC data:

c
#include "stm32f4xx_hal.h"

#define BUFFER_SIZE 256
uint16_t ADC_Buffer[BUFFER_SIZE];
uint16_t ProcessedBuffer[BUFFER_SIZE/2];

#define FILTER_SIZE 8
uint32_t FilterSum = 0;
uint16_t FilterBuffer[FILTER_SIZE] = {0};
uint8_t FilterIndex = 0;

volatile uint8_t HalfBufferReady = 0;
volatile uint8_t FullBufferReady = 0;

// Callback functions (as in the previous example)
// ...

void ProcessData(uint16_t* data, uint16_t size, uint16_t* output)
{
// Apply moving average filter to the data
for (uint16_t i = 0; i < size; i++)
{
// Remove oldest sample from sum
FilterSum -= FilterBuffer[FilterIndex];

// Add new sample to buffer and sum
FilterBuffer[FilterIndex] = data[i];
FilterSum += data[i];

// Update index with wrap-around
FilterIndex = (FilterIndex + 1) % FILTER_SIZE;

// Calculate average
output[i] = FilterSum / FILTER_SIZE;
}
}

int main(void)
{
// ... (Initialize system, peripherals, DMA, ADC)

// Start ADC with DMA in circular mode
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_Buffer, BUFFER_SIZE);

while (1)
{
// Process first half of buffer when ready
if (HalfBufferReady)
{
ProcessData(&ADC_Buffer[0], BUFFER_SIZE/2, &ProcessedBuffer[0]);
// Use the processed data (display, transmit, etc.)
HalfBufferReady = 0;
}

// Process second half of buffer when ready
if (FullBufferReady)
{
ProcessData(&ADC_Buffer[BUFFER_SIZE/2], BUFFER_SIZE/2, &ProcessedBuffer[0]);
// Use the processed data (display, transmit, etc.)
FullBufferReady = 0;
}
}
}

Important Considerations

When working with DMA Circular Mode, keep these points in mind:

  1. Buffer Size: Choose an appropriate buffer size based on your application's requirements. Larger buffers provide more time for processing but consume more RAM.

  2. Interrupt Priority: Set DMA interrupt priorities appropriately to ensure timely processing of data.

  3. Data Consistency: When reading from a circular buffer that's being written to by DMA, be careful about data consistency. Use the techniques shown above (separate processing of half-buffers) to avoid reading data that's currently being written.

  4. Buffer Overruns: If your processing can't keep up with incoming data, you'll experience buffer overruns. Monitor and handle these situations appropriately.

  5. Alignment: Some STM32 DMA controllers require specific memory alignment for efficient operation. Check your device's documentation.

  6. DMA Channel Selection: Ensure you're using the correct DMA channel/stream for the peripheral you're working with.

Debugging DMA Circular Mode

Debugging DMA operations can be challenging. Here are some tips:

  1. Use Status Flags: Check DMA status flags to identify issues.
c
// Check if DMA is active
if (HAL_DMA_GetState(&hdma_adc1) != HAL_DMA_STATE_BUSY)
{
// DMA is not running
}

// Check for DMA errors
if (HAL_DMA_GetError(&hdma_adc1) != HAL_DMA_ERROR_NONE)
{
// Handle DMA error
}
  1. LED Indicators: Use LEDs to signal when half/complete callbacks are triggered.

  2. Test Points: Add GPIO toggles at key points in your code to monitor timing with an oscilloscope.

c
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // Signal start of processing
HalfBufferReady = 1;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // Signal end of processing
}

Real-World Application: Audio Processing

Here's a simplified example of using DMA circular mode for audio processing:

c
#include "stm32f4xx_hal.h"

#define AUDIO_BUFFER_SIZE 1024
int16_t AudioInBuffer[AUDIO_BUFFER_SIZE];
int16_t AudioOutBuffer[AUDIO_BUFFER_SIZE];

// Handle structures
I2S_HandleTypeDef hi2s2;
DMA_HandleTypeDef hdma_spi2_rx;
DMA_HandleTypeDef hdma_spi2_tx;

volatile uint8_t HalfBufferReady = 0;
volatile uint8_t FullBufferReady = 0;

void ProcessAudio(int16_t* input, int16_t* output, uint16_t size)
{
// Simple echo effect (50% of original signal)
for (uint16_t i = 0; i < size; i++)
{
output[i] = input[i] / 2;
}
}

// DMA callbacks
void HAL_I2S_RxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
// Process first half of buffer
ProcessAudio(&AudioInBuffer[0], &AudioOutBuffer[0], AUDIO_BUFFER_SIZE/2);
HalfBufferReady = 1;
}

void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s)
{
// Process second half of buffer
ProcessAudio(&AudioInBuffer[AUDIO_BUFFER_SIZE/2], &AudioOutBuffer[AUDIO_BUFFER_SIZE/2], AUDIO_BUFFER_SIZE/2);
FullBufferReady = 1;
}

int main(void)
{
// Initialize peripherals, DMA, I2S
// ...

// Start receiving audio data via I2S with DMA
HAL_I2S_Receive_DMA(&hi2s2, (uint16_t*)AudioInBuffer, AUDIO_BUFFER_SIZE);

// Start transmitting processed audio via I2S with DMA
HAL_I2S_Transmit_DMA(&hi2s2, (uint16_t*)AudioOutBuffer, AUDIO_BUFFER_SIZE);

while (1)
{
// Main application loop (system can do other tasks)
// The audio processing happens in the DMA callbacks
}
}

This example demonstrates a real-time audio processing system where:

  1. Input audio is continuously received using DMA circular mode
  2. Processing is done in the DMA callbacks
  3. Output audio is continuously transmitted using another DMA channel

Summary

STM32 DMA Circular Mode is a powerful feature that enables continuous data transfers without CPU intervention. It's ideal for applications requiring continuous data streaming or buffering, such as ADC sampling, audio processing, and communication interfaces.

Key points to remember:

  • Circular mode automatically resets to the beginning of the buffer after reaching the end
  • Half-complete and complete callbacks enable efficient double-buffering techniques
  • Proper buffer management is crucial to avoid data corruption
  • The CPU is free to perform other tasks while DMA handles data transfers

By mastering DMA circular mode, you can create more efficient and responsive embedded applications that handle continuous data streams with minimal CPU overhead.

Exercises

  1. Basic Implementation: Configure an STM32 to continuously sample an analog input (e.g., potentiometer) using ADC with DMA circular mode, and display the values on an LED bar graph or UART.

  2. Buffer Processing: Implement a moving average filter on continuous ADC data using DMA circular mode.

  3. Advanced Application: Create a digital oscilloscope that continuously samples an analog signal, processes the data, and displays it on an attached display or transmits it to a PC application.

  4. Challenge: Implement a circular buffer data structure with producer/consumer roles where DMA is the producer (writing data) and your main application code is the consumer (reading and processing data).

Further Reading

  • STM32 Reference Manual: DMA Controller sections
  • STM32 Application Notes (especially AN4031 on using DMA for memory-to-memory transfers)
  • STM32 HAL API documentation


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)