Sequenza di Scansione ADC con DMA su STM32: Acquisizione Multi-Canale Continua su STM32F4

2026-06-06 · Davide Carrese
STM32 · ADC · DMA · STM32F4 · Embedded

Ogni sistema embedded che legge segnali analogici — tensione di batteria, sensore di corrente, termistore, potenziometro, trasduttore di pressione — ha bisogno dell'ADC. Su STM32F4, l'ADC è un convertitore SAR a 12 bit capace di fino a 2.4 MSPS, con un motore di scansione flessibile che sequenzia automaticamente più canali. Se combinato con DMA, si ottiene un'acquisizione continua in anello senza un singolo ciclo di CPU speso in polling. Questo articolo descrive passo-passo la configurazione a livello di registro della modalità scan, conversione continua, trasferimento DMA e calibrazione su STM32F401.

Architettura dell'ADC su STM32F4

Lo STM32F401 ha un singolo ADC SAR a 12 bit con fino a 16 canali esterni (più canali interni per sensore di temperatura, VREFINT e VBAT). Il convertitore può essere configurato in due gruppi indipendenti:

Per la maggior parte dei task di acquisizione dati, si utilizza il gruppo regular in modalità scan. Il sequenziatore di scansione scorre i canali ordinati (SQ1…SQ16) in sequenza, convertendoli uno dopo l'altro. Ogni conversione attiva il flag EOC (end of conversion), e quando l'intera sequenza termina viene impostato il flag EOS (end of sequence).

Modalità scan + conversione continua

La modalità scan (bit SCAN in ADC_CR1) abilita il sequenziatore. Senza di essa, viene convertito solo SQ1. Con scan abilitato, l'ADC scorre tutti i rank configurati.

La conversione continua (bit CONT in ADC_CR2) fa sì che l'ADC riavvii la sequenza immediatamente dopo aver terminato una scansione, senza attendere un nuovo trigger. Questa è la modalità desiderata per acquisizione dati free-running.

Il trasferimento DMA (bit DMA in ADC_CR2) trasmette in streaming ogni dato convertito da ADC_DR non appena scatta EOC. Con DDS (DMA disable selection) azzerato, la richiesta DMA viene generata dopo ogni dato della sequenza, non solo a fine sequenza.

Lunghezza della sequenza e ranking dei canali

Il numero di canali nella scansione viene impostato in ADC_SQR1 (bits L[3:0]). La codifica è: 0 = 1 canale, 1 = 2 canali, …, 15 = 16 canali. Ogni rank registra il numero effettivo del canale nei registri SQR:

Ogni campo è un numero di canale a 5 bit (0–18 su STM32F401). L'ordine nei registri SQR definisce l'ordine di conversione, che è indipendente dal numero del canale.

Tempo di campionamento per canale

Ogni canale può avere un tempo di campionamento indipendente programmato in ADC_SMPR1 (canali 10–18) e ADC_SMPR2 (canali 0–9). Il tempo di campionamento influisce direttamente sul tempo totale di conversione:

TCONV = Tempo di campionamento + 12 cicli (per risoluzione 12 bit)

Sullo STM32F401, l'ADC è alimentato dal clock APB2 diviso da un prescaler (bits ADCPRE[1:0] in ADC_CCR). A 84 MHz APB2, impostando ADCPRE = 2 (÷4) si ottiene un clock ADC di 21 MHz — ben al di sotto del massimo di 36 MHz. Ogni ciclo ADC è quindi di circa 47.6 ns.

Opzioni del tempo di campionamento: 3, 15, 28, 56, 84, 112, 144 o 480 cicli. Per un'impedenza di sorgente di 10 kΩ che pilota un condensatore di campionamento da 4 pF, 15 cicli (≈ 0.71 µs) sono generalmente sufficienti; per sensori ad alta impedenza (come un partitore di tensione con resistenza serie da 100 kΩ), utilizzare 112 o 480 cicli.

Calibrazione dell'ADC

L'ADC dello STM32F4 ha una calibrazione integrata che compensa le variazioni di fabbricazione dell'offset del comparatore. La calibrazione deve essere eseguita quando l'ADC è acceso ma in idle:

