Arduino

Come gestire ingressi analogici e uscite PWM con Arduino

Come sfruttare al massimo i comandi analogRead e analogWrite

Uno dei motivi per cui Arduino è diventato così diffuso è la semplicità con cui permette di leggere segnali analogici e pilotare uscite PWM. Con poche istruzioni è possibile leggere un potenziometro, acquisire il valore di un sensore o regolare la luminosità di un LED.

Questa semplicità, però, può generare qualche equivoco. Gli ingressi analogici non leggono “valori analogici” in modo continuo: usano un convertitore analogico-digitale, chiamato ADC. Allo stesso modo, la funzione analogWrite() non produce quasi mai una vera tensione analogica, ma un segnale digitale PWM, cioè un’onda quadra con duty cycle variabile.

Capire questa differenza è fondamentale per progettare circuiti più precisi, scegliere la giusta tensione di riferimento e sfruttare correttamente la risoluzione disponibile sulla propria scheda.

Che cosa fa davvero analogRead()

La funzione analogRead() legge una tensione applicata a un pin analogico e la converte in un numero intero. La tensione deve rimanere compresa tra GND e la tensione di riferimento analogica, spesso indicata come Vref.

Sulle schede Arduino classiche basate su AVR, come Arduino UNO R3, la risoluzione dell’ADC è di 10 bit. Questo significa che il valore restituito è compreso tra 0 e 1023, per un totale di 1024 livelli.

Valore minimo letto: 0
Valore massimo letto: 1023
Numero di livelli: 1024
Risoluzione: 10 bit

Se la tensione di riferimento è 5 V, ogni passo dell’ADC corrisponde teoricamente a circa 4,88 mV.

5 V / 1024 = 0,00488 V
0,00488 V = 4,88 mV

Se invece la scheda lavora a 3,3 V o se si usa una tensione di riferimento più bassa, ogni passo rappresenta una tensione minore. Questo aumenta la sensibilità della misura, a patto che il segnale da leggere resti entro la tensione di riferimento scelta: oltre Vref la lettura satura a 1023 (senza danni), mentre oltre la tensione di alimentazione del microcontrollore (o sotto GND) si rischia di danneggiare il pin.

3,3 V / 1024 = 0,00322 V
0,00322 V = 3,22 mV

Vref: la tensione di riferimento dell’ADC

L’ADC non misura una tensione in senso assoluto: la confronta con una tensione di riferimento. Il valore restituito da analogRead() dipende quindi dal rapporto tra la tensione presente sul pin e Vref.

valore_ADC = tensione_ingresso * 1024 / Vref

È la formula riportata nel datasheet dell’ATmega328P: con un ADC a 10 bit il risultato viene troncato all’intero inferiore e il valore massimo leggibile è 1023, che corrisponde a una tensione pari a Vref meno un passo. Per esempio, su Arduino UNO con riferimento a 5 V, una tensione di 2,5 V restituisce 512.

2,5 V * 1024 / 5 V = 512

La scelta della tensione di riferimento influenza direttamente la precisione pratica della lettura. Se il sensore non supera mai 1 V, usare una Vref da 5 V significa sprecare gran parte della scala disponibile. In quel caso può essere utile usare un riferimento più basso, se supportato dalla scheda e dal circuito.

Usare analogReference()

La funzione analogReference() permette di modificare la tensione di riferimento usata dall’ADC. Le opzioni disponibili cambiano in base alla scheda e al microcontrollore utilizzato.

analogReference(DEFAULT);
analogReference(INTERNAL);
analogReference(EXTERNAL);

Attenzione: i nomi delle costanti non sono uguali su tutte le schede, e una costante non prevista dalla scheda selezionata fa fallire la compilazione (ad esempio INTERNAL su Arduino Mega).

Scheda Opzioni di analogReference()
UNO R3, Nano (ATmega328P) DEFAULT (5 V), INTERNAL (1,1 V), EXTERNAL (pin AREF)
Mega 2560 DEFAULT (5 V), INTERNAL1V1 (1,1 V), INTERNAL2V56 (2,56 V), EXTERNAL
Leonardo, Micro (ATmega32U4) DEFAULT (5 V), INTERNAL (2,56 V), EXTERNAL
UNO R4 Minima/WiFi costanti con prefisso AR_ (ad esempio AR_DEFAULT, AR_EXTERNAL): verifica l’elenco completo nella documentazione ufficiale

Su molte schede Arduino classiche, DEFAULT usa la tensione di alimentazione della scheda: 5 V sulle schede a 5 V, 3,3 V sulle schede a 3,3 V.

