Sequenza di Scansione ADC con DMA su STM32: Acquisizione Multi-Canale Continua su STM32F4
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:
- Gruppo regular — fino a 16 canali sequenziati in ordine programmabile. Le conversioni vengono innescate da software, timer o pin esterno. I risultati finiscono in un unico registro
ADC_DR. - Gruppo injected — fino a 4 canali con priorità sul gruppo regular. Quando si verifica un trigger injected, la conversione regular viene interrotta, i canali injected vengono convertiti e la conversione regular riprende da dove si era fermata. Ogni canale injected ha il proprio registro dati (
ADC_JDR1..4).
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:
ADC_SQR3— SQ1 (bits31:28) … SQ6 (bits11:8)ADC_SQR2— SQ7 … SQ12ADC_SQR1— SQ13 … SQ16
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:
- DMA circolare —
CIRCfa sì che lo stream si riavvolga nel buffer. Il buffer si comporta come un anello; si leggono gli ultimiADC_BUF_LENcampioni in qualsiasi momento. L'interrupt di completamento trasferimento scatta ogni volta che il buffer si riempie, consentendo di scandire l'elaborazione ai frame boundary. - Trasferimenti a 16 bit — Il risultato ADC è a 12 bit allineato a destra o sinistra in un registro a 16 bit. Con
PSIZEeMSIZEimpostati a 16 bit, ogni beat DMA trasferisce un risultato di conversione. - Selezione del canale DMA — DMA2 stream 0 con canale 0 si mappa su ADC1 su STM32F401. Verificare sempre il mapping delle richieste DMA nel manuale di riferimento (Tabella 40 in RM0368 per F401).
- SWSTART vs trigger timer — Il codice usa
SWSTARTper avviare la prima conversione. In modalità continua, dopo SWSTART l'ADC continua a ciclare. Per acquisizione periodica precisa, collegare un trigger di uscita timer (es. TIM2_TRGO) all'ADC tramiteADC_CR2_EXTSEL.
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:
- Allineamento a destra (
ALIGN = 0) — i bit[11:0]contengono il risultato,[15:12]sono zero. Si estrae conadc_buf[i] & 0x0FFF. - Allineamento a sinistra (
ALIGN = 1) — i bit[15:4]contengono il risultato shiftato a sinistra di 4. Utile per calcolare percentuali direttamente:value >> 4dà un range 0–4095. Si estrae conadc_buf[i] >> 4.
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
- Eseguire la calibrazione ad ogni avvio — Saltare la calibrazione può introdurre fino a ±10 LSB di errore di offset, che su un ADC a 12 bit con riferimento 3.3 V corrisponde a circa ±8 mV. Per un monitor di batteria che legge 7.4 V attraverso un partitore 3:1, sono ±24 mV di tolleranza aggiuntiva a letture già rumorose.
- Verificare la frequenza del clock ADC — Leggere
ADC_CCRper confermareADCPRE. A 84 MHz APB2, ÷4 dà 21 MHz; ÷6 dà 14 MHz. Il massimo dell'ADC su F401 è 36 MHz. Un clock troppo alto riduce la precisione; un clock troppo basso aumenta inutilmente il tempo di conversione. - Controllare il mapping del canale DMA — Ogni istanza ADC si collega a stream/canali DMA specifici. Su F401: ADC1 → DMA2 stream 0/4, canale 0. ADC2 → DMA2 stream 2/3, canale 1. ADC3 → DMA2 stream 1, canale 2. Un mapping errato significa nessun movimento dati.
- Puntatore di lettura del buffer circolare — In modalità circolare continua, il DMA sovrascrive i dati vecchi. L'applicazione deve leggere i campioni più velocemente di un ciclo di riempimento del buffer. A 4 canali × 450 kSPS = 1.8 MSPS, un buffer di 256 campioni (1024 byte per 16-bit × 512 halfword) si riempie in ~142 µs. Questa è una finestra stretta per un loop principale — considerare uno schema a doppio buffer con il flag
CTdel DMA o l'interrupt di completamento trasferimento per commutare tra due buffer ping-pong. - Gruppo injected per misure urgenti — Se un ingresso analogico critico (ad esempio, un sensore di sovracorrente) deve essere misurato immediatamente senza attendere la fine della scansione regular, assegnarlo al gruppo injected. Le conversioni injected interrompono la scansione regular, memorizzano nei registri
JDRxe la scansione regular riprende da dove si era fermata. - Watchdog sui canali analogici — L'analogue watchdog (AWD) in
ADC_CR1può monitorare uno o tutti i canali e generare un interrupt se il valore convertito è al di fuori di una finestra programmata. Utile per rilevamento guasti senza polling.
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:
- Trigger hardware — Sostituire SWSTART con un trigger di uscita timer (es. TIM8_TRGO a 1 kHz) in modo che la frequenza di conversione sia deterministica e indipendente dalla latenza degli interrupt.
- DMA ping-pong — Utilizzare due buffer (modalità doppio buffer DMA su STM32F4:
DBM = 1). Mentre il DMA riempie il buffer B, l'applicazione elabora il buffer A e viceversa. Questo elimina la race condition sul puntatore circolare. - Oversampling — Per ambienti rumorosi (azionamenti motore, alimentatori switching), accumulare 16 o 64 letture e shiftare a destra per ottenere bit di risoluzione aggiuntivi. Un oversampling 64× su un ADC a 12 bit produce 3 bit extra (15 bit effettivi) al costo di una produttività 64× inferiore.
- Calibrazione offset DC — In produzione, campionare il canale VREFINT interno per misurare la VDDA effettiva, quindi correggere tutte le letture esterne per la variazione di alimentazione.
- Filtro media mobile per canale — Una media mobile esponenziale per canale invece della media a blocchi basata su frame, in modo che l'uscita si aggiorni gradualmente ad ogni scansione anziché a raffiche.
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
- STMicroelectronics, RM0368 — STM32F401 Reference Manual: Capitolo 13 (ADC), descrizione dei registri e mapping richieste DMA.
- STMicroelectronics, AN2834 — How to get the best ADC accuracy in STM32 microcontrollers: impedenza di sorgente, calcolo del tempo di campionamento e tecniche di riduzione del rumore.
- STMicroelectronics, AN3116 — STM32 ADC modes and applications: modalità scan, modalità discontinua, esempi gruppo injected.
- STMicroelectronics, STM32CubeF4 Firmware Package —
Projects/STM32F401RE-Nucleo/Examples/ADC/ADC_DMA_Transfer: riferimento basato su HAL per integrazione ADC + DMA. - STMicroelectronics, STM32F401xE datasheet: caratteristiche ADC, frequenza massima del clock, valori dei condensatori di campionamento.
- Memfault Blog — "How to Read and Calibrate an STM32 ADC" e la serie "Interrupt" sui pattern di acquisizione dati embedded.
- ARM, CMSIS-Core (Cortex-M4): funzioni intrinsic per memory barrier e configurazione NVIC.

Commenti
Hai commenti? Scrivimi un'email.