Bare Metal STM32 · Part 5 of 7

ADC DMA Circular Mode: Continuous Sampling

STM32 ADC DMA Intermediate Source Code
Bare Metal STM32 Part 5
Bare Metal STM32 — Part 5
ADC DMA Circular Mode: Continuous Sampling
29:45
Part 5 / 7
29:45
Intermediate
ADC1 · DMA2
01

Overview

Polling the ADC end-of-conversion flag wastes CPU time and introduces jitter between samples. Combine continuous conversion mode with DMA circular transfers and the ADC fills a buffer automatically — the CPU only wakes up at half-buffer and full-buffer boundaries to process data. This is the foundation for audio processing, data logging, and any application that needs a clean, continuous sample stream.

  • Configure ADC1 in scan + continuous mode across 4 channels
  • Set up DMA2 Stream 0 in circular mode to auto-refill a double buffer
  • Handle HT and TC interrupts to process each half of the buffer while the other fills
  • Choose the right ADC sample time to avoid cross-channel contamination
02

Core Concepts

🔄
Circular DMA
DMA auto-reloads NDTR after each complete transfer, wrapping around to the start of the buffer. Fire-and-forget — never needs restarting. HT fires at N/2, TC fires at N samples.
DMA_SxCR: CIRC | HTIE | TCIE
📡
Scan Mode
ADC steps through a sequence of channels automatically. With continuous mode, it loops the sequence indefinitely. Each conversion result is written sequentially to the DR register, which DMA captures.
ADC_CR1: SCAN · ADC_CR2: CONT
⏱️
Sample Time
Longer sample time = more accurate reads but slower throughput. For high-impedance sources (>1kΩ), use at least 56 cycles. For low-impedance sources 15 cycles is sufficient.
ADC_SMPRx: SMP[2:0]
03

Code

01
ADC1 + DMA2 circular — 4 channels, double-buffered
adc_dma.c
C
#define NUM_CH   4
#define BUF_LEN  (NUM_CH * 32)   /* 32 samples per channel */

volatile uint16_t adc_buf[BUF_LEN];

void ADC1_DMA_Init(void) {
    /* Clocks */
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA2EN;
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;

    /* PA0–PA3 as analog inputs (MODER = 11) */
    GPIOA->MODER |= 0xFF;

    /* ADC prescaler: PCLK2/4 = 21 MHz (max ADC clock is 36 MHz) */
    ADC->CCR = ADC_CCR_ADCPRE_0;

    /* ADC1: scan, continuous, DMA circular, 12-bit, 4 conversions */
    ADC1->CR1 = ADC_CR1_SCAN;
    ADC1->CR2 = ADC_CR2_CONT | ADC_CR2_DMA | ADC_CR2_DDS;
    ADC1->SQR1 = ((NUM_CH - 1) << ADC_SQR1_L_Pos);  // 4 conversions

    /* Sequence: CH0→CH1→CH2→CH3, 56-cycle sample time each */
    ADC1->SQR3 = (0 << 0) | (1 << 5) | (2 << 10) | (3 << 15);
    ADC1->SMPR2 = 0x00B6DB6D;  // 56 cycles for ch 0–3

    /* DMA2 Stream0 Channel0 — ADC1 */
    DMA2_Stream0->CR  = DMA_SxCR_CIRC    // Circular
                      | DMA_SxCR_MINC    // Memory increment
                      | DMA_SxCR_PSIZE_0 // Periph 16-bit
                      | DMA_SxCR_MSIZE_0 // Memory 16-bit
                      | DMA_SxCR_HTIE    // Half-transfer IRQ
                      | DMA_SxCR_TCIE;   // Transfer-complete IRQ
    DMA2_Stream0->PAR  = (uint32_t)&ADC1->DR;
    DMA2_Stream0->M0AR = (uint32_t)adc_buf;
    DMA2_Stream0->NDTR = BUF_LEN;
    DMA2_Stream0->CR  |= DMA_SxCR_EN;

    NVIC_EnableIRQ(DMA2_Stream0_IRQn);

    ADC1->CR2 |= ADC_CR2_ADON;   // Power on ADC
    ADC1->CR2 |= ADC_CR2_SWSTART; // Start conversions
}

void DMA2_Stream0_IRQHandler(void) {
    if (DMA2->LISR & DMA_LISR_HTIF0) {
        DMA2->LIFCR = DMA_LIFCR_CHTIF0;
        Process_Samples(adc_buf, BUF_LEN / 2); // First half ready
    }
    if (DMA2->LISR & DMA_LISR_TCIF0) {
        DMA2->LIFCR = DMA_LIFCR_CTCIF0;
        Process_Samples(adc_buf + BUF_LEN/2, BUF_LEN/2); // Second half
    }
}
💡
DDS bit prevents DMA from stopping after one cycle
Set ADC_CR2_DDS (DMA Disable Selection) alongside the DMA bit. Without it, the ADC stops issuing DMA requests after the first buffer fill — making circular mode effectively useless.

Continue the Series

Work through all 7 parts of Bare Metal STM32 to master low-level embedded development from the ground up.