INTERNAL usa un riferimento interno al microcontrollore, per esempio 1,1 V su molte schede basate su ATmega328P. Questa modalità è utile quando si devono misurare segnali piccoli e si vuole sfruttare meglio la risoluzione dell’ADC.

EXTERNAL usa invece la tensione applicata al pin AREF. Questa soluzione permette di usare un riferimento preciso esterno, ma richiede attenzione: la tensione su AREF non deve superare i limiti della scheda e non deve essere collegata in modo errato mentre è attivo un riferimento interno.

void setup() {
  analogReference(EXTERNAL);
  Serial.begin(9600);
}

void loop() {
  int valore = analogRead(A0);
  Serial.println(valore);
}

Quando si usa un riferimento esterno, è buona pratica impostare analogReference(EXTERNAL) prima di effettuare letture analogiche. Inoltre il GND della sorgente di riferimento deve essere collegato al GND di Arduino.

Calcolare la tensione reale da analogRead()

Per convertire il valore restituito da analogRead() in una tensione, bisogna conoscere la risoluzione e la tensione di riferimento utilizzata.

tensione = valore_ADC * Vref / 1024

Con Arduino UNO, ADC a 10 bit e Vref pari a 5 V, si divide quindi per 1024 (210), coerentemente con l’ampiezza del passo di 4,88 mV calcolata sopra.

int pinSensore = A0;
float vref = 5.0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  int valore = analogRead(pinSensore);
  float tensione = valore * vref / 1024.0;

  Serial.print("Valore ADC: ");
  Serial.print(valore);
  Serial.print(" - Tensione: ");
  Serial.print(tensione);
  Serial.println(" V");

  delay(500);
}

In molti sketch si trova 1023 al posto di 1024: in questo modo il valore massimo letto corrisponde esattamente a Vref. La differenza tra le due scelte è inferiore a un passo dell’ADC (meno di 5 mV con Vref = 5 V), quindi trascurabile rispetto agli altri errori di misura; la formula con 1024 è però quella coerente con il datasheet e con il calcolo del passo.

analogReadResolution(): quando serve davvero

La funzione analogReadResolution() consente di modificare la risoluzione usata da analogRead(), ma non è disponibile su tutte le schede. Sulle schede AVR classiche, come Arduino UNO R3, la risoluzione hardware dell’ADC resta a 10 bit.

Su schede più recenti, come Arduino Due, Zero, MKR, Nano 33, UNO R4 e altre piattaforme compatibili, è possibile impostare risoluzioni superiori, per esempio 12 bit. In questo caso il valore letto può arrivare a 4095.

void setup() {
  Serial.begin(9600);
  analogReadResolution(12);
}

void loop() {
  int valore = analogRead(A0);
  Serial.println(valore);
  delay(500);
}

Con una risoluzione a 12 bit si ottengono 4096 livelli.

Valore minimo letto: 0
Valore massimo letto: 4095
Numero di livelli: 4096
Risoluzione: 12 bit

Se la tensione di riferimento è 3,3 V, ogni passo corrisponde teoricamente a circa 0,805 mV.

3,3 V / 4096 = 0,000805 V
0,000805 V = 0,805 mV

Aumentare la risoluzione non rende automaticamente più accurata la misura. La risoluzione indica quanti livelli sono disponibili, mentre l’accuratezza dipende anche da rumore elettrico, stabilità della tensione di riferimento, qualità del sensore, cablaggio, alimentazione e caratteristiche reali dell’ADC.

Esempio pratico: lettura di un potenziometro

Un potenziometro è uno degli esempi più semplici per comprendere la lettura analogica. Collegando i due estremi a 5 V e GND e il cursore centrale ad A0, Arduino leggerà un valore proporzionale alla posizione della manopola. Con lo stesso principio, usando un partitore, si legge anche una fotoresistenza (LDR); per gli ingressi on/off vedi invece input digitali con Arduino.

const int pinPotenziometro = A0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  int valore = analogRead(pinPotenziometro);

  Serial.print("Valore letto: ");
  Serial.println(valore);

  delay(200);
}

Su Arduino UNO il valore sarà compreso tra 0 e 1023. Su una scheda configurata a 12 bit, invece, potrà essere compreso tra 0 e 4095. Per riportare la lettura su una scala diversa si usa la funzione map(), descritta nella sezione La funzione map().

Arduino UNO R4: ADC fino a 14 bit e DAC reale

