STM32 DMA Design Guide
Direct Memory Access, Circular Buffers, Double Buffer & Peripheral Streaming
1. Introduction — Why DMA Matters in STM32
Direct Memory Access (DMA) is the hardware subsystem that offloads data movement from the CPU, enabling peripherals to read and write system memory autonomously. In an STM32 application, DMA is not an optimization — it is the enabler of high-throughput peripherals (ADC continuous sampling at 5 MSPS, SDIO 50 MHz, DCMI camera streaming) that would otherwise consume 100% of the CPU in interrupt-driven data copying.
A single DMA transfer on STM32F4/H7 can move up to 64,000 data units (16-bit FIFO) per transaction, consuming zero CPU cycles during the transfer. When properly configured with circular or double-buffer modes, DMA enables glitch-free, real-time data pipelines that form the backbone of digital signal processing, motor control, and communication stacks.
2. STM32 DMA Architecture — Stream, Channel, and Request Mapping
2.1 DMA Controller Variants Across STM32 Families
| STM32 Family | DMA Controller | Streams | Channels per Stream | FIFO | Dual-Port |
|---|---|---|---|---|---|
| F0/F1/L0/L1 | DMA1, DMA2 | 7 channels each | Fixed (per channel) | No | No |
| F2/F4/F7 | DMA1, DMA2 | 8 streams each; 8 ch/stream | Flexible (stream × ch mux) | 4-word × 16 streams | AHB Matrix |
| H7 (single-core) | DMA1, DMA2, BDMA, MDMA | 8+8+8+16 | Peripheral request mux | Yes + BDMA | Yes (D1/D2/D3 domain) |
| G0/G4/L4/L5 | DMA1, DMA2 (optional), DMAMUX | 7–14 channels | Flexible via DMAMUX | No | No |
2.2 Stream and Channel Configuration (F4/F7)
On F4/F7 DMA controllers, a Stream is an independent transfer engine (8 per controller). Each Stream has a 4-word FIFO and is assigned to one of 8 Channels via the DMA_SxCR CHSEL bits. The Channel selects which peripheral request triggers the transfer. Key constraint: each Stream can only be assigned to one Channel at a time, and multiple Streams cannot be assigned to the same Channel simultaneously.
Priority between Streams is configurable (Low/Medium/High/Very High), with a round-robin tiebreaker for equal-priority Streams.
3. DMA Transfer Modes
3.1 Transfer Direction and Data Width
| Direction | DIR Bits | Source | Destination | Typical Use |
|---|---|---|---|---|
| P2M (Periph to Mem) | 00 | Peripheral DR (fixed) | Memory (incrementing) | ADC, UART RX, SPI RX, I2S RX |
| M2P (Mem to Periph) | 01 | Memory (incrementing) | Peripheral DR (fixed) | DAC, UART TX, SPI TX, I2S TX |
| M2M (Mem to Mem) | 10 | Memory (incrementing) | Memory (incrementing) | Buffer copy, framebuffer blit |
3.2 FIFO and Burst Transfers
Each DMA stream on F4/F7 includes a 4-word FIFO that decouples the AHB bus timing from peripheral timing. The FIFO threshold (1/4, 1/2, 3/4, full) determines how many data items accumulate before a burst transfer is attempted:
FIFO Threshold: Must be ≥ burst size. For a 16-beat burst, use Full threshold (4 words × 4 bytes/word = 16 bytes, but burst size is in beats, not bytes).
Rule of Thumb: For peripheral streaming (ADC, DAC), use single transfer (no burst) — peripherals have no burst capability. For M2M copy, use INCR16 + Full FIFO for maximum throughput.
4. Circular Buffer and Double Buffer Modes
4.1 Circular Mode (Continuous Sampling)
In circular mode (DMA_SxCR CIRC = 1), the DMA controller automatically wraps the memory pointer back to the start address after transferring NDTR items. This is the standard mode for ADC continuous conversion and DAC waveform generation:
// Circular DMA for ADC1 continuous scan (3 channels) on STM32F407
#define ADC_BUF_SIZE 300 // 100 samples × 3 channels
uint16_t adc_buffer[ADC_BUF_SIZE]; // in D2 SRAM (not CCM for DMA!)
void DMA2_Stream0_IRQHandler(void) {
if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HTIF0)) { // Half-Transfer
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HTIF0);
process_adc_data(adc_buffer, ADC_BUF_SIZE / 2); // first half ready
}
if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) { // Transfer Complete
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
process_adc_data(&adc_buffer[ADC_BUF_SIZE / 2],
ADC_BUF_SIZE / 2); // second half ready
}
}
void init_dma_adc(void) {
DMA_InitTypeDef dma;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_DeInit(DMA2_Stream0);
dma.DMA_Channel = DMA_Channel_0;
dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
dma.DMA_Memory0BaseAddr = (uint32_t)adc_buffer;
dma.DMA_DIR = DMA_DIR_PeripheralToMemory;
dma.DMA_BufferSize = ADC_BUF_SIZE;
dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
dma.DMA_MemoryInc = DMA_MemoryInc_Enable;
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
dma.DMA_Mode = DMA_Mode_Circular; // ← CIRCULAR
dma.DMA_Priority = DMA_Priority_High;
dma.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_Init(DMA2_Stream0, &dma);
DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE); // both interrupts
DMA_Cmd(DMA2_Stream0, ENABLE);
}
Key design pattern: The HT (Half-Transfer) + TC (Transfer Complete) interrupt pair enables ping-pong processing — the CPU operates on one half of the buffer while DMA fills the other half, providing zero-copy real-time processing with no data loss.
4.2 Double Buffer Mode
Double buffer mode (DMA_SxCR DBM = 1) provides two independent memory base addresses (M0AR and M1AR). The DMA controller automatically switches between them at the end of each transfer, and the current target is indicated by the CT (Current Target) bit. This is preferred over HT+TC when the processing interval exactly matches the buffer fill time:
| Feature | Circular + HT/TC | Double Buffer (DBM) |
|---|---|---|
| Buffer Count | 1 (split logically) | 2 (physically separate M0AR/M1AR) |
| Memory Layout | Contiguous (fixed start + NDTR) | Non-contiguous (arbitrary addresses) |
| Robustness | Less — race condition if CPU falls behind | More — hardware swap, no race |
5. ADC Continuous Sampling with DMA — Full Worked Example
5.1 Requirements
Sample 4 ADC channels (IN0–IN3) continuously at 1 MSPS aggregate (250 kSPS per channel) on STM32F407. Data must be processed in 1 ms chunks (4000 samples = 1000 per channel) with zero CPU intervention during acquisition.
5.2 Configuration
// ADC1: Scan mode, continuous, DMA circular
// TIM2 TRGO: triggers ADC at 1 MHz (APB1 = 42 MHz, PSC=0, ARR=41)
#define CHANNEL_COUNT 4
#define SAMPLES_PER_CH 1000
#define BUF_SIZE (CHANNEL_COUNT * SAMPLES_PER_CH)
uint16_t adc_dma_buf[BUF_SIZE]; // 8000 bytes. NOT in CCM (0x10000000)!
void init_adc_dma(void) {
// --- TIM2: 1 MHz trigger ---
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseInitTypeDef tim;
TIM_TimeBaseStructInit(&tim);
tim.TIM_Prescaler = 0;
tim.TIM_Period = 41; // 42 MHz / (0+1) / (41+1) = 1 MHz
TIM_TimeBaseInit(TIM2, &tim);
TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
TIM_Cmd(TIM2, ENABLE);
// --- ADC1: Scan 4 channels, triggered by TIM2 ---
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_CommonInitTypeDef adc_common;
ADC_InitTypeDef adc;
adc_common.ADC_Mode = ADC_Mode_Independent;
adc_common.ADC_Prescaler = ADC_Prescaler_Div2; // 84/2 = 42 MHz ADC clock
ADC_CommonInit(&adc_common);
adc.ADC_Resolution = ADC_Resolution_12b;
adc.ADC_ScanConvMode = ENABLE;
adc.ADC_ContinuousConvMode = DISABLE; // discontinuous, triggered by TIM2
adc.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO;
adc.ADC_DataAlign = ADC_DataAlign_Right;
adc.ADC_NbrOfConversion = 4;
ADC_Init(ADC1, &adc);
// Configure 4 channels (IN0–IN3), sample time 3 cycles each
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_3Cycles);
ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);
// --- DMA2 Stream0: P2M, Circular ---
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_InitTypeDef dma;
DMA_DeInit(DMA2_Stream0);
dma.DMA_Channel = DMA_Channel_0; // ADC1 on DMA2 Stream0 Ch0
dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
dma.DMA_Memory0BaseAddr = (uint32_t)adc_dma_buf;
dma.DMA_DIR = DMA_DIR_PeripheralToMemory;
dma.DMA_BufferSize = BUF_SIZE;
dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
dma.DMA_MemoryInc = DMA_MemoryInc_Enable;
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
dma.DMA_Mode = DMA_Mode_Circular;
dma.DMA_Priority = DMA_Priority_VeryHigh; // critical: avoid overrun
dma.DMA_FIFOMode = DMA_FIFOMode_Disable; // half-word transfer = no FIFO needed
DMA_Init(DMA2_Stream0, &dma);
DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE);
DMA_Cmd(DMA2_Stream0, ENABLE);
ADC_Cmd(ADC1, ENABLE);
}
5.3 Interrupt Handler with Ping-Pong Processing
void DMA2_Stream0_IRQHandler(void) {
static uint32_t half_flag = 0; // tracks which half just completed
if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HTIF0)) {
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HTIF0);
half_flag = 0; // first half: indices 0..1999
} else if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) {
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
half_flag = 1; // second half: indices 2000..3999
}
// Signal processing task to consume the ready half
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(adc_sem, &woken);
portYIELD_FROM_ISR(woken);
}
// Processing task (FreeRTOS)
void vADCTask(void *pv) {
for (;;) {
xSemaphoreTake(adc_sem, portMAX_DELAY);
uint16_t *buf = &adc_dma_buf[half_flag * (BUF_SIZE / 2)];
// buf[] now contains: [CH0_s0, CH1_s0, CH2_s0, CH3_s0,
// CH0_s1, CH1_s1, CH2_s1, CH3_s1, ...]
for (int i = 0; i < SAMPLES_PER_CH; i++) {
float vals[4];
for (int ch = 0; ch < 4; ch++)
vals[ch] = buf[i * 4 + ch] * (3.3f / 4096.0f);
process_sample(vals); // CPU-intensive, safe in task context
}
}
}
6. UART/SPI/I2S DMA Streaming
| Peripheral | DMA Mode | Typical Throughput | Key Gotcha |
|---|---|---|---|
| UART RX (idle line) | Normal (NDTR=1) + UART_IDLE ISR to restart | Up to 10.5 Mbps (F7/H7) | Variable-length frames: use IDLE interrupt to detect end of frame, re-init DMA count |
| SPI TX+RX (full duplex) | 2 DMA streams (TX + RX) on same SPI | Up to 50 Mbps (SPI at 50 MHz) | RX DMA must be enabled BEFORE TX DMA; peripheral clock must be on |
| I2S (audio streaming) | Circular DMA (TX) + double buffer | 48 kHz × 16-bit × 2ch = 192 kB/s | I2Sext vs I2Sx clock source; DMA channel fixed per SPI/I2S instance |
6.1 UART DMA with IDLE Line Detection (Variable-Length Frames)
// UART1 RX DMA + IDLE line: handles arbitrary frame lengths
#define UART_RX_BUF_SIZE 256
uint8_t uart_rx_buf[UART_RX_BUF_SIZE];
void init_uart_dma(void) {
// DMA1 Stream5 Ch4: USART1_RX → SRAM
DMA_InitTypeDef dma;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
DMA_DeInit(DMA1_Stream5);
dma.DMA_Channel = DMA_Channel_4;
dma.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
dma.DMA_Memory0BaseAddr = (uint32_t)uart_rx_buf;
dma.DMA_DIR = DMA_DIR_PeripheralToMemory;
dma.DMA_BufferSize = UART_RX_BUF_SIZE;
dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
dma.DMA_MemoryInc = DMA_MemoryInc_Enable;
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
dma.DMA_Mode = DMA_Mode_Normal; // ← NORMAL, not circular
dma.DMA_Priority = DMA_Priority_High;
dma.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_Init(DMA1_Stream5, &dma);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // IDLE line detection
DMA_Cmd(DMA1_Stream5, ENABLE);
}
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_IDLE)) {
USART_ReceiveData(USART1); // dummy read to clear IDLE flag
DMA_Cmd(DMA1_Stream5, DISABLE);
uint16_t len = UART_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Stream5);
// Process len bytes in uart_rx_buf[]
process_uart_frame(uart_rx_buf, len);
// Re-arm DMA for next frame
DMA_SetCurrDataCounter(DMA1_Stream5, UART_RX_BUF_SIZE);
DMA_Cmd(DMA1_Stream5, ENABLE);
}
}
7. BDMA and DMA2D — H7-Specific Features
7.1 BDMA (Basic DMA) — Low-Power Domain Transfers
On STM32H7, the BDMA controller operates in the D3 domain (SRAM4 + Backup SRAM) and remains active in STOP mode, enabling low-power peripheral data acquisition without waking the CPU. BDMA has 8 channels with DMAMUX for flexible request routing. Unlike DMA1/DMA2, BDMA accesses are limited to D3-domain SRAM and APB4 peripherals (LPUART, LPTIM, SPI6, I2C4).
7.2 DMA2D (Chrom-ART Accelerator) — Graphics Blit Engine
DMA2D is a specialized DMA for 2D graphics operations: rectangular block copy, color fill, pixel format conversion (RGB565↔ARGB8888), and alpha blending. It performs these operations at up to 200 Mpixel/s on H7 and is essential for smooth GUI rendering with STemWin/TouchGFX:
// DMA2D: Copy framebuffer with pixel format conversion (RGB565 → ARGB8888)
void dma2d_blit_convert(void *src, void *dst, uint32_t w, uint32_t h) {
DMA2D->CR = 0; // reset
DMA2D->OPFCCR = 0x4; // output format: ARGB8888
DMA2D->OOR = 0; // output offset (pitch − width)
DMA2D->OMAR = (uint32_t)dst;
DMA2D->FGPFCCR = 0x2; // foreground format: RGB565
DMA2D->FGOR = 0;
DMA2D->FGMAR = (uint32_t)src;
DMA2D->NLR = (w <CR = DMA2D_CR_START | (0x0 <CR & DMA2D_CR_START); // wait for completion (or use IRQ)
}
8. Common Pitfalls
8.1 DMA Buffer in CCMRAM (Core-Coupled Memory)
Problem: Placing DMA buffers in CCMRAM (0x10000000 on F4) for “fast access.” The DMA controller cannot access CCMRAM because it is on the D-bus only, not the system bus matrix. Data silently goes to a phantom location or causes a bus fault.
Fix: Always place DMA buffers in regular SRAM (SRAM1/SRAM2 at 0x20000000). Use linker section attributes: __attribute__((section(".dma_buffer"))) with a custom linker script section in SRAM, not CCM. On H7, ensure the buffer is in a domain accessible to the DMA controller (D2 SRAM for DMA1/2, D3 SRAM for BDMA).
8.2 Forgetting DMA_Cmd Before Peripheral_Cmd
Problem: Enabling the ADC with DMA request before enabling the DMA stream. The first conversion result arrives before the DMA is ready, and the DMA never catches up — all subsequent transfers are shifted by one.
Fix: Order matters: (1) Configure DMA stream (deinit, init, IT config). (2) DMA_Cmd(ENABLE). (3) Peripheral DMA request enable. (4) Peripheral enable (ADC_Cmd, TIM_Cmd, etc.).
8.3 DMA Stream Collision on Same Channel
Problem: Using DMA2 Stream0 Ch0 for ADC1 and DMA2 Stream3 Ch0 for TIM1_CC1 simultaneously. Both request the same Channel arbitration logic — one transfer blocks the other. If both are high-priority peripherals (ADC + motor control timer), one will experience FIFO underrun or overrun.
Fix: Audit all DMA channel assignments in a spreadsheet before coding. Each peripheral’s DMA request is hardwired to a specific (Stream, Channel) pair on F4/F7. Use the STM32CubeMX DMA tab to visualize conflicts. If conflicts are unavoidable, use a lower-throughput peripheral on the shared channel with lower priority, and ensure the high-priority Stream has higher priority settings.
9. Frequently Asked Questions
- Q: Circular mode vs double buffer — which should I use?
- Use circular mode with HT+TC interrupts when your processing time is shorter than half the buffer fill time. Use double buffer mode when processing time is close to the full buffer time (no mid-buffer switch point available), or when the two buffers reside at non-contiguous addresses (e.g., one in SRAM1, one in SRAM2). Double buffer mode is also preferred for audio — it provides a clean ping-pong boundary without worrying about HT timing jitter.
- Q: How do I calculate the maximum DMA throughput?
- DMA throughput is limited by the AHB bus matrix arbitration. On F4 at 168 MHz, sustained DMA throughput is ~80 MB/s (single stream) or ~40 MB/s per stream with two active streams. The formula: Tmax = fAHB × bus_width / (transfer_cycles + arbitration_cycles). A 32-bit transfer takes 1 AHB cycle (data) + 1 cycle (arbitration) = 2 cycles → 168M × 4B / 2 = 336 MB/s theoretical, but bus contention and SRAM access latency reduce this to ~80 MB/s sustained.
- Q: Why does my ADC DMA sometimes miss samples?
- Three causes: (1) DMA priority too low — a higher-priority DMA stream or CPU access stalls the ADC DMA just long enough to miss a conversion. Raise DMA priority to VeryHigh. (2) Buffer in CCMRAM — DMA can’t reach it. Use SRAM1/SRAM2. (3) ADC overrun (OVR flag) — the ADC converts faster than the DMA can unload the DR register. Reduce ADC clock prescaler or increase DMA priority. Check the ADC_SR OVR flag in the debugger — it’s the definitive sign.
- Q: Can I use DMA to transfer data from Flash to SRAM?
- Yes, on F4/F7/H7. The DMA controller can access Flash via the AHB bus. Use M2M mode (no peripheral trigger, software-triggered) with MEM2MEM bit set. However, for Flash-to-SRAM transfers, the CPU memcpy is often faster for 4 KB), DMA with INCR16 burst is 2–3× faster than CPU memcpy because the CPU is freed for other work during the transfer.
- Q: What is the MDMA on STM32H7 and when should I use it?
- MDMA (Master DMA) is the H7’s highest-performance DMA controller with 16 channels, 128-byte FIFO, linked-list mode (scatter-gather), and access to all memory domains (D1/D2/D3 + external SDRAM/QSPI). It achieves up to 1.2 GB/s throughput. Use MDMA for: (1) transfers between D1, D2, and D3 domains where regular DMA can’t cross domains; (2) complex scatter-gather operations via linked-list descriptors; (3) bulk external memory transfers (SDRAM framebuffer updates). Regular DMA1/DMA2 are sufficient for most peripheral streaming use cases.
References
- STM32F4 Reference Manual (RM0090), §9 “DMA Controller (DMA)”
- STM32H7 Reference Manual (RM0433), §15 “DMA Controller” and §18 “MDMA”
- AN4031: “Using the STM32F2, STM32F4 and STM32F7 Series DMA Controller”
- AN4666: “Getting Started with STM32H7 Series DMA”
- STMicroelectronics Wiki: “DMA2D Chrom-ART Accelerator Overview”