Segnale analogico e ingresso analogico

Un ingresso digitale distingue soltanto due condizioni logiche: livello basso e livello alto. Un ingresso analogico consente invece di misurare una tensione variabile compresa nell’intervallo ammesso dal microcontrollore.

La tensione applicata al pin può assumere qualsiasi valore continuo entro tale intervallo. Per esempio, un sensore può produrre:

0,8 V
2,35 V
4,1 V

Il microcontrollore non elabora direttamente un valore analogico continuo. La tensione viene acquisita dal convertitore analogico-digitale, indicato con la sigla ADC da Analog-to-Digital Converter, che la trasforma in un numero intero utilizzabile dal programma.

La conversione associa quindi una tensione reale a uno dei valori numerici disponibili:

tensione analogica → conversione ADC → valore numerico

La misura non è continua nel tempo. L’ADC acquisisce il segnale in un determinato istante e restituisce un valore rappresentativo della tensione presente durante la conversione. Letture successive permettono di osservare l’evoluzione del segnale.

Un ingresso analogico misura una tensione rispetto alla massa del circuito. Non misura direttamente corrente, resistenza, temperatura, pressione o altre grandezze fisiche. Queste devono essere convertite in una tensione mediante un sensore o un circuito adeguato.

La tensione applicata non deve superare i limiti elettrici previsti per il pin. Un segnale proveniente da circuiti alimentati a tensioni superiori richiede un partitore, un amplificatore, un circuito di condizionamento o un’altra interfaccia appropriata.

ADC dell’ATmega328P e risoluzione a 10 bit

L’ATmega328P integra un convertitore analogico-digitale con risoluzione nominale di 10 bit.

Una risoluzione di 10 bit consente di rappresentare il risultato della conversione mediante:

2¹⁰ = 1024 livelli

I valori restituiti dall’ADC sono compresi tra:

0 e 1023

Altri microcontrollori integrano ADC con risoluzioni differenti:

ATmega328P → 10 bit
SAMD21     → 12 bit
AVR DA     → 12 bit
RP2040     → 12 bit
ESP32      → 12 bit
STM32G0    → 12 bit
STM32H7    → fino a 16 bit

Il numero di bit indica la risoluzione nominale del convertitore, ma non garantisce che tutti i bit siano realmente significativi o privi di rumore.

Il valore 0 corrisponde a una tensione prossima a 0 V, mentre il valore 1023 corrisponde a una tensione prossima alla tensione di riferimento utilizzata dall’ADC.

L’intervallo compreso tra 0 V e la tensione di riferimento viene quindi suddiviso in 1024 intervalli. La dimensione teorica di un singolo intervallo, indicata come passo di quantizzazione, vale:

passo ADC = tensione di riferimento / 1024

Utilizzando una tensione di riferimento di 5 V:

5 V / 1024 = 0,0048828125 V

cioè circa:

4,88 mV per livello

Una variazione della tensione inferiore a questo valore può non produrre un cambiamento del risultato numerico.

La conversione introduce quindi una quantizzazione: tensioni leggermente differenti possono essere rappresentate dallo stesso valore ADC.

Per esempio, con riferimento a 5 V:

valore ADC 0    → circa 0 V
valore ADC 512  → circa 2,5 V
valore ADC 1023 → tensione prossima a 5 V

La risoluzione indica il numero di livelli disponibili, ma non garantisce da sola la precisione assoluta della misura. Il risultato dipende anche dalla stabilità della tensione di riferimento, dal rumore elettrico, dalle caratteristiche dell’ADC e dal circuito collegato all’ingresso.

Tensione di riferimento dell’ADC

La conversione analogico-digitale viene eseguita rispetto a una tensione di riferimento, indicata con VREF. Il valore numerico restituito dall’ADC rappresenta il rapporto tra la tensione applicata all’ingresso e questa tensione di riferimento.

L’intervallo misurabile è compreso approssimativamente tra:

0 V e VREF

La relazione teorica è:

valore ADC ≈ tensione ingresso × 1024 / VREF

La tensione può essere ricavata dal valore ADC mediante:

tensione ingresso ≈ valore ADC × VREF / 1024

Nell’ATmega328P possono essere utilizzate tre sorgenti principali di riferimento:

AVCC                  → tensione di alimentazione analogica
riferimento interno   → circa 1,1 V
AREF esterno          → tensione applicata al pin AREF

Utilizzando AVCC, se l’alimentazione effettiva è 5 V, il passo teorico è circa 4,88 mV. Se però la tensione reale vale, per esempio, 4,85 V, utilizzare nei calcoli il valore nominale di 5 V introduce un errore sistematico.

Il riferimento interno di circa 1,1 V consente di misurare con maggiore risoluzione segnali di ampiezza ridotta. Il passo teorico diventa:

1,1 V / 1024 ≈ 1,07 mV

Con questo riferimento, la tensione applicata all’ingresso non deve però superare l’intervallo previsto dalla conversione.

Un riferimento esterno permette di scegliere una tensione adatta alla misura, purché siano rispettati i limiti elettrici del componente e la configurazione indicata dal costruttore.

La qualità della conversione dipende direttamente dalla stabilità di VREF. Rumore, variazioni dell’alimentazione e cadute di tensione producono variazioni del risultato anche quando il segnale misurato rimane costante.

Canali analogici e multiplexer

L’ATmega328P dispone di un solo convertitore analogico-digitale, condiviso tra più ingressi analogici mediante un multiplexer.