Le schede Arduino UNO R4 Minima e UNO R4 WiFi, basate sul microcontrollore Renesas RA4M1 (Arm Cortex-M4), mantengono la logica a 5 V e la piedinatura della UNO R3, ma hanno periferiche analogiche più evolute:

  • l’ADC lavora per compatibilità a 10 bit, ma con analogReadResolution() può essere portato a 12 o 14 bit (valori da 0 a 16383);
  • il pin A0 dispone di un vero DAC a 12 bit: con analogWriteResolution(12) e analogWrite(A0, valore) si ottiene una tensione continua proporzionale al valore, senza filtri;
  • le opzioni di analogReference() hanno nomi diversi da quelle delle schede AVR: conviene verificarle nella documentazione ufficiale della scheda.
void setup() {
  analogReadResolution(14);   // letture da 0 a 16383
  analogWriteResolution(12);  // DAC su A0: valori da 0 a 4095
}

void loop() {
  analogWrite(A0, 2048);      // circa metà della tensione di riferimento del DAC
  int valore = analogRead(A1);
}

Che cosa fa davvero analogWrite()

Il nome analogWrite() può trarre in inganno. Sulla maggior parte delle schede Arduino, questa funzione non genera una vera tensione analogica variabile. Genera invece un segnale PWM, cioè un segnale digitale che passa rapidamente da LOW a HIGH.

Variando il tempo in cui il segnale resta alto rispetto al periodo totale, si modifica il duty cycle. Un LED collegato a un pin PWM apparirà più o meno luminoso perché l’occhio umano percepisce una luminosità media.

Duty cycle 0%: segnale sempre LOW
Duty cycle 50%: segnale HIGH per metà del tempo
Duty cycle 100%: segnale sempre HIGH

Sulle schede classiche, analogWrite() usa normalmente una risoluzione a 8 bit. Il valore passato alla funzione è quindi compreso tra 0 e 255.

analogWrite(pin, 0);    // 0%
analogWrite(pin, 127);  // circa 50%
analogWrite(pin, 255);  // 100%

Su Arduino UNO R3 i pin PWM sono indicati dal simbolo ~ e corrispondono ai pin digitali 3, 5, 6, 9, 10 e 11. La frequenza del segnale PWM è di circa 490 Hz sui pin 3, 9, 10 e 11 e di circa 980 Hz sui pin 5 e 6: è un dato da considerare quando si dimensiona un filtro RC o si pilota un motore, perché a queste frequenze il PWM può produrre un fischio udibile.

Arduino UNO R3 - pin PWM:
3, 5, 6, 9, 10, 11

Esempio pratico: fading di un LED

Questo esempio aumenta e diminuisce gradualmente la luminosità di un LED collegato a un pin PWM.

const int pinLed = 9;

void setup() {
  pinMode(pinLed, OUTPUT);
}

void loop() {
  for (int luminosita = 0; luminosita <= 255; luminosita++) {
    analogWrite(pinLed, luminosita);
    delay(5);
  }

  for (int luminosita = 255; luminosita >= 0; luminosita--) {
    analogWrite(pinLed, luminosita);
    delay(5);
  }
}

Il LED non riceve una tensione realmente continua e variabile: riceve impulsi digitali molto rapidi. Per carichi come LED e piccoli motori DC, il PWM è spesso sufficiente. Per ottenere una vera tensione analogica è necessario usare un DAC oppure filtrare il PWM con un circuito opportuno.

analogWriteResolution(): PWM e DAC sulle schede compatibili

Su alcune schede Arduino moderne è disponibile la funzione analogWriteResolution(), che permette di modificare la risoluzione dei valori usati da analogWrite().

void setup() {
  analogWriteResolution(12);
}

void loop() {
  analogWrite(9, 2048);
}

Con una risoluzione a 12 bit, il valore massimo diventa 4095.

Risoluzione: 12 bit
Valore minimo: 0
Valore massimo: 4095
Valore centrale: circa 2048

Questa funzione è utile sulle schede che supportano PWM a risoluzione configurabile o che dispongono di vere uscite DAC. È importante controllare sempre la documentazione della propria scheda, perché non tutti i pin hanno le stesse capacità.

PWM non significa DAC

Una distinzione importante riguarda PWM e DAC. Il PWM alterna rapidamente due soli livelli logici: LOW e HIGH. Un DAC, invece, genera direttamente una tensione analogica proporzionale al valore impostato.

Per esempio, scrivere un valore intermedio con analogWrite() su un pin PWM non significa ottenere automaticamente 2,5 V stabili su una scheda a 5 V. Significa ottenere un segnale digitale che, mediamente, può produrre un effetto equivalente su alcuni carichi.

