Informatica e Telecomunicazioni, classe quarta
Arduino: I/O digitale
Sei lezioni per passare da «accendo un LED» a «gestisco più compiti insieme senza bloccare il microcontrollore».
Perché il pulsante non risponde mentre il LED lampeggia?
È la domanda che attraversa tutta l'unità. La incontriamo nella lezione 4, la risolviamo nella 5 e nella 6 la usiamo per costruire un semaforo pedonale con chiamata.
- 1Dal C ad ArduinoStruttura di uno sketch, tipi di variabili, IDE, Tinkercad, debug seriale
- 2Pinout e limiti di correnteCosa c'è sulla scheda, differenze tra modelli, resistenza per un LED
- 3Leggere e scrivere i pinpinMode, digitalWrite, digitalRead, pull-up, fronti e rimbalzi
- 4Il problema di delay()Perché un programma che aspetta non ascolta
- 5millis()Guardare l'orologio invece di aspettare, più compiti in parallelo
- 6Polling e macchine a statiDebounce, semaforo pedonale, progetto finale
- ✓VerificaProva pratica, domande teoriche, griglia di valutazione
«Testo grande» ingrandisce tutto per la LIM. I riquadri con il pallino giallo sono interattivi: si possono usare dal vivo davanti alla classe. Il tema chiaro si legge meglio con il proiettore.
Lezione 1
Dal C ad Arduino
Arduino si programma in C++. Quello che sapete del C vale quasi tutto: cambiano il punto di partenza del programma, l'input/output e la dimensione di alcuni tipi.
Uno sketch è un programma con il main nascosto
Negli sketch non scriviamo main(). Lo aggiunge l'IDE al momento della compilazione, e in forma semplificata è questo:
// Quello che l'IDE aggiunge per te (semplificato)
int main() {
init(); // prepara timer, seriale, pin
setup(); // eseguita una sola volta, all'accensione
for (;;) {
loop(); // eseguita all'infinito
}
}
Ne seguono due conseguenze. setup() serve per le configurazioni iniziali, loop() per il lavoro vero. Un programma per microcontrollore non termina mai: non c'è un sistema operativo a cui tornare.
C sul PC e Arduino a confronto
| C sul PC | Arduino | |
|---|---|---|
| Punto di ingresso | main() | setup() + loop() |
| Fine del programma | return 0; | Non termina mai |
| Stampare testo | printf() | Serial.print(), Serial.println() |
| Leggere input | scanf() da tastiera | Pin, sensori, Serial.read() |
| Librerie | stdio.h, funzioni | Oggetti C++: Serial.begin() è un metodo |
Dimensione di int | 4 byte (32 bit) | 2 byte (16 bit) su Uno |
I tipi su Arduino Uno
| Tipo | Byte | Intervallo | Uso tipico |
|---|---|---|---|
bool | 1 | true / false | Stato di un LED o di un pulsante |
byte | 1 | 0 … 255 | Numero di pin, piccoli contatori |
int | 2 | −32.768 … 32.767 | Contatori, letture |
unsigned int | 2 | 0 … 65.535 | |
long | 4 | circa ±2,1 miliardi | |
unsigned long | 4 | 0 … 4.294.967.295 | Tempi con millis() (lezione 5) |
float | 4 | ~6–7 cifre significative | Tensioni, temperature |
Debug attraverso la seriale
La seriale è la nostra «printf». Arduino invia testo al PC attraverso il cavo USB e lo leggiamo nel Serial Monitor. La velocità scelta in Serial.begin() deve coincidere con quella impostata nel monitor.
int contatore = 0;
void setup() {
Serial.begin(9600); // stessa velocità nel Serial Monitor
Serial.println("Avvio completato");
}
void loop() {
Serial.print("Giro numero: ");
Serial.println(contatore);
contatore++;
delay(500);
}
Quando un programma non fa quello che vi aspettate, stampate i valori delle variabili e dei messaggi «sono arrivato qui». Vi dicono quale parte del codice viene eseguita davvero.
IDE e Tinkercad
Nell'IDE di Arduino: scegliere scheda e porta, poi Verifica (compila) e Carica (compila e invia alla scheda). Il Serial Monitor si apre dall'icona della lente in alto a destra.
Su Tinkercad Circuits: nuovo circuito, trascinare un Arduino Uno R3, poi Codice → modalità Testo, e infine Avvia simulazione. Il monitor seriale è in basso nel pannello del codice.
Esercitazione
- Blink su TinkercadFate lampeggiare il LED integrato con
LED_BUILTIN, 500 ms acceso e 500 ms spento. Poi cambiate i tempi e osservate. - Quanto è grande?Scrivete uno sketch che stampi sulla seriale
sizeof()di ogni tipo della tabella. Riproducete l'overflow diintvisto sopra. - Stesso codice, altro computerScrivete in C sul PC lo stesso programma con
printf. Perchésizeof(int)dà un risultato diverso?
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
}
Aprite la pagina di int nella Language Reference. Quanto è grande un int sulle schede con processore ARM, come Arduino Due o Zero?
Lezione 2
Pinout e limiti di corrente
Prima di collegare qualcosa bisogna sapere quali pin esistono, che cosa sanno fare e quanta corrente possono dare senza danneggiarsi.
Non tutte le schede sono uguali
Il principio non cambia da una scheda all'altra, ma i limiti sì. I valori qui sotto sono indicativi: quelli precisi vanno sempre verificati sul datasheet.
| Scheda | Tensione logica | Corrente per pin | Attenzione a |
|---|---|---|---|
| Uno R3, Nano (ATmega328P) | 5 V | 20 mA consigliati, 40 mA massimo assoluto | Circa 200 mA in totale su tutti i pin |
| Uno R4 Minima / WiFi | 5 V | Molto meno, circa 8 mA | Uno sketch «da R3» può sovraccaricare i pin |
| Nano 33, MKR | 3,3 V | Pochi mA | 5 V in ingresso possono danneggiare la scheda |
Perché il LED ha bisogno di una resistenza
Un LED non limita da solo la corrente. Collegato direttamente al pin, assorbirebbe tutta la corrente possibile, danneggiando il LED o il pin. La resistenza in serie fissa la corrente:
R = (Vpin − VLED) / I
Esempio: (5 V − 2 V) / 0,015 A = 200 Ω, quindi si sceglie il valore commerciale superiore, 220 Ω.
Esercitazione
- LED esternoSu Tinkercad collegate un LED rosso al pin 8 con la resistenza calcolata. Inserite un multimetro in serie, impostato come amperometro, e confrontate la corrente misurata con quella calcolata.
- Semaforo sempliceTre LED (rosso, giallo, verde) su tre pin diversi, con la sequenza verde 4 s, giallo 1 s, rosso 4 s. Per ora usate
delay(): nella lezione 4 capiremo perché non basta. - ProblemaVolete accendere contemporaneamente 12 LED a 20 mA su un Uno R3. Si può fare? E un motorino che assorbe 300 mA? Motivate la risposta con i limiti della tabella.
Nel datasheet dell'ATmega328P trovate la sezione «Absolute Maximum Ratings». Qual è la corrente massima per pin? E quella totale sui pin VCC e GND?
Lezione 3
Leggere e scrivere i pin
Tre funzioni bastano per il mondo digitale: una configura il pin, una lo scrive, una lo legge.
| Funzione | Cosa fa | Esempio |
|---|---|---|
pinMode(pin, modo) | Configura il pin come OUTPUT, INPUT o INPUT_PULLUP. Va in setup(). | pinMode(8, OUTPUT); |
digitalWrite(pin, valore) | Porta il pin a HIGH (5 V) o LOW (0 V) | digitalWrite(8, HIGH); |
digitalRead(pin) | Restituisce HIGH o LOW | bool p = digitalRead(2); |
Il pin flottante e la resistenza di pull-up
Un pulsante collega il pin a GND solo quando è premuto. Da rilasciato il pin non è collegato a niente: è flottante e legge valori casuali, perché capta i disturbi come un'antenna. La soluzione è una resistenza che lo «tiri su» a 5 V quando il pulsante è aperto. L'ATmega328P ne ha una integrata, che si attiva con INPUT_PULLUP.
Livello o fronte?
«Il LED è acceso finché tengo premuto» dipende dal livello del pulsante. «Ogni pressione accende o spegne il LED» dipende dal fronte: bisogna accorgersi del momento in cui il pulsante passa da rilasciato a premuto. Per farlo si ricorda la lettura precedente.
const byte PULSANTE = 2, LED = 8;
bool statoPrec = HIGH; // con INPUT_PULLUP, rilasciato = HIGH
bool ledAcceso = false;
void setup() {
pinMode(PULSANTE, INPUT_PULLUP);
pinMode(LED, OUTPUT);
}
void loop() {
bool stato = digitalRead(PULSANTE);
if (statoPrec == HIGH && stato == LOW) { // fronte di discesa: appena premuto
ledAcceso = !ledAcceso;
digitalWrite(LED, ledAcceso);
}
statoPrec = stato;
}
I rimbalzi
Un pulsante vero non si chiude in modo pulito: i contatti metallici rimbalzano per qualche millisecondo. Il microcontrollore è abbastanza veloce da vedere ogni rimbalzo come una pressione diversa.
Stampare la lettura del pulsante a ogni giro di loop() produce migliaia di righe al secondo. Stampate solo quando il valore cambia: è lo stesso ragionamento del fronte.
Esercitazione
- Pulsante e LEDIl LED è acceso finché il pulsante è premuto. Usate
INPUT_PULLUP, senza resistenze esterne. - InterruttoreOgni pressione cambia lo stato del LED (codice sopra). Provate a togliere il controllo sul fronte: che cosa succede?
- ContatoreStampate sulla seriale il numero di pressioni. Se avete una scheda vera, osservate i salti dovuti ai rimbalzi.
Nella pagina di digitalWrite(): che cosa succede se si scrive HIGH su un pin configurato come INPUT?
Lezione 4
Il problema di delay()
delay(ms) ferma il programma per il numero di millisecondi indicato. Proprio questo è il problema: mentre aspetta, Arduino non fa nient'altro.
L'esperimento
LED1 lampeggia con delay(). LED2 deve accendersi quando teniamo premuto il pulsante. Premete il pulsante in momenti diversi e guardate la riga evidenziata, cioè quella che il microcontrollore sta eseguendo.
Che cosa abbiamo visto
Il pulsante viene letto una sola volta ogni 2 secondi, alla riga 6. Se lo premiamo e lo rilasciamo durante un delay(), la pressione va persa. Se lo teniamo premuto, LED2 si accende con un ritardo fino a 2 secondi. Nel grafico si vede lo scarto tra la riga blu (pulsante) e quella gialla (LED2).
Più compiti aggiungiamo, peggio va: ogni delay() blocca tutti gli altri.
La regola è una semplificazione utile. delay() va bene in setup() o in un programma che fa una cosa sola, come il blink della lezione 1. Diventa un problema appena Arduino deve fare più di una cosa, cioè quasi sempre nei progetti veri. Per questo conviene abituarsi fin da subito a farne a meno.
Esercitazione
- Riprodurre il problemaSu Tinkercad costruite il circuito della simulazione: due LED e un pulsante. Scrivete lo stesso codice e verificate il ritardo.
- Il semaforo con chiamataRiprendete il semaforo della lezione 2 e aggiungete un pulsante pedonale: quando viene premuto, al termine della fase verde deve partire il giallo. Con
delay()la pressione viene persa. Annotate quando e perché. - DiscussioneRendere i delay più brevi, per esempio 10 volte 100 ms controllando il pulsante in mezzo, risolve il problema? Quali difetti ha questa soluzione?
Nella pagina di delay(): che cosa continua a funzionare durante l'attesa? (Cercate le parole «interrupts», «serial», «PWM».)
Lezione 5
millis(): guardare l'orologio invece di aspettare
millis() restituisce i millisecondi trascorsi dall'accensione della scheda. Non ferma niente: dice solo che ore sono.
L'idea
Pensate a chi cuoce una torta. Con delay() si siede davanti al forno per 40 minuti e non fa nient'altro. Con millis() annota l'ora di inizio, fa altre cose e ogni tanto guarda l'orologio: «sono passati 40 minuti?». Se sì, tira fuori la torta.
const unsigned long INTERVALLO = 1000;
unsigned long tPrec = 0; // quando ho cambiato stato l'ultima volta
bool statoLed = LOW;
void loop() {
if (millis() - tPrec >= INTERVALLO) { // è passato abbastanza tempo?
tPrec = millis(); // annoto l'ora
statoLed = !statoLed;
digitalWrite(LED1, statoLed);
}
// qui il loop è libero di fare altro, subito
}
Lo stesso esperimento, senza delay()
Più compiti, tempi diversi
Ogni compito ha la sua variabile tPrec e il suo intervallo. Con delay() questo è impossibile, con millis() è solo una ripetizione dello stesso schema.
unsigned long tA = 0, tB = 0;
bool statoA = LOW, statoB = LOW;
void loop() {
if (millis() - tA >= 300) { tA = millis(); statoA = !statoA; digitalWrite(LED_A, statoA); }
if (millis() - tB >= 700) { tB = millis(); statoB = !statoB; digitalWrite(LED_B, statoB); }
}
Perché unsigned long, e perché la sottrazione
millis() restituisce un unsigned long a 32 bit. Dopo 4.294.967.295 ms, circa 49,7 giorni, ricomincia da zero (overflow). Il confronto va scritto con la sottrazione: grazie all'aritmetica senza segno, millis() - tPrec dà il tempo trascorso giusto anche a cavallo dell'overflow. La somma invece sbaglia.
Esercitazione
- Blink senza delayRiscrivete il blink della lezione 1 con
millis(). - Due LED indipendentiLED_A a 300 ms e LED_B a 700 ms, come nel riquadro sopra.
- Risolvere l'esperimentoRiprendete il circuito della lezione 4: LED1 lampeggia e LED2 risponde subito al pulsante.
- Un messaggio ogni 2 secondiAggiungete un terzo compito che stampa sulla seriale il tempo trascorso ogni 2 s, senza rallentare gli altri.
Esiste anche micros(). Qual è la sua risoluzione su Arduino Uno? Dopo quanto tempo va in overflow?
Lezione 6
Polling e macchine a stati
Il polling è il modo più semplice di fare più cose insieme: il loop() passa in rassegna ogni compito e chiede «tocca a te?». Nessun compito deve mai bloccare.
Le regole del polling
Ogni compito controlla se ha qualcosa da fare, lo fa in fretta ed esce. Niente delay() e niente cicli while che aspettano un evento. Il tempo si misura con millis(), lo stato si conserva in variabili globali.
Debounce con millis()
Ora possiamo risolvere i rimbalzi della lezione 3: accettiamo un cambiamento solo se la lettura è rimasta stabile per almeno 50 ms.
const unsigned long DEBOUNCE = 50;
bool statoStabile = HIGH, ultimaLettura = HIGH;
unsigned long tCambio = 0;
bool appenaPremuto() { // vero solo sul fronte di pressione
bool lettura = digitalRead(PULSANTE);
if (lettura != ultimaLettura) tCambio = millis(); // sta ancora rimbalzando
ultimaLettura = lettura;
if (millis() - tCambio >= DEBOUNCE && lettura != statoStabile) {
statoStabile = lettura;
return statoStabile == LOW;
}
return false;
}
La macchina a stati
Quando un sistema attraversa fasi diverse, come un semaforo, conviene dare un nome a ogni fase (lo stato) e scrivere quando si passa da una all'altra (le transizioni). Nel codice diventa una variabile e uno switch.
enum Stato { VERDE_AUTO, GIALLO_AUTO, VERDE_PEDONI, LAMPEGGIO_PEDONI };
Stato stato = VERDE_AUTO;
unsigned long tStato = 0; // quando sono entrato nello stato attuale
bool chiamata = false;
void cambiaStato(Stato nuovo) {
stato = nuovo;
tStato = millis();
Serial.println(nuovo);
}
void loop() {
if (appenaPremuto()) chiamata = true; // compito 1: ingresso
unsigned long t = millis() - tStato; // compito 2: macchina a stati
switch (stato) {
case VERDE_AUTO:
if (chiamata && t >= 4000) cambiaStato(GIALLO_AUTO);
break;
case GIALLO_AUTO:
if (t >= 2000) cambiaStato(VERDE_PEDONI);
break;
case VERDE_PEDONI:
if (t >= 4000) cambiaStato(LAMPEGGIO_PEDONI);
break;
case LAMPEGGIO_PEDONI:
if (t >= 2000) { chiamata = false; cambiaStato(VERDE_AUTO); }
break;
}
aggiornaLuci(); // compito 3: uscite
}
Progetto finale
- CircuitoSu Tinkercad: tre LED per le auto, due per i pedoni, un LED che segnala la chiamata e un pulsante con
INPUT_PULLUP. - Macchina a statiImplementate i quattro stati del diagramma. Scrivete la funzione
aggiornaLuci(), che accende i LED in base allo stato. Nello statoLAMPEGGIO_PEDONIil verde pedonale lampeggia ogni 250 ms, sempre conmillis(). - Log serialeA ogni cambio di stato stampate il tempo e il nome dello stato, non il numero.
- Per chi finisce primaUn buzzer che suona durante il verde pedonale, oppure un interruttore che attiva la modalità notturna con il giallo lampeggiante.
Link al progetto Tinkercad condiviso, codice commentato e una breve relazione: diagramma degli stati, scelte fatte e pagine della documentazione consultate.
Fine unità
Verifica
Una prova pratica su Tinkercad, che vale circa il 70%, e alcune domande brevi, circa il 30%.
Prova pratica: esempio di consegna
Due pulsanti, P1 e P2, e tre LED.
LED1 lampeggia sempre a 1 Hz. Ogni pressione di P1 accende o spegne LED2, e viene registrata sulla seriale con il tempo. Finché P2 è premuto, LED3 lampeggia a 4 Hz; altrimenti è spento.
Non si può usare delay() dentro loop().
Griglia di valutazione della prova pratica
| Criterio | Punti |
|---|---|
| Circuito corretto, resistenze dimensionate e giustificate | 10 |
Uso corretto di pinMode, digitalRead, digitalWrite e INPUT_PULLUP | 10 |
Temporizzazioni con millis(), nessun delay() nel loop, tipi corretti | 20 |
| Riconoscimento del fronte e gestione dei rimbalzi su P1 | 15 |
| Uso della seriale per log e debug | 5 |
| Leggibilità: costanti con nome, commenti, struttura in funzioni | 10 |
| Totale | 70 |
Domande teoriche
- CalcoloUn LED verde con VLED = 2,2 V deve assorbire 10 mA da un pin a 5 V. Quale resistenza serve? Quale valore commerciale scegliete?
- delay()Spiegate perché un programma con
delay(1000)nelloop()può perdere la pressione di un pulsante. - Trova l'errorePerché
if (millis() >= tPrec + 1000)può dare risultati sbagliati? Come va scritto? - Pull-upChe differenza c'è tra
INPUTeINPUT_PULLUP? ConINPUT_PULLUP, che valore leggedigitalReada pulsante premuto? - TipiSu Arduino Uno,
int x = 20000; x = x * 2;: quanto valex? Perché?
Soluzioni per il docente: 1) 280 Ω, si sceglie 330 Ω (≈ 8,5 mA). 3) Overflow della somma; si scrive millis() - tPrec >= 1000. 4) Premuto = LOW. 5) −25536: 40000 supera 32767 e si ricomincia dai negativi.