Il multiplexer seleziona quale pin deve essere collegato all’ADC prima di ogni conversione. I diversi ingressi non vengono quindi convertiti contemporaneamente, ma uno alla volta.

Nel contenitore DIP a 28 pin dell’ATmega328P sono disponibili sei ingressi analogici principali:

ADC0
ADC1
ADC2
ADC3
ADC4
ADC5

Questi canali corrispondono ai pin del port C:

ADC0 → PC0
ADC1 → PC1
ADC2 → PC2
ADC3 → PC3
ADC4 → PC4
ADC5 → PC5

La selezione del canale viene effettuata mediante i bit MUX del registro ADMUX.

Quando il programma passa da un canale analogico a un altro, il condensatore interno di campionamento deve caricarsi alla nuova tensione. Se il segnale proviene da una sorgente con impedenza elevata, la prima conversione successiva al cambio di canale può risultare meno affidabile.

In questi casi può essere opportuno:

  • attendere il tempo necessario alla stabilizzazione;
  • scartare la prima conversione dopo il cambio di canale;
  • ridurre l’impedenza della sorgente;
  • utilizzare un buffer analogico.

I pin analogici possono essere utilizzati anche come ingressi o uscite digitali, ma non possono svolgere contemporaneamente entrambe le funzioni sullo stesso collegamento. La configurazione deve essere coerente con il circuito e con il programma.

Clock dell’ADC e tempo di conversione

Il convertitore analogico-digitale utilizza un proprio clock, ottenuto dividendo la frequenza del microcontrollore mediante un prescaler.

Nell’ATmega328P il divisore viene selezionato attraverso i bit ADPS2, ADPS1 e ADPS0 del registro ADCSRA. I valori disponibili permettono di dividere il clock principale per:

2
4
8
16
32
64
128

Per ottenere la risoluzione completa a 10 bit, la frequenza del clock dell’ADC deve normalmente rimanere tra 50 kHz e 200 kHz. Frequenze superiori consentono conversioni più rapide, ma possono ridurre la risoluzione effettiva.

Con un clock principale di 16 MHz e un prescaler pari a 128:

frequenza ADC = 16 000 000 / 128
               = 125 000 Hz
               = 125 kHz

Questo valore rientra nell’intervallo previsto per le conversioni a 10 bit.

Una conversione normale richiede 13 cicli del clock ADC. Con una frequenza di 125 kHz, la durata teorica è quindi:

tempo di un ciclo ADC = 1 / 125 000
                      = 8 µs

tempo di conversione = 13 × 8 µs
                     = 104 µs

La prima conversione eseguita dopo l’attivazione dell’ADC richiede invece 25 cicli, perché il circuito analogico interno deve essere inizializzato.

Il tempo necessario per una conversione limita la frequenza massima con cui possono essere acquisite misure indipendenti. Con conversioni da circa 104 µs, il valore teorico massimo è poco inferiore a:

1 / 0,000104 s ≈ 9615 conversioni al secondo

Nella pratica, il programma richiede ulteriore tempo per selezionare il canale, avviare la conversione, leggere il risultato ed elaborare o trasmettere il dato. La frequenza effettiva di acquisizione risulta quindi inferiore.

Campionamento e impedenza della sorgente

Prima della conversione, l’ADC deve acquisire la tensione presente sul canale selezionato. Il circuito interno collega temporaneamente l’ingresso a un condensatore di campionamento, che deve caricarsi fino a raggiungere una tensione sufficientemente vicina a quella del segnale.

La velocità di carica dipende dall’impedenza della sorgente collegata all’ingresso.

Una sorgente con impedenza bassa riesce a caricare rapidamente il circuito interno. Una sorgente con impedenza elevata richiede invece più tempo e può produrre un risultato inferiore o instabile se la conversione viene avviata prima della completa stabilizzazione.

Per ottenere la precisione prevista, la sorgente collegata all’ADC dell’ATmega328P dovrebbe presentare normalmente un’impedenza di uscita non superiore a circa:

10 kΩ

Un potenziometro, un sensore analogico o un partitore resistivo non devono quindi essere valutati soltanto in base alla tensione prodotta, ma anche in base alla resistenza equivalente vista dal pin.

Quando l’impedenza risulta troppo elevata si possono adottare diversi accorgimenti:

  • ridurre i valori delle resistenze del partitore;
  • attendere un tempo maggiore dopo la selezione del canale;
  • scartare la prima conversione;
  • aggiungere un condensatore vicino all’ingresso;
  • utilizzare un amplificatore operazionale configurato come buffer.

Il condensatore esterno può stabilizzare il segnale e fornire rapidamente la corrente richiesta durante il campionamento, ma introduce anche un filtraggio che rallenta la risposta alle variazioni.

Quando vengono letti più canali in sequenza, il condensatore interno può conservare parte della tensione del canale precedente. Il problema risulta più evidente quando i due segnali hanno valori molto differenti e provengono da sorgenti ad alta impedenza.

In queste condizioni è opportuno selezionare il nuovo canale, eseguire una prima conversione da scartare e utilizzare il risultato della conversione successiva.

Limiti elettrici e protezione dell’ingresso analogico

La tensione misurata dall’ADC deve rimanere compresa tra massa e la tensione di riferimento utilizzata:

0 V ≤ tensione ingresso ≤ VREF

