STM32 DMA Modes
Introduction
Direct Memory Access (DMA) is a powerful feature in STM32 microcontrollers that allows data to be transferred between memory and peripherals without CPU intervention. This significantly improves performance by freeing up the processor to handle other tasks while data transfers occur in the background.
In this tutorial, we'll explore the different DMA modes available on STM32 microcontrollers, understand when to use each mode, and implement practical examples to demonstrate their applications.
Understanding DMA Modes
STM32 microcontrollers offer several DMA modes, each designed for specific use cases:
- Normal Mode - Basic one-time data transfer
- Circular Mode - Continuous data transfers with automatic reload
- Double Buffer Mode - Seamless switching between two memory buffers
- Memory-to-Memory Mode - Direct transfers between memory locations
Let's explore each mode in detail.
Normal Mode
Normal mode is the simplest DMA operation mode. In this mode, the DMA controller transfers a specified amount of data from a source to a destination and then stops.
How Normal Mode Works
When to Use Normal Mode
- One-time data transfers
- Handling large but infrequent data blocks
- Operations that don't need to repeat automatically
Normal Mode Configuration Example
Here's how to configure a DMA channel in normal mode to transfer data from memory to a UART peripheral:
/* DMA Normal Mode Configuration */
void ConfigDMA_NormalMode(void)
{
/* Enable clock for DMA1 */
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
/* Reset DMA1 Stream3 configuration */
DMA1_Stream3->CR = 0;
/* Wait until DMA stream is disabled */
while(DMA1_Stream3->CR & DMA_SxCR_EN);
/* Configure DMA Stream */
DMA1_Stream3->PAR = (uint32_t)&USART3->DR; /* Peripheral address */
DMA1_Stream3->M0AR = (uint32_t)txBuffer; /* Memory address */
DMA1_Stream3->NDTR = TX_BUFFER_SIZE; /* Number of data items to transfer */
/* Configure stream:
* Memory-to-peripheral, Normal mode,
* Memory increment, Priority high */
DMA1_Stream3->CR |= DMA_SxCR_DIR_0 | /* Memory to peripheral */
DMA_SxCR_MINC | /* Memory increment mode */
DMA_SxCR_PL_1 | /* Priority high */
DMA_SxCR_CHSEL_0 | DMA_SxCR_CHSEL_1; /* Channel 6 selection for USART3_TX */
/* Enable DMA transfer complete interrupt */
DMA1_Stream3->CR |= DMA_SxCR_TCIE;
/* Enable DMA Stream */
DMA1_Stream3->CR |= DMA_SxCR_EN;
/* Enable USART DMA transmit request */
USART3->CR3 |= USART_CR3_DMAT;
}
Handling the DMA Interrupt
/* DMA1 Stream3 interrupt handler */
void DMA1_Stream3_IRQHandler(void)
{
/* Check transfer complete flag */
if(DMA1->HISR & DMA_HISR_TCIF3)
{
/* Clear transfer complete flag */
DMA1->HIFCR |= DMA_HIFCR_CTCIF3;
/* Handle transfer complete event */
TransferComplete_Callback();
}
}
Circular Mode
Circular mode allows the DMA controller to automatically reload and restart the transfer once it completes. This creates a continuous data stream and is especially useful for handling periodic data like ADC samples or audio streams.
How Circular Mode Works
When to Use Circular Mode
- Continuous data sampling (ADC)
- Audio processing
- Communication protocols requiring continuous data streams
- Real-time data acquisition systems
Circular Mode Configuration Example
Here's how to configure a DMA channel in circular mode to continuously receive data from an ADC:
/* DMA Circular Mode Configuration for ADC */
void ConfigDMA_CircularMode(void)
{
/* Enable clock for DMA2 */
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
/* Reset DMA2 Stream0 configuration */
DMA2_Stream0->CR = 0;
/* Wait until DMA stream is disabled */
while(DMA2_Stream0->CR & DMA_SxCR_EN);
/* Configure DMA Stream */
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; /* Peripheral address (ADC data register) */
DMA2_Stream0->M0AR = (uint32_t)adcBuffer; /* Memory address */
DMA2_Stream0->NDTR = ADC_BUFFER_SIZE; /* Number of data items to transfer */
/* Configure stream:
* Peripheral-to-memory, Circular mode,
* Peripheral data size: half-word (16-bit),
* Memory increment, Priority high */
DMA2_Stream0->CR |= DMA_SxCR_CIRC | /* Circular mode */
DMA_SxCR_MINC | /* Memory increment mode */
DMA_SxCR_PSIZE_0 | /* Peripheral data size: half-word (16-bit) */
DMA_SxCR_MSIZE_0 | /* Memory data size: half-word (16-bit) */
DMA_SxCR_PL_1 | /* Priority high */
DMA_SxCR_CHSEL_0; /* Channel 0 selection for ADC1 */
/* Enable DMA half transfer and transfer complete interrupts */
DMA2_Stream0->CR |= DMA_SxCR_HTIE | DMA_SxCR_TCIE;
/* Enable DMA Stream */
DMA2_Stream0->CR |= DMA_SxCR_EN;
/* Enable ADC DMA request */
ADC1->CR2 |= ADC_CR2_DMA;
}
Handling Half-Transfer and Complete-Transfer Interrupts
/* DMA2 Stream0 interrupt handler */
void DMA2_Stream0_IRQHandler(void)
{
/* Check half transfer flag */
if(DMA2->LISR & DMA_LISR_HTIF0)
{
/* Clear half transfer flag */
DMA2->LIFCR |= DMA_LIFCR_CHTIF0;
/* Process first half of buffer while DMA fills second half */
ProcessFirstHalfOfBuffer();
}
/* Check transfer complete flag */
if(DMA2->LISR & DMA_LISR_TCIF0)
{
/* Clear transfer complete flag */
DMA2->LIFCR |= DMA_LIFCR_CTCIF0;
/* Process second half of buffer while DMA fills first half */
ProcessSecondHalfOfBuffer();
}
}
Double Buffer Mode
Double buffer mode is an advanced feature available on some STM32 devices. It allows the DMA controller to automatically switch between two memory buffers, providing a ping-pong mechanism for continuous data processing.
How Double Buffer Mode Works
When to Use Double Buffer Mode
- Real-time audio processing
- High-speed data acquisition
- Applications where you need to process one buffer while the other is being filled
- Scenarios requiring zero data loss during processing
Double Buffer Mode Configuration Example
Here's how to configure a DMA channel in double buffer mode for continuous ADC sampling with two buffers:
/* DMA Double Buffer Mode Configuration */
void ConfigDMA_DoubleBufferMode(void)
{
/* Enable clock for DMA2 */
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
/* Reset DMA2 Stream0 configuration */
DMA2_Stream0->CR = 0;
/* Wait until DMA stream is disabled */
while(DMA2_Stream0->CR & DMA_SxCR_EN);
/* Configure DMA Stream */
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR; /* Peripheral address (ADC data register) */
DMA2_Stream0->M0AR = (uint32_t)adcBuffer0; /* Memory 0 address (first buffer) */
DMA2_Stream0->M1AR = (uint32_t)adcBuffer1; /* Memory 1 address (second buffer) */
DMA2_Stream0->NDTR = ADC_BUFFER_SIZE; /* Number of data items to transfer */
/* Configure stream:
* Peripheral-to-memory, Circular mode, Double buffer mode,
* Memory increment, Priority very high */
DMA2_Stream0->CR |= DMA_SxCR_CIRC | /* Circular mode */
DMA_SxCR_DBM | /* Double buffer mode */
DMA_SxCR_MINC | /* Memory increment mode */
DMA_SxCR_PSIZE_0 | /* Peripheral data size: half-word (16-bit) */
DMA_SxCR_MSIZE_0 | /* Memory data size: half-word (16-bit) */
DMA_SxCR_PL_1 | DMA_SxCR_PL_0 | /* Priority very high */
DMA_SxCR_CHSEL_0; /* Channel 0 selection for ADC1 */
/* Enable DMA transfer complete interrupt */
DMA2_Stream0->CR |= DMA_SxCR_TCIE;
/* Enable DMA Stream */
DMA2_Stream0->CR |= DMA_SxCR_EN;
/* Enable ADC DMA request */
ADC1->CR2 |= ADC_CR2_DMA;
}
Handling Double Buffer Mode Interrupts
/* DMA2 Stream0 interrupt handler for double buffer mode */
void DMA2_Stream0_IRQHandler(void)
{
/* Check transfer complete flag */
if(DMA2->LISR & DMA_LISR_TCIF0)
{
/* Clear transfer complete flag */
DMA2->LIFCR |= DMA_LIFCR_CTCIF0;
/* Check which buffer is currently being used by DMA */
if(DMA2_Stream0->CR & DMA_SxCR_CT)
{
/* DMA is currently using Memory 1, so we can process Memory 0 */
ProcessBuffer(adcBuffer0, ADC_BUFFER_SIZE);
}
else
{
/* DMA is currently using Memory 0, so we can process Memory 1 */
ProcessBuffer(adcBuffer1, ADC_BUFFER_SIZE);
}
}
}
Memory-to-Memory Mode
Memory-to-Memory mode allows the DMA controller to transfer data directly from one memory area to another, without involving peripherals. This is particularly useful for fast memory copy operations.
How Memory-to-Memory Mode Works
When to Use Memory-to-Memory Mode
- Fast memory copy operations
- Display frame buffer updates
- Moving large blocks of data between RAM regions
- Data preprocessing before sending to peripherals
Memory-to-Memory Mode Configuration Example
Here's how to configure a DMA channel for memory-to-memory transfer:
/* DMA Memory-to-Memory Mode Configuration */
void DMA_MemToMem_Transfer(uint32_t *srcAddress, uint32_t *destAddress, uint32_t dataSize)
{
/* Enable clock for DMA2 */
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
/* Reset DMA2 Stream0 configuration */
DMA2_Stream0->CR = 0;
/* Wait until DMA stream is disabled */
while(DMA2_Stream0->CR & DMA_SxCR_EN);
/* Configure DMA Stream */
DMA2_Stream0->PAR = (uint32_t)srcAddress; /* Source address */
DMA2_Stream0->M0AR = (uint32_t)destAddress; /* Destination address */
DMA2_Stream0->NDTR = dataSize; /* Number of data items to transfer */
/* Configure stream:
* Memory-to-memory mode,
* Source and destination memory increment,
* Priority very high */
DMA2_Stream0->CR |= DMA_SxCR_DIR_1 | /* Memory to memory mode */
DMA_SxCR_MINC | /* Memory increment mode (destination) */
DMA_SxCR_PINC | /* Peripheral increment mode (source) */
DMA_SxCR_PSIZE_1 | /* Peripheral data size: word (32-bit) */
DMA_SxCR_MSIZE_1 | /* Memory data size: word (32-bit) */
DMA_SxCR_PL_1 | DMA_SxCR_PL_0; /* Priority very high */
/* Enable DMA transfer complete interrupt */
DMA2_Stream0->CR |= DMA_SxCR_TCIE;
/* Enable DMA Stream (start transfer) */
DMA2_Stream0->CR |= DMA_SxCR_EN;
}
Practical Application: Fast Frame Buffer Copy
/* Example: Copy a frame from back buffer to display buffer */
void UpdateDisplay(void)
{
/* Copy the back buffer to the display buffer using DMA */
DMA_MemToMem_Transfer(
(uint32_t*)backBuffer,
(uint32_t*)displayBuffer,
DISPLAY_WIDTH * DISPLAY_HEIGHT / 4 /* Size in 32-bit words */
);
/* The CPU can continue with other tasks while DMA performs the copy */
PrepareNextFrame();
}
Real-World Application Examples
Example 1: Audio Processing with DMA Circular Mode
This example demonstrates how to use DMA in circular mode to continuously sample audio data from a microphone connected to the ADC:
#define AUDIO_BUFFER_SIZE 1024
uint16_t audioBuffer[AUDIO_BUFFER_SIZE];
volatile uint8_t audioBufferHalfFull = 0;
volatile uint8_t audioBufferFull = 0;
void InitAudioCapture(void)
{
/* Configure ADC for audio sampling */
ConfigureADC();
/* Configure DMA in circular mode */
/* Enable clock for DMA2 */
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
/* Configure DMA2 Stream0 for ADC1 */
DMA2_Stream0->CR = 0;
while(DMA2_Stream0->CR & DMA_SxCR_EN);
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;
DMA2_Stream0->M0AR = (uint32_t)audioBuffer;
DMA2_Stream0->NDTR = AUDIO_BUFFER_SIZE;
/* Peripheral to memory, Circular mode, Half-word size, Interrupts */
DMA2_Stream0->CR |= DMA_SxCR_CIRC | DMA_SxCR_MINC |
DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0 |
DMA_SxCR_HTIE | DMA_SxCR_TCIE |
DMA_SxCR_CHSEL_0;
/* Enable DMA interrupts in NVIC */
NVIC_EnableIRQ(DMA2_Stream0_IRQn);
/* Start DMA */
DMA2_Stream0->CR |= DMA_SxCR_EN;
/* Enable ADC DMA request */
ADC1->CR2 |= ADC_CR2_DMA;
/* Start ADC conversion */
ADC1->CR2 |= ADC_CR2_SWSTART;
}
/* DMA interrupt handler */
void DMA2_Stream0_IRQHandler(void)
{
/* Half transfer complete */
if(DMA2->LISR & DMA_LISR_HTIF0)
{
DMA2->LIFCR |= DMA_LIFCR_CHTIF0;
audioBufferHalfFull = 1;
}
/* Transfer complete */
if(DMA2->LISR & DMA_LISR_TCIF0)
{
DMA2->LIFCR |= DMA_LIFCR_CTCIF0;
audioBufferFull = 1;
}
}
/* Main processing loop */
void ProcessAudio(void)
{
while(1)
{
/* Process first half of buffer */
if(audioBufferHalfFull)
{
audioBufferHalfFull = 0;
ApplyAudioFilter(&audioBuffer[0], AUDIO_BUFFER_SIZE/2);
}
/* Process second half of buffer */
if(audioBufferFull)
{
audioBufferFull = 0;
ApplyAudioFilter(&audioBuffer[AUDIO_BUFFER_SIZE/2], AUDIO_BUFFER_SIZE/2);
}
}
}
Example 2: Efficient UART Communication with DMA Normal Mode
This example shows how to use DMA in normal mode to send large data blocks over UART without blocking the CPU:
#define TX_BUFFER_SIZE 256
uint8_t txBuffer[TX_BUFFER_SIZE];
volatile uint8_t transferComplete = 0;
void SendDataOverUART(uint8_t *data, uint32_t size)
{
/* Copy data to transmit buffer */
for(uint32_t i = 0; i < size && i < TX_BUFFER_SIZE; i++)
{
txBuffer[i] = data[i];
}
/* Configure DMA for UART TX */
DMA1_Stream3->CR = 0;
while(DMA1_Stream3->CR & DMA_SxCR_EN);
DMA1_Stream3->PAR = (uint32_t)&USART3->DR;
DMA1_Stream3->M0AR = (uint32_t)txBuffer;
DMA1_Stream3->NDTR = size;
/* Memory to peripheral, Normal mode, Memory increment */
DMA1_Stream3->CR |= DMA_SxCR_DIR_0 | DMA_SxCR_MINC |
DMA_SxCR_TCIE |
DMA_SxCR_CHSEL_0 | DMA_SxCR_CHSEL_1; /* Channel 6 for USART3_TX */
/* Reset transfer complete flag */
transferComplete = 0;
/* Enable DMA */
DMA1_Stream3->CR |= DMA_SxCR_EN;
/* Enable UART DMA TX request */
USART3->CR3 |= USART_CR3_DMAT;
/* CPU can now do other tasks while DMA handles the transfer */
}
/* DMA interrupt handler */
void DMA1_Stream3_IRQHandler(void)
{
/* Check transfer complete flag */
if(DMA1->HISR & DMA_HISR_TCIF3)
{
/* Clear transfer complete flag */
DMA1->HIFCR |= DMA_HIFCR_CTCIF3;
/* Set transfer complete flag */
transferComplete = 1;
}
}
Comparison of DMA Modes
Mode | Key Features | Best For | Limitations |
---|---|---|---|
Normal | One-time transfer Simple setup | Infrequent transfers Simple operations | Requires CPU to restart for each transfer |
Circular | Continuous operation Auto-reload | Continuous sampling Real-time applications | Fixed buffer size Can't change config during operation |
Double Buffer | Ping-pong buffers Continuous processing | High-performance applications Zero-loss processing | More complex to implement Not available on all STM32 devices |
Memory-to-Memory | Direct memory copies High-speed transfers | Fast memory operations Data preprocessing | Uses more DMA bandwidth Not for peripheral transfers |
Debugging DMA Issues
When working with DMA, you might encounter some common issues:
-
Transfer Not Starting
- Check DMA stream enable bit (DMA_SxCR_EN)
- Verify peripheral DMA request is enabled
- Ensure addresses are properly aligned
-
Data Corruption
- Check data width settings (PSIZE and MSIZE)
- Verify buffer alignment matches data width
- Ensure source/destination addresses are valid
-
Interrupt Not Triggering
- Verify interrupt flags are enabled in DMA_SxCR
- Check NVIC configuration
- Clear pending flags before starting transfer
-
Buffer Overruns
- Ensure buffer size matches NDTR value
- For circular mode, process data fast enough
- Consider using double buffer mode for high data rates
Summary
In this tutorial, we've explored the different DMA modes available on STM32 microcontrollers:
- Normal Mode: For one-time transfers with simplicity
- Circular Mode: For continuous operations with automatic reload
- Double Buffer Mode: For high-performance continuous processing
- Memory-to-Memory Mode: For efficient data movement between memory areas
Each mode serves specific use cases and provides different advantages in terms of performance, flexibility, and implementation complexity. By choosing the right DMA mode for your application, you can significantly improve system performance while reducing CPU load.
Exercises
- Configure a DMA channel in normal mode to transfer a block of data from memory to SPI peripheral.
- Implement an ADC data acquisition system using DMA circular mode to continuously sample sensor data.
- Create a double-buffer audio processing system that captures audio samples with one buffer while processing the other.
- Implement a fast display update mechanism using memory-to-memory DMA to copy frame buffers.
- Compare the performance difference between CPU-based memory copy and DMA memory-to-memory transfer for different block sizes.
Additional Resources
- STM32 Reference Manuals (RM0090 for STM32F4xx series)
- STM32 DMA Driver Documentation
- ST Community Forums
- Application Notes:
- AN4031: Using the STM32F2, STM32F4 and STM32F7 Series DMA controller
- AN4640: Managing memory with DMAMUX in STM32 devices
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)