void adc_calibrate(void)
{
    // Abilita il regolatore di tensione dell'ADC (bit ADON)
    ADC1->CR2 |= ADC_CR2_ADON;
    // Attendi l'avvio del regolatore (~10 µs)
    for (volatile int i = 0; i < 200; i++);
    // Avvia la calibrazione
    ADC1->CR2 |= ADC_CR2_CAL;
    while (ADC1->CR2 & ADC_CR2_CAL);
}

Il fattore di calibrazione viene memorizzato in ADC_DR dopo il completamento e applicato automaticamente. La calibrazione è valida per la VDDA e la temperatura correnti — eseguirla nuovamente se l'alimentazione o la temperatura cambiano significativamente. Alcune applicazioni eseguono la calibrazione ad ogni avvio; altre la eseguono una volta e memorizzano il fattore per i cicli di sospensione/ripresa.

Configurazione ADC + DMA a livello di registro

Ecco una sequenza di inizializzazione completa per una scansione a 4 canali (PA0–PA3 → ADC1_IN0…ADC1_IN3) a 21 MHz di clock ADC con tempo di campionamento 15 cicli, modalità continua e trasferimento DMA circolare in un buffer di 256 campioni:

#define ADC_BUF_LEN  256
static volatile uint16_t adc_buf[ADC_BUF_LEN];  // DMA scrive qui
static volatile uint32_t sample_count = 0;       // incrementato dall'ISR ADC

static void adc_gpio_init(void)
{
    // PA0..PA3 = ingresso analogico
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    GPIOA->MODER |= (3 << (0*2)) | (3 << (1*2)) | (3 << (2*2)) | (3 << (3*2));
    GPIOA->PUPDR &= ~(3 << (0*2)) | (3 << (1*2)) | (3 << (2*2)) | (3 << (3*2));
}

static void adc_init(void)
{
    // 1. Abilita clock ADC1 e DMA2
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
    __DSB();

    // 2. Registro comune ADC: prescaler = /4 → clock ADC 21 MHz
    ADC->CCR = (2 << ADC_CCR_ADCPRE_Pos);

    // 3. Calibrazione
    adc_calibrate();

    // 4. SCAN + CONT + DMA + DDS
    //    SCAN=1 (scan mode), EOCIE=0 (usiamo DMA), RES=0 (12-bit)
    ADC1->CR1 = ADC_CR1_SCAN;
    //    CONT=1 (continuo), DMA=1 (richieste DMA), DDS=0 (richiesta per dato)
    ADC1->CR2 = ADC_CR2_CONT | ADC_CR2_DMA;

    // 5. Tempo di campionamento: 15 cicli sui canali 0..3
    ADC1->SMPR2 = (1 << (0*3)) | (1 << (1*3)) | (1 << (2*3)) | (1 << (3*3));

    // 6. Sequenza: 4 canali (L=3 → 4 canali)
    ADC1->SQR1 = (3 << ADC_SQR1_L_Pos);                     // 4 canali totali
    ADC1->SQR3 = (0 << ADC_SQR3_SQ1_Pos)                    // SQ1 = ch0
                | (1 << ADC_SQR3_SQ2_Pos)                    // SQ2 = ch1
                | (2 << ADC_SQR3_SQ3_Pos)                    // SQ3 = ch2
                | (3 << ADC_SQR3_SQ4_Pos);                   // SQ4 = ch3

    // 7. DMA2 stream 0, canale 0 → ADC1 (vedi tabella mapping richieste DMA)
    DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;                 // sorgente = ADC DR
    DMA2_Stream0->M0AR = (uint32_t)adc_buf;                  // destinazione
    DMA2_Stream0->NDTR = ADC_BUF_LEN;                         // numero di trasferimenti
    DMA2_Stream0->CR = DMA_SxCR_CHSEL_0                     // canale = 0
                     | DMA_SxCR_MSIZE_0                     // memoria 16-bit
                     | DMA_SxCR_PSIZE_0                     // periferica 16-bit
                     | DMA_SxCR_MINC                        // incremento memoria
                     | DMA_SxCR_CIRC                        // modalità circolare
                     | DMA_SxCR_TCIE;                       // interrupt su completamento
    DMA2_Stream0->FCR = DMA_SxCR_DMDIS;                     // modalità diretta (nessun FIFO)

    // 8. Abilita stream DMA e ADC
    DMA2_Stream0->CR |= DMA_SxCR_EN;
    ADC1->CR2 |= ADC_CR2_ADON;                               // accensione ADC
    ADC1->CR2 |= ADC_CR2_SWSTART;                            // primo trigger software
}