Applicare una tensione superiore a VREF non produce un valore ADC maggiore di 1023. Il convertitore raggiunge il fondo scala e restituisce un valore prossimo al massimo, senza fornire informazioni sull’entità reale della sovratensione.

La tensione presente sul pin deve inoltre rispettare i limiti elettrici dell’ATmega328P. In condizioni normali non deve scendere sotto massa né superare la tensione di alimentazione del microcontrollore.

Il componente dispone di strutture interne di protezione contro piccole sovratensioni transitorie, ma queste non devono essere utilizzate come sistema ordinario di adattamento del segnale.

Quando la tensione da misurare può superare l’intervallo ammesso, è necessario utilizzare un circuito di condizionamento. La soluzione più semplice è un partitore resistivo:

Vin → R1 → punto di misura → R2 → GND
                    |
                 ingresso ADC

La tensione applicata all’ingresso vale:

VADC = Vin × R2 / (R1 + R2)

Per esempio, con:

R1 = 10 kΩ
R2 = 10 kΩ

si ottiene:

VADC = Vin / 2

Una tensione di ingresso di 8 V viene quindi ridotta a circa 4 V.

Il rapporto del partitore deve essere scelto in modo che la massima tensione prevista non porti mai il pin oltre il limite ammesso. Devono essere considerate anche le tolleranze delle resistenze e le possibili variazioni dell’alimentazione.

Una resistenza in serie può limitare la corrente in caso di sovratensione accidentale, ma non sostituisce un partitore o un circuito di protezione correttamente dimensionato.

Per segnali provenienti da ambienti disturbati o da circuiti esterni possono essere necessari ulteriori elementi:

  • filtro RC;
  • diodi di protezione;
  • diodo Zener o soppressore di transitori;
  • amplificatore operazionale;
  • isolamento galvanico;
  • circuito specifico per segnali industriali.

La massa del generatore del segnale deve essere collegata alla massa del microcontrollore, salvo che venga utilizzato un sistema di isolamento. Senza un riferimento comune, la tensione letta dall’ADC non è definita correttamente.

Accuratezza, precisione e rumore della misura

La risoluzione dell’ADC indica il numero di livelli disponibili, ma non coincide automaticamente con l’accuratezza della misura.

Un convertitore a 10 bit può rappresentare 1024 livelli, ma il valore ottenuto può differire da quello teorico a causa di diversi fattori:

  • tolleranza e instabilità della tensione di riferimento;
  • rumore presente sull’alimentazione;
  • disturbi sul segnale analogico;
  • impedenza della sorgente;
  • errori interni dell’ADC;
  • variazioni di temperatura;
  • qualità dei collegamenti e della massa.

L’errore di offset produce uno spostamento pressoché costante dei risultati. Per esempio, con ingresso collegato a massa, l’ADC potrebbe restituire un valore leggermente superiore a zero.

L’errore di guadagno modifica invece la proporzione tra tensione reale e valore convertito. Il risultato può essere corretto vicino a zero ma discostarsi progressivamente dal valore teorico avvicinandosi al fondo scala.

La non linearità indica che i livelli ADC non sono distribuiti in modo perfettamente uniforme lungo l’intero intervallo di misura.

Il rumore può far oscillare il risultato anche quando la tensione applicata rimane costante:

511
512
510
513
512

Queste variazioni non indicano necessariamente un cambiamento reale del segnale.

Per misure più affidabili è opportuno:

  • utilizzare una tensione di riferimento stabile;
  • collegare correttamente AVCC e AREF;
  • installare i condensatori di disaccoppiamento previsti;
  • mantenere corti i collegamenti analogici;
  • separare i segnali analogici dalle linee che commutano carichi;
  • utilizzare una massa comune ben realizzata;
  • evitare conversioni durante forti commutazioni digitali, quando possibile.

La calibrazione può ridurre gli errori sistematici. Consiste nel confrontare i risultati dell’ADC con una o più tensioni note e nel correggere successivamente il valore mediante il programma.

Media, scarto degli outlier e altri filtri numerici verranno trattati separatamente nell’articolo dedicato al filtraggio dei segnali analogici.

Collegamento utilizzato nei programmi

Negli esempi successivi viene utilizzato un potenziometro lineare da 10 kΩ per generare una tensione variabile compresa tra 0 V e 5 V.

I collegamenti sono:

potenziometro da 10 kΩ:

un terminale esterno → +5 V
terminale centrale   → A0
altro terminale      → GND

Ruotando il potenziometro, il terminale centrale fornisce una tensione variabile:

circa 0 V → valore ADC prossimo a 0

circa 2,5 V → valore ADC prossimo a 512

circa 5 V → valore ADC prossimo a 1023

Un potenziometro da 10 kΩ presenta un’impedenza adeguata al circuito di campionamento dell’ADC. Nel punto centrale, la resistenza equivalente vista dall’ingresso è circa 2,5 kΩ, quindi inferiore al valore indicativo di 10 kΩ.

Per rendere immediatamente osservabile il risultato viene utilizzato anche un LED:

pin digitale 8
→ resistenza da 330 Ω
→ anodo del LED

catodo del LED
→ GND

Il programma accende il LED quando la tensione misurata supera circa 2,5 V e lo spegne quando scende sotto tale valore.

Avvio della conversione e lettura del risultato

Prima di eseguire una conversione, il programma deve configurare:

  • la tensione di riferimento;
  • il canale analogico;
  • la frequenza del clock ADC;
  • l’attivazione del convertitore.

Il registro ADMUX seleziona il riferimento e il canale di ingresso.