analogWrite(9, 127);

Su un pin PWM di Arduino UNO, il comando precedente genera un duty cycle di circa il 50%. Non genera una tensione continua di 2,5 V.

Per ottenere una tensione continua più stabile si può usare un filtro passa-basso RC, un DAC esterno, oppure una scheda dotata di DAC integrato.

Un esempio di filtro RC: con R = 10 kΩ in serie al pin PWM e C = 10 µF verso GND la frequenza di taglio è fc = 1/(2π·R·C) ≈ 1,6 Hz, molto inferiore ai 490 Hz del PWM. Con duty cycle del 50% sul condensatore si ottengono circa 2,5 V con un’ondulazione residua di circa 25 mV picco-picco (ΔV ≈ V·D·(1−D)/(f·R·C)); la costante di tempo R·C = 0,1 s fa sì che la tensione si assesti in circa mezzo secondo. L’uscita del filtro non può alimentare carichi: se serve corrente va seguita da un inseguitore di tensione. Per il dimensionamento vedi Filtri RC e RL.

Leggere un ingresso analogico e pilotare un’uscita PWM

Uno degli usi più comuni consiste nel leggere un valore analogico e usarlo per controllare un’uscita PWM. Poiché analogRead() su Arduino UNO restituisce valori da 0 a 1023, mentre analogWrite() accetta valori da 0 a 255, bisogna rimappare la scala.

const int pinPotenziometro = A0;
const int pinLed = 9;

void setup() {
  pinMode(pinLed, OUTPUT);
}

void loop() {
  int valoreAnalogico = analogRead(pinPotenziometro);
  int valorePwm = map(valoreAnalogico, 0, 1023, 0, 255);

  analogWrite(pinLed, valorePwm);
}

In alternativa, poiché 1024 diviso 256 fa 4, si può dividere il valore letto per 4.

int valorePwm = analogRead(A0) / 4;
analogWrite(9, valorePwm);

La funzione map() è più leggibile e più flessibile, soprattutto quando i limiti del sensore non coincidono con l’intera scala dell’ADC.

La funzione map()

La funzione map() riporta un valore da un intervallo di partenza a un nuovo intervallo con estremi scelti da noi, eseguendo la proporzione lineare al posto nostro. La struttura è la seguente:

valoreScalato = map(valore, minimoOrigine, massimoOrigine, nuovoMinimo, nuovoMassimo);

Se ad esempio vogliamo riportare una lettura analogica di Arduino UNO (0–1023) in un intervallo compreso tra 10 e 20, scriveremo:

int valoreScalato = map(analogRead(A0), 0, 1023, 10, 20);

Due dettagli da ricordare:

  • map() lavora con numeri interi: eventuali parti decimali del risultato vengono troncate.
  • map() non limita il risultato: se il valore di ingresso esce dall’intervallo di partenza, anche il risultato esce dal nuovo intervallo. Quando serve, si può aggiungere constrain(), ad esempio constrain(valoreScalato, 10, 20).

Lo schema seguente mostra il collegamento per regolare la luminosità di un LED con un potenziometro su A0. Nello schema il LED è collegato al pin PWM 3: per usarlo con lo sketch precedente basta impostare pinLed = 3.

Schema elettrico per la regolazione della luminosità di un LED con analogRead e map

Ridurre il rumore nelle letture analogiche

Le letture analogiche possono variare anche quando il segnale sembra stabile. Questo è normale: l’ADC è sensibile al rumore elettrico, alla qualità dell’alimentazione, alla lunghezza dei cavi e alla stabilità del riferimento analogico.

Per migliorare la qualità della lettura si possono adottare alcune accortezze:

  • usare collegamenti corti e ordinati;
  • collegare correttamente tutte le masse;
  • evitare che i cavi analogici passino vicino a motori, relè o carichi rumorosi;
  • usare condensatori di disaccoppiamento dove necessario;
  • mediare più letture consecutive;
  • usare una tensione di riferimento stabile.

Una media semplice può ridurre le oscillazioni casuali.

const int pinSensore = A0;

int leggiMediaAnalogica(int pin, int campioni) {
  long somma = 0;

  for (int i = 0; i < campioni; i++) {
    somma += analogRead(pin);
    delay(2);
  }

  return somma / campioni;
}

void setup() {
  Serial.begin(9600);
}

void loop() {
  int valoreMedio = leggiMediaAnalogica(pinSensore, 20);
  Serial.println(valoreMedio);
  delay(200);
}