void DMA2_Stream0_IRQHandler(void)
{
    if (DMA2_Stream0->SR & DMA_SR_TCIF0) {
        DMA2_Stream0->SR &= ~DMA_SR_TCIF0;
        sample_count += ADC_BUF_LEN;                         // un buffer completo acquisito
    }
}

Alcune note su questa configurazione:

Allineamento dei dati ADC

Il risultato a 12 bit può essere allineato a sinistra o a destra nel registro dati a 16 bit tramite il bit ALIGN in ADC_CR2:

Utilizzo allineamento a destra in tutti i progetti. Preserva il valore raw a 12 bit senza shift, importante quando si devono applicare coefficienti di calibrazione o lookup-table indicizzate dal codice a 12 bit completo.

Esempio pratico: data logger a 4 canali

Immaginate un progetto cliente che monitora quattro segnali analogici: tensione di batteria (partitore ÷3 su PA0), un termistore NTC da 10 kΩ (PA1), l'uscita di un amplificatore di current-sense (PA2) e un potenziometro di setpoint esterno (PA3). L'ADC converte continuamente tutti e quattro i canali a ~1.8 MSPS totali (~450 kSPS per canale). Ogni 256 scansioni complete (1024 campioni), l'ISR DMA imposta un flag e il loop principale elabora l'ultimo frame.

// Chiamato dal loop principale quando sample_count avanza
void process_adc_frame(void)
{
    // adc_buf contiene gli ultimi 256 campioni in ordine di scansione:
    //   [ch0_0, ch1_0, ch2_0, ch3_0, ch0_1, ch1_1, ...]
    // 64 campioni per canale in ogni frame (256 / 4 = 64)

    uint32_t sum_ch0 = 0, sum_ch1 = 0, sum_ch2 = 0, sum_ch3 = 0;
    uint16_t min_ch0 = 0xFFFF, max_ch0 = 0;

    for (int i = 0; i < ADC_BUF_LEN; i += 4) {
        uint16_t v0 = adc_buf[i + 0] & 0x0FFF;  // allineato a destra
        uint16_t v1 = adc_buf[i + 1] & 0x0FFF;
        uint16_t v2 = adc_buf[i + 2] & 0x0FFF;
        uint16_t v3 = adc_buf[i + 3] & 0x0FFF;

        sum_ch0 += v0;
        sum_ch1 += v1;
        sum_ch2 += v2;
        sum_ch3 += v3;

        if (v0 < min_ch0) min_ch0 = v0;
        if (v0 > max_ch0) max_ch0 = v0;
    }

    uint16_t avg_ch0 = sum_ch0 / 64;

    // Conversione in unità fisiche
    float vbat = avg_ch0 * 3.3f / 4096.0f * 3.0f;    // ×3 per il partitore
    float temp_raw = (float)sum_ch1 / 64.0f;
    // ... lookup-table NTC o equazione di Steinhart-Hart
}

L'ordine di scansione garantisce che gli indici dei canali siano interlacciati in uno schema deterministico. Se i canali del sensore hanno requisiti di tempo di campionamento diversi (ad esempio, un canale ad alta impedenza necessita di 480 cicli), è necessario assegnare tempi di campionamento per canale in SMPR1/SMPR2. L'ADC applica il tempo di campionamento del canale quando raggiunge quel rank nella scansione.

Checklist pratica

Come lo affronterei su un progetto cliente

Il codice sopra è lo scheletro che uso per validare l'hardware ADC su una nuova scheda — quattro canali casuali, free-running, DMA in un buffer, verifica con oscilloscopio che il front-end analogico sia pulito e i pin correttamente instradati. Una volta validato, sostituisco il buffer raw con una vera pipeline di condizionamento del segnale:

In un recente progetto industriale, ho utilizzato esattamente questo schema per un ADC a 6 canali su STM32F410 che leggeva PT1000 RTD attraverso un multiplexer e un amplificatore per strumentazione. Il doppio buffer DMA con conversione triggerata da timer a 200 Hz per canale ha prodotto una risoluzione effettiva di 14 bit puliti dopo oversampling 16× — il tutto senza un singolo ciclo CPU sprecato per lo spostamento dati.

Fonti e approfondimenti

Commenti

Hai commenti? Scrivimi un'email.