Per utilizzare AVCC come riferimento e A0 come ingresso:

ADMUX = (1 << REFS0);

Il bit REFS0 seleziona AVCC. I bit di selezione del canale rimangono a zero, quindi viene utilizzato A0.

Il risultato viene mantenuto allineato a destra perché il bit ADLAR rimane a zero. I 10 bit della conversione vengono memorizzati nei registri ADCL e ADCH.

Il registro ADCSRA abilita il convertitore e seleziona il prescaler:

ADCSRA =
    (1 << ADEN)  |
    (1 << ADPS2) |
    (1 << ADPS1) |
    (1 << ADPS0);

Con i tre bit ADPS impostati a uno, il clock principale viene diviso per 128.

La conversione viene avviata impostando il bit ADSC:

ADCSRA |= (1 << ADSC);

Durante la conversione il bit rimane a uno. Quando la conversione termina, viene riportato automaticamente a zero.

Il programma può quindi attendere il completamento mediante:

while (ADCSRA & (1 << ADSC))
{
}

Il risultato viene letto attraverso il registro a 16 bit:

uint16_t valore_adc = ADC;

La definizione ADC permette al compilatore di leggere correttamente prima ADCL e poi ADCH, come richiesto dall’ATmega328P.

La prima conversione eseguita dopo l’attivazione dell’ADC richiede più tempo delle successive. Nel programma viene eseguita e scartata durante la configurazione iniziale.

Programma in C

Il programma utilizza l’ingresso analogico A0.

Il LED è collegato al pin digitale 8. L’uscita viene attivata quando la tensione misurata raggiunge o supera circa 2,5 V.

Il calcolo assume una tensione di riferimento nominale di 5000 mV. Per una misura più accurata, il valore VREF_MV deve essere sostituito con la tensione reale utilizzata come riferimento.

#define F_CPU 16000000UL

#include <avr/io.h>
#include <stdint.h>

#define VREF_MV 5000UL
#define SOGLIA_MV 2500UL

void configura_adc(void)
{
    /*
     * Riferimento ADC: AVCC.
     * Risultato allineato a destra.
     * Ingresso utilizzato: A0.
     */
    ADMUX = (1 << REFS0);

    /*
     * ADC abilitato.
     * Prescaler pari a 128.
     *
     * Clock principale: 16 MHz
     * Clock ADC: 125 kHz
     */
    ADCSRA =
        (1 << ADEN)  |
        (1 << ADPS2) |
        (1 << ADPS1) |
        (1 << ADPS0);

    /*
     * Disattivazione del buffer digitale
     * sul pin A0.
     */
    DIDR0 |= (1 << ADC0D);

    /*
     * Prima conversione eseguita
     * e scartata.
     */
    ADCSRA |= (1 << ADSC);

    while (ADCSRA & (1 << ADSC))
    {
    }

    (void)ADC;
}

uint16_t leggi_adc0(void)
{
    /*
     * Avvio della conversione.
     */
    ADCSRA |= (1 << ADSC);

    /*
     * Attesa non temporizzata del completamento
     * della singola conversione.
     *
     * Il ciclo termina dopo circa 104 µs
     * con clock ADC pari a 125 kHz.
     */
    while (ADCSRA & (1 << ADSC))
    {
    }

    /*
     * Lettura del risultato a 10 bit.
     */
    return ADC;
}

uint16_t converti_in_millivolt(uint16_t valore_adc)
{
    /*
     * La conversione viene eseguita a 32 bit
     * per evitare il superamento della capacità
     * di una variabile a 16 bit.
     */
    return (uint16_t)(
        ((uint32_t)valore_adc * VREF_MV) / 1024UL
    );
}

int main(void)
{
    uint16_t valore_adc;
    uint16_t tensione_mV;

    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    DDRB |= (1 << DDB0);

    /*
     * LED inizialmente spento.
     */
    PORTB &= ~(1 << PORTB0);

    configura_adc();

    while (1)
    {
        valore_adc = leggi_adc0();

        tensione_mV =
            converti_in_millivolt(valore_adc);

        /*
         * LED acceso quando la tensione
         * è uguale o superiore a 2,5 V.
         */
        if (tensione_mV >= SOGLIA_MV)
        {
            PORTB |= (1 << PORTB0);
        }
        else
        {
            PORTB &= ~(1 << PORTB0);
        }
    }
}

Spiegazione del programma in C

La costante:

#define VREF_MV 5000UL

rappresenta la tensione di riferimento nominale, espressa in millivolt.

Il valore deve corrispondere alla tensione realmente utilizzata dall’ADC. Se l’alimentazione misurata fosse, per esempio, 4,87 V, per una conversione più accurata si dovrebbe utilizzare:

#define VREF_MV 4870UL

La costante:

#define SOGLIA_MV 2500UL

definisce la tensione alla quale il LED cambia stato.

La funzione:

void configura_adc(void)

configura il convertitore analogico-digitale.

L’istruzione:

ADMUX = (1 << REFS0);

seleziona AVCC come tensione di riferimento e prepara la lettura dell’ingresso A0.

Il risultato resta allineato a destra, permettendo di ottenere direttamente un valore compreso tra 0 e 1023.

Il registro ADCSRA viene configurato mediante:

ADCSRA =
    (1 << ADEN)  |
    (1 << ADPS2) |
    (1 << ADPS1) |
    (1 << ADPS0);

Il bit ADEN abilita l’ADC.