La media rende il valore più stabile, ma rallenta la risposta. Per sensori lenti, come potenziometri o sensori di temperatura, è spesso una soluzione efficace. Per segnali rapidi, invece, bisogna scegliere con attenzione frequenza di campionamento e tecnica di filtraggio.

Quando cambiare la tensione di riferimento

Cambiare Vref è utile quando il segnale da misurare occupa solo una parte ridotta della scala disponibile. Per esempio, se un sensore produce al massimo 1 V e la scheda usa una Vref da 5 V, gran parte dei livelli dell’ADC non verrà mai usata.

Sensore: 0 - 1 V
Vref: 5 V
ADC: 10 bit

Livelli teoricamente usati:
1 V / 5 V * 1024 = circa 205 livelli

Usando un riferimento vicino a 1,1 V, se supportato e sicuro per il circuito, si sfrutterebbe quasi tutta la scala dell’ADC.

Sensore: 0 - 1 V
Vref: 1,1 V
ADC: 10 bit

Livelli teoricamente usati:
1 V / 1,1 V * 1024 = circa 931 livelli

Il vantaggio è evidente, ma c’è una regola da non ignorare: il segnale applicato al pin analogico deve restare entro la tensione di riferimento scelta, altrimenti la lettura satura al valore massimo (1023) e la misura è persa; inoltre non deve mai superare i limiti elettrici del microcontrollore (sull’ATmega328P da −0,5 V a Vcc + 0,5 V), oltre i quali il pin può danneggiarsi. Con Vref = 1,1 V, quindi, un segnale di 3 V non danneggia la scheda ma viene letto come 1023.

Errori comuni da evitare

Un errore frequente è pensare che tutti i pin analogici possano generare PWM. In realtà, su Arduino UNO i pin analogici sono ingressi ADC, mentre i pin PWM sono pin digitali specifici indicati dal simbolo ~.

Un altro errore comune è credere che analogWrite() generi sempre una tensione analogica. Nella maggior parte dei casi genera PWM, non una vera uscita analogica.

Bisogna inoltre evitare di applicare ai pin analogici tensioni superiori alla tensione ammessa dalla scheda. Un sensore a 5 V non è automaticamente compatibile con una scheda a 3,3 V: in questo caso riduci il segnale con un partitore resistivo (ad esempio 10 kΩ in serie e 18 kΩ verso GND: 5 V · 18/(10 + 18) ≈ 3,2 V) o con un traslatore di livello.

Infine, non bisogna confondere risoluzione e accuratezza. Passare da 10 a 12 bit aumenta il numero di livelli disponibili, ma non elimina errori di misura, rumore, offset, instabilità dell’alimentazione o limiti del sensore.

Riepilogo operativo

analogRead()
- Legge una tensione su un pin analogico.
- Su Arduino UNO R3 restituisce valori da 0 a 1023.
- La scala dipende dalla tensione di riferimento Vref.

analogReference()
- Cambia la tensione di riferimento dell'ADC.
- Le opzioni disponibili dipendono dalla scheda.
- Va usata con attenzione, soprattutto con riferimenti esterni.

analogReadResolution()
- Modifica la risoluzione di lettura sulle schede compatibili.
- Non aumenta la risoluzione hardware sulle schede che non la supportano.

analogWrite()
- Genera normalmente un segnale PWM.
- Sulle schede classiche usa valori da 0 a 255.
- Non produce una vera tensione analogica, salvo pin DAC specifici.

analogWriteResolution()
- Modifica la risoluzione di scrittura sulle schede compatibili.
- È utile con PWM avanzato o vere uscite DAC.

Gestire ingressi analogici e uscite PWM con Arduino è semplice, ma per ottenere risultati affidabili è importante sapere cosa avviene realmente nel microcontrollore.

analogRead() converte una tensione in un numero digitale usando un ADC e una tensione di riferimento. analogWrite(), invece, nella maggior parte dei casi genera un segnale PWM e non una vera uscita analogica.

Modificare la tensione di riferimento, usare la giusta risoluzione e distinguere tra PWM e DAC permette di realizzare misure più sensate, controlli più precisi e progetti più robusti.

Admin | MST-Tutorial

Appassionato di elettronica, elettrotecnica e programmazione. Fondatore di MST-Tutorial, condivido con entusiasmo conoscenze, esperienze e progetti legati all’innovazione tecnologica, con l’obiettivo di rendere accessibili concetti tecnici complessi attraverso tutorial chiari e pratici.

Articoli Correlati

Pulsante per tornare all'inizio