STM32 DMA Design Guide — Direct Memory Access, Circular Buffers, Double Buffer & Peripheral Streaming

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:

Burst Size: INCR4, INCR8, INCR16 (single AHB burst of 4/8/16 beats).
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”

发表评论