I bit ADPS2, ADPS1 e ADPS0 impostano un prescaler pari a 128. Con una frequenza principale di 16 MHz, il clock dell’ADC diventa:

16 000 000 / 128 = 125 000 Hz

L’istruzione:

DIDR0 |= (1 << ADC0D);

disattiva il buffer digitale sul pin A0. Il pin continua a funzionare come ingresso analogico, ma la parte digitale non resta inutilmente attiva.

Dopo l’abilitazione viene eseguita una prima conversione:

ADCSRA |= (1 << ADSC);

Il programma attende che il bit ADSC ritorni a zero:

while (ADCSRA & (1 << ADSC))
{
}

Il risultato della prima conversione viene scartato:

(void)ADC;

La funzione:

uint16_t leggi_adc0(void)

avvia una nuova conversione e restituisce il valore a 10 bit.

Il ciclo di attesa:

while (ADCSRA & (1 << ADSC))
{
}

è bloccante soltanto per la durata della singola conversione, circa 104 µs con il clock ADC impostato a 125 kHz.

Il risultato viene restituito mediante:

return ADC;

La funzione:

uint16_t converti_in_millivolt(uint16_t valore_adc)

converte il valore numerico in millivolt:

return (uint16_t)(
    ((uint32_t)valore_adc * VREF_MV) / 1024UL
);

La conversione a uint32_t viene eseguita prima della moltiplicazione. Senza questa conversione, il prodotto tra il valore ADC e 5000 potrebbe superare la capacità di una variabile a 16 bit.

Nel ciclo principale viene acquisito il valore:

valore_adc = leggi_adc0();

e successivamente viene calcolata la tensione:

tensione_mV =
    converti_in_millivolt(valore_adc);

Il confronto:

if (tensione_mV >= SOGLIA_MV)

accende il LED quando la tensione presente su A0 raggiunge o supera 2500 mV.

Il LED viene acceso mediante:

PORTB |= (1 << PORTB0);

e spento mediante:

PORTB &= ~(1 << PORTB0);

Programma equivalente in C++

Il programma seguente mantiene lo stesso collegamento:

potenziometro → A0
LED           → pin digitale 8

La gestione dell’ADC viene raccolta nella classe IngressoAnalogico.

#define F_CPU 16000000UL

#include <avr/io.h>
#include <stdint.h>

constexpr uint32_t VREF_MV = 5000UL;
constexpr uint16_t SOGLIA_MV = 2500U;

class IngressoAnalogico
{
public:
    IngressoAnalogico()
    {
        configura();
    }

    uint16_t leggiValore()
    {
        /*
         * Avvio della conversione.
         */
        ADCSRA |= (1 << ADSC);

        /*
         * Attesa del completamento.
         */
        while (ADCSRA & (1 << ADSC))
        {
        }

        return ADC;
    }

    uint16_t convertiInMillivolt(
        uint16_t valore_adc
    ) const
    {
        return static_cast<uint16_t>(
            (
                static_cast<uint32_t>(valore_adc) *
                VREF_MV
            ) / 1024UL
        );
    }

private:
    void configura()
    {
        /*
         * Riferimento AVCC.
         * Ingresso utilizzato: A0.
         */
        ADMUX = (1 << REFS0);

        /*
         * ADC abilitato.
         * Prescaler pari a 128.
         */
        ADCSRA =
            (1 << ADEN)  |
            (1 << ADPS2) |
            (1 << ADPS1) |
            (1 << ADPS0);

        /*
         * Buffer digitale disattivato su A0.
         */
        DIDR0 |= (1 << ADC0D);

        /*
         * Prima conversione eseguita e scartata.
         */
        ADCSRA |= (1 << ADSC);

        while (ADCSRA & (1 << ADSC))
        {
        }

        (void)ADC;
    }
};

int main()
{
    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    DDRB |= (1 << DDB0);

    /*
     * LED inizialmente spento.
     */
    PORTB &= ~(1 << PORTB0);

    IngressoAnalogico ingresso;

    while (true)
    {
        uint16_t valore_adc =
            ingresso.leggiValore();

        uint16_t tensione_mV =
            ingresso.convertiInMillivolt(
                valore_adc
            );

        if (tensione_mV >= SOGLIA_MV)
        {
            PORTB |= (1 << PORTB0);
        }
        else
        {
            PORTB &= ~(1 << PORTB0);
        }
    }
}

Spiegazione del programma in C++

Le costanti:

constexpr uint32_t VREF_MV = 5000UL;
constexpr uint16_t SOGLIA_MV = 2500U;

definiscono la tensione di riferimento nominale e la soglia di commutazione del LED.

La parola chiave constexpr indica che i valori sono costanti e possono essere determinati durante la compilazione.

La classe:

class IngressoAnalogico

raccoglie la configurazione, la lettura e la conversione del valore analogico.

Il costruttore:

IngressoAnalogico()
{
    configura();
}

richiama automaticamente la funzione privata configura() quando viene creato l’oggetto.

La funzione:

uint16_t leggiValore()

avvia una conversione, attende il completamento e restituisce il valore compreso tra 0 e 1023.

La funzione:

uint16_t convertiInMillivolt(
    uint16_t valore_adc
) const

converte il risultato in millivolt.

La parola chiave const, posta dopo le parentesi, indica che la funzione non modifica lo stato dell’oggetto.

La conversione:

static_cast<uint32_t>(valore_adc)

porta il valore ADC a 32 bit prima della moltiplicazione.

L’espressione completa è:

(
    static_cast<uint32_t>(valore_adc) *
    VREF_MV
) / 1024UL

La funzione privata:

void configura()

imposta il riferimento, il prescaler, l’ingresso A0 e disattiva il relativo buffer digitale.

Nel main() viene creato l’oggetto:

IngressoAnalogico ingresso;

La configurazione dell’ADC viene quindi eseguita automaticamente.

Nel ciclo principale:

uint16_t valore_adc =
    ingresso.leggiValore();

acquisisce la lettura di A0.

La tensione viene calcolata mediante:

uint16_t tensione_mV =
    ingresso.convertiInMillivolt(
        valore_adc
    );

Il LED viene infine comandato in base alla soglia:

if (tensione_mV >= SOGLIA_MV)

La classe separa la gestione dell’ingresso analogico dalla logica applicativa. Il programma principale utilizza quindi il valore acquisito senza dover ripetere ogni volta le operazioni necessarie alla configurazione e alla conversione.

Programma equivalente con framework Wiring

Il programma seguente legge la tensione presente su A0 e controlla il LED collegato al pin digitale 8.

La funzione analogRead() restituisce un valore compreso tra 0 e 1023. Il valore viene convertito in millivolt assumendo una tensione di riferimento nominale di 5000 mV.

const int pin_ingresso = A0;
const int pin_led = 8;

const unsigned long vref_mV = 5000UL;
const unsigned int soglia_mV = 2500U;

void setup()
{
    /*
     * A0 utilizzato come ingresso analogico.
     * Non è necessario chiamare pinMode()
     * prima di analogRead().
     */

    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    pinMode(pin_led, OUTPUT);
    digitalWrite(pin_led, LOW);

    /*
     * Riferimento analogico predefinito:
     * tensione di alimentazione analogica.
     */
    analogReference(DEFAULT);

    /*
     * Prima conversione eseguita e scartata.
     */
    analogRead(pin_ingresso);
}

void loop()
{
    int valore_adc;
    unsigned int tensione_mV;

    valore_adc = analogRead(pin_ingresso);

    tensione_mV = (unsigned int)(
        ((unsigned long)valore_adc * vref_mV) /
        1024UL
    );

    if (tensione_mV >= soglia_mV)
    {
        digitalWrite(pin_led, HIGH);
    }
    else
    {
        digitalWrite(pin_led, LOW);
    }
}

Spiegazione del programma con framework Wiring

Le costanti:

const int pin_ingresso = A0;
const int pin_led = 8;

identificano l’ingresso analogico e l’uscita utilizzata per il LED.

La tensione di riferimento nominale è definita da:

const unsigned long vref_mV = 5000UL;

Il valore è espresso in millivolt. Se la tensione reale utilizzata come riferimento fosse diversa da 5 V, la costante dovrebbe essere modificata di conseguenza.

La soglia di commutazione è:

const unsigned int soglia_mV = 2500U;

Il LED viene quindi acceso quando la tensione misurata raggiunge o supera 2500 mV.

Nel setup(), il pin digitale 8 viene configurato come uscita:

pinMode(pin_led, OUTPUT);

Lo stato iniziale viene impostato a livello basso:

digitalWrite(pin_led, LOW);

La funzione:

analogReference(DEFAULT);

seleziona il riferimento analogico predefinito.

La prima chiamata:

analogRead(pin_ingresso);

esegue una conversione iniziale il cui risultato non viene utilizzato.

Nel ciclo principale, la lettura viene acquisita mediante:

valore_adc = analogRead(pin_ingresso);

Il risultato è un numero intero compreso tra 0 e 1023.

La conversione in millivolt viene eseguita con:

tensione_mV = (unsigned int)(
    ((unsigned long)valore_adc * vref_mV) /
    1024UL
);

La conversione a unsigned long viene effettuata prima della moltiplicazione per evitare che il risultato intermedio superi la capacità di una variabile a 16 bit.

Il confronto:

if (tensione_mV >= soglia_mV)

determina lo stato del LED.

La funzione:

digitalWrite(pin_led, HIGH);

accende il LED, mentre:

digitalWrite(pin_led, LOW);

lo spegne.

Dal valore ADC alla tensione

Il valore restituito dall’ADC rappresenta una frazione della tensione di riferimento.

La relazione utilizzata nei programmi è:

tensione = valore ADC × VREF / 1024

Con riferimento nominale di 5 V:

valore ADC 0
→ 0 × 5 / 1024
→ circa 0 V
valore ADC 256
→ 256 × 5 / 1024
→ circa 1,25 V
valore ADC 512
→ 512 × 5 / 1024
→ circa 2,50 V
valore ADC 768
→ 768 × 5 / 1024
→ circa 3,75 V
valore ADC 1023
→ 1023 × 5 / 1024
→ circa 4,995 V

Il valore massimo non rappresenta necessariamente una tensione esattamente uguale a VREF, ma l’ultimo intervallo disponibile prima del fondo scala.

Quando il risultato viene espresso in millivolt, la formula diventa:

tensione in mV = valore ADC × VREF in mV / 1024

L’utilizzo di millivolt consente di eseguire il calcolo con numeri interi, evitando l’impiego di variabili in virgola mobile.

Uso di una soglia sul valore ADC

Quando non è necessario conoscere la tensione in volt o millivolt, il confronto può essere eseguito direttamente sul valore ADC.

Con riferimento a 5 V, una soglia di circa 2,5 V corrisponde a:

valore soglia = 2,5 × 1024 / 5
              = 512

Il controllo può quindi essere scritto direttamente come:

int valore_adc = analogRead(A0);

if (valore_adc >= 512)
{
    digitalWrite(8, HIGH);
}
else
{
    digitalWrite(8, LOW);
}

Questa soluzione evita la conversione in millivolt ed è adatta quando il programma deve soltanto confrontare la misura con una soglia.

La soglia numerica dipende però dalla tensione di riferimento. Se VREF cambia, lo stesso valore ADC non rappresenta più la stessa tensione.

La conversione in millivolt rende invece più chiaro il significato fisico della soglia e permette di esprimerla direttamente come tensione.

Isteresi nella gestione della soglia

Quando la tensione misurata rimane vicina alla soglia, il valore ADC può oscillare leggermente a causa del rumore:

510
512
513
511
514

Con una sola soglia impostata a 512, il LED potrebbe accendersi e spegnersi ripetutamente anche se la tensione reale rimane pressoché costante.

Per evitare questa commutazione continua si possono utilizzare due soglie:

soglia di accensione   → 520
soglia di spegnimento  → 504

Il LED viene acceso soltanto quando il valore raggiunge o supera 520. Dopo l’accensione rimane attivo finché il valore non scende fino a 504 o al di sotto.

const int soglia_accensione = 520;
const int soglia_spegnimento = 504;

bool stato_led = false;

int valore_adc = analogRead(A0);

if (!stato_led && valore_adc >= soglia_accensione)
{
    stato_led = true;
}
else if (stato_led && valore_adc <= soglia_spegnimento)
{
    stato_led = false;
}

digitalWrite(8, stato_led ? HIGH : LOW);

La distanza tra le due soglie costituisce l’isteresi:

520 − 504 = 16 livelli ADC

Con riferimento nominale di 5 V, 16 livelli corrispondono a circa:

16 × 5 / 1024 ≈ 0,078 V

cioè circa 78 mV.

L’isteresi non modifica la misura acquisita. Interviene soltanto sulla decisione presa dal programma e impedisce commutazioni ripetute quando il segnale si trova vicino alla soglia.

Lettura periodica non bloccante

Non sempre è necessario eseguire conversioni analogiche alla massima velocità possibile. Per grandezze che cambiano lentamente può essere sufficiente una lettura ogni 100 ms, ogni secondo o a intervalli ancora maggiori.

La temporizzazione può essere gestita senza delay() mediante millis():

const int pin_ingresso = A0;

const unsigned long intervallo_lettura_ms = 100;

unsigned long tempo_ultima_lettura = 0;
int valore_adc = 0;

void loop()
{
    unsigned long tempo_attuale = millis();

    if (
        tempo_attuale - tempo_ultima_lettura >=
        intervallo_lettura_ms
    )
    {
        tempo_ultima_lettura = tempo_attuale;

        valore_adc = analogRead(pin_ingresso);
    }

    /*
     * In questo punto il programma può continuare
     * a gestire altri ingressi, uscite e temporizzazioni.
     */
}

La sottrazione:

tempo_attuale - tempo_ultima_lettura

mantiene corretta la temporizzazione anche quando il contatore di millis() supera il proprio valore massimo e riparte da zero.

La frequenza di acquisizione deve essere scelta in funzione della velocità con cui varia il segnale. Un potenziometro azionato manualmente non richiede migliaia di letture al secondo, mentre un segnale rapido può richiedere intervalli molto più brevi.

Lettura di più ingressi analogici

Più segnali analogici possono essere collegati, per esempio, agli ingressi:

A0
A1
A2

Il convertitore legge un ingresso alla volta. Il programma seleziona il canale richiesto mediante la chiamata a analogRead():

int valore_a0 = analogRead(A0);
int valore_a1 = analogRead(A1);
int valore_a2 = analogRead(A2);

Quando si passa da un ingresso a un altro, il circuito interno di campionamento deve adattarsi alla nuova tensione.

Se le sorgenti hanno impedenza elevata oppure presentano tensioni molto differenti, può essere opportuno scartare la prima lettura dopo il cambio di ingresso:

analogRead(A0);
int valore_a0 = analogRead(A0);

analogRead(A1);
int valore_a1 = analogRead(A1);

La prima chiamata permette al circuito interno di stabilizzarsi; il secondo risultato viene utilizzato dal programma.

Questo accorgimento non è sempre necessario, ma può ridurre l’influenza della misura precedente quando le condizioni elettriche del circuito non permettono una carica sufficientemente rapida del condensatore interno.

Selezione del riferimento nel framework Wiring

Il riferimento analogico viene selezionato mediante:

analogReference();

Con l’ATmega328P sono disponibili le configurazioni principali:

analogReference(DEFAULT);

Utilizza la tensione di alimentazione analogica come riferimento.

analogReference(INTERNAL);

Utilizza il riferimento interno nominale di circa 1,1 V.

analogReference(EXTERNAL);

Utilizza la tensione applicata al pin AREF.

La selezione deve essere eseguita nel setup() prima delle letture:

void setup()
{
    analogReference(INTERNAL);

    analogRead(A0);
}

Con il riferimento interno, il valore massimo misurabile è circa 1,1 V. Una tensione superiore non deve essere applicata direttamente quando il circuito è progettato per utilizzare questo intervallo di conversione.

Con riferimento esterno, la tensione applicata ad AREF deve rispettare i limiti previsti per il componente. Non deve essere collegata una sorgente esterna al pin AREF mentre il programma utilizza una configurazione incompatibile.

Programma Wiring completo

Il programma seguente legge periodicamente la tensione presente su A0, la converte in millivolt e controlla il LED collegato al pin digitale 8.

La lettura viene eseguita ogni 100 ms senza utilizzare delay(). Due soglie distinte introducono un’isteresi e impediscono commutazioni ripetute quando la tensione rimane vicina al valore centrale di 2,5 V.

const int pin_ingresso = A0;
const int pin_led = 8;

const unsigned long vref_mV = 5000UL;

const unsigned int soglia_accensione_mV = 2550U;
const unsigned int soglia_spegnimento_mV = 2450U;

const unsigned long intervallo_lettura_ms = 100UL;

unsigned long tempo_ultima_lettura = 0;

int valore_adc = 0;
unsigned int tensione_mV = 0;

bool stato_led = false;

unsigned int converti_in_millivolt(int valore)
{
    return (unsigned int)(
        ((unsigned long)valore * vref_mV) /
        1024UL
    );
}

void aggiorna_led(unsigned int tensione)
{
    /*
     * Il LED viene acceso soltanto quando
     * la tensione raggiunge o supera 2,55 V.
     */
    if (
        stato_led == false &&
        tensione >= soglia_accensione_mV
    )
    {
        stato_led = true;
    }

    /*
     * Dopo l’accensione, il LED viene spento
     * soltanto quando la tensione scende
     * fino a 2,45 V o al di sotto.
     */
    else if (
        stato_led == true &&
        tensione <= soglia_spegnimento_mV
    )
    {
        stato_led = false;
    }

    digitalWrite(
        pin_led,
        stato_led ? HIGH : LOW
    );
}

void setup()
{
    /*
     * Pin digitale 8 configurato
     * come uscita per il LED.
     */
    pinMode(pin_led, OUTPUT);
    digitalWrite(pin_led, LOW);

    /*
     * Riferimento analogico predefinito.
     */
    analogReference(DEFAULT);

    /*
     * Prima conversione eseguita e scartata.
     */
    analogRead(pin_ingresso);

    /*
     * Prima lettura utile eseguita all’avvio.
     */
    valore_adc = analogRead(pin_ingresso);

    tensione_mV =
        converti_in_millivolt(valore_adc);

    aggiorna_led(tensione_mV);

    tempo_ultima_lettura = millis();
}

void loop()
{
    unsigned long tempo_attuale = millis();

    /*
     * Lettura periodica non bloccante.
     */
    if (
        tempo_attuale - tempo_ultima_lettura >=
        intervallo_lettura_ms
    )
    {
        tempo_ultima_lettura = tempo_attuale;

        valore_adc =
            analogRead(pin_ingresso);

        tensione_mV =
            converti_in_millivolt(valore_adc);

        aggiorna_led(tensione_mV);
    }

    /*
     * In questo punto possono essere gestiti
     * altri ingressi, uscite e temporizzazioni.
     */
}

Schema e collegamenti completi

Il circuito utilizza un potenziometro lineare da 10 kΩ per applicare ad A0 una tensione variabile compresa tra 0 V e 5 V.

I collegamenti del potenziometro sono:

primo terminale esterno → +5 V
terminale centrale      → A0
secondo terminale esterno → GND

Ruotando l’alberino, la tensione presente sul terminale centrale varia tra i due valori estremi:

posizione verso GND  → tensione prossima a 0 V
posizione centrale   → tensione prossima a 2,5 V
posizione verso +5 V → tensione prossima a 5 V

Il LED viene collegato nel modo seguente:

pin digitale 8
→ resistenza da 330 Ω
→ anodo del LED

catodo del LED
→ GND

La resistenza da 330 Ω limita la corrente che attraversa il LED.

Tutti i componenti devono condividere la stessa massa:

GND del potenziometro
GND del LED
GND della scheda

Il terminale centrale del potenziometro deve essere collegato esclusivamente ad A0. I terminali esterni possono essere invertiti: in questo caso cambia soltanto il verso di rotazione con cui la tensione aumenta o diminuisce.

Lo schema deve mostrare chiaramente:

  • alimentazione a +5 V;
  • potenziometro da 10 kΩ;
  • terminale centrale collegato ad A0;
  • collegamento comune a GND;
  • LED sul pin digitale 8;
  • resistenza da 330 Ω in serie al LED.

La tensione applicata ad A0 non deve superare la tensione ammessa dal circuito. Segnali provenienti da sorgenti superiori a 5 V richiedono un partitore o un circuito di condizionamento adeguato.

Fonti principali

Microchip Technology, ATmega328P Data Sheet — convertitore analogico-digitale, tensioni di riferimento, registri di configurazione, prescaler, tempi di conversione, multiplexer e caratteristiche elettriche.

Microchip Technology, AVR ADC documentation — funzionamento del circuito di campionamento, impedenza della sorgente, frequenza del clock ADC e modalità di acquisizione.

Documentazione tecnica della scheda basata su ATmega328P — corrispondenza degli ingressi analogici, alimentazione, collegamenti di A0, AREF, AVCC e massa.

Wiring Language Reference — funzioni analogRead(), analogReference(), pinMode(), digitalWrite() e millis().

Documentazione tecnica dei produttori di potenziometri — resistenza nominale, terminale centrale, legge di variazione e tolleranza.