Il 0% ha trovato utile questo documento (0 voti)
15 visualizzazioni318 pagine

Esp 32

Il documento è una guida completa sull'ESP32, una scheda per la realizzazione di prototipi elettronici interattivi e connessi, scritta da Paolo Aliverti. Il libro è strutturato in sei capitoli che coprono argomenti dalla storia delle schede Espressif, all'installazione di software, programmazione, utilizzo di hardware e periferiche, fino alla trasmissione e ricezione di dati tramite WiFi e Bluetooth. È destinato a maker, studenti e professionisti interessati a sviluppare progetti elettronici connessi.

Caricato da

Giuseppe Carbone
Copyright
© All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
15 visualizzazioni318 pagine

Esp 32

Il documento è una guida completa sull'ESP32, una scheda per la realizzazione di prototipi elettronici interattivi e connessi, scritta da Paolo Aliverti. Il libro è strutturato in sei capitoli che coprono argomenti dalla storia delle schede Espressif, all'installazione di software, programmazione, utilizzo di hardware e periferiche, fino alla trasmissione e ricezione di dati tramite WiFi e Bluetooth. È destinato a maker, studenti e professionisti interessati a sviluppare progetti elettronici connessi.

Caricato da

Giuseppe Carbone
Copyright
© All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd

Copertina

ESP32
Guida completa

Paolo Aliverti
ESP32 | Guida completa

Autore: Paolo Aliverti

Collana:

Publisher: Giulia Galimberti


Progetto grafico e copertina: Roberta Venturieri
Immagine di copertina: Paolo Aliverti

© 2023 Edizioni LSWR* – Tutti i diritti riservati

ISBN 979-12-5491-112-9
eISBN 979-12-5491-113-6

I diritti di traduzione, di memorizzazione elettronica, di riproduzione e adattamento totale o parziale


con qualsiasi mezzo (compresi i microfilm e le copie fotostatiche), sono riservati per tutti i Paesi. Le
fotocopie per uso personale del lettore possono essere effettuate nei limiti del 15% di ciascun volume
dietro pagamento alla SIAE del compenso previsto dall’art. 68, commi 4 e 5, della legge 22 aprile
1941 n. 633. Le fotocopie effettuate per finalità di carattere professionale, economico o commerciale
o comunque per uso diverso da quello personale possono essere effettuate a seguito di specifica
autorizzazione rilasciata da CLEARedi, Centro Licenze e Autorizzazioni per le Riproduzioni
Editoriali, Corso di Porta Romana 108, 20122 Milano, e-mail autorizzazioni@[Link] e sito
web [Link].

AVVERTENZA IMPORTANTE
Tutti i consigli e le indicazioni riportati nel presente libro sono stati verificati accuratamente e
secondo scienza e coscienza dall’autore. Ogni lettrice e ogni lettore è responsabile delle proprie
azioni, ovvero di decidere se applicare i metodi, i consigli e le disposizioni descritti nel libro. Gli
autori e la casa editrice non si ritengono responsabili per danni e problemi derivanti dall’esecuzione
dei consigli pratici contenuti nella presente opera.

Via G. Spadolini, 7
20141 Milano
Tel. 02 881841
[Link]

* Edizioni LSWR è un marchio di La Tribuna Srl. La Tribuna Srl fa parte di


Indice

Introduzione
Argomenti del libro e descrizione dei capitoli
Per chi è pensato questo libro?
Il materiale necessario
Attrezzi
Strumenti
Componenti elettronici
Dove acquistare componenti?
Se le cose non funzionano
Libri consigliati
Avvisi e precauzioni
Il canale video
La mia attrezzatura
Chi sono?
Ringraziamenti

1. Le schede di Espressif
Espressif
ESP8266 vs Arduino
ESP32
Schede di sviluppo
ESP32 DevKit
NodeMCU-32S
HUZZAH32
ESP32-PICO-KIT
TTGO T-Display ESP32
Arduino Nano 32 e R4 WiFi
2. Hello ESP!
Installare Arduino IDE
Windows
Mac OS X
GNU/Linux
Configurazione del progetto e caricamento del programma
Installare Visual Studio Code con PlatformIO
PlatformIO
Creazione di un progetto

3. Muovere i primi passi


La scheda di riferimento
Come sistemare la ESP32-DevKitC su due breadboard
Come alimentare un modulo ESP32
Scegliere i pin
Controllo di flusso, pulsanti e LED
Pause non bloccanti
Porte seriali
Ricevere dati dalla seriale
Ingressi analogici
Uscite analogiche
Ripetere istruzioni con il ciclo for
Generare segnali analogici

4. Coding like a “PRO”


Puntatori
Array
Array di caratteri
Funzioni
Passaggio valori per indirizzo
Enumerazioni
Strutture
Macchine a stati
Controllo per porta o cancello
Interrupt
Debouncing
Debouncing con delay
Debouncing non bloccante
Fronti di salita e discesa
Misurare il tempo di pressione di un pulsante
Clic multipli su un pulsante

5. Hardware & periferiche


Controllare correnti con i transistor
Accendere una lampada o una striscia di LED
Pilotare un relè con un transistor
Utilizzare un MOSFET
Controllare la velocità di un motore
Utilizzare un servomotore
Installare una libreria per il servomotore
I²C
Trasmettere byte tra master e slave
Collegare il sensore di temperatura e pressione BMP280
I²C Scanner
Collegare un display I2C
SPI
Collegare più dispositivi a una porta SPI
Utilizzare HSPI e VSPI
Collegare una SD Card via SPI
SPIFFS: un’alternativa alle SD Card
Real Time Clock (RTC)
Sleep mode
Active Mode
Modem Sleep Mode
Light Sleep Mode
Deep Sleep Mode
Hibernation Mode
Mettere a dormire ESP32 e metodi per il risveglio
EEPROM
Utilizzare due CPU

6. Trasmettere e ricevere dati


WiFi
Station e Access Point Mode
Cercare una rete
Gestire la connessione
Eventi
Impostare i parametri di rete
WiFiClient per leggere una pagina web
Chiamate in HTTPS
HTTP Server
Servire una pagina dinamica
Controllo di un LED da pagina HTML
Web Server con più pagine
Web Server con SPIFFS
Access Point
Altri protocolli
REST
MQTT
TCP
UDP
Ethernet
Bluetooth
Controllare un LED
Inviare dati del sensore DHT11
BLE
BLE Server
Server BLE con advertising
Client BLE
ESPNow
Ricevitore
Trasmettitore
OTA: aggiornare lo sketch da WiFi
OTA con Arduino IDE
OTA con Web Server integrato

7. Rudimenti di elettronica
Come leggere le resistenze
Elenco dei componenti utili per un generico laboratorio elettronico
Introduzione

In questi ultimi anni abbiamo potuto osservare un ritorno dell’elettronica


grazie alla piattaforma Arduino, ormai diventata uno standard per lo
sviluppo di prototipi e circuiti elettronici. Il mondo inizialmente creato da
Arduino si è espanso incredibilmente facendo nascere centinaia di nuove
schede di prototipazione rapida. Molti appassionati di elettronica, quelli che
“smanettavano” negli anni ’80, hanno rispolverato i loro saldatori per
tornare alla loro passione. Molte nuove figure si sono avvicinate a questo
mondo: studenti, inventori, maker, hobbisti, designer, startupper. Arduino
UNO permette di creare prototipi elettronici di ogni tipo ma ha sempre
avuto una mancanza: la connettività integrata, cioè la possibilità di
comunicare con la Rete. Sono comparse numerose schede aggiuntive per
poter aggiungere Ethernet, WiFi, Bluetooth, GSM e altre tecnologie radio,
ma sempre con qualche difficoltà. I problemi maggiori di queste soluzioni
sono legati al costo e al consumo di risorse. Per poter chiamare dei servizi
online sono necessari protocolli di comunicazione complessi e che quindi
richiedono un consumo di memoria notevole. Purtroppo Arduino UNO ha
2K di memoria RAM, che si esauriscono in un istante!
Con la diffusione di nuovi sistemi e applicazioni (pensate alla domotica)
la necessità di connettere una scheda è diventata fondamentale. Oggi pare
che ogni oggetto debba essere connesso (con tutti i pro e contro che questo
comporta) e quindi quello che una volta era un requisito utile ma non
fondamentale, oggi ha acquistato un’importanza primaria.
Gli oggetti connessi parlano tra di loro direttamente o passando da
Internet; scambiano dati rilevati con i sensori a bordo, possono essere
localizzati e possiamo sapere come stanno funzionando. In questo modo
possiamo creare prototipi “distribuiti”, formati da più schede distribuite
nello spazio. La connettività apre le porte a realizzazioni un tempo
impensabili: possiamo controllare un intero edificio oppure rilevare lo stato
di una coltivazione punto per punto. Non è più necessario stendere
chilometri di cavi per connettere i dispositivi: abbiamo numerose possibilità
tra cui scegliere.
Queste applicazioni distribuite fanno parte di quello che definiamo
l’Internet degli Oggetti (IoT – Internet of Things) e delle smart cities.
In questo panorama molto vario e un po’ confuso, alcuni anni fa è
comparsa una piccola schedina dotata di WiFi che si è fatta subito notare
per le dimensioni e il costo ridotto. Questa scheda era prodotta dalla
Espressif e si chiamava ESP8266. Inizialmente era venduta come se fosse
una specie di modem da aggiungere ad Arduino e da controllare con
comandi via seriale. La scheda presentava svariate problematiche ma
poteva essere facilmente riprogrammata con Arduino IDE. Molti maker se
ne sono interessati e l’hanno adottata per i loro progetti scoprendo anche
che la dotazione di memoria e la velocità dell’ESP erano notevolmente
maggiori di quelle di Arduino. In poco tempo le schede con
microcontrollori e i kit di sviluppo prodotti da Espressif si sono largamente
diffusi, diventando oggi una delle soluzioni più adottate per realizzare
prototipi connessi. Nel 2023 anche Arduino ha adottato i chip Espressif
introducendoli nelle nuove revisioni delle proprie schede: Arduino Nano
ESP32 e Arduino R4 WiFi.
Figura 0.1 – Espressif Dev Kit C con ESP32.
Figura 0.2 – Arduino UNO R4 WiFi con modulo ESP32-S3.

Argomenti del libro e descrizione dei capitoli


Ho organizzato il libro in sei capitoli più un’introduzione:
• Introduzione: introduzione e informazioni per iniziare riguardo alle
schede e al materiale;
• Capitolo 1: Espressif – breve storia di Espressif e di come nascono le
schede ESP32;
• Capitolo 2: strumenti per scrivere il codice e caricarlo su ESP32;
• Capitolo 3: le nozioni di base sui pin e sulla scheda incluse informazioni
sulla programmazione in C/C++;
• Capitolo 4: tecniche avanzate per la programmazione e per l’utilizzo di
speciali meccanismi di ESP32;
• Capitolo 5: approfondimenti sull’hardware, sul pilotaggio di dispositivi,
sensori e attuatori, I2C e SPI;
• Capitolo 6: come utilizzare WiFi, Bluetooth, BLE e ESPNow per
trasmettere e ricevere dati.

Per chi è pensato questo libro?


Questo libro parla di ESP32, una scheda per realizzare prototipi elettronici
interattivi e connessi. Nel libro troverete argomenti di elettronica e
programmazione software (in C/C++). Ho voluto scrivere un testo sui
ESP32 perché attualmente è argomento di estremo interesse nel mondo
maker e non solo. Negli ultimi anni ho sviluppato vari progetti e prototipi
“connessi” utilizzando i prodotti Espressif che mi hanno permesso di
raccogliere esperienze e conoscenze riassunte in questo libro.
Il libro è pensato per dare supporto e offrire un percorso il più possibile
completo a:
• maker, inventori e hobbisti: appassionati di elettronica che vogliono
realizzare progetti connessi (per esempio domotica, automazione,
controllo automezzi e camper);
• studenti: per avere una guida aggiuntiva nella realizzazione di tesi, tesine
ed elaborati o per affiancare i libri di testo e le attività in laboratorio;
• startupper, professionisti, imprenditori: per sviluppare rapidamente idee
“connesse” (MVP1) che poi si possono trasformare in prodotti.

Come nel mio consueto stile ho cercato di scrivere in modo semplice e


discorsivo, presentando gli elementi fondamentali necessari per
comprendere e iniziare a utilizzare le schede ESP32. Lo sviluppo di
prototipi elettronici è un’attività complessa che richiede una minima
familiarità con concetti elettronici e la programmazione. Nel libro troverete
spiegazioni sulle schede ESP32 affiancate a descrizioni dei circuiti e
componenti utilizzati oltre che a elementi base di programmazione così da
facilitare chi è completamente a digiuno di tali concetti.
Non pretendo di essere un guru della programmazione o dell’elettronica e
molti potranno obbiettare che esistono modi migliori per sviluppare alcuni
degli argomenti incontrati: ci sono magari soluzioni più compatte, più
rapide o più eleganti. Ovviamente non c’è mai un solo modo per sviluppare
le cose e ognuno segue la strada che preferisce. Oggi, dovendo tracciare io
la via ho cercato di seguire quella meno impervia, quella più sicura, con la
minor pendenza, sperando di portare tutti in vetta.
Scrivere un libro è un lavoro molto complesso che richiede uno sforzo
notevole: ho provato tutti gli sketch e gli esempi e il testo è stato riletto più
volte da più persone, tuttavia qualche errore potrebbe sempre capitare.
Vi prego di essere clementi e di non lasciare ingiuste recensioni online,
ma di comunicarmeli per poterli correggere nelle prossime edizioni
inviando una mail a zeppelinmaker@[Link].

Il materiale necessario
Per poter seguire nel migliore dei modi i contenuti presentati nel libro è
consigliabile avere a disposizione una scheda ESP32, alcuni attrezzi e dei
componenti elettronici. Per programmare le schede utilizzeremo il software
gratuito Arduino IDE che potete scaricare all’indirizzo
[Link]
Per realizzare gli esperimenti riportati nel libro è necessario disporre di
una dotazione minima di hardware elettronico. Nella maggior parte dei casi
saranno sufficienti pochi e semplici componenti che potreste avere già nelle
vostre cassettiere: resistori, pulsanti, potenziometri, qualche LED, jumper e
una breadboard.
Esistono svariati kit di sviluppo e schede ESP32 che troverete descritte
nei prossimi capitoli: potete utilizzare quella che preferite. Il modello che
ho utilizzato durante la stesura del libro è una ESP32-DevKitC.

Attrezzi
Per realizzare i progetti presentati nel libro non vi serve una grande
strumentazione. In genere lavoreremo con componenti pronti all’uso che
vanno solo collegati. In ogni caso potrebbe capitare di dover svolgere
qualche semplice operazione per cui vi farebbe comodo allestire un piccolo
laboratorio dotato di alcuni attrezzi “di sopravvivenza”:
• forbici da elettricista: adatte per tagliare fili, recidere terminali e reofori e
spelare fili;
• tronchesino: per tagliare in modo netto e preciso i terminali rigidi;
• pinza: utile per afferrare componenti, stringere o serrare. Spesso usata per
disperdere il calore durante le saldature;
• pinzette di precisione di varie dimensioni: utilizzabili per aiutarsi a
posizionare componenti sulle breadboard;
• cacciaviti a taglio e a stella di varie dimensioni: non c’è molto da svitare!
Consigliati per regolare i trimmer;
• pinza spela-fili: non strettamente necessaria ma utile. La considero un di
più perché per spelare i fili uso spesso la forbice da elettricista o un
semplice taglierino (facendo molta attenzione!). Una bella pinza spela-fili
è provvista di più tacche e regolazioni per sguainare i fili rapidamente e
con precisione.

Quando acquistate degli attrezzi non fatevi ammaliare dai super kit
economici completi di tutto che troneggiano all’ingresso dei negozi di
bricolage e dei supermercati. Spesso l’acciaio utilizzato è di scarsa qualità e
ve ne accorgerete al primo utilizzo. Meglio investire qualcosa di più per un
cacciavite di marca: non si romperà mai e durerà anni. Alcuni dei miei
attrezzi hanno più di quarant’anni e sembrano quasi nuovi!
Nelle situazioni di emergenza faccio sempre affidamento su un coltellino
svizzero multi uso. Mi è stato regalato dalla mia carissima amica
Alessandra nel 1997 e da allora è sempre con me. Il mio coltellino ha due
lame, una pinza, una pinzetta, una forbice, alcuni “bit” da inserire in uno
stelo che fa da cacciavite multiuso e vari altri strumenti. È sempre nelle mie
tasche e lo uso quotidianamente nel mio lavoro.

Figura 0.3 – Attrezzi per il laboratorio: 1) coltellino svizzero multifunzione, 2) taglierino, 3) pinzette
di precisione, 4) cacciaviti a taglio e a stella, 5) tronchesino, 6) pinza spela-fili, 7) pinza a becco
piatto, 8) forbici da elettricista.
Strumenti
Presentiamo un elenco di alcuni strumenti utili per allestire un piccolo
laboratorio elettronico.

Tester o multimetro digitale


Per poter avere una prima idea di quello che accade in un circuito, il tester è
fondamentale. Potrete usarlo per misurare tensioni, correnti, resistenza e la
continuità dei circuiti. Oggi un multimetro digitale costa meno di una
calcolatrice e si può acquistare online. Per le nostre applicazioni non è
necessario che sia di tipo professionale, ormai anche i modelli economici
hanno una precisione ottima. Alcuni multimetri possono anche misurare
transistor, capacità, frequenze e temperature. Oggi troviamo in commercio:
• strumenti digitali portatili ed economici (indicati per il nostro scopo);
• strumenti digitali portatili professionali, altamente precisi e dal costo
molto elevato;
• strumenti a lancetta, dal gusto retrò, ma comunque affidabili. Alcuni li
preferiscono perché non effettuano medie sul segnale e, se ci sono
“oscillazioni”, potrebbero essere rilevate;
• strumenti da banco utilizzati per misure di alta precisione.
Figura 0.4 – Multimetro digitale (1) con sonde e tester analogico (2).

Saldatore per elettronica


In un laboratorio elettronico non può mancare un saldatore. Non è uno
strumento essenziale per realizzare i progetti del libro ma può far comodo
per assemblare sensori o schede o per fissare componenti tra di loro. Per
realizzare prototipi si utilizzano basette pre-forate (dette “mille-fori”) in cui
infilare i componenti per poi saldarli. Il saldatore ha una punta riscaldata a
250/350 °C che utilizziamo per fondere una lega metallica (stagno, piombo
e altri metalli) e unire i componenti elettronici.
Esistono vari modelli di saldatori:
• “a stilo”, pratici e rapidi da utilizzare, spesso a temperatura preimpostata.
La soluzione più economica se siete alle prime armi;
• stazioni saldanti: lo stilo è controllato da una piccola centralina per
regolare la temperatura. Lo stilo ha di solito la punta sostituibile ed è
possibile quindi lavorare con diverse punte e a diverse temperature;
• saldatori ad aria: usati per saldare e dissaldare componenti a montaggio
superficiale.

La saldatura è un’operazione semplice ma è necessario sviluppare una certa


abilità e sicurezza per eseguirla con rapidità. La pratica e l’esperienza sono
importanti per ottenere saldature pulite e di qualità, evitando danni ai
componenti elettronici sensibili al calore. È importante utilizzare un
saldatore con la giusta temperatura e dimensione della punta in base alle
esigenze di saldatura specifiche.
Una saldatura ben fatta è lucida e non presenta bolle. Per facilitare il
processo, molte persone utilizzano della “pasta saldante”, una crema che
facilita la fusione dello stagno e abbassa la temperatura di fusione. La pasta
saldante lascia però notevoli residui che devono essere eliminati utilizzando
un pennello e un solvente (io uso l’acetone puro). Le leghe di stagno
attualmente in commercio includono già al loro interno una sostanza che
facilita la saldatura (rosin o flussante) e normalmente non è necessario
ricorrere alla pasta saldante se non in casi eccezionali: superfici ossidate,
opache o su cui lo stagno non attacca.

Per qualche informazione aggiuntiva sul processo di saldatura e una


presentazione delle tecniche si vedano:
• La saldatura: [Link]
• Saldare a stagno: [Link]

Figura 0.5 – Stazione saldante digitale (1) e saldatore a stilo a temperatura fissa (2).

Stagno
Lo stagno è l’elemento principale per la buona riuscita di una saldatura. Un
tempo si utilizzavano leghe di stagno (Sn) e piombo (Pb) (60% stagno e
40% piombo), il piombo però è dannoso per l’ambiente e quindi oggi sono
preferibili leghe in cui il metallo sia assente o ridotto. Le leghe più comuni
sono quelle che utilizzano stagno, argento (Ag) e rame (Cu), come per
esempio la lega di stagno al 96,5% e argento al 3% e rame allo 0,5% (nota
come stagno 96,5/3/0,5). Queste leghe senza piombo hanno un punto di
fusione più elevato rispetto allo stagno al piombo (generalmente intorno ai
217-227 °C) e richiedono temperature di saldatura leggermente più elevate.
La temperatura di fusione della lega SnPb 60/40 è in teoria a circa 180 °C
ma, per via delle dispersioni termiche che si hanno durante la saldatura, si
lavora sempre sopra i 250 °C. Al filo di saldatura sono aggiunte sostanze
flussanti che facilitano la fusione e la rimozione di scorie e ossidazioni. Il
filo saldante è in genere abbastanza caro: diffidate delle offerte troppo
economiche e preferite stagno di buona qualità, anche pagandolo qualcosa
in più.

Spugne e pagliette
La punta del saldatore va spesso ripulita dai residui di stagno e di flussante
raggrumato. Possiamo utilizzare una spugna umida (non di plastica!) su cui
strofinare la punta dello strumento. In alcuni kit trovate a volte delle
pagliette metalliche che assorbono e rimuovono i rimasugli sulla punta ma
la possono anche danneggiare, infatti l’estremità è trattata con alcuni strati
metallici per poter resistere meglio alle alte temperature e per accogliere lo
stagno.

Terza mano
La “terza mano” è un dispositivo che si utilizza per tenere fermi in
posizione i componenti da saldare, poiché quando state per saldare avete il
saldatore in una mano e lo stagno nell’altra.
Figura 0.6 – Accessori per la saldatura a stagno: (1) terza mano per sorreggere i componenti dotata
di lente d’ingrandimento e supporto per saldatore, (2) pasta saldante, (3) rocchetti di stagno con filo
di vari diametri, (4) supporto per saldatore con spugnetta da inumidire.

Dissaldare
Cosa fare se una saldatura è venuta male? Come rimuovere un componente
saldato? Nulla è irrevocabile! Potete utilizzare vari metodi per rimuovere lo
stagno e liberare i componenti, anche se è un’operazione non sempre
semplice e che richiede una certa pazienza. Possiamo rimuovere lo stagno
con:
• pompetta dissaldante: è uno stilo con stantuffo a molla, una specie di
siringa. Lo caricate e poi premendo un tasto aspirerà lo stagno fuso dalla
punta. Dovete prima ravvivare la saldatura da rimuovere con il saldatore
e poi avvicinare il dissaldatore a pompetta;
• treccia di rame: assorbe lo stagno e lo trattiene nelle sue maglie. Esistono
in diverse lunghezze e formati. Si utilizza per rimuovere lo stagno in
eccesso dalle saldature. Una volta che lo stagno è stato assorbito dovete
tagliare la parte utilizzata con un taglierino e buttarla;
• stazione dissaldante: è la soluzione più efficace perché utilizza un
saldatore con la punta forata. Una pompa aspira lo stagno fuso dal foro e
lo raccoglie in un serbatoio. Ideale se ci si occupa di riparazioni e si deve
dissaldare spesso. Ha un costo notevole!

Dissaldare è più complicato che saldare. L’importante è non usare le


maniere forti e mantenere la calma, riprovando più volte.

Figura 0.7 – Strumenti per la dissaldatura: (1) pistola dissaldante, (2) supporto per dissaldatore, (3)
stazione dissaldante con controllo digitale della temperatura, (4) treccia di rame per la rimozione
dello stagno, (5) pompetta dissaldante.

Lampade, lenti e ingranditori


Per lavorare bene è importante illuminare il vostro tavolo. Utilizzate varie
lampade che forniscano una luce naturale e potente: più luce c’è e meglio si
vede! Se la luce non è sufficiente e se iniziate ad avere qualche anno sulle
spalle potete dotarvi di una lampada con lente e di varie lenti di
ingrandimento.
Esistono occhiali ingranditori a distanza ma i modelli migliori hanno
prezzi molto elevati perché impiegano lenti in vetro di qualità. Una
soluzione molto economica ed efficace sono i monocoli da orefice: costano
pochi euro e hanno ingrandimenti fino a 10x!

Figura 0.8 – (1) lente d’ingrandimento e (2) monocolo da oreficeria a 10x ingrandimenti.

Alimentatore
Un alimentatore da laboratorio può servire per poter fornire energia ai
circuiti. La vostra scheda ESP32 lavora a 3,3 V ma vi capiterà spesso di
aver bisogno di tensioni superiori (per esempio a 12 V) e con una certa
disponibilità di corrente (anche fino a 5-10 A). Gli alimentatori stabilizzati
da laboratorio raggiungono anche una trentina di volt con 10 o 15 A e sono
più che sufficienti per le comuni esigenze del progettista amatoriale.

Componenti elettronici
Passiamo in rassegna un elenco di componenti e materiale utile per il nostro
piccolo laboratorio.

Figura 0.9 – Alcuni componenti fondamentali per allestire un piccolo laboratorio con cui realizzare
vari progetti: (1) breadboard, (2) resistori assortiti, (3) jumper, (4) servomotore in miniatura, (5)
motore in cc, (6) display LCD 16x2, (7) varie breakout board, (8) foto resistori, (9) LED RGB, (10)
LED assortiti, (11) pulsanti normalmente aperti (n.o.), (12) condensatori elettrolitici, (13)
condensatori ceramici e MLCC, (14) termistore, (15) chip, (16) transistor assortiti, (17) trimmer.

Breadboard
La breadboard è una tavoletta di plastica in cui sono immersi dei contatti
elettrici. Utilizzando la breadboard possiamo realizzare dei circuiti senza
utilizzare il saldatore. Esistono tavolette di varie dimensioni e dal costo
contenuto. Conviene acquistare quella “grande” così da avere più spazio per
lavorare. Alcune schede ESP32 purtroppo sono troppo larghe per poter stare
sulla breadboard. Possiamo risolvere questo problema acquistando due
breadboard e unendole tra di loro (si veda il Capitolo 2).
Jumper
Per realizzare i collegamenti elettrici tra i componenti infilati nella
breadboard servono dei fili. Troviamo in commercio dei “ponticelli” o
jumper pronti all’uso e di varie lunghezze. I jumper si trovano sia rigidi che
flessibili. Quelli rigidi sono realizzati con degli spezzoni di filo elettrico con
anima rigida, mentre quelli flessibili hanno terminali “crimpati” alle
estremità. Se intendete realizzare da voi i collegamenti sulla breadboard,
non utilizzate del semplice filo elettrico flessibile perché le estremità non
farebbero correttamente contatto con la breadboard.

Resistori
I resistori sono componenti elettronici usati per limitare la corrente che
circola in un circuito. Sono onnipresenti in ogni circuito elettronico. Il
modello più diffuso ha la forma di un piccolo cilindretto con le estremità
rigonfie. In appendice trovate una tabella per imparare a leggere il valore
che è indicato tramite una serie di bande colorate. La resistenza si misura in
ohm (Ω): maggiore è il valore e meno corrente passerà attraverso il
componente. Esistono svariati tipi di resistenza con valori che vanno da
pochi ohm fino a qualche milione (MΩ). Nei progetti di questo libro
utilizzeremo un numero limitato di resistenze del tipo da ¼ di watt. Questi i
valori in ohm che vi serviranno (per ogni valore procuratevi una decina di
resistori):
100 Ω, 270 Ω, 390 Ω, 1 kΩ, 2,2 kΩ, 4,7 kΩ, 10 kΩ, 100 kΩ.

Trimmer e potenziometri
Possiamo generare semplici segnali di riferimento utilizzando resistenze
variabili, cioè trimmer e potenziometri. I trimmer si regolano con un
cacciavite e variano il loro valore resistivo da 0 al valore massimo
nominale. I potenziometri a differenza dei trimmer hanno un albero che
facilita la rotazione. Per i nostri esperimenti potete procurarvi qualche
trimmer da 10 kΩ.

Condensatori
I condensatori immagazzinano energia elettrica per rilasciarla in un secondo
momento, quando necessario. Si usano per livellare le correnti e le tensioni
oppure per realizzare filtri ed eliminare disturbi. La capacità si misura in
farad e sottomultipli. Esistono diversi tipi di condensatori ma quelli usati
più frequentemente nei nostri progetti sono di due tipi:
• condensatori ceramici multistrato (MLCC): tipicamente da 100 nF per
eliminare i disturbi sulle linee di alimentazione e vicino a
microcontrollori e circuiti integrati. Si collegano tra i pin di
alimentazione positivo e negativo, più vicino possibile al dispositivo da
“proteggere”. La tensione massima di questi condensatori è di solito
attorno a 50 V e quindi non rischieremo di danneggiarli;
• elettrolitici: possiamo recuperarne qualcuno con valore da 100 µF o 220
µF (micro farad = 1/1000 di farad) con tensione massima da 16 o 25 V
(la tensione massima va rispettata per non danneggiare il condensatore).
Sono condensatori polarizzati, cioè hanno un terminale positivo e uno
negativo e non devono essere collegati invertiti. Si collegano in parallelo
alla linea di alimentazione principale, per esempio tra 5 V e GND o tra
3,3 V e GND e servono per far fronte alle richieste di energia dei
dispositivi connessi.

LED
Immancabili in ogni prototipo una manciata di LED. I LED sono
componenti elettronici che fanno scorrere la corrente in una sola direzione.
Quelli utilizzati comunemente con le breadboard sono da 5 mm di diametro
anche se spesso si trovano anche da 3 mm o con forme particolari.
Procuratevi una decina di LED assortiti e se volete aggiungete anche un
LED RGB, una variante del componente che può generare ogni tipo di
colore.

Diodi
I diodi fanno scorrere la corrente in una sola direzione. Sono utilizzati per
proteggere i circuiti o per modificare segnali e correnti. Un diodo classico
ha il corpo cilindrico, in plastica o vetro, è dotato di due terminali e ha una
polarità. Il catodo, cioè il terminale negativo, è solitamente indicato da una
banda nera o bianca. Per i progetti del libro potete procurarvi qualche diodo
raddrizzatore come gli 1N4005 e dei diodi di segnale 1N4148.

Pulsanti
I pulsanti sono molto utilizzati nei prototipi su breadboard perché sono
semplici da controllare e forniscono un metodo per interagire con
l’utilizzatore del circuito. Esistono infiniti modelli di pulsanti anche se
quelli che utilizziamo solitamente sono di tipo “normalmente aperto”, che
significa che nessuna corrente attraverserà il dispositivo fino a che non lo
premeremo. I modelli da preferire sono quelli che possono essere infilati su
breadboard come i pulsanti “momentanei” e tattili a quattro piedini. Un
pulsante fa passare la corrente solo quando lo premiamo, per questo è detto
momentaneo. Se invece “mantiene” la posizione si chiama “interruttore”
(quello che usiamo per accendere la luce in una stanza). I modelli più adatti
di pulsanti sono quelli della serie B3F, che assomigliano a piccoli ragni:
procuratevene una decina.

Altri componenti
Finora abbiamo elencato una serie di componenti fondamentali per poter
lavorare. Per i circuiti presentati nelle prossime pagine vi serviranno altri
oggetti che presenteremo di volta in volta. Riporto brevemente cosa
potrebbe servire o cosa vi potreste procurare per avere un piccolo
laboratorio pronto a tutto:
• relè a 5 e 12 V SPDT: spesso disponibili su piccoli circuiti stampati,
dotati di transistor di pilotaggio e diodo di protezione e pronti all’uso;
• transistor bipolari: (BJT) NPN e PNP (per esempio 2N2222, 2N3904 o
BC337, BC327);
• transistor MOSFET: IRF520 o meglio IRL540 definito Logic Level
MOSFET, adatto a essere pilotato con basse tensioni;
• display LCD o OLED: meglio se dotati di interfaccia I2C o SPI così da
poterli collegare con pochi fili;
• sensori: fotoresistenze, termistori e qualche sensore digitale assortito per
rilevare varie grandezze fisiche (distanza tramite IR e ultrasuoni, luce,
suono con un microfono…);
• motori elettrici: per pilotarli senza rischiare di danneggiare la vostra
ESP32 è bene aggiungere dei driver pronti all’uso. A seconda del motore
vi servirà un driver specifico. Potreste procurarvi un motore in corrente
continua, un piccolo passo-passo e magari un servomotore.

Kit per ESP32 e Arduino


Recuperare tutti i componenti non è un compito facile, soprattutto oggi che
i negozi di elettronica sono una vera rarità. L’ultimo che ho visto era una
ricostruzione presso il museo della tecnica elettrica di Pavia (!). Potete
ovviamente trovare tutto online ma dovete dedicare un po’ di tempo
all’approvigionamento dei pezzi. Una soluzione rapida ed economica è
quella di acquistare kit di componenti. Potete cercare uno starter kit per
ESP32 che contiene tutto il necessario per iniziare.

Figura 0.10 – Kit di componenti per ESP32: un’ottima soluzione per iniziare!

Dove acquistare componenti?


Per acquistare componenti e attrezzature elettroniche oggi abbiamo molte
alternative. Il movimento dei maker ha riportato un po’ di interesse per
l’elettronica e sono nate molte società che vendono componenti e materiale
elettronico, soprattutto online. Spesso non è facile scegliere!

Negozi
Un tempo per acquistare componenti elettronici ci si rivolgeva a uno dei
tanti negozietti frequentati da antennisti e riparatori TV. Rispetto a un
tempo, oggi abbiamo molte possibilità, prezzi più bassi e manca
quell’aspetto un po’ scomodo di dover andare di persona per acquistare
quell’integrato o il transistor che ci serve. L’acquisto era anche l’occasione
per conoscere altri riparatori o progettisti, scambiare quattro parole e
ricevere qualche consiglio. Se il componente non era disponibile, il
negoziante ve lo poteva ordinare e sarebbe arrivato, magari, la settimana
dopo. Purtroppo oggi i negozi di elettronica sono scomparsi. Se avete la
fortuna di averne uno vicino a casa, tenetevelo stretto e cercate di
sostenerlo.

E-commerce per i professionisti


Già negli anni ’90 circolavano mitici cataloghi di componenti elettronici
disponibili però solo per aziende e professionisti. Mi ricordo alcuni di
questi libroni composti da migliaia di pagine che però potevate solo
sfogliare ammirando quella moltitudine di oggetti: l’acquisto era infatti
dedicato ad aziende e professionisti con partita iva. Con il tempo i cataloghi
sono finiti in Rete e si sono trasformati in e-commerce, prima per le aziende
ma ora aperti a tutti, anche per i privati: i soldi son soldi…
Questi fornitori vendono ogni tipo di componente possibile e spesso
anche in minime quantità, anche un solo pezzo, anche se ultimamente
qualcuno, a causa della crisi, cerca di ottimizzare i costi vendendo solo
pacchetti con quantità minime di 5, 10 e a volte 50(!) unità. I prezzi sono in
ogni caso ottimi e le spedizione con corriere molto rapide e abbastanza
affidabili. Queste aziende sono l’ideale per acquistare componenti discreti
“sfusi”, difficilmente troverete breakout board o schede “da maker”. I
metodi di pagamento accettati sono bonifici e carte di credito. I maggiori
rivenditori online sono:
• Rs Components ([Link] azienda inglese in attività dal
1954. Ha un magazzino in Italia, offre spedizioni economiche in 24 ore
ma purtroppo spesso impone quantità minime sul singolo pezzo;
• Farnell ([Link] anche questa azienda ha la sede principale
in Inghilterra, ma magazzini in tutta Europa. Rapidi nelle spedizioni e
abbastanza economici;
• Mouser ([Link] azienda americana che vende però
anche in Europa e a privati. Le spedizioni sono leggermente più costose
rispetto agli altri concorrenti ma ha una fornitura completissima;
• Digikey ([Link] azienda americana molto ben fornita e
paragonabile a Mouser;
• TME ([Link] azienda polacca con sede nella bellissima
cittadina di Łódź, nella Polonia centrale. Dispone di un’ottima fornitura e
tempi di spedizione celeri e abbastanza economici.

E-commerce tematici
Per acquistare kit robotici o componenti per maker potete cercare e-
commerce tematici, che offrono componenti di un certo tipo. In Italia ci
sono vari siti dedicati alla robotica che vendono strutture per realizzare
robot, motori oltre che sensori e componenti vari. Uno dei siti pionieri e
quindi copiato da molti è sicuramente SparkFun
([Link] fondato nel 2003 da Nathan Seidle, studente
presso l’università del Colorado. Nathan iniziò a produrre circuiti stampati
per poi specializzarsi nella realizzazione di breakout board open source per
il mondo dei maker, riconoscibili per il loro colore rosso fuoco. I suoi
prodotti sono caratterizzati da un grande cura e un’ottima qualità. Il sito di
SparkFun è anche un riferimento per ottenere informazioni sul
funzionamento delle schede, riportando schemi, istruzioni e numerosi
tutorial (in inglese). Solitamente i suoi prodotti son poi copiati dalla miriade
di aziende cinesi che li rivendono a bassissimo prezzo. È possibile
acquistare i prodotti di SparkFun anche dall’Italia pagandoli però a peso
d’oro a causa del costo di spedizione.
Seeedstudio ([Link] è un concorrente di
SparkFun con sede a Shenzhen. Rispetto a molti altri produttori produce
schede originali e non si limita a copiare i prodotti esistenti.

eBay
Da alcuni anni anche siti come eBay offrono componenti elettronici,
venduti per lo più da privati che li acquistano all’ingrosso dalla Cina e poi li
rivendono localmente o a livello europeo o nazionale. Potete trovare
principalmente breakout board, sensori specifici per ESP32 e Arduino, e
schede di prototipazione. Per il pagamento si può usare una carta di credito
o PayPal (consigliabile). Prima di concludere l’acquisto controllate che tipo
di spedizione è offerto e se potete scegliete metodi tracciabili (evitate
spedizioni non tracciate con le poste). Se non avete fretta impostate i filtri
di ricerca così da includere anche i venditori esteri. Potete così acquistare da
venditori cinesi a prezzi molto bassi. La spedizione in questi casi può
richiedere anche qualche settimana (fino a un mese se avviene via nave) ma
risparmierete parecchio. Prima di acquistare consultate le recensioni e le
valutazioni sul venditore!

Amazon
Amazon è diventato un gigante della vendita online, anche per kit,
componenti e dispositivi elettronici. Io lo utilizzo in genere quando ho
molta fretta: con il programma Prime si può ricevere un pezzo in meno di
24 ore. Su Amazon acquisto in genere strumentazione e attrezzature di
laboratorio ed eventualmente kit di componenti (per cui valgono sempre le
valutazioni riportate qualche pagina fa). I kit troppo economici sono spesso
di bassa qualità, ma potete farvi un’idea generale della bontà di un prodotto
consultando le recensioni. Oggi Amazon offre numerosi metodi di
pagamento (escluso PayPal) e quello principale e più rapido resta la carta di
credito. In generale troverete gli stessi prodotti presenti su eBay ma con
prezzi leggermente superiori: se non avete fretta, utilizzate quindi eBay.
Purtroppo noto che la qualità generale delle attrezzature prodotte in Cina e
paesi limitrofi è molto scarsa e gli oggetti tendono a rompersi e consumarsi
rapidamente. Se vi servono attrezzature di qualità le pagherete sempre un
po’ di più.

AliExpress e Alibaba
La risposta cinese a eBay è AliExpress ([Link] un e-
commerce dai prezzi aggressivi, di proprietà della società cinese Alibaba,
con sede ad Hangzhou. Unisce numerosi produttori cinesi che offrono
prodotti a prezzi bassissimi. Alla fine sono gli stessi prodotti che trovate su
eBay o su Amazon ma con prezzi molto più bassi perché li acquistate
direttamente dal produttore. Si può acquistare utilizzando un carta di credito
e il sito è molto affidabile (in ogni caso, per gli acquisti online cercate di
usare sempre una carta ricaricabile). In alcuni casi è possibile anche pagare
con PayPal. Ogni produttore offre diverse soluzioni per la spedizione: con
le poste cinesi non pagherete nulla o pochi euro, ma se avete fretta è
possibile richiedere una spedizione con corriere. In questi casi ricordatevi
che il pacco potrebbe essere fermato in dogana dove sarà applicata l’iva
sulla cifra dichiarata oltre alle tasse di sdoganamento (attualmente tra 10 e
20 euro).

Mercatini e fiere
In giro per l’Italia sono numerosi le fiere e i mercatini dedicati a elettronica,
informatica e radiantistica. In queste occasioni potete trovare molte
bancarelle che vendono componenti. Purtroppo osserviamo una certa
omologazione dei prodotti e una mancanza di originalità. Esclusi i
componenti discreti e quelli di recupero (surplus), il resto delle schedine,
breakout board ecc. sono copie di prodotti ideati da SparkFun, Arduino e
pochi altri veri innovatori. L’unica nota positiva che posso trovare in questi
prodotti è che democratizzano l’elettronica permettendo a chiunque di
acquistare materiale per sperimentare. Anche io compro spesso materiale di
bassa qualità per fare degli esperimenti, ma poi cerco di premiare il suo
ideatore, acquistando appena possibile l’oggetto originale o dando un
contributo.
Se visitate una fiera vi potrebbe capitare di trovare ancora qualche
rivenditore di surplus, cioè materiale recuperato da fallimenti o svendite.
Potreste fare dei veri affari considerato che vendono il materiale a peso o a
pacchetti in cui trovate un po’ di tutto. Sono l’ideale per popolare
rapidamente le vostre cassettiere.
Se le cose non funzionano
Se il vostro circuito non funziona verificatelo con attenzione e ripassate i
vari passaggi alla ricerca di errori. Una possibile check list potrebbe essere
la seguente.
• Verificate i collegamenti elettrici e lo schema. È tutto corretto? Seguite lo
schema e controllate i collegamenti uno a uno.
• Provate a sostituire la breadboard e i jumper, soprattutto se consumati.
• La scheda ESP32 si accende? Il LED “ON” è acceso (non tutte le schede
ESP32 ne hanno uno!)? Verificate il cavo USB ed eventualmente
sostituitelo. Se il LED è spento potrebbe esserci un corto circuito.
Provate a collegare ESP32 “da sola” (senza sensori, breadboard e
componenti).
• L’hardware che utilizzate necessita di alimentazione esterna? State
fornendo sufficiente tensione e corrente? Avete messo in comune le
masse?
• Serve qualche driver per la porta seriale? Lo avete installato
correttamente?
• Provate con una scheda di riserva oppure collegate la vostra a un
computer differente. Fate delle prove incrociate.
• Il firmware si carica? Ci sono errori durante l’upload?
• Provate a caricare un Blink o uno sketch di prova.
• Se ci sono errori nel codice, verificate che le parentesi siano sempre
accoppiate, che ci siano i punti e virgola in fondo alle linee di codice…
consultate gli errori presentati nella console per avere qualche
indicazione.
• Se lo sketch si carica ma le cose non funzionano, provate a inserire dei
[Link]() per controllare l’avanzamento del codice. Siate creativi
nel cercare una soluzione al vostro problema.
• Se avete dubbi sui sensori collegati, verificateli uno a uno con degli
sketch su misura e prima ancora verificate che siano correttamente
alimentati con un tester.
• Cercate aiuto chiedendo a qualche amico, su qualche forum, Facebook o
provate a cercare il vostro errore su Google.

Libri consigliati
• Paolo Aliverti, Elettronica per Maker. Guida completa, LSWR: un libro
semplice per chi inizia a interessarsi di elettronica.
• Paolo Aliverti, Elettronica Trucchi e segreti. 130 idee per risolvere ogni
problema, LSWR: in questo libro approfondisco vari concetti di
elettronica presentando formule, calcoli ed esempi pratici.
• Paul Scherz, Simon Monk, Practical Electronics for Inventors, McGraw
Hill: un bellissimo libro, in inglese, sull’elettronica con numerosissime
illustrazioni, schemi elettrici e informazioni.
• Paul Horowitz, Winfield Hill, The Art of Electronics, Cambridge
University Press: una pietra miliare della divulgazione elettronica (in
inglese).
• Brian W. Kernighan, Dennis M. Ritchie, Il linguaggio C. Principi di
programmazione e manuale di riferimento, Pearson: per avere un
riferimento e approfondire la conoscenza del linguaggio C.
• Paolo Aliverti, Programmare. Impara a scrivere qualsiasi tipo di
software, Amazon/Lulu: un manuale per capire come ragiona un
programmatore e come iniziare a scrivere del software in autonomia.

Avvisi e precauzioni
Anche se gli esercizi e le attività presentati in questo libro utilizzano basse
tensioni mi preme ricordarvi che la corrente elettrica può essere molto
pericolosa: è invisibile e se non si è consapevoli o sicuri di quello che si fa
si può incorrere in incidenti gravi o mortali. Non utilizzate per i vostri
esperimenti la tensione di rete a 220 V (a meno che sia necessario e
comunque in caso di dubbi, prima fatevi consigliare da qualcuno che ne sa
più di voi). Impiegate solo pile, batterie ed eventualmente alimentatori,
prestando comunque la massima attenzione.
Se non siete sicuri o avete dubbi chiedete a un esperto, un amico, un
professionista… Su Internet si trovano molti siti e gruppi (anche su
Facebook), anche se non è facile capire se una persona sia veramente
esperta da quello che scrive.
Né io né l’editore possiamo assumerci nessuna responsabilità per gli esiti
che potrebbero avere gli esperimenti descritti in questo libro. Non possiamo
rendere conto per incidenti o danni subiti da cose, persone e animali che
potrebbero verificarsi durante gli esperimenti che condurrete. Fate ogni
cosa con la massima attenzione e cercate di prevedere le conseguenze di ciò
che fate (il che è utile anche nella vita…).

Il canale video
I contenuti di questo libro sono stati presentati come video tutorial sul mio
canale YouTube ([Link] dove ho
iniziato a pubblicare materiale a partire dal 2011, prima abbastanza
casualmente, poi con sempre maggior frequenza e regolarità. I miei primi
video erano abbastanza insensati: filmavo quello che avevo costruito senza
alcuna spiegazione o commento. Negli anni YouTube è cresciuto ed è
diventato un vero e proprio “social media”. Mi sono accorto che poteva
essere un canale di comunicazione potente e molto diretto e quindi ho
studiato i video degli YouTuber di successo per capire come impostare i
miei.
Per girare un video non ci si può improvvisare. Anche se molti sembrano
molto caserecci, dietro a un video di successo c’è sempre una certa
organizzazione. Ho cercato di creare qualcosa di valido con un minimo di
studio e organizzazione. Una semplice macchina fotografica non basta. È
fondamentale ma deve essere accompagnata anche da un buon audio. Ho
recuperato un ottimo microfono e vari cavalletti. Un’altra cosa importante è
l’illuminazione a cui ho provveduto con varie lampade e vi assicuro che
sono sempre poche!
I miei primi video erano girati quasi al buio, più per necessità che per
scelta. L’unico posto a mia disposizione era un locale nel mio sottotetto e
potevo girare video solo quando tutti dormivano. Riguardandoli oggi vedo
quanto siano cupi e anche un po’ inquietanti. La mia voce era molto
dimessa perché non potevo parlare a voce alta!
Un altro elemento fondamentale è il montaggio, che richiede tempo e
pazienza. Audio e video devono essere uniti e il tutto va ripulito da rumori,
balbettii, pause ed errori. Non c’è un vero e proprio copione ma per evitare
di divagare, ogni video è preparato prima su carta e provato prima della
registrazione.
Mi sono attrezzato così perché volevo creare qualcosa di “buono”,
semplice e con una certa qualità. La maggior parte dei video “tecnici” girati
in Italia sono molto approssimativi e abbastanza deludenti. Spesso non si
giunge al punto, ci sono lunghe divagazioni. Alcuni di questi sono girati
con un cellulare senza cavalletto: le immagini traballano e oscillano.
Vedendo questi video mi sono detto: “Di video capisco poco, non sono
bello come Johnny Depp… ma posso fare qualcosa di meglio della media”.
Ho iniziato a creare tanti video: uno al giorno, tutti i giorni, pubblicati alle 9
in punto. Viste le premesse un impegno notevole!
Questa tattica ha fatto crescere il mio canale fino a 76.000 iscritti (luglio
2023) e più di 10.000.000 di visualizzazioni (sono sempre una goccia
nell’oceano di YouTube). Una piccola platea di appassionati mi scrive,
commenta e critica ogni giorno. I messaggi che ricevo ogni giorno sono la
miglior ricompensa per tutto il tempo che investo con passione nella mia
attività divulgativa.

La mia attrezzatura
Spesso mi chiedono che attrezzatura utilizzo. Non ho segreti:
• webcam Logitech Stream Cam FullHD;
• microfono Røde Wireless Go;
• OpenBoard come lavagna con tavoletta grafica Wacom Intuos;
• OBS o QuickTime per la registrazione dello schermo;
• iMovie per il montaggio dei video.

Chi sono?
In questi anni mi sono occupato di molte attività e tecnologie vicine al
mondo dei maker. Ho sperimentato con startup e imprese, cambiando pelle
varie volte, ricercando un business che funzionasse. Nel 2011 ho fondato
Frankenstein Garage e poi FabLab Milano: tra le prime startup a occuparsi
di nuovo di hardware al posto che di software e App. In seguito fondai la
società Fabb S.r.l. che si è occupata senza grande successo di stampa 3D,
ologrammi e infine di progettazione IoT. Da alcuni anni ho avviato Reelco
([Link]): REborn ELectronic Company, che esegue riparazioni
elettroniche industriali e prototipi elettronici.
Negli ultimi anni mi sono dedicato alla didattica e alla divulgazione.
Amo scrivere, disegnare e soprattutto condividere il sapere. In questi anni
ho scritto vari libri come il Manuale del Maker tradotto in inglese da Make
Media, alcuni testi sulla stampa 3D e altri volumi di questa collana (Il
manuale di Arduino, Elettronica per Maker, Riparare (quasi) ogni cosa,
Arduino Trucchi e segreti, Elettronica Trucchi e segreti). Potete contattarmi
tramite il mio sito [Link].

Ringraziamenti
Ringrazio di cuore i miei amatissimi follower e abbonati che hanno
contribuito alla revisione di questo libro accollandosi l’ingrato compito di
correggere tutte le mie sviste:

Andrea Anigoni, Riccardo Bramati, Simone Gallina, Egidio Fabris,


Paolo Petenatti.

A loro, a tutti gli abbonati e a tutti i follower tutti i miei ringraziamenti:


senza di voi nulla di ciò sarebbe possibile! Grazie!

1
MVP o in inglese minimum viable product: il prodotto minimo funzionante, cioè la versione di un
prodotto con caratteristiche appena sufficienti per essere utilizzabile dai primi clienti, i quali possono
quindi fornire feedback per lo sviluppo futuro dell’oggetto.
1
Le schede di Espressif
Quando e come sono nate le schede ESP32 e ESP8266? Scopriamo la storia
di Espressif, di come si intreccia con quella di Arduino e i vari modelli di
schede attualmente disponibili.

Quasi una ventina di anni fa, a Ivrea, fu realizzata la scheda Arduino


prendendo spunto dal progetto. Wiring di Hernando Barragán (2003).
Wiring è un framework di programmazione open source, creato a partire da
Processing, utilizzato come base per lo sviluppo di Arduino. Nonostante i
microcontrollori e l’elettronica fossero degli oggetti per ingegneri e
smanettoni, prima Barragán e poi il team di Arduino cercarono di
semplificare più possibile le fasi dello sviluppo e della programmazione,
creando una piattaforma semplice e accessibile anche ai non esperti.
Promossero poi la scheda presso università, scuole, designer, inventori,
artisti e creativi. Oggi la scheda si può considerare uno standard nello
sviluppo di prototipi e molti altri produttori si sono allineati a questo
processo di programmazione.

Negli anni sono apparse innumerevoli schede elettroniche derivate da


Arduino UNO: con gli stessi chip o differenti, con più o meno dotazioni
hardware, estensioni e interfacce. Molte di queste schede sono durate pochi
mesi, alcune invece hanno lasciato il segno, come le schede di sviluppo
ESP32 e ESP8266 prodotte da Espressif con costi competitivi, alte
prestazioni e grande compatibilità con il software e l’hardware pre-
esistente, sviluppato per la piattaforma di Arduino. Nel tempo è poi emersa
sempre più la necessità di avere dell’hardware connesso, cosa che per i
dispositivi Espressif è nativa, integrando WiFi e Bluetooth.

Espressif
Il produttore dei moduli ESP è l’azienda cinese Espressif Systems,
specializzata nello sviluppo di microcontrollori e moduli per la connettività
wireless. Fondata nel 2008, Espressif si è affermata come un importante
attore nel settore dell’Internet of Things (IoT), grazie alla produzione di
chip altamente integrati e soluzioni software per la connettività wireless.
Uno dei prodotti più noti di Espressif è il chip ESP8266, un
microcontrollore con funzionalità WiFi integrata che ha rivoluzionato il
settore dell’IoT. Successivamente, Espressif ha sviluppato il chip ESP32,
che ha ampliato le capacità della famiglia di prodotti ESP, offrendo non
solo connettività WiFi, ma anche Bluetooth, maggiore potenza di
elaborazione e una vasta gamma di funzionalità. I chip ESP di Espressif
sono diventati estremamente popolari tra la comunità degli sviluppatori e
sono ampiamente utilizzati in una vasta gamma di applicazioni IoT, tra cui
domotica, automazione industriale, elettronica indossabile, monitoraggio
ambientale e molto altro.

Per lo sviluppo del software sui propri prodotti, Espressif offre anche un
ambiente di sviluppo completo chiamato ESP-IDF (Espressif IoT
Development Framework), basato sul linguaggio di programmazione C.
Questo framework offre agli sviluppatori una vasta gamma di librerie,
strumenti e documentazione per semplificare lo sviluppo di applicazioni
basate su ESP anche se la compatibilità con l’ambiente di sviluppo Arduino
IDE è stata il lasciapassare per il successo, permettendo il riutilizzo delle
numerosissime librerie ed esempi già presenti. La notevole potenza di
elaborazione di questi piccoli microcontrollori permette la realizzazione di
applicazioni fino a poco tempo fa impensabili oltre all’utilizzo di tecniche e
algoritmi di intelligenza artificiale. La connettività offerta dalle schede è
sicura e veloce e le rende ideali per la realizzazione di applicazioni per
Internet degli Oggetti (IoT).

CPU, microcontrollori e SoC

Chi si avvicina al mondo dello sviluppo embedded potrebbe fare


confusione con i termini CPU, microcontrollore e SoC che sono
ovviamente tre differenti oggetti anche se esternamente potrebbero
CPU, microcontrollori e SoC

apparire molto simili! L’aspetto esteriore di questi componenti è sempre


quello di un circuito integrato, quindi un parallelepipedo di plastica nera,
più o meno grande, dotato di piedini per la connessione al circuito
stampato. I piedini a volte sono esterni e ben visibili, disposti sui lati
oppure, nei modelli più moderni, sono piccolissimi e disposti sul perimetro
dell’oggetto. Alcuni circuiti integrati molto compatti possono avere tutti i
pin posti nella parte inferiore e quindi nascosti alla vista una volta saldati.

La CPU (Central Processing Unit) è il componente principale di un


computer o di un sistema embedded. Si tratta di un circuito integrato
logico progettato per eseguire operazioni, istruzioni e funzioni più o meno
complesse. La CPU esegue programmi effettuando calcoli e operazioni
con i dati. Solitamente include unità di elaborazione, registri, memorie e
altre componenti necessarie per l’esecuzione di istruzioni. La CPU non
può funzionare da sola ma ha bisogno di un segnale di temporizzazione
esterno, delle memorie, e di tutte le periferiche hardware (ingressi, uscite,
dispositivi per le comunicazioni, per l’input e l’output). Sarà il progettista
del sistema a collegare tutto quanto è necessario. Troviamo le CPU, per
esempio, all’interno dei nostri computer portatili o da desktop.

Un microcontrollore è un dispositivo integrato che combina una CPU con


altri componenti essenziali come memoria, periferiche di input/output
(I/O), timer e interfacce di comunicazione. È progettato per applicazioni
specifiche che richiedono un controllo embedded, come l’elettronica di
consumo, l’automazione industriale, i dispositivi medicali. Rispetto a una
CPU generale, un microcontrollore è caratterizzato da dimensioni ridotte,
basso consumo energetico e costo contenuto. È in grado di eseguire
compiti di controllo e gestione delle periferiche senza la necessità di
componenti esterni particolari. Il progettista in questo caso aggiunge
solamente sensori e attuatori necessari al suo dispositivo.

Un SoC (System on a Chip) è un singolo chip che integra tutti o la


maggior parte dei componenti di un sistema embedded. Avremo una CPU,
delle memorie (RAM, ROM, EEPROM e Flash), periferiche di I/O,
CPU, microcontrollori e SoC

interfacce di comunicazione (WiFi e Bluetooth) e altri componenti


necessari per il funzionamento del sistema. Un SoC può essere progettato
per un’ampia gamma di dispositivi, come smartphone, tablet, smart TV,
dispositivi IoT e altro ancora. Spesso, un SoC è progettato per un uso
specifico e ottimizzato per le prestazioni, il consumo energetico e la
dimensione del dispositivo.

La principale differenza tra CPU, microcontrollore e SoC sta nella loro


complessità e nell’ambito di utilizzo. La CPU è il processore centrale di un
sistema di elaborazione generico come può essere un pc, mentre il
microcontrollore è un dispositivo integrato per il controllo di una scheda
che svolge una precisa e specifica funzione (per esempio scheda lavatrice).
Il SoC invece integra tutti i componenti necessari per un sistema completo,
compreso il processore (per esempio smart TV, smartphone, tablet).
Mentre la CPU e il SoC sono spesso utilizzati in dispositivi di
elaborazione generale, il microcontrollore è specificamente progettato per
applicazioni embedded.

ESP8266 vs Arduino

Una delle difficoltà maggiori che incontra chi realizza prototipi con
Arduino è quella di connetterlo in rete. I problemi sono vari:

• economici: l’hardware per una connessione Ethernet o per il WiFi


costa parecchio (rispetto al costo di una scheda Arduino);
• prestazioni: per instaurare connessioni di rete e trasmettere dati con
server o servizi avanzati servono librerie e tanta memoria il che porta a
consumare rapidamente lo spazio sulla flash memory e nella RAM;
• dimensioni: chip aggiuntivi e antenne occupano spazio e sempre più
spesso si vorrebbero realizzare circuiti minuscoli.

Nel 2014 apparì sul mercato un piccolo modulo chiamato ESP-01 (prodotto
da AI-Thinker), con una decina di pin e dal costo irrisorio capace di
collegarsi al WiFi. Il chip utilizzato da ESP-01 era un ESP8266 prodotto da
Espressif. L’oggetto, apparentemente, poteva essere preso per un modem.
Inizialmente questi oggetti avevano un firmware precaricato e
comunicavano via seriale con Arduino. Per instaurare una connessione e
scambiare dati si dovevano usare comandi AT. Farli funzionare non era
assolutamente semplice anche perché esistevano numerose varianti del
firmware di base, sempre scrupolosamente poco documentato!

Il modulo però poteva essere riprogrammato con la piattaforma Arduino e


usato come scheda a sé. Vista la straordinaria economicità e le prestazioni
strabilianti gli ESP8266 ebbero un incredibile successo e si diffusero
rapidamente tra i maker. Nacquero numerosi cloni e varianti e sul mercato
si diffuse un certo numero di schede leggermente più grandi di un Arduino
Nano, prima con chip ESP8266 e poi con moduli ESP32. Le differenze tra
un Arduino UNO e un chip ESP8266 sono notevoli e le trovate raccolte
nella Tabella 1.1.

Tabella 1.1 – Confronto tra ESP8266 e Arduino UNO (ATMEGA328).

Arduino
ESP8266
UNO

Memoria Flash – Archiviazione


32 kB 4 MB
programma

RAM 2 kB Da 80 a 160 kB

Da 80 a 160
CPU 16 MHz
MHz

Architettura 8 bit 32 bit


Figura 1.1 – La scheda ESP8266 di Espressif: da sola e montata
su un adattatore USB per facilitare la programmazione.

ESP32

Il successore di ESP8266 è ESP32, un chip che dispone di notevoli risorse


per scrivere codice complesso sfruttando 4 MB di flash memory (aggiunta
esternamente) e 2MB di RAM. L’ESP32 è dotato di WiFi e Bluetooth oltre
a numerose interfacce e funzionalità indispensabili per sviluppare ogni tipo
di progetto. ESP32 non è un microcontrollore ma un SoC, un System On
Chip, cioè un sistema completo, integrato e miniaturizzato che permette di
creare prototipi elettronici con facilità. Il chip principale è una CPU Xtensa
LX6 a 32 bit, di tipo dual core, ciò significa che abbiamo a disposizione
due CPU che possono eseguire del codice contemporaneamente. I due core
sono indipendenti e funzionano a 160 e 240 MHz. I computer moderni
utilizzano da tempo sistema multi core che permettono l’esecuzione di più
programmi contemporaneamente grazie al sistema operativo. Poter sfruttare
queste funzionalità (spesso agevolate dall’impiego di librerie specifiche) ci
aiuta a realizzare sistemi e prototipi molto reattivi.

L’ESP32 ha 34 pin GPIO che possono essere utilizzati come ingressi o


uscite come accade di solito con tutti i microcontrollori, inoltre è dotato di
varie porte seriali (UART), ingressi e uscite analogici (ADC e DAC),
interfacce SPI e I2C, generatori PWM e controllo per “pin touch”. Per la
programmazione possiamo scegliere se utilizzare il C/C++ con Arduino,
PlatformIO, l’ambiente di sviluppo di Espressif oppure anche Python!

Dall’uscita di ESP32 sono state create delle varianti:

• ESP32-S2: CPU Xtensa LX7 singolo core a 240 MHz con circa 327
kB di RAM; i pin GPIO sono 43, c’è WiFi ma non il Bluetooth;
• ESP32-C3: CPU RISC-V a 160 MHz e singolo core con 400 kB di
SRAM, WiFi e Bluetooth 5. I pin GPIO sono 22;
• ESP32-S3: CPU dual core Xtensa LX7 CPU a 240 MHz e 160 MHz,
500 kB di SRAM, 44 pin GPIO con WiFi e Bluetooth 5;
• ESP32-C6: 32 bit RISC-V CPU a 160 MHz con 500 kB di SRAM,
30 o 22 pin GPIO, WiFi e Bluetooth 5;
• ESP32-H2: 32 bit RISC-V CPU a 96 MHz con 250 kB di SRAM, 26
pin GPIO, ZigBee e Bluetooth 5.

I SoC ESP 32 devono poi essere saldati su dei circuiti stampati


aggiungendo le risorse necessarie per il loro funzionamento come per
esempio un modulo per la flash memory, antenne per WiFi e Bluetooth,
circuiti di alimentazione e clock. Espressif produce anche moduli SMD
simili a piccoli francobolli, pronti all’uso. I moduli includono già memoria
flash, antenne e circuiti di alimentazione e secondari, devono solo essere
saldati su un circuito stampato. Uno dei moduli più noti è l’ESP32-
WROOM utilizzato nelle schede di sviluppo DevKit di Espressif.

Per maggiori informazioni potete consultare il sito di Espressif dove trovate


elenco e caretteristiche dei vari prodotti:

• SoC: [Link]
• moduli: [Link]
• DevKit: [Link]

Figura 1.2 – Un modulo ESP32-WROOM.

NodeMCU
NodeMCU

Molte persone chiamano le schede di sviluppo con ESP32 anche con il


termine “NodeMCU”. Il termine però si riferisce a una piattaforma open
source per programmare le schede con ESP8266 con il linguaggio di
programmazione Lua. NodeMCU è un firmware che si carica sulla scheda
e che permette l’utilizzo del linguaggio Lua, giudicato più pulito e
semplice da capire. Lua interpreta dei listati caricati sulla scheda e grazie
alle sue funzioni ad alto livello (rispetto al C/C++) facilita la realizzazione
di progetti connessi e per l’IoT.

La prima versione di NodeMCU esce verso la fine del 2014 poco dopo
l’uscita del modulo ESP8266. Alcuni mesi dopo escono delle versioni per
dei moduli hardware chiamati “DevKit”, ancora oggi molto diffusi, da qui
il legame tra le schede di sviluppo e il nome NodeMCU. Nel tempo sono
state aggiunte varie librerie tra cui quella per il protocollo MQTT ritenuta
importante per lo sviluppo di applicazioni IoT. I fondatori di NodeMCU
abbandonarono il progetto nel 2015 lasciandolo alla comunità di
sviluppatori. Purtroppo gli sviluppi e gli aggiornamenti vanno a rilento e il
destino di NodeMCU è ormai segnato.

Schede di sviluppo
Per realizzare prototipi rapidi con ESP32 la soluzione migliore è quella di
acquistare una scheda pronta all’uso anche chiamata kit di sviluppo.
Abbiamo numerose possibilità e ora che abbiamo le idee più chiare sui vari
modelli di ESP32 sapremo scegliere la soluzione più adatta. Le varie
versioni esistenti differiscono per il formato e numero di pin e per il
modulo presente. Per gli esercizi di questo libro ho utilizzato una scheda
con modulo ESP32-WROOM. Altre schede simili possono andare bene,
eventualmente dovrete verificare l’assegnazione dei pin fatta dal
produttore.
Vediamo qui di seguito un elenco (non completo) di schede di
prototipazione.

ESP32 DevKit

Sul sito ufficiale di Espressif trovate varie schede di sviluppo pronte


all’uso. Ne esistono di vari tipi per poter provare i diversi moduli. Una
delle schede più utilizzate e anche “clonate legalmente” da vari produttori è
la ESP32 DevKitC che impiega un modulo ESP32-WROOM. Tutti i pin
della scheda sono accessibili e il prezzo è molto contenuto.

Gli schemi di questi kit di sviluppo sono sempre open source così che
chiunque possa utilizzarli come base per creare i propri progetti includendo
i moduli ESP32. Le caratteristiche degli altri modelli si possono consultare
sul sito di Espressif ([Link]

Figura 1.3 – Disegno raffigurante la scheda di sviluppo DevKitC


di Espressif.

NodeMCU-32S

La NodeMCU-32S è una popolare scheda di sviluppo basata su ESP32 che


utilizza il modulo ESP32-WROOM. È molto simile alla DevKitC ma la si
riconosce dall’aspetto più tozzo e dal fatto che la delicata antenna del
modulo ESP32 non sporge dal bordo della scheda. È nata per supportare il
progetto NodeMCU, che inizialmente impiegava schede con ESP8266.

HUZZAH32

HUZZAH32 è la scheda con modulo ESP32-WROOM prodotta da


Adafruit. Il kit di sviluppo è equipaggiato con una memoria flash da 4 MB
e anche un caricabatterie LiPo integrato per poter creare immediatamente
dispositivi alimentati a batteria. Nel sito Adafruit trovate anche numerose
schede shield aggiuntive per aggiungere funzionalità senza neppure
accendere il saldatore.
ESP32-PICO-KIT

ESP32-PICO-KIT è un’altra scheda ufficiale di Espressif che impiega il


modulo ESP32-PICO-D4. La scheda ha dimensioni più contenute e può
stare comodamente su una breadboard. Tutti i pin sono accessibili ed è una
ottima alternativa alla DevKitC.

TTGO T-Display ESP32

TTGO T-Display ESP32 è un kit di prototipazione basato su ESP32 con un


display TFT a colori integrato. È l’ideale per creare etichette intelligenti o
piccoli display che possono essere controllati da remoto via WiFi o
Bluetooth.

Arduino Nano 32 e R4 WiFi

Nell’estate del 2023 anche Arduino ha abbracciato i moduli ESP32 creando


le schede Arduino Nano 32 e Arduino R4 WiFi. La prima utilizza un
modulo Ublox con all’interno un chip di Espressif, mentre la seconda
monta un modulo di Espressif affiancato a un microcontrollore Renesas. Il
microcontrollore e il modulo ESP32 comunicano attraverso una libreria
“bridge” che rende trasparenti tutte le operazioni svolte sul modulo ESP32.
La scheda Arduino Nano 32 è invece pienamente compatibile con le
ESP32.
2

Hello ESP!

In questa sezione troverai le risposte ai più


comuni problemi d’installazione e
configurazione per la scheda ESP32.
Utilizzeremo Arduino IDE e PlatformIO
per Visual Studio Code.

Ogni nostro progetto per funzionare ha bisogno di un elenco di istruzioni,


cioè del codice. Il codice può essere scritto in vari linguaggi anche, se
solitamente, per il firmware si utilizza il C/C++ oppure direttamente
l’assembler. Recentemente, con schede più prestanti, è possibile anche
caricare un interprete Python. Il codice, scritto in un linguaggio di
programmazione comprensibile agli umani, deve poi essere convertito in
codice comprensibile alla CPU (il codice macchina), cioè una sequenza di
byte. Esistono vari programmi che possiamo utilizzare per la scrittura del
codice, la compilazione (cioè la traduzione in codice macchina) e il
trasferimento sulla scheda. Espressif mette a disposizione il suo ambiente di
programmazione ESP-IDF ma la soluzione più semplice è quella di adottare
Arduino IDE, il software di programmazione delle schede Arduino.
Arduino IDE è meno sofisticato di ESP-IDF ed è anche più utilizzato.
Troveremo a disposizione maggiori aiuti in caso di difficoltà, inoltre le
librerie sviluppate per ESP32 dalla comunità sono numerose e già
disponibili all’interno dell’ambiente di programmazione. In alternativa ad
Arduino IDE è possibile utilizzare Visual Studio Code con l’aggiunta del
plugin PlatformIO.
Per iniziare procuratevi una scheda ESP32 (io utilizzo la DevKitC
acquistata su Amazon) e un cavo per collegarla al vostro computer. Il cavo
dipende dal tipo di modulo che avete acquistato. Su queste schede troviamo
di solito o connettori micro USB o USB di tipo C (vedi Figura 2.1).
Se la scheda è nuova infilate il cavo e fate attenzione che il connettore
entri bene nell’alloggiamento. Quando utilizzate i cavi micro USB, fate
molta attenzione perché, anche se la presa micro USB è saldata sul circuito
stampato, è molto facile spezzarla con una piccola flessione. Se sulla scheda
vedete accendersi qualche LED significa che si accende e il cavo è inserito
correttamente. Potete scollegarla e procedere con l’installazione del
software.

Figura 2.1 Cavi USB per collegare la scheda con ESP32 al vostro computer. A seconda del modello
potrebbe servirvi un cavo micro USB (1) o un USB C (2). Nel riquadro (3) potete vedere un dettaglio
dei due connettori.
Installare Arduino IDE
Scaricate Arduino IDE dal sito [Link] e
seguite le istruzioni fornite per il vostro sistema (Windows, Mac o Linux).
Arduino IDE è un programma che dovete installare sul vostro computer per
poter programmare le schede ESP32. Nel momento in cui scrivo la versione
disponibile è la 2.1.1. Scegliete la versione compatibile con il vostro
sistema e scaricatela in una cartella.

Windows
La procedura di installazione di Arduino è in genere abbastanza semplice.
Visitate la pagina per il download del software sul sito di Arduino
all’indirizzo: [Link] Scegliete la versione
adatta al vostro sistema (potrebbe essere “Windows Win 10 and newer, 64
bits”) (Figura 2.2). Prima del download apparirà una pagina in cui si chiede
una donazione per lo sviluppo del software (Figura 2.3). Scegliete l’importo
oppure cliccate semplicemente “just download” per procedere e salvare il
file in una cartella. Al termine del download cliccate sul file per eseguirlo e
installarlo nel vostro computer. Potete far riferimento alle istruzioni
pubblicate sul sito di Arduino ([Link]
Figura 2.2 Pagina per il download del software Arduino IDE.
Figura 2.3 Lasciate una donazione per il software o semplicemente procedete con il download (just
download).

Se non avete mai collegato una scheda ESP32 al vostro computer, potrebbe
essere necessario installare un driver per la porta seriale. Vi servono i driver
per il chip CP2102 (quello che troviamo di solito sulle schede DevKitC,
comunque verificate di che tipo di chip seriale dispone la vostra scheda),
che si possono scaricare dal sito di Silicon Labs
([Link]
tab=downloads). Scaricate il pacchetto idoneo e installatelo. Per verificare
se il driver è già presente nel vostro computer o se si è installato
correttamente aprite “Gestione dispositivi” e controllate se sotto la voce
“Porte COM e LPT” appare il driver per CP2102. Per aprire “Gestione
dispositivi”, accedete al “Pannello di controllo”, quindi “Sistema e
sicurezza” e infine “Sistema”. Nella finestra “Sistema”, aprite “Gestione
dispositivi”. Se il driver è installato correttamente, di fianco alla voce
relativa dovreste vedere anche il nome della porta seriale associata (in
Figura 2.4 è la COM4).

Figura 2.4 Aprendo “Gestione dispositivi” dovreste poter vedere la porta seriale associata al chip
CP2102: nel mio caso la porta è la COM4.

Ora potete collegare la vostra scheda ESP32 al computer: la scheda


dovrebbe accendersi (si illumina un LED1). Avviate Arduino cliccando
sull’icona apparsa sul Desktop o nel menu di avvio. La prima volta che si
utilizza una scheda con hardware Espressif è necessario installare il
framework di sviluppo. Con il nuovo IDE di Arduino la procedura è molto
semplice:
1. nella barra laterale di destra della finestra di Arduino cliccate sulla
seconda icona (a forma di Arduino) per accedere al “Boards Manager”
(Figura 2.5);
2. nella casella di ricerca del “Boards Manager” digitiamo “ESP32” e
attendiamo il caricamento dei risultati;
3. clicchiamo sul pulsante “Install” del riquadro “esp32 by Espressif”. Se
volete utilizzare una delle nuove schede con ESP32 prodotte da Arduino
dovrete scegliere la voce “Arduino ESP32 Boards by Arduino”.
Ovviamente potete installare contemporaneamente tutti i framework di
sviluppo che desiderate;
4. attendiamo il download dei pacchetti e l’installazione dei tool di
sviluppo;
5. è consigliabile chiudere e riaprire Arduino IDE.

Figura 2.5 Con il “Boards Manager” possiamo facilmente installare il framework di sviluppo per
ESP32.
Mac OS X
Per OS X la procedura d’installazione è ancora più semplice e potete far
riferimento alle istruzioni pubblicate sul sito di Arduino
([Link] Scaricate il file di installazione
come descritto nella sezione per Windows, quindi dovreste trovare un file
compresso (.zip) nella cartella “Download” o “Desktop” del vostro mac.
Scompattate il file con un doppio clic. Vedrete apparire un file [Link]:
trascinatelo nella cartella “Applicazioni” per installarlo. Avviate Arduino
IDE con un doppio clic sull’icona che trovate nella cartella Applicazioni.
In genere non c’è bisogno di fare altro e non dovrete installare nessun
tipo di driver, tuttavia se per qualche motivo non vedete apparire in Arduino
IDE una porta seriale tipo “/dev/[Link]-0001” potete provare a
installare i driver CP2102 scaricandoli da:
[Link]
tab=downloads.

GNU/Linux
Se utilizzate una distribuzione moderna di Linux (per esempio Ubuntu o
simili) troverete “Arduino IDE” disponibile nel programma per
l’installazione dei pacchetti. L’installazione è in genere molto semplice
(Figura 2.6) e si risolve con pochi clic.
Figura 2.6 Installazione di Arduino IDE dal repository di Xubuntu.

Potete installare Arduino IDE anche da terminale, cercandolo nei


“repository” di sistema (“universe”) e utilizzando il comando apt-get per
individuare il pacchetto e installarlo:
sudo apt-get update && sudo apt-get install arduino arduino-core

Dovrete inserire la vostra password perché state utilizzando il comando


“sudo”. Al termine dell’installazione troverete Arduino IDE tra i
programmi installati.
Purtroppo la versione disponibile nei repository non è sempre molto
aggiornata e quindi conviene scaricare direttamente il pacchetto da
installare dal sito di Arduino:
1. scaricate l’ultima versione del software da
[Link]
ide_latest_Linux_64bit.AppImage;
2. cercate il file che avete scaricato con un File Manager;
3. il file è l’applicazione e quindi spostatelo in una cartella adatta;
4. assicuratevi che il file con estensione AppImage sia eseguibile cliccando
sulla sua icona con il tasto destro del mouse;
5. scegliete “proprietà” dalla finestra di dialogo e poi “Permessi”;
6. mettete una spunta sulla voce “Allow executing file as program box”
(“permetti l’esecuzione del file”);
7. fate doppio clic sul file arduino-ide_latest_Linux_64bit.AppImage per
eseguirlo.

Figura 2.7 – Impostazione dei permessi di esecuzione del file di Arduino IDE.
Se Arduino IDE non parte e appare l’errore “dlopen(): error loading
[Link].2” o qualche messaggio relative a “FUSE” potrebbero mancare
delle dipendenze che però si possono installare con pochi passaggi:
1. aprite un terminale;
2. digitate il comando:
sudo apt-get -y install libfuse2

3. al termine dell’installazione dei pacchetti aggiuntivi chiudete il


terminale e provate ad avviare di nuovo Arduino IDE.

In alcuni casi potrebbe anche essere necessario installare Java. Installate il


pacchetto open-jdk utilizzando synaptic o da terminale.
Se ci dovessero essere problemi con le porte seriali verificate che il
vostro utente sia nel gruppo “dialout”, così che abbia i permessi per
utilizzare le porte seriali. Se così non fosse (o in caso di dubbio) aggiungete
il gruppo con:
sudo usermod -aG dialout paolo

Sostituite “paolo” con il nome del vostro utente.

Configurazione del progetto e caricamento del


programma
Ora possiamo finalmente iniziare a programmare la nostra scheda ESP32.
Per poter caricare uno sketch dovrete prima configurare il progetto
assegnando la scheda e la porta che intendete utilizzare. L’assegnazione
dell’hardware, in particolare della porta seriale, potrebbe essere necessaria
ogni volta che si riapre il progetto. Possiamo assegnare scheda e porta
seriale aprendo il menu a tendina posto nella toolbar superiore (Figura 2.8).
Se la scheda e la porta sono già assegnate o sono state riconosciute,
dovreste vederle apparire tra le voci del menu, altrimenti dovrete cliccare
sulla voce “Select other board and port” per effettuare la vostra selezione.
Apparirà una finestra in cui potremo scegliere la scheda nel pannello di
sinistra e la porta seriale in quello di destra. Le schede utilizzabili con
Arduino IDE sono numerose, quindi, nel campo per la ricerca inserite parte
del nome della vostra scheda per esempio “DOIT” o “DevKit” (se utilizzate
una DevKitC) per limitare le scelte possibili. Scegliete la scheda corretta e
la porta seriale abbinata e date “OK” (Figura 2.9).

Figura 2.8 – Per assegnare scheda e porta seriale utilizzeremo il menu a tendina posto nella toolbar
superiore.
Figura 2.9 – Selezione della scheda e della porta seriale.

Quando scheda e porta seriale sono selezionate e abbinate, le vedrete


apparire nel menu a tendina della toolbar superiore (Figura 2.10).
Figura 2.10 – La scheda attualmente in uso è visualizzata nel menu a tendina superiore.

Per caricare uno sketch sulla scheda ora non dovrete fare altro che cliccare
prima sul tasto “Verify”, posto all’inizio della toolbar superiore, che si
occuperà della compilazione del vostro codice in una serie di byte
comprensibili al microcontrollore (Figura 2.11).
La compilazione del codice può richiedere anche qualche decina di
secondi: aspettate pazientemente!
Nella parte inferiore della finestra, se tutto è andato bene, dovreste
leggere solo delle scritte bianche. Nel caso le scritte fossero di colore
arancione significa che ci sono stati degli errori: leggete quanto è riportato
per individuare e risolvere il problema.
Figura 2.11 – Premete il tasto verify per compilare il codice.

Al termine della compilazione possiamo premere il tasto per l’upload del


codice sulla scheda. Prima di procedere verificate che la scheda sia
collegata al computer con un cavo USB adatto. Anche in questo caso
potrebbe essere necessario aspettare un po’ perché il codice sarà di nuovo
ricompilato da capo e poi trasferito.
Figura 2.12 – Premendo il tasto di upload trasferiremo il codice compilato sulla scheda ESP32.

Figura 2.13 – Durante il trasferimento del codice, nella console, possiamo leggere lo stato di
avanzamento.
Questa è la procedura da seguire per trasferire il codice da Arduino IDE alla
scheda. Se incappate in errori verificate che la scheda sia stata
correttamente selezionata, assieme alla porta seriale e controllate il cavo
USB. A volte un cavo di bassa qualità può compromettere la procedura di
caricamento. Su alcune schede più datate potrebbero esserci problemi di
caricamento. A bordo del modulo ESP32 c’è un software residente
chiamato bootloader che in alcune situazioni particolari (per esempio dopo
un reset) sta in ascolto sulla seriale per capire se qualcuno sta cercando di
trasferire del codice sulla scheda. A volte il bootloader non parte
correttamente. Per forzare il caricamento del codice da parte del bootloader
potete provare a premere i due pulsanti (EN o Reset e Boot) che trovate
sulla scheda in questa sequenza:
1. premete boot e tenetelo premuto;
2. premete reset;
3. rilasciate reset;
4. rilasciate boot;
5. provate a effettuare l’upload del codice.

A volte potrebbe essere necessario intervenire sull’hardware della scheda


(solo su quelle più datate) aggiungendo un condensatore elettrolitico da 10
µF e 16 V tra il piedino EN della scheda e GND (o G). Il terminale positivo
del condensatore va su EN e il negativo su GND.

Installare Visual Studio Code con PlatformIO


In alternativa ad Arduino IDE possiamo utilizzare anche Visual Studio
Code con l’aggiunta del plugin PlatformIO. Visual Studio Code è un editor
di codice sorgente sviluppato da Microsoft per Windows, Linux e macOS.
Include il supporto per debugging, un controllo per Git integrato, syntax
highlighting, e refactoring del codice. Molti sviluppatori di software
embedded utilizzano questa piattaforma perché può essere utilizzata con
migliaia di schede hardware e perché offre una gestione dei progetti più
professionale includendo svariati strumenti per lo sviluppo e l’integrazione
come Git per tenere traccia delle modifiche sui sorgenti. Per iniziare dovete
scaricare Visual Studio Code dal sito [Link]
Figura 2.14 – Home page del programma Visual Studio Code.

Nella pagina di download scegliete la versione adatta al vostro sistema


operativo. Scaricate il pacchetto di installazione ed eseguitelo.
Figura 2.15 – Pagina di download del programma Visual Studio Code.
Figura 2.16 –Visual Studio Code.

PlatformIO
Una volta installato Visual Studio Code dovrete aggiungere l’estensione
PlatformIO: un ambiente di sviluppo integrato open-source per lo sviluppo
di software embedded basato su piattaforme come Arduino, ESP8266,
ESP32 e molte altre. PlatformIO offre una vasta gamma di funzionalità e
strumenti per semplificare lo sviluppo di progetti embedded, compresa la
gestione delle librerie, la compilazione del codice, il caricamento sul
dispositivo e il debug.

Figura 2.17 – Home page del plugin PlatformIO per Visual Studio Code.

Il plugin PlatformIO IDE è un’estensione che aggiunge il supporto


completo per lo sviluppo su microcontrollori e dispositivi embedded a
Visual Studio Code, consentendo agli sviluppatori di programmare,
compilare, caricare e debuggare il codice direttamente dall’ambiente di
sviluppo di Visual Studio Code. Con PlatformIO potrete creare progetti
indipendenti tra di loro e con una gestione dedicata delle librerie: ogni
progetto può utilizzare una versione diversa e specifica di librerie che non
sono “globali” come in Arduino IDE. La compilazione e il caricamento del
codice avvengono direttamente, utilizzando strumenti integrati
nell’ambiente di sviluppo. Il debug è integrato per i dispositivi che offrono
questa funzionalità senza hardware aggiuntivo. Maggiori informazioni su
PlatformIO si possono ottenere visitando il sito ufficiale:
[Link] e la pagina dedicata al plugin per Visual Studio Code
([Link]
L’installazione del plugin è molto semplice: aprite Visual Studio Code e
aprite il gestore delle estensioni cliccando sull’icona che trovate nella barra
laterale di sinistra (riporta alcuni quadratini accostati). La vista per la
gestione delle estensioni presenta una barra laterale per la ricerca dei plugin
(Figura 2.18). Nella parte superiore della barra laterale possiamo scrivere il
nome del plugin desiderato: “Platformio” e attendere che appaia nell’area
sottostante. Nel riquadro del plugin dovremmo vedere un pulsante con la
scritta “install”: premiamolo e attendiamo il tempo necessario. Potrebbe
essere necessario riavviare il programma prima di poterlo utilizzare.

Figura 2.18 – Home page del plugin PlatformIO per Visual Studio Code.
Se il plugin si è installato correttamente dovremmo vedere nella toolbar
laterale (nera) di Visual Studio Code l’icona di PlatformIO (la testa della
formica). Cliccando sulla testa della formica attiveremo il plugin che aprirà
una seconda colonna sul lato sinistro della finestra. Cliccando la voce PIO
Home > Open apriremo la home page del plugin da cui potremo creare e
gestire progetti e librerie (Figura 2.19).

Figura 2.19 – Home page del plugin PlatformIO per Visual Studio Code.

Creazione di un progetto
Dalla pagina principale di PlatformIO in Visual Studio Code premete “+
New Project” oppure selezionate la voce sulla toolbar secondaria “Projects”
e quindi “+ New Project”.
Figura 2.20 – Creazione di un nuovo progetto.

Passerete dalla vista Projects anche per riaprire un progetto esistente o su


cui avete lavorato in precedenza.
Una finestra di dialogo vi chiederà il nome del progetto, la scheda da
utilizzare e il framework di sviluppo. Come scheda indicate il vostro
modello (per esempio digitate “DevKit”) o una parte del nome e poi
scegliete tra i risultati che appaiono. Il framework da utilizzare è quello di
Arduino.
La prima volta che creerete un progetto per una nuova piattaforma sarà
necessario attendere qualche minuto perché Visual Studio dovrà scaricare
tutto il software necessario per lo sviluppo. I file del progetto saranno poi
organizzati in una struttura ad albero che appare nella sezione posta a
sinistra della finestra principale.
Figura 2.21 – Organizzazione del progetto e file principale [Link].

Il file principale del progetto si chiama [Link] e deve includere sempre


un riferimento alla libreria di Arduino.h:
#include <Arduino.h>

Troveremo poi loop() e setup() in cui inserire il nostro codice.


Un altro file molto importante è [Link] che contiene le definizioni
e le impostazioni del progetto. In questo file possiamo impostare le librerie
o modificare a mano piattaforma e target anche se di solito è modificato
automaticamente da Visual Studio Code. Qui di seguito riporto un esempio
di file di configurazione con impostate alcune librerie e la configurazione
del terminale seriale (analogo al Serial Monitor di Arduino):
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps =
Wire
adafruit/Adafruit BME280 Library@^2.2.2
adafruit/Adafruit Unified Sensor@^1.1.9
adafruit/Ethernet2@^1.0.4
bblanchon/ArduinoJson@^6.20.1

I comandi per la compilazione e l’upload del progetto sono disposti sulla


barra di stato inferiore (blu). A partire dall’angolo inferiore sinistro della
finestra di Visual Studio Code, nella sua status bar troverete:
• l’icona di una casa: link alla home di PlatformIO;
• un segno di spunta: compilazione del codice;
• una freccia →: upload;
• cestino della spazzatura: clean del progetto;
• provetta: per l’esecuzione delle batterie di test automatici;
• spina: terminale seriale.

Per caricare un progetto dovrete semplicemente:


1. scrivere il codice dentro a [Link];
2. compilare premendo l’icona a forma di “V”;
3. attendere il termine della compilazione;
4. premere l’icona a forma di freccia (non dovrete specificare la porta
seriale a cui è connessa la vostra scheda perché ci penserà in automatico
PlatformIO).

Ecco un link a un video esplicativo sul mio canale YouTube: “Utilizzare


PlatformIO con ESP32 - Video 704” [Link]

Simulare Arduino con Wokwi


Se non disponete di un dispositivo hardware ESP32 potete comunque
assemblare e simulare un circuito elettronico virtualmente, online,
utilizzando Wokwi ([Link] una piattaforma che fornisce un
simulatore di circuiti elettronici e un ambiente di sviluppo per varie schede
a microcontrollore. Consente agli utenti di progettare, simulare e testare
circuiti elettronici, compresi microcontrollori, sensori, attuatori e altri
componenti, tutto attraverso un’interfaccia web interattiva.
Wokwi consente agli utenti di creare circuiti elettronici usando una vasta
gamma di componenti elettronici e piattaforme di sviluppo (Arduino,
ESP32, Raspberry…). Questi componenti possono essere collegati tra loro
con strumenti grafici molto intuitivi per creare circuiti complessi. Wokwi
offre sia componenti elettronici comuni (pulsanti, LED, resistenze,
transistor) sia sensori e attuatori complessi (display I2C, motori,
servomotori…). I progetti creati possono essere facilmente condivisi con
altre persone attraverso link pubblici per facilitare la collaborazione e la
condivisione delle idee. Per utilizzare Wokwi è necessario registrarsi. Il link
per registrarsi appare nell’angolo superiore destro della home page. Cliccate
su “Sign up” per creare un account. Una volta registrati, tramite il menu
visibile in alto a destra potrete creare e gestire i vostri progetti o accedere
alla documentazione in linea.

Figura 2.22 – La home page di Wokwi.

Scegliendo la voce “My projects” dal menu potrete accedere alla lista dei
vostri progetti dove troverete anche un link “+ NEW PROJECT” che
cliccheremo per creare un nuovo progetto.
Prima di tutto dovrete scegliere che tipo di microcontrollore o
piattaforma vorrete utilizzare. Le voci disponibili sono parecchie ma noi
siamo interessati a creare progetti con “ESP32”.
Nella pagina “My projects” troverete elencati tutti i vostri lavori, qui
potrete riaprirli per modificarli o eseguirli.

Figura 2.23 – La vista progetti di Wokwi.

Una volta creato o riaperto un progetto apparirà la schermata principale di


Wokwi divisa in due aree principali: quella di sinistra in cui scriveremo il
codice e quella di destra per la composizione dei circuiti.
Inizialmente (a meno di aver selezionato un progetto “blank”) nella parte
di destra vedremo solamente la scheda che abbiamo selezionato in fase di
creazione (Figura 2.24).
Prima di procedere è consigliabile salvare il progetto assegnandogli un
nome significativo. La finestra dialog chiederà anche se il progetto dovrà
essere pubblico o privato. Se vi siete registrati con il profilo base potrete
solo creare progetti a visibilità pubblica (Figura 2.25).
Figura 2.24 – La schermata principale di Wokwi con l’editor del codice e l’area di composizione dei
circuiti.
Figura 2.25 – Salvataggio del progetto.

Per comporre il circuito dovremo aggiungere dei componenti elettronici


cliccando sul pulsante circolare con il segno “+”. Un menu a tendina si
srotolerà presentando vari componenti elettronici.
Scegliamo un LED e una resistenza. Il resistore avrà un valore di 1 kΩ,
selezionandolo con un clic potremo impostare il suo valore a 100 Ω, valore
più consono per le uscite a 3,3 V della ESP32.
Figura 2.26 – Aggiunta di componenti elettronici.

I componenti si possono facilmente trascinare e spostare nell’area di lavoro.


Possiamo anche “specchiarli” per facilitare i cablaggi. I “fili” si tirano
cliccando sul punto di partenza (per esempio pin D4) e poi muovendosi fino
al terminale che vogliamo collegare.
Componiamo un semplice circuito con il LED e la resistenza da 100 Ω,
come illustrato in Figura 2.27.
Figura 2.27 – Collegamento di un LED e un resistore alla scheda ESP32.

Ultimato il circuito possiamo passare alla scrittura del codice. Per far
lampeggiare un LED collegato sul pin D4 della scheda ESP32 basterà
ricopiare il seguente codice che ci sarà più chiaro nel prossimo capitolo:
void setup() {
pinMode(4, OUTPUT);
}

void loop() {
digitalWrite(4, HIGH);
delay(300); // this speeds up the simulation
digitalWrite(4, LOW);
delay(300);
}

Il codice imposta il pin D4 (a cui facciamo riferimento semplicemente con


il numero 4) come un’uscita, quindi nella “sezione” loop() il pin è
continuamente acceso e spento a intervalli di 300 ms. Premendo il tasto
circolare di colore verde con la freccia bianca possiamo simulare il
progetto: dovremmo vedere il LED lampeggiare.

Figura 2.28 – Avviando la simulazione il LED collegato a ESP32 inizierà a lampeggiare.

Potete condividere il vostro lavoro premendo il tasto “share” posto sulla


toolbar superiore: sarà generato un link univoco che potrete condividere con
chi vorrete.

1
Non tutte le schede ESP32 sono dotate di un LED di accensione!
3

Muovere i primi passi

Imparare a conoscere le funzionalità di


base delle schede ESP32. Utilizzare i pin
per rilevare e produrre segnali, accendere
LED e leggere pulsanti. Apprendere le
tecniche fondamentali di programmazione.

La scheda di riferimento
Le schede di sviluppo che utilizzano i chip ESP32 sono numerose. Per
avere un riferimento comune adotteremo uno dei modelli più diffusi ed
economici, l’ESP32-DevKitC.
Questa scheda potrebbe assomigliare a un Arduino Nano anche se con
dimensioni leggermente maggiori, il che ci costringe purtroppo a qualche
espediente per poterla sistemare su una breadboard, infatti la scheda è
troppo larga ed è necessario utilizzare due tavolette di prototipazione.
Vedremo tra poco come possiamo risolvere questo piccolo problema e
riuscire a lavorare comodamente con il nostro kit di sviluppo.
Figura 3.1 – ESP32 DevKitC.

Di seguito un elenco con le caratteristiche principali del modulo ESP32:


• processore Tensilica Xtensa Dual-Core 32-bit LX6;
• clock a 240 MHz;
• 520 kB di RAM per dati e istruzioni;
• 8 kB di RAM veloce per la modalità sleep per il processore principale;
• 8 kB di RAM per il co-processore in modalità sleep;
• connettività WiFi a 150 Mbps;
• Bluetooth classico e BLE (Bluetooth Low Energy);
• Flash incorporata da 4 MB;
• 48 pin GPIO (non tutti esposti!);
• ADC;
• DACs;
• I²C;
• UART;
• SPI;
• I²S (Integrated Interchip Sound);
• Hardware specifico per AES e SSL/TLS.
Le breadboard
Per realizzare rapidamente prototipi elettronici senza utilizzare stagno e
saldatore si utilizzano le breadboard o basette sperimentali. Una
breadboard è una piccola tavoletta forata dotata di contatti elettrici.
Inserendo i terminali dei componenti nei fori possiamo creare dei
circuiti molto rapidamente e senza dover accendere il saldatore. Il nome
breadboard deriva dall’analogia con un tipo di tagliere per il pane
formato da un vassoio sopra di cui è posta una griglia con dei fori
rettangolari. Quando si taglia il pane, le briciole si raccolgono nel
vassoio sottostante passando per i fori.
Le breadboard sono disponibili in vari formati (intera, mezza e mini) ma
presentano tutte caratteristiche comuni nella disposizione dei contatti.
La distanza tra i fori è di 2,54 mm, una misura particolare pari a un
decimo di pollice, il passo standard utilizzato nei circuiti integrati e
nella maggior parte dei componenti elettronici. Le breadboard hanno
anche un solco centrale che permette l’inserimento di circuiti integrati. I
contatti sono disposti in gruppi da cinque (detti strip), in file parallele
disposte lungo i lati del solco centrale. I fori di ogni riga sono in
contatto tra di loro così che per collegare due componenti è sufficiente
inserire uno dei loro terminali nella stessa fila. Richiamando l’analogia
con l’acqua, in cui i fili elettrici sono come tubi idraulici, i cinque fori di
una riga della breadboard sono paragonabili a un tubo di raccordo con
cinque vie: introducendo l’acqua da un foro potete prelevarla da uno dei
rimanenti.
Molte breadboard sono dotate di lunghe file di pin laterali (chiamati rail
o strip) molto comodi per distribuire le tensioni di alimentazione nelle
varie parti del circuito.
Le breadboard a formato intero hanno solitamente 50 o 60 righe di
contatti mentre i modelli più piccoli circa 30 (mezza breadboard) o
anche meno (una ventina per le mini breadboard).
Per realizzare i contatti è consigliabile utilizzare speciali cavetti
flessibili, “intestati” con un piccolo terminale rigido, adatto a essere
inserito nei fori della breadboard. Esistono anche jumper rigidi a forma
di “C”, di varie misure, molto utili per ottenere circuiti più compatti.
Quando il circuito non è più necessario possiamo riutilizzare la tavoletta
rimuovendo componenti e jumper.
Le breadboard
Le breadboard non sono adatte per circuiti “critici”, in radiofrequenza o
che lavorano a frequenze troppo elevate per via delle capacità parassite
che presentano e anche per la loro intrinseca instabilità dovuta ai
contatti sempre precari offerti dai jumper.

Figura 3.2 – BreadBoard full-size.

Come sistemare la ESP32-DevKitC su due


breadboard
I moduli ESP32 purtroppo hanno dimensioni tali che il loro utilizzo su
breadboard è difficoltoso: sono troppo larghi e su un lato non è possibile
inserire nessun jumper rendendo la scheda inutilizzabile (Figura 3.3).
Per ovviare a questo problema utilizzo due breadboard che separo nelle
loro parti per poi riassemblarle in modo da poter sistemare l’ESP32
DevKitC al centro disponendo di maggiore spazio lungo i lati. Le
breadboard sono infatti modulari (se della stessa marca) e possono essere
divise e ricomposte a piacere. Lasciando nella parte centrale uno dei “rail”
laterali possiamo ricavare una zona centrale più ampia, come visibile in
Figura 3.4 e Figura 3.5.

Figura 3.3 – Il DevKitC è troppo largo per una breadboard unica e i jumper possono essere inseriti
solo su uno dei due lati.
Figura 3.4 – Le breadboard sono modulari e possono essere divise e ricomposte a piacere.
Figura 3.5 – Ricomponendo due breadboard possiamo creare una soluzione per alloggiare
comodamente il DevKitC di ESP32.

Come alimentare un modulo ESP32


Come tutti i dispositivi elettronici, ESP32 deve essere alimentato per poter
funzionare. Durante la fase di sviluppo non ci preoccupiamo molto di come
fornire energia alla scheda perché, connettendola con un cavo USB, sarà il
nostro stesso computer ad alimentarla: come noto all’interno di un cavo
USB è presente anche una tensione di alimentazione a 5 V. Una volta
terminata la fase di sviluppo il nostro progetto dovrà essere autonomo e
quindi sarebbe abbastanza problematico alimentarlo con un pc. La
soluzione più rapida è quella di utilizzare un alimentatore per cellulari con
presa USB oppure, in mancanza di tensione di rete, un power bank USB per
cellulari. Tenete presente che la scheda quando è accesa con WiFi attivo
può consumare anche 100 mA: una quantità di corrente in apparenza
piccola ma in grado di scaricare una batteria in poco tempo. Potete fare un
rapido calcolo rilevando sulla batteria o power bank il valore di capacità di
corrente per poi dividerlo per l’assorbimento della scheda. Se il power bank
è da 5000 mAh il calcolo da fare è il seguente:

durata in ore = capacità power bank / consumo = 5000 mAh / 100 mAh =
50 h

In realtà la scheda si spegnerà ben prima di due giorni perché questi calcoli
sono indicativi e perché la batteria smetterà di erogare potenza al di sotto di
una certa soglia. Possiamo quindi supporre che il nostro circuito potrebbe
funzionare in modo continuativo per poco più di una giornata. Per
prolungare la durata della batteria fino a settimane o mesi di funzionamento
dovremo utilizzare tecniche speciali che “addormenteranno” ESP32
risparmiando energia.
ESP32 presenta un pin 5 V a cui possiamo collegare un alimentatore da 5
V. Su molte implementazioni e kit ESP32 il pin 5 V è collegato a uno
stabilizzatore di tensione interno e quindi è possibile applicare anche più di
5 V ma controllate sempre lo schema elettrico del vostro prodotto prima di
collegare qualcosa al pin, per non danneggiarlo, in quanto, se non sono state
prese opportune precauzioni, i 5 V applicati esternamente potrebbero
andare in conflitto anche con l’alimentazione USB e collegando
contemporaneamente un pc e l’alimentazione esterna potremmo rischiare di
danneggiare non solo il modulo ESP32 ma anche le porte USB del pc.
Il modulo ESP32 per funzionare richiede 3,3 V che sono ricavati da un
regolatore interno a cui arriva la tensione dalla porta USB o dal pin
denominato “5 V”. Possiamo quindi alimentare direttamente il modulo
collegando un alimentatore a 3,3 V al pin. La tensione sarà dunque
applicata direttamente al modulo ESP32. Controllate scrupolosamente che
l’alimentatore che collegate non superi i 3,3 V altrimenti potreste
distruggere irreparabilmente il vostro modulo ESP32.
Figura 3.6 – Tre metodi per alimentare una scheda ESP32: (1) utilizzando un cavo USB connesso a
un computer o a un power bank, (2) tramite alimentatore stabilizzato a 5 V, (3) con alimentatore
stabilizzato a 3,3 V.

Le tre soluzioni per alimentare il modulo ESP32 servono per “accenderlo” e


farlo funzionare ma non sono adatte per alimentare moduli o dispositivi
esterni che richiedono maggiori potenze. Per esempio possiamo utilizzare i
5 V o i 3,3 V per alimentare sensori, display, pulsanti e LED singoli ma non
per controllare relè, motori, lampade e strisce di LED, cioè dispositivi che
richiedono molta energia per funzionare. In questi casi è necessario
predisporre opportuni circuiti di pilotaggio e fornire alimentazioni separate
e specifiche come vedremo nelle prossime pagine.

Scegliere i pin
Volendo realizzare un progetto con ESP32, la prima cosa da fare è quella di
definire quali componenti elettroniche faranno parte dell’oggetto da
realizzare. Io parto sempre tracciando uno schema di massima sul mio
quaderno.
Lo schema di massima non è un vero e proprio schema elettrico ma
assomiglia più a un diagramma a blocchi dove indico le parti necessarie, i
sensori, i moduli di alimentazione e quanto necessario e così via. Indico
questi elementi come semplici blocchi uniti da linee ma questa operazione è
già sufficiente per farmi capire approssimativamente quanti e quali pin
saranno necessari. È un po’ la conta dei pin, che purtroppo in ogni
microcontrollore sono una risorsa preziosa e limitata.
In ogni progetto dobbiamo effettuare delle scelte, prendere dei
compromessi dovuti ai costi, dimensioni, prestazioni e anche al numero di
pin. Solitamente i pin di un microcontrollore possono svolgere più funzioni
e sta a noi decidere quale utilizzare.
Su ESP32 quasi tutti i pin possono essere utilizzati come ingressi o uscite
digitali (GPIO – General Purpose In/Out pin) ma spesso capita che lo stesso
pin sia utilizzato anche per una seriale o qualche altra funzione.
Fortunatamente la maggior parte di queste funzioni avanzate è riassegnabile
da software, ma le fasi iniziali della definizione di un progetto sono come
comporre i primi pezzi di un puzzle… Spesso le scelte iniziali vincolano
tutto lo sviluppo successivo!

Figura 3.7 – Esempio diagramma di massima del progetto: i collegamenti sono indicati in modo
semplificato, senza scendere nei dettagli. Lo schema può servire anche per farsi un’idea dei pin che
verranno occupati.
Figura 3.8 – Pinout della scheda di sviluppo ESP32 DEV Kit: a sinistra elenco dei pin con le loro
funzioni principali, a destra sono evidenziati gli ingressi (verde) e le uscite (rosso).

L’ESP32 ha 48 pin GPIO, quasi tutti utilizzabili come ingressi, alcuni


riservati, alcuni configurabili come uscite. Non tutti i pin sono però
disponibili e possono esserci piccole differenze tra i vari kit di sviluppo. La
Tabella 3.1 indica quali pin sono utilizzabili come ingressi e quali come
uscite sul modulo ESP32 DevKitC. Alcuni pin sono “di servizio” e in fase
di avvio della scheda possono emettere segnali oppure non dovrebbero
essere utilizzati del tutto per evitare malfunzionamenti. Alcuni pin come già
detto possono funzionare solo come ingressi ed è facile dimenticarsene…
purtroppo se avete già realizzato il vostro circuito (magari creando un
circuito stampato PCB) potrebbe essere molto complicato trovare un pin
alternativo!
Nella tabella ho riportato un elenco dei pin con le loro funzioni. Fate
riferimento a questa pagina durante la fase di definizione “elettronica” del
vostro progetto.
Tabella 3.1 – Elenco dei pin del modulo ESP32 con indicato il possibile utilizzo come ingresso o
uscita.
I pin possono essere usati come ingressi o come uscite se nella colonna
corrispondente della tabella è presente un “Sì”.
È preferibile non configurare il pin e quindi utilizzarlo se la funzione è
contrassegnata con una “X”.

Lo sketch
Utilizzando l’ambiente di sviluppo di Arduino, lo sketch, cioè il listato, ha
una forma ben precisa in cui troviamo sempre due funzioni principali.
La prima è chiamata setup() ed è richiamata solo all’avvio della scheda
o dopo la pressione del tasto reset (contrassegnato con la sigla EN).
La seconda funzione è chiamata loop() e contiene il codice che terrà in
vita la nostra applicazione.

Ecco un esempio:
void setup() {
// put your setup code here, to run once:
}

void loop() {
// put your main code here, to run repeatedly:
}

Listato 3.1 – Lo sketch.

Nel setup() troveranno posto le istruzioni di configurazione e di


preparazione, da eseguire una sola volta, come per esempio le impostazioni
dei pin, la creazione di oggetti o l’assegnazione di variabili. Il codice posto
nel loop() sarà ripetuto fino a che la scheda è alimentata. Il loop() serve
per tenere “in vita” l’applicazione che vogliamo realizzare. Non è
obbligatorio inserire codice in queste due funzioni, infatti possiamo avere
casi particolari in cui una o l’altra sono vuote!
All’interno del codice possiamo inserire commenti e note, come visibile
anche nel Listato 3.1, dove sono stati usati commenti su una sola riga.
Queste annotazioni sono precedute da una doppia barra inclinata //: quanto
posto alla sua destra è ignorato dal compilatore. Questi commenti occupano
una sola riga: non possono andare a capo. A volte possono essere posti
anche di fianco a delle istruzioni:
i = 0; //azzero il contatore

In questo esempio abbiamo del codice seguito da un commento. Se è


necessario scrivere commenti più complessi è possibile usare la coppia di
delimitatori /* */ che può estendersi su più righe. Tutto quello che
scriveremo tra i due delimitatori sarà isolato come nel seguente esempio:
/*
Release del 12/01/2023
Autore: Paolo Aliverti
Funzione: riceve parametri e produce un risultato
*/

Spesso i programmatori utilizzano questi delimitatori per “disattivare”


velocemente porzioni di codice senza doverle cancellare.

Come configurare un pin


Utilizzando il framework “Arduino” è possibile configurare un pin, cioè
determinare il suo comportamento come ingresso o come uscita utilizzando
l’istruzione pinMode() che richiede due parametri:
• il numero del pin;
• la sua funzione (INPUT o OUTPUT).

Per impostare il pin 25 come uscita scriveremo:


pinMode(25, OUTPUT);

Per configurare il pin 36 come ingresso invece avremo:


pinMode(36, INPUT);

Quando a un pin colleghiamo un pulsante possiamo utilizzare


configurazioni speciali che “legano il pin” alla tensione positiva o agli 0 V
(GND). Abbiamo quindi anche le impostazioni:

• INPUT_PULLUP;
• INPUT_PULLDOWN.

Per maggiori dettagli sul collegamento dei pulsanti e sul significato dei
termini “pull up e pull down” si veda a pagina 55 e seguenti.
Solitamente l’istruzione pinMode() è inserita nella sezione setup() di
uno sketch, destinata alle configurazioni iniziali dell’hardware e del nostro
codice ma nessuno vieta di riconfigurare un pin in ogni punto del nostro
programma (ovviamente dipende dalla funzione del programma e dalla
configurazione dell’hardware esterno).
L’esempio completo di configurazione di due pin all’interno di uno
sketch:
void setup() {
pinMode(25, OUTPUT);
pinMode(36, INPUT);
}

void loop() {}

Listato 3.2 – Impostazione di due pin come uscita e ingresso.


Il loop() di questo semplice programma è vuoto perché non ci sono
istruzioni da ripetere.

Accendere un LED, comandare un pin digitale


Una volta che un pin è stato configurato con pinMode() come uscita
possiamo “accenderlo” o “spegnerlo” a piacere via codice con l’istruzione
digitalWrite(), indicando il numero del pin e lo stato da applicare che
sarà HIGH o 1 oppure LOW o 0. Per accendere il pin 25 precedentemente
configurato scriveremo:
digitalWrite(25, HIGH);

per spegnerlo:
digitalWrite(25, LOW);

Possiamo verificare il funzionamento di digitalWrite() costruendo un


semplice circuito con un LED collegato al modulo ESP32. IL LED va
collegato utilizzando un resistore di valore opportuno. Nel caso di
un’alimentazione a 3,3 V (la tensione che troviamo sui pin della scheda
ESP32) possiamo ricavare il valore di resistenza con la legge di Ohm:

V=I×R

Per ricavare R dobbiamo conoscere il valore di I e di V. I è ricavabile dalle


caratteristiche del LED e solitamente ha un valore di circa 10/20 mA a
seconda del tipo di LED. I è la corrente necessaria per far accendere il LED
correttamente. La tensione ai capi della resistenza è pari alla tensione
fornita dal pin (3,3 V) meno la tensione necessaria per accendere il LED
(circa 2 V a seconda del modello) quindi possiamo scrivere:

R = (3,3 V – 2 V) / 15 mA = 1,3 V / 0,015 A = 86,6 Ω

Possiamo quindi usare un resistore da 100 Ω ¼ di watt. Colleghiamo LED e


resistenza come indicato nello schema di Figura 3.9 e allestiamo il circuito
su una breadboard.
Figura 3.9 – Schema elettrico con una resistenza collegata a un LED. Il tutto è alimentato a 3,3 V; la
corrente assorbita dal LED è di circa 15 mA e questo ci permette di determinare il valore corretto per
la resistenza.

Figura 3.10 – Schema elettrico di un LED con resistore da 100 Ω, collegato al pin 25 della scheda
ESP32. A destra sono riportati un LED e un resistore con i relativi simboli.
Figura 3.11 – Disposizione su breadboard di un LED con resistore da 100 Ω.

Per provare ad accendere il LED possiamo caricare tramite Arduino IDE il


seguente codice:
void setup() {
pinMode(25, OUTPUT);
}

void loop() {
digitalWrite(25, HIGH);
}

Listato 3.3 – Accensione di un LED collegato al pin 25.

Nel loop continueremo a impostare lo stato del LED mantenendolo acceso.


Osservate che potremmo ottenere lo stesso risultato anche con il seguente
sketch dove il loop() non contiene istruzioni:
void setup() {
pinMode(25, OUTPUT);
digitalWrite(25, HIGH);
}

void loop() {}
Listato 3.4 – Accensione di un LED collegato al pin 25.

Infatti digitalWrite() si comporta come quando premiamo l’interruttore


per accendere la luce in una stanza: una volta premuto la luce rimane accesa
fino a che non la spegniamo. Infatti digitalWrite() opera su un registro
interno del modulo ESP32 semplicemente impostando un bit che attiva la
corrispondente uscita. Una volta che il bit è impostato l’uscita resterà
accesa o spenta fino a che non modificheremo il valore nel registro.

Blink e pause bloccanti


Può essere necessario inserire un pausa nel codice dei nostri programmi,
così nel framework di Arduino troviamo la funzione delay() con cui è
semplice inserire delle pause. Dobbiamo fornire a delay() un numero, un
parametro, per specificare per quanti millisecondi il programma si dovrà
bloccare. Possiamo far lampeggiare un LED accendendolo e spegnendolo
ogni mezzo secondo con il classico codice dell’esempio “Blink”:
void setup() {
pinMode(25, OUTPUT);
}

void loop() {
digitalWrite(25, HIGH);
delay(500);
digitalWrite(25, LOW);
delay (500);
}

Listato 3.5 – Codice per far lampeggiare un LED collegato al pin 25 (Blink).

Se provenite dal mondo di Arduino conoscerete bene questo esempio. Dopo


aver impostato il pin 25 come uscita, nel loop() accenderemo il LED con
digitalWrite(), quindi faremo una pausa di mezzo secondo per poi
spegnere il LED e attendere un altro mezzo secondo prima di ricominciare
tutto da capo… per sempre! La funzione delay() presenta alcuni problemi:
• arriva al massimo a una trentina di secondi;
• è bloccante!

Entrambi gli inconvenienti sono molto gravi. Intanto può capitare di dover
attendere un’azione per minuti o per ore… un limite di trenta secondi è
abbastanza seccante. Ma la cosa peggiore di tutte è il fatto che quando la
CPU si imbatte in una delay() è come se si chiudesse in un buco nero
senza fare più nulla fino allo scadere del tempo. La pausa introdotta da
delay() è realizzata con un ciclo “vuoto” che termina allo scadere del
tempo impostato. La CPU non farà altro che restare bloccata senza far
nulla. In un programma “reattivo” non è ammissibile un simile
comportamento e le pause non possono essere “bloccanti”. Oltre a delay()
esiste anche la funzione delayMicroseconds() che permette di fare pause
di microsecondi (1000 µs = 1 ms e 1000 ms = 1 s). Anche
delayMicroseconds() è bloccante.
I principianti si scontrano subito con queste problematiche scrivendo
codici che non potranno funzionare o che hanno comportamenti indesiderati
a causa dell’uso delle delay(). Come scrivereste un programma che fa
lampeggiare un LED e può anche leggere un pulsante? Provate a pensarci…
vedremo tra poco la soluzione.

Variabili e tipi di dati (in breve)


Per memorizzare dati e informazioni all’interno del nostro codice
utilizzeremo delle variabili. Una variabile è comparabile a una specie di
cassettino mantenuto nella memoria del processore. Il cassettino è
individuabile tramite un nome che gli abbiamo assegnato. Il nome di una
variabile dovrebbe ricordare il contenuto e aiutarci nella comprensione del
codice. Per recuperare il contenuto di un cassetto dobbiamo solo ricordarci
del suo nome. Alcuni linguaggi richiedono che le variabili siano dichiarate
specificando il tipo di dati che vi memorizzeremo. Il C/C++ utilizzato per
ESP32 è tra questi. Le variabili più usate sono quelle che memorizzano
numeri interi e quindi le dichiareremo utilizzando la seguente scrittura:
int n = 0;

La “parola chiave” int definisce il tipo di dato che intendiamo


memorizzare mentre n è il nome assegnato alla variabile. In questa scrittura
assegniamo direttamente un valore alla variabile utilizzando il segno di =
seguito da un numero. Ogni istruzione in C deve sempre terminare con il
punto e virgola.
Alcuni dei tipi utilizzabili sono:
• byte: per memorizzare piccoli numeri da 0 a 255, senza segno;
• char: per singoli caratteri;
• int: 16 bit per numeri con segno da -32.768 a 32.767;
• long: 32 bit per valori da -[Link] a [Link];
• float: numeri con virgola mobile;
• double: sulla piattaforma Arduino il tipo di dato double è un sinonimo di
float.

Ai tipi semplici possono essere aggiunti dei modificatori come:


• static: per creare variabili private, cioè interne a una porzione di codice,
che mantengono il valore tra una chiamata e l’altra;
• unsigned: per usare solo i valori positivi (unsigned int crea una
variabile con valori da 0 a 64.000 circa);
• const: per variabili che non possono essere modificate.

La scelta del nome di una variabile richiede una certa attenzione per due
motivi: è preferibile utilizzare nomi significativi e che aiutino la
comprensione del codice; nel linguaggio C non possiamo utilizzare
qualsiasi combinazione di caratteri per definire il nome di una variabile, per
esempio il nome di una variabile non può iniziare con un numero e non può
avere spazi. Non possiamo neppure utilizzare operatori speciali come quelli
aritmetici.
Se una variabile è come un cassetto, un array è un cassetto con scomparti.
Ecco un esempio:
int temperature[20];

Con questa scrittura abbiamo definito un array chiamato “temperature” con


20 scomparti. Possiamo accedere alle 20 posizioni utilizzando un indice che
va da 0 a 19. Per richiamare il primo elemento scriveremo:
temperature[0].
Gli array sono utili per automatizzare il codice ed elaborare gruppi di
informazioni. Possiamo assegnare gli elementi di un array un elemento per
volta con:
temperature[0] = 12;
temperature[1] = 23;
temperature[2] = 45;

Possiamo assegnare tutti i valori in un sol colpo, ma solo durante la


dichiarazione inziale utilizzando una coppia di parentesi graffe:
int gradi[5] = {12, 23, 34, 45, 56};

Un tipo particolare di array sono quelli formati da caratteri spesso


denominati anche “stringhe di caratteri” o “stringhe”. Un testo è una
sequenza di caratteri e possiamo crearlo così:
char msg[20] = "hello world";

L’array è stato dichiarato con una capacità superiore al necessario: 20 posti.


Il messaggio che assegniamo occupa poi solo 11 caratteri. Di solito c’è
anche un carattere aggiuntivo che specifica la fine del testo. Questo
carattere non è visibile ed è \0.
Non è pratico contare i caratteri di una stringa per poi dimensionarla, per
questo il C permette di dichiarare un array di caratteri con la scrittura:
char saluto[] = "ciao mondo!";

Si occuperà il compilatore di contare i caratteri per noi. Lavorando con gli


array di caratteri bisogna fare attenzione a come si muove l’indice e quindi
a cosa stiamo leggendo. È facile leggere in posizioni di memoria
“sbagliate” e ottenere risultati imprevedibili.
La piattaforma Arduino mette a disposizione il tipo di dato String che
permette una gestione più semplice e intuitiva rispetto agli array di caratteri.

Collegare pulsanti
I pin digitali (GPIO) possono essere utilizzati come ingressi a cui collegare
dispositivi di vario tipo. I componenti più semplici che possiamo utilizzare
sono i pulsanti con cui registrare le interazioni degli utenti. Il collegamento
di un pulsante richiede qualche accorgimento in più rispetto ai LED, infatti
quando un pin è configurato come ingresso è più soggetto a rilevare disturbi
che possono fornire “false letture” e in alcuni casi anche creare inspiegabili
comportamenti. Quando si collega un pulsante è sempre necessario
aggiungere anche un resistore per tenere il pin agganciato a una tensione di
riferimento. I resistori possono essere interni o esterni.
I pulsanti che troviamo solitamente negli starter kit sono di tipo “tattile” e
sono pensati per essere saldati su circuiti stampati (o per l’utilizzo su una
breadboard). Questo tipo di pulsanti ha quattro terminali che sono collegati
due a due. Potete osservare in Figura 3.12 come sono disposti i contatti. Il
pulsante si mette di solito a cavallo del solco centrale della breadboard per
dargli maggiore stabilità. Ne esistono di vari tipi e dimensioni, più o meno
larghi e con azionamento più o meno alto.
Per il collegamento serve un resistore del valore di qualche chiloohm (una
decina). La configurazione più naturale prevede che il resistore tenga il pin
di ingresso collegato a GND. In questo modo il pin rileverà sempre e
stabilmente 0 V. Il pulsante invece è collegato tra la tensione di
alimentazione (3,3 V) e il pin. Premendo il tasto porteremo i 3,3 V sul pin
digitale e potremo rilevare via software il cambio di stato. La resistenza è
fondamentale perché se non ci fosse e quindi collegassimo direttamente il
pin a GND, premendo il tasto creeremmo un corto circuito collegando i 3,3
V direttamente con GND! La configurazione con resistore collegato tra il
pin e GND si chiama “pull down”.
Quando i pin sono usati con resistori esterni di pull down dobbiamo
impostarli usando pinMode(). Per impostare il pin 24 come ingresso
semplice scriveremo nel setup() di uno sketch:
pinMode(24, INPUT);

La lettura dello stato del pin si fa con la funzione digitalRead() a cui


forniremo come parametro il numero del pin e che restituirà un numero
corrispondente allo stato del pin (0 se sul pin ci sono 0 V, 1 se abbiamo 3,3
V). Il valore restituito può essere utilizzato direttamente oppure salvato in
una variabile per essere usato successivamente.
Ecco un esempio di utilizzo con pulsante collegato in “pull down” sul pin
23 che pilota un LED sul pin 25:
void setup() {
pinMode(23, INPUT);
pinMode(25, OUTPUT);
}

void loop() {
int v = digitalRead(23);
digitalWrite(25, v);
}

Listato 3.6 – Codice per leggere un pulsante e accendere un LED.

Nel setup() dello sketch configuriamo i due pin usando pinMode(). Nel
loop() leggiamo continuamente il valore del pulsante utilizzando
digitalRead(). La lettura è eseguita più rapidamente possibile (parliamo di
micro secondi). Il valore rilevato da digitalRead() è subito salvato nella
variabile v e poi utilizzato con digitalWrite() per accendere o spegnere il
LED. Osserviamo che v può assumere i valori 0 o 1, mentre per pilotare il
LED di solito usiamo HIGH e LOW. HIGH e LOW sono equivalenti ai
valori 1 e 0 e si usano per rendere più leggibile o comprensibile il codice.

Figura 3.12 – Schema di collegamento di un pulsante con resistore di pull down e di un LED a una
scheda ESP32. Sulla destra vediamo raffigurato un pulsante tattile con il suo simbolo elettrico e i
colori di una resistenza da 10 kΩ.

Un pin di ingresso può essere configurato in altri tre modi:


• con resistore interno di pull down;
• con resistore esterno di pull up;
• con resistore interno di pull up.

Per semplici circuiti o per semplificare i cablaggi su breadboard possiamo


utilizzare un resistore di pull down interno al posto di quello esterno.
Configurando il pin con questa istruzione:
pinMode(23, INPUT_PULLDOWN);

sarà il modulo ESP32 ad aggiungere per noi, internamente, una resistenza di


pull down. Il pulsante in questo caso sarà collegato direttamente tra il pin e
3,3 V (si veda lo schema di Figura 3.14). Il codice da utilizzare per
accendere un LED sarà il seguente:
void setup() {
pinMode(23, INPUT_PULLDOWN);
pinMode(25, OUTPUT);
}

void loop() {
int v = digitalRead(23);
digitalWrite(25, v);
}

Listato 3.7 – Codice per leggere un pulsante con resistore di pull down interno.

Figura 3.13 – Disposizione su breadboard di un pulsante con resistore di pull down e di un LED con
resistore.
Figura 3.14 – Quattro differenti metodi per collegare un pulsante a ESP32: (1) con resistore di pull
down esterno da 10 kΩ, (2) con resistore di pull up esterno, (3) con resistore di pull down interno e
(4) con resistore di pull up interno.

La terza possibilità che abbiamo per collegare un pulsante è con una


resistenza di pull up, cioè un resistore da una decina di chiloohm collegato
tra il pin e 3,3 V. Il pulsante sarà collegato tra il pin della scheda ESP32 e
GND (vedi schema nella Figura 3.14 – 3). Il pin prescelto come ingresso
avrà sempre applicata una tensione positiva e quindi leggendo il suo stato
con digitalRead() ricaveremo un valore “HIGH” (o 1). Alla pressione del
pulsante collegato, il pin sarà portato a 0 V (GND) e digitalRead()
restituirà “LOW” (o il valore 0). Anche in questo caso il resistore esterno è
fondamentale per evitare di creare un corto circuito tra 3,3 V e GND alla
pressione del tasto. Il comportamento logico che otteniamo con una
configurazione pull up è logicamente invertito:
• a pulsante premuto leggiamo LOW;
• a pulsante non premuto leggiamo HIGH.

Ecco quindi il codice per la lettura del pulsante in configurazione pull up:
void setup() {
pinMode(23, INPUT);
pinMode(25, OUTPUT);
}

void loop() {
int v = digitalRead(23);
digitalWrite(25, v);
}

Listato 3.8 – Codice per leggere un pulsante con resistore di pull up esterno.

Osserviamo che il pin di ingresso è stato configurato nel modo consueto,


con pinMode(), utilizzando INPUT. La lettura del pin si fa con
digitalRead(), nel modo consueto. Caricate lo sketch e osservate cosa
accade: il LED sarà acceso e si spegnerà alla pressione del pulsante. Questo
comportamento potrebbe confondere le idee perché siamo portati a
ragionare in logica “positiva”: tasto premuto, valore HIGH e viceversa.
Possiamo compensare questa situazione invertendo immediatamente il
valore logico rilevato da digitalRead() con l’operatore NOT (invertitore
logico) ! che posto prima di digitalRead() ne capovolge il valore.
Anteponendo ! così che la variabile v contenga “HIGH” se il tasto è
premuto o “LOW” se non lo è. Provate a modificare lo sketch in questo
modo:
void setup() {
pinMode(23, INPUT);
pinMode(25, OUTPUT);
}

void loop() {
int v = !digitalRead(23);
digitalWrite(25, v);
}

Listato 3.9 – Codice per leggere un pulsante con resistore di pull up esterno.

Ora le cose tornano “normali”: premendo il tasto il LED si accende,


altrimenti resta spento.
ESP32 offre la possibilità di collegare una resistenza interna di pull up
attivabile con pinMode() e il parametro INPUT_PULLUP. Il pulsante esterno
andrà collegato tra il pin e GND e non serviranno altri componenti (Figura
3.14-d). La lettura del pin, ottenuta con digitalRead() sarà “invertita”
come nel caso precedente e per evitare confusione io consiglio di applicare
l’operatore di inversione !. Di seguito lo sketch per la lettura del pulsante:
void setup() {
pinMode(23, INPUT_PULLUP);
pinMode(25, OUTPUT);
}

void loop() {
int v = !digitalRead(23);
digitalWrite(5, v);
}

Listato 3.10 – Codice per leggere un pulsante con resistore di pull up interno.

Perché ci sono a disposizione così tante alternative? Esistono svariati tipi di


hardware che possiamo collegare alla nostra scheda e alcuni richiedono
resistenze di pull up o pull down per funzionare, in più queste devono avere
particolari valori. I valori delle resistenze interne per ESP32 sono:
• pull up circa 50 kΩ;
• pull down circa 17 kΩ.

Non sempre i valori sono adatti e per questo si ricorre a componenti esterni.
Una resistenza di pull up/down non dovrebbe poi avere un valore troppo
basso perché altrimenti alla pressione del tasto faremmo circolare troppa
corrente. Un valore troppo alto invece combinato con le normali correnti di
perdita che troviamo sui pin logici potrebbe modificare lo stato del pin
quando il tasto non è premuto oltre a rallentare i tempi di commutazione.
Un valore ideale per le resistenze di pull up e down esterne è tra 4,7 e 10
kΩ.

Controllo di flusso, pulsanti e LED


Utilizzando pulsanti e LED introduciamo il concetto di “controllo del
flusso” di un programma, argomento fondamentale per ogni linguaggio di
programmazione. Un programma è una sequenza di istruzioni che saranno
eseguite una dopo l’altra. In questo modo abbiamo delle istruzioni da
eseguire in ordine. È come preparare una ricetta: se non seguiamo le varie
fasi in sequenza potremmo non ottenere il risultato desiderato. Una
sequenza ordinata è utile ma abbastanza rigida. Cosa fare se le cose non
vanno come ci siamo prefissati? Come possiamo adattare la ricetta agli
ingredienti che abbiamo nella dispensa? Cosa fare se al posto del guanciale
abbiamo la pancetta? Possiamo procedere ugualmente a preparare una pasta
alla carbonara? Prendere decisioni è una parte fondamentale della
programmazione. Un programma che “prende decisioni” non è intelligente,
ma può “adattarsi” e funzionare in modo migliore e più aderente alla realtà.
Utilizzando speciali istruzioni possiamo quindi controllare il flusso del
nostro software che prenderà decisioni (“programmate” o previste dal
programmatore) e quindi modificherà le sue azioni. La modifica del flusso
di un programma avviene con istruzioni che permettono di scegliere una o
più strade alternative dopo aver valutato dei fatti. Avremo quindi dei test
che valutano il valore di una espressione e quindi decidono come far
proseguire il programma. In C/C++ questa istruzione è if(). Tra le
parentesi di if() inseriremo l’espressione da valutare e questa dovrà
restituire sempre vero o falso. In C vero o falso si possono esprimere anche
con semplici numeri: 1 o 0 (e spesso ogni numero diverso da 0 è
considerato al pari di “vero”). Le espressioni da “testare” sono di vario tipo,
per esempio:
• temperatura > 20;
• a < b;
• messaggio == “ciao”;
• livello >= 87,0.

In questi esempi temperatura, a, b, messaggio sono variabili che in qualche


punto del programma hanno “caricato” un valore. Questo valore viene
confrontato con dei valori “fissi” come numeri o stringhe oppure anche con
altre variabili (a < b). Si noti che in C/C++ quando vogliamo verificare se
una variabile contiene un certo valore usiamo un doppio segno di
uguaglianza ==. È una cosa un po’ strana ma possiamo leggerla come
“proprio uguale a” (questo perché l’uguaglianza singola è già utilizzata per
assegnare un valore alle variabili).
Le espressioni precedenti se valutate possono assumere solo due possibili
valori: vero o falso. Possiamo quindi inserirle nelle parentesi di if() e
utilizzarle per eseguire del codice oppure no. Abbiamo due possibilità
fondamentali:
• se expr è vera allora eseguo del codice;
• se expr è vera eseguo un certo codice, altrimenti ne eseguo dell’altro.

Poi vedremo come prevedere altri casi più complessi.

Nel primo caso, il più semplice, abbiamo la forma:


if (expr) {
//codice da eseguire
}

L’istruzione if() è seguita da delle parentesi graffe {} che costituiscono il


corpo dell’istruzione e che contengono il blocco di codice che sarà eseguito
nel caso l’espressione valutata sia vera. Se la variabile temperatura contiene
un valore maggiore di 20 allora accendo il LED sul pin 25:
if (temperatura > 20) {
//codice da eseguire
digitalWrite(25, HIGH);
}

A volte capita di trovare la forma contratta, senza le parentesi graffe. Si può


utilizzare se l’istruzione da eseguire è unica. Possiamo scrivere così
l’esempio precedente:
if (temperatura > 20) digitalWrite(25, HIGH);

Se a seguito della valutazione dell’espressione prevediamo due possibilità


useremo la forma if-else (se-altrimenti). Se l’espressione è vera eseguo il
primo blocco di codice altrimenti eseguiremo il secondo blocco:
if (expr) {
//codice da eseguire nel caso di espressione vera
} else {
//codice da eseguire se l'espressione è falsa
}
Figura 3.15 – (1) Diagramma di flusso di istruzioni in sequenza semplice; (2) diagramma di flusso
dell’istruzione if e (3) dell’if-else con doppia diramazione.

Ecco un esempio:
if (temperatura > 20) {
//codice da eseguire
digitalWrite(25, HIGH);
[Link]("ALLARME TEMPERATURA!");
} else {
//codice da eseguire se l'expr è falsa
digitalWrite(25, LOW);
[Link]("TEMPERATURA: ok");
}

L’istruzione per leggere lo stato dei pin, digitalRead(), restituisce sempre


un solo valore che può essere vero HIGH o falso LOW, cioè un boolean.
Possiamo utilizzare digitalRead() come espressione all’interno di un
if().
Figura 3.16 – Led con pulsante. Led sul 25 (100 Ω) e pulsante sul 23 con pull down da 10k.

Realizziamo un semplice circuito collegando un LED con resistore da 100


Ω al pin 25 e un pulsante con resistore di pull down sul pin 23 (vedi schema
di Figura 3.16). L’istruzione digitalRead() può essere utilizzata
direttamente per determinare il comportamento dell’if(). Ecco uno sketch
completo in cui il LED si accenderà alla pressione del pulsante:1
void setup() {
pinMode(23, INPUT);
pinMode(25, OUTPUT);
}

void loop() {
if (digitalRead(23)) {
//accendo il LED
digitalWrite(25, HIGH);
} else {
//spengo il LED
digitalWrite(25, LOW);
}
}

Listato 3.11 – Codice per leggere un pulsante e controllare un LED utilizzando il costrutto if-else.

Ovviamente questo esempio ci serviva solo per dimostrare l’utilizzo


dell’istruzione if(). In realtà può essere notevolmente semplificato e
potreste eliminare l’if() pilotando direttamente il LED:
void loop(){
digitalWrite(25, digitalRead(23));
}

Le espressioni logiche possono essere combinate per coprire casistiche più


complesse. Immaginate di aver creato un robot e di doverlo programmare
utilizzando delle regole. Le regole possono essere codificate tramiute
l’if(), combinando più situazioni. Ecco alcune regole:
• SE NON “piove”, allora esci;
• SE “piove” E “oggi giorno di spesa” ALLORA prendi l’ombrello;
• SE “mele” O “pere” ALLORA acquistane un chilogrammo.

Il primo esempio è una “negazione” e “capovolge” il valore


dell’espressione. Possiamo codificare la regola utilizzando uno speciale
simbolo che equivale alla “negazione” (o NOT). Il simbolo è un punto
esclamativo da anteporre all’espressione:
!(expr)

Immaginiamo che il nostro robot possa capire se piove utilizzando uno


speciale comando piove(). “Se non piove, esci” potrebbe diventare:
if (!piove()) {
esci();
}

La seconda regola del robot combina due situazioni che devono essere vere
contemporaneamente affinché l’azione possa essere attivata. Abbiamo un
“AND” logico che si rappresenta con il simbolo &&. Tradotto in codice sarà:
if (piove() && oggiSpesa()) {
ombrello();
}

L’azione si attiverà quando entrambe le espressioni saranno vere. Possiamo


rappresentare il comportamento dell’AND anche con una tabella.
Tabella 3.2 – Tavola della verità per l’operatore logico AND.

A B Risultato
falso falso falso

falso vero falso


vero falso falso

vero vero vero

Quando si utilizzano espressioni matematiche o di confronto e vi si


applicano degli operatori logici può essere buona cosa delimitarle con
parentesi. Dal punto di vista del compilatore non cambia nulla, ma il codice
sarà più leggibile:
(x < 10) && (x >20)

La terza regola presenta un’alternativa. Quando almeno una delle due


espressioni sarà vera allora sarà eseguito il codice nel corpo dell’if().
L’operatore logico “OR” si rappresenta con due barre verticali ||. Ecco la
regola tradotta in codice:
if (mele() || pere()) {
acquista(1);
}

L’azione si attiverà quando almeno una delle espressioni sarà vera.


Possiamo rappresentare il comportamento dell’OR anche con una tabella.
Tabella 3.3 - Tavola della verità per l’operatore logico OR.

A B Risultato
falso falso falso

falso vero vero

vero falso vero

vero vero vero

Modifichiamo il circuito di Figura 3.16 aggiungendo un secondo pulsante


che collegheremo al pin 22 con resistore di pull down da 10 kΩ.
Combiniamo l’azione dei due pulsanti con un AND così che per accendere
il LED sia necessario premerli entrambi.
Ecco qui di seguito lo sketch completo:
void setup() {
pinMode(22, INPUT);
pinMode(23, INPUT);
pinMode(25, OUTPUT);
}

void loop() {
if (digitalRead(23) && digitalRead(22)) {
//accendo il LED
digitalWrite(25, HIGH);
} else {
//spengo il LED
digitalWrite(25, LOW);
}
}

Listato 3.12 – Codice per accendere un LED con due pulsanti e l’istruzione AND (&&).

Figura 3.17 – Schema elettrico con LED sul pin 25 e due pulsanti con resistenza di pull down sui pin
22 e 23.

Utilizzando il circuito di Figura 3.17 proviamo a combinare l’azione dei


due pulsanti con una OR così che per accendere il LED sia sufficiente
premere un solo pulsante. Ecco qui di seguito lo sketch completo:
void setup() {
pinMode(22, INPUT);
pinMode(23, INPUT);
pinMode(25, OUTPUT);
}

void loop() {
if (digitalRead(23) || digitalRead(22)) {
//accendo il LED
digitalWrite(25, HIGH);
} else {
//spengo il LED
digitalWrite(25, LOW);
}
}

Listato 3.13 – Codice per accendere un LED con due pulsanti e l’istruzione OR (||).

L’if-else permette di scegliere tra due alternative. A volte, nella realtà, le


cose sono più complicate e i test sulle espressioni devono poter attivare più
alternative. Possiamo allora utilizzare gli else-if con cui aggiungere più
ramificazioni. Ogni else-if valuta un’espressione e quindi attiverà una parte
di codice. Combinando più espressioni differenti bisogna prestare
attenzione a non creare situazioni ambigue in cui, per esempio, troviamo
che un certo blocco di codice sarà attivabile in più combinazioni. Le
espressioni che utilizziamo nell’if-else-if dovrebbero sempre essere
mutuamente esclusive, cioè solo una per volta dovrebbe essere vera. Ecco il
formato da utilizzare:
if (expr1) {
//codice da eseguire se expr1 è vera
} else if (expr2) {
//codice da eseguire se expr2 è vera
} else if (expr3) {
//codice da eseguire se expr3 è vera
} else {
//codice da eseguire in tutti i casi non previsti
}

A volte capita che i differenti percorsi del flusso di esecuzione siano legati
direttamente al valore di una variabile. Potremmo utilizzare un if-else-if,
oppure l’operatore switch che valuta il valore della variabile fornita (che
deve essere numerico o di tipo char) e attiva il codice necessario. La forma
di switch() è:
switch (var) {
case 0:
//se la variabile vale 0 salto qui
//codice
break;
case 1:
//se la variabile vale 1 salto qui
//codice
break;
default:
//in ogni altro caso
//codice
break;
}

La forma switch() utilizza delle etichette (label) che corrispondono al


valore che può assumere la variabile. switch() confronta il valore della
variabile e salta nel punto associato alla label. Se nessun valore è
riconosciuto ed è presente il blocco default, l’esecuzione salterà lì,
altrimenti uscirà dallo switch(). All’interno dei “casi” non è possibile
dichiarare delle variabili o meglio i case non possono avere variabili
private. Nel caso servissero dovete crearle all’esterno.

Pause non bloccanti


Abbiamo imparato a utilizzare la comodissima funzione delay() per far
lampeggiare LED e far funzionare più lentamente il codice che scriviamo.
Spesso i delay() sono usati anche per annullare l’effetto di rimbalzo dei
contati di un pulsante (debouncing). I delay() riportano il codice a una
dimensione più “umana”, rallentandolo, ma inevitabilmente introducono
malfunzionamenti inspiegabili per un neofita della programmazione del
firmware. Provate a collegare alla vostra scheda due LED e un pulsante
come in Figura 3.18.
Figura 3.18 – Schema elettrico di una ESP32 con collegato un pulsante con resistore di pull down al
pin 24 e due LED con resistore da 100 Ω sui pin 25 e 26.

Poi scrivete uno sketch che fa lampeggiare uno dei LED e


contemporaneamente vi fa accendere il secondo LED alla pressione del
tasto. Vi presento la tipica soluzione (errata):
void setup() {
pinMode(24, INPUT);
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);
}

void loop() {
//lettura pulsante
int v = digitalRead(24);
digitalWrite(25, v);
//blink
digitalWrite(26, HIGH);
delay(500);
digitalWrite(26, LOW);
delay (500);
}

Listato 3.14 – Codice per provare a leggere un pulsante e far lampeggiare un LED.

Purtroppo caricandolo e provandolo vi accorgerete che il programma non si


comporta esattamente come atteso e che il comportamento è “non
responsivo”: il LED controllato dal pulsante non si accende
immediatamente ma solo in certi istanti ed è come se il tasto non sempre
funzionasse. Quando incontriamo certi “comportamenti” negli oggetti
quotidiani ci innervosiamo! Il problema sta nell’uso delle delay() che
bloccano il processore in una inutile attesa. Quando la CPU di ESP32
incappa in una delay() si ferma e non fa niente altro fino a che la pausa
non termina. Il programma è bloccato! Per questo motivo i programmatori
esperti evitano le delay() come la peste! Ma come si può fare per non
usare queste comodissime funzioni?
Bisogna cambiare modo di vedere le cose e pensare al loop() come
qualcosa che deve sempre “girare” alla massima velocità possibile, senza
“intoppi”. Per ricordarci di accendere o spegnere un LED dobbiamo trovare
un sistema che utilizza dei promemoria da controllare a ogni giro.
Immagino siate persone molto impegnate e dobbiate fare moltissime cose
durante la vostra giornata. Per non dimenticare tutti i miei impegni faccio
un uso massivo di post-it. Segno l’ora di quello che mi devo ricordare e li
appunto tutti sulla scrivania. Regolarmente vi “butto un occhio” e controllo
se qualcosa è giunto a scadenza: ore 10:30 riunione con meet, ore 11:00
telefonare a Carlo, ore 12:00 prepara il pranzo!
Dobbiamo trovare un sistema per portare l’approccio “post-it” nel nostro
codice.
ESP32, così come tutti i microcontrollori (anche Arduino), offre una
funzione che è una specie di orologio interno. Questa funzione si chiama
millis() e fornisce il numero di millisecondi dall’accensione della scheda.
Non è un vero e proprio orologio con data (si chiamano RTC e sono chip
dotati di una batteria) ma per i nostri scopi millis() è un’ottima soluzione.
L’approccio che propongo è anche simile a preparare la pasta. Per una
pasta al dente mettiamo sul fuoco una pentola e attendiamo che l’acqua
bolla. Per non sbagliare la cottura dei nostri spaghetti sappiamo che devono
cuocere per 10 minuti esatti. Nel momento in cui buttiamo la pasta nella
pentola ci segniamo l’orario su un post-it (sì, siamo molto smemorati):
12:40. Per sapere quando scolare la pasta controlliamo di tanto in tanto
l’orologio che abbiamo al polso e lo confrontiamo con l’orario riportato sul
post-it:
• 12:42 – sono passati due minuti;
• 12:44 – sono passati 4 minuti;
• 12:45 – 5 minuti…
• 12:50 – 10 minuti! È ora di scolare la pasta.
Se vogliamo cuocere altra pasta useremo un nuovo post-it. In codice:
• l’orologio sarà millis();
• il post-it è una variabile in cui inserire il valore;
• la verifica dell’orario la faremo con un if().

Quindi per creare un timer non bloccante:


1. fuori dal loop() creiamo una variabile postit1 in cui memorizzare
l’istante iniziale;
2. nel loop() controlliamo più spesso possibile l’ora con millis();
3. confrontiamo la differenza di tempo tra postit1 e millis();
4. quando la differenza millis()-postit1 è pari al tempo desiderato,
attiviamo l’azione temporizzata.

Utilizziamo lo schema di Figura 3.18 e creiamo all’inizio dello sketch il


nostro timer/post-it come una variabile:
unsigned long postit1;

Il tipo utilizzato è unsigned long perché millis() ritorna numeri che


crescono moltissimo. Il valore massimo memorizzabile in questa variabile è
pari a [Link] ms che corrispondono a una cinquantina di giorni.
Possiamo quindi fare delle pause lunghissime che superano anche il limite
massimo imposto da delay() di 30 secondi.
Nel setup() inseriamo dentro a postit1 il tempo inziale fornito da
millis():

postit1 = millis();

Nel loop() controlleremo costantemente la differenza tra il tempo corrente,


dato da millis() e quello salvato nella variabile postit1:
millis() – postit1

Decidiamo di far scattare il nostro timer dopo 3 s, cioè 3000 ms,


utilizzeremo un if() che attiverà il LED allo scadere del tempo:
if ((millis() – postit1) > 3000) {
digitalWrite(26, HIGH);
}
Una volta scaduto il tempo, l’espressione controllata da if() risulterà
sempre vera e quindi continueremo a dare il comando di accensione del pin,
ma in questo caso non è un problema. Ecco il codice completo che
possiamo caricare sulla scheda utilizzando le connessioni riportate in Figura
3.18:
//creo il mio post-it / timer
unsigned long postit1;

void setup() {
pinMode(26, OUTPUT);

//segno l'ora iniziale sul post-it


postit1 = millis();
}

void loop() {
if ((millis() – postit1) > 3000) {
digitalWrite(26, HIGH);
}
}

Listato 3.15 – Codice per creare un semplice “timer” utilizzando la funzione millis().

Come possiamo modificarlo per far lampeggiare il LED ogni secondo?


Dobbiamo semplicemente “riarmare” il timer quando scatta. All’interno
dell’if(), prima di uscire, scriviamo di nuovo il valore di millis() nella
variabile postit1, in questo modo riattiviamo il timer. Per far cambiare di
stato il pin a ogni attivazione del timer possiamo usare la scrittura:
digitalWrite(26, !digitalRead(26));

I pin del microcontrollore sono gestiti con dei registri. Anche se un pin è
utilizzato come un’uscita, possiamo leggere il suo stato utilizzando
digitalRead(). Nell’istruzione precedente impostiamo il pin 26 leggendo
il suo stato e invertendolo con !. Se il LED è acceso, digitalRead(26)
fornirà HIGH, ma la negazione posta di fronte a digitalRead() imposterà il
pin al valore opposto. Questa “elegante” scrittura ci permette di far
lampeggiare il LED con una sola riga di codice. Ecco il listato completo per
un Blink non bloccante a un secondo:
//creo il mio post-it / timer
unsigned long postit1;
void setup() {
pinMode(26, OUTPUT);

//segno l'ora iniziale sul post-it


postit1 = millis();
}

void loop() {
if ((millis() – postit1) > 1000) {
digitalWrite(26, !digitalRead(26));

//aggiorno la nuova scadenza


postit1 = millis();
}
}

Listato 3.16 – Codice per un blink non bloccante.

Apparentemente non vediamo differenze con un blink normale. Il loop() in


questo caso non si “rallenta” mai e inserendo le istruzioni per leggere lo
stato di un pulsante potremo apprezzare la reattività del nostro nuovo
codice. Ecco il codice del blink con aggiunto il pulsante come dallo schema
di Figura 3.18:
//creo il mio post-it / timer
unsigned long postit1;

void setup() {
pinMode(24, INPUT);
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);

//segno l'ora iniziale sul post-it


postit1 = millis();
}

void loop() {
//lettura pulsante
int v = digitalRead(24);
digitalWrite(25, v);
if ((millis() – postit1) > 1000) {
digitalWrite(26, !digitalRead(26));
postit1 = millis();
}
}
Listato 3.17 – Codice per un blink non bloccante con lettura di un pulsante.

Per essere ancora più rapidi e intercettare degli istanti infinitesimali può
fare comodo utilizzare la funzione micros() che fornisce il numero di
microsecondi dall’accensione della scheda. Se siamo interessati a
interazioni con gli esseri umani, i millisecondi sono più che sufficienti!

Porte seriali
La nostra ESP32 è dotata di almeno tre porte seriali che possiamo utilizzare
per vari scopi. L’utilizzo più frequente della porta seriale è per stampare
informazioni di “servizio” o di “debug” che ci permettono di capire cosa sta
accadendo a bordo della scheda. Una delle porte seriali è collegata alla
porta USB del kit di sviluppo e per abilitarla sarà sufficiente impostare la
velocità di trasmissione desiderata nel setup():
void setup() {
[Link](115200);
}

La velocità è espressa in baud o “simboli al secondo” e possiamo scegliere


vari valori. Nell’ambiente Arduino è prassi utilizzare 9600 baud, ma con
schede molto prestanti possiamo utilizzare trasmissioni più rapide, per
esempio a 115.200. È sempre importante, in una trasmissione seriale, che
trasmettitore e ricevitore si accordino sulle modalità di trasmissione e sulla
velocità. Impostando una velocità errata potreste non ricevere nulla oppure
una sequenza intellegibile di simboli senza senso. Con la Serial possiamo
inviare informazioni tramite la porta USB al computer che stiamo
utilizzando per programmare la scheda. Nell’ambiente Arduino troviamo il
Serial Monitor che possiamo aprire per leggere le informazioni inviate dalla
ESP32. Quando aprite il Serial Monitor controllate che la velocità impostata
sia la stessa di quella utilizzata dalla ESP32. La velocità del Serial Monitor
si modifica con un menu a tendina posto nella parte bassa della finestra.
Una volta modificata la velocità conviene chiudere e riaprire la finestra per
confermare le nuove impostazioni.
Figura 3.19 - Serial Monitor in Arduino IDE: premendo (1) si apre il pannello (2) nella parte
inferiore della finestra. Verificate che i baud (3), cioè la velocità di trasmissione, siano impostati sul
valore corretto.

Per inviare informazioni al Serial Monitor di Arduino IDE da ESP32


scriveremo:
[Link]("hello");

Con questa scrittura inviamo un testo o una stringa e poi inviamo anche i
caratteri per andare a capo \n\r. Se volete stampare messaggi senza andare
a capo usate print(). Ecco un programma d’esempio per stampare alcuni
messaggi e un contatore:
int counter = 0;

void setup() {
[Link](115200);
[Link]("Hello ESP32!");
}

void loop() {
[Link]("Count: ");
[Link](counter);
counter++;
delay(1000);
}

Listato 3.18 – Codice per inviare messaggi al Serial Monitor.

ESP32 ha altre due seriali oltre a quella “standard” chiamate Serial2 e


Serial3. Queste seriali occupano quattro pin per una perché oltre ai pin per
trasmettere e ricevere dati hanno anche i pin per il controllo di flusso (CTS
e RTS). Per le applicazioni più comuni i pin RX e TX sono più che
sufficienti. Il pin CTS (Clear To Send) serve per segnalare alla scheda che
un dispositivo esterno è pronto per scambiare dati mentre il pin RTS
(Request To Send) è utilizzato da ESP32 per segnalare al dispositivo
collegato che è pronto per scambiare dati. La piedinatura standard delle due
seriali è riportata nella seguente Tabella 3.4.
Tabella 3.4 – Le seriali su ESP32 e la posizione dei loro pin.

Le seriali sono identificate con la sigla UART (Universal Asynchronous


Receiver/Transmitter) e inizialmente sono associate ai pin come indicato in
Tabella 3.4. Nulla vieta però di riassegnare i pin a piacimento perché per
esempio sul modulo ESP32 DevKit i pin 9 e 10 non sono accessibili! Il
fatto di poter riassegnare le funzioni dei pin rende questo tipo di hardware
molto flessibile e adattabile alle diverse situazioni che possiamo incontrare.
Possiamo assegnare i pin alle seriali come indicato qui di seguito:
void setup() {
[Link](115200);
[Link](115200, SERIAL_8N1, 25, 26); //RX, TX
[Link](115200, SERIAL_8N1, 12, 13); //RX, TX
}

void loop() {}

Listato 3.19 – Riassegnazione dei pin delle seriali.


La Serial1 è associata ai pin 25 (RX) e 26 (TX) mentre la Serial2 ai pin 12
(RX) e 13 (TX). L’istruzione di configurazione delle porte richiede però un
parametro aggiuntivo per specificare anche la modalità di trasmissione
(SERIAL_8N1). In questo modo la trasmissione avvien 8 bit per volta, non
c’è un bit di parità (N) e c’è un solo bit di stop (1). SERIAL_8N1 è
l’impostazione più comune.
L’istruzione print() prevede un secondo parametro opzionale che
possiamo utilizzare per formattare i numeri inviati sulla seriale. I possibili
valori dei parametri sono:
• BIN per stampare numeri in formato binario;
• OCT per stampare in formato ottale;
• DEC per stampare in formato decimale;
• HEX per stampare in formato esadecimale;
• un numero intero per specificare, nel caso di numeri con virgola, quanti
decimali far comparire dopo la virgola.

Ecco un esempio:
void setup() {
[Link](115200);
[Link](123, DEC);
[Link](123, BIN);
[Link](123, OCT);
[Link](123, HEX);
[Link](3.14159, 0);
[Link](3.14159, 2);
[Link](3.14159, 4);
}

void loop() {}

Listato 3.20 – Stampa con formattazione sul Serial Monitor.

Aprendo il Serial Monitor leggeremo:


123
1111011173
7B
3
3.14
3.1416

ESP32 offre un ulteriore metodo per stampare e formattare informazioni:


printf(template, variabili…);

Con questa istruzione possiamo fornire un template da stampare su Serial


Monitor a cui aggiungere il valore delle variabili fornite. Il template è una
stringa in cui possiamo scrivere quello che desideriamo, aggiungendo
speciali “segnaposto” che poi saranno sostituiti con i valori delle variabili
fornite. I segnaposto sono sempre preceduti dal segno %. A seguito del
template, forniremo tante variabili quanti sono i segnaposto indicati nel
template. I caratteri più usati nella formattazione sono:
• %n e %i per variabili intere;
• %f per numeri con virgola;
• %x per valori esadecimali;
• %s per array di caratteri (non stringhe!).

Per visualizzare due variabili decimali scriveremo:


int x = 123;
int y = 45;
[Link]("le coordinate sono: %i, %i \n", x, y);

Si noti che in fondo al pattern abbiamo aggiunto il carattere \n necessario


per andare a capo.
In alcuni casi possiamo intervenire sul numero visualizzato limitando per
esempio il numero di decimali come nel caso di una variabile di tipo float.
Per riportare solo due decimali dopo la virgola inseriremo .2 nel
segnaposto:
float y = 1.2;
[Link]("y: %.2f \n", y, y);

Analogamente per eseguire un padding, cioè per aggiungere degli zero a dei
numeri così che occupino tutti lo stesso spazio, indicheremo il carattere da
usare come riempimento, in questo caso uno 0 seguito dal numero di
ripetizioni. Nel seguente esempio creiamo una maschera con cinque 0 e la
utilizziamo per visualizzare un 3:
int n = 3;
[Link]("n con padding: %05i \n", n);

Il padding è molto utilizzato nel visualizzare date e orari. Nel seguente


listato troviamo vari esempi:
void setup() {
[Link](115200);

int x = 36;
float y = 1.2;

[Link]("x vale: %d \n", x);


[Link]("y: %f oppure y: %.2f \n", y, y);
char str[] = "Hello World!";
[Link]("La stringa: %s \n", str);

x = 0x1A;
[Link]("numero esadecimale : %02x \n", x);
int h = 12;

int m = 2;
int s = 3;
[Link]("orario : %02i:%02i:%02i \n", h,m,s);

void loop() {}

Listato 3.21 – Codice dimostrativo per l’utilizzo di printf.

Logging
Le piattaforme di sviluppo professionale includono anche dei “debugger”
che permettono di eseguire il codice passo per passo e seguirne
l’evoluzione. Lavorando con Arduino IDE dobbiamo utilizzare sistemi
meno sofisticati. Il metodo più semplice prevede l’utilizzo di
[Link]() per stampare quando necessario e riportare informazioni
sulle variabili e l’evoluzione del codice. Alcuni microcontrollori prevedono
anche la possibilità di attivare un semplice debugger anche tramite Arduino
IDE. Non sempre chi si avvicina a un sistema di sviluppo è in grado di
impostare il debugger. ESP32 offre un sistema alternativo con cui possiamo
produrre informazioni utili per comprendere come funzione il nostro
codice. Il sistema è definito logging e permette di riportare informazioni
solo quando necessario. Abbiamo a disposizione cinque livelli di logging:
None, Error, Warn, Info, Debug. Impostando il livello di log direttamente
da Arduino IDE possiamo poi far apparire le informazioni solo quando
necessario. Il livello di logging si imposta nel menu Tools > Core debug
level. Il livello di Debug è quello massimo, che riporta il maggior numero
di informazioni. Per stampare i messaggi non useremo le [Link]()
ma specifiche istruzioni:
log_v("Verbose");
log_d("Debug");
log_i("Info");
log_w("Warning");
log_e("Error");

Inseriremo queste istruzioni nei punti di nostro interesse. Per esempio


useremo log_e() per segnalare il verificarsi di errori, log_w() per avvisare
nel caso di situazioni particolari. Log_i() è adatta per mettere in evidenza
alcune fasi del funzionamento o il verificarsi di particolari eventi mentre
log_d() e log_v() si inseriscono per dare informazioni dettagliate circa il
funzionamento del programma o per avere indicazioni passo per passo su
quanto accade. Normalmente i livelli debug e verbose si usano in fase di
sviluppo o per cercare qualche errore. Selezionando dal menu il livello
desiderato attiveremo i messaggi corrispondenti. Ecco uno sketch
dimostrativo per stampare vari tipi di messaggi:
void setup() {
[Link](115200);

[Link](true);

log_v("Verbose");
log_d("Debug");
log_i("Info");
log_w("Warning");
log_e("Error");
}

void loop() {}

Listato 3.22 – Funzioni di logging in ESP32.

Con il livello di logging impostato su warn nel Serial Monitor leggeremo:


[ 35][W][2_18_serial_debug.ino:10] setup(): Warning
[ 35][E][2_18_serial_debug.ino:11] setup(): Error

Ricevere dati dalla seriale


Le porte seriali possono anche ricevere dati che possiamo utilizzare per
modificare il comportamento del nostro software. Il meccanismo di lettura
richiede un ciclo che sia sempre attivo perché i dati potrebbero arrivare in
ogni istante. Per questa ragione inseriremo le istruzioni di ricezione dati per
la porta seriale nel loop() dello sketch dove verificheremo di continuo con
l’istruzione [Link]() se ci sono dati in arrivo.
La funzione restituisce un numero che indica quanti byte riceveremo. Se
il numero di byte è maggiore di 0 viene interpretato come “true” logico e
possiamo quindi procedere alla lettura dei dati. Colleghiamo un LED al pin
25 della scheda come riportato nello schema di Figura 3.20 e apriamo una
nuova finestra di Arduino IDE.

Figura 3.20 – Schema elettrico di un LED collegato sul pin 25 della scheda ESP32.

Nel setup() configureremo la porta seriale a 115.200 baud e il pin 25 come


uscita:
void setup() {
[Link](115200);
pinMode(25, OUTPUT);
}

Nel loop() utilizzeremo [Link]() per attendere l’arrivo delle


informazioni. Se ci sono dati disponibili entreremo nel corpo dell’if() e
leggeremo carattere per carattere con [Link]():
if ([Link]()) {
char comando = [Link]();
}

Per inviare dei caratteri dal nostro computer alla scheda ESP32 possiamo
usare un qualsiasi terminale seriale (per esempio CoolTerm) oppure
direttamente dal Serial Monitor attivando il pannello (clic in alto a destra
sull’icona a forma di lente d’ingrandimento) e poi inserendo i caratteri
seguiti dal tasto INVIO nella casella di testo che troviamo nella parte alta
del Serial Monitor (vedi Figura 3.21).

Figura 3.21 – Invio di caratteri tramite il Serial Monitor di Arduino.


Salviamo i caratteri ricevuti nella variabile di tipo char, comando. Stiamo
ricevendo dei byte che trasformiamo in caratteri. Byte e caratteri sono
essenzialmente la stessa cosa. Confrontando il contenuto di comando con il
singolo carattere a, accenderemo il LED e scriveremo un messaggio nel
Serial Monitor:
if (comando == 'a') {
digitalWrite(25, HIGH);
[Link]("ON 25");
}

Inviando una s sul Serial Monitor spegneremo il LED e stamperemo un


messaggio:
if (comando == 's') {
digitalWrite(25, LOW);
[Link]("OFF 25");
}

Il codice completo dell’esercizio è riportato di seguito:


void setup() {
[Link](115200);
pinMode(25, OUTPUT);
[Link]("Serial read()");
}
void loop() {
if ([Link]()) {
char comando = [Link]();
if (comando == 'a') {
digitalWrite(25, HIGH);
[Link]("ON 25");
}
if (comando == 's') {
digitalWrite(25, LOW);
[Link]("OFF 25");
}
}
}

Listato 3.23 – Invio di caratteri su porta seriale per controllare lo stato di un pin.

Ogni volta che inviamo un carattere via seriale con il metodo appena
presentato dobbiamo ricordarci che non riceveremo solo una semplice a o
una s, infatti il Serial Monitor aggiunge automaticamente dopo il singolo
carattere anche l’informazione di “a capo” (carattere \n). È importante
tenerne conto quando riceviamo i dati e magari vogliamo interpretare
stringhe o strutture più complesse (si veda più avanti).

Ingressi analogici
Il nostro modulo ESP32 ha ben quindici ingressi analogici (ADC)
utilizzabili per rilevare segnali esterni provenienti da sensori. La
funzionalità ADC è condivisa con i pin GPIO e quindi in fase di
progettazione dovremo decidere quale pin utilizzare come ingresso
analogico o digitale. Un’altra limitazione è legata all’utilizzo del WiFi: con
il sistema attivo, non è possibile usare alcuni dei pin! Gli ADC a bordo
della nostra scheda sono a 12 bit, il che significa che il segnale applicato
(solitamente una tensione da 0 a 3,3 V) sarà ricondotto a un numero che va
da 0 a 4095. La tensione minima rilevabile sarà quindi di:

3300 / 4096 = 0,81 mV.

Se necessario il numero di bit del convertitore ADC può essere ridotto via
software (possiamo impostarlo da 9 a 12 bit).
I circuiti di conversione analogico-digitale sono due, collegati a quindici
canali. L’ADC2 non è utilizzabile se è attivo il WiFi! Quando assegnate i
pin analogici, partite sempre dai canali collegati all’ADC1. Nel caso i pin
non fossero sufficienti e dobbiate utilizzare il WiFi, potete adottare delle
schede di espansione aggiuntive (per esempio modulo ADC via I2C). Di
seguito una tabella con la mappatura dei pin.
Tabella 3.5 – Mappatura dei pin analogici su ESP32.

Pin digitale Canale ADC


GPIO36 ADC1_0

GPIO39 ADC1_3

GPIO34 ADC1_6

GPIO35 ADC1_7

GPIO32 ADC1_4

GPIO33 ADC1_5
GPIO25 ADC2_8

GPIO26 ADC2_9

GPIO27 ADC2_7

GPIO14 ADC2_6

GPIO12 ADC2_5

GPIO13 ADC2_4

GPIO15 ADC2_3

GPIO2 ADC2_2

GPIO4 ADC2_0

Figura 3.22 – Posizione dei pin ADC su ESP32.

In un convertitore analogico digitale perfetto ogni valore del segnale


d’ingresso dovrebbe corrispondere a una lettura, a un numero tra 0 e 4095,
in modo univoco. Si dice che il comportamento del dispositivo è lineare.
Quando non c’è una corrispondenza precisa abbiamo una non-linearità e
questo è purtroppo il caso dei convertitori ADC interni degli ESP32 che
sono imprecisi “agli estremi” dell’intervallo di tensione previsto, cioè non
riescono a distinguere in modo univoco e preciso le tensioni molto prossime
a 0 e 3,3 V (una decina di mV attorno a 0 V e circa 25 mV a 3,3 V).
I pin non vanno configurate con pinMode() come accade per quelli
digitali e per la lettura di un ingresso analogico utilizzeremo la funzione:
analogRead(PIN);

Il numero da passare alla funzione è il numero del pin GPIO. Se si volesse


utilizzare il canale ADC1_6, il pin da specificare sarà il numero 34. Per
modificare la risoluzione in bit si userà:
analogReadResolution(numero_di_bit);

dove numero_di_bit è un numero tra 9 e 12. Se non si specifica la


risoluzione sarà usato il valore predefinito pari a 12 bit. Proviamo a
collegare un trimmer da 10 kΩ al pin 34 della scheda ESP32 come illustrato
in Figura 3.23.
Figura 3.23 – Utilizzo di un trimmer con collegamento al pin 34 di ESP32.

Il trimmer2 è una resistenza “variabile”, dotata di tre piedini. Intervenendo


con un cacciavite possiamo modificarne il valore da 0 al suo valore
massimo nominale. Collegando il trimmer tra GND e 3,3 V potremo
prelevare dal pin centrale una tensione “variabile” a piacere. Ruotando il
cursore centrale con un cacciavite la tensione andrà da 0 a 3,3 V e potremo
quindi leggerla con analogRead().
Ecco un esempio completo per leggere la tensione presente sull’ADC1_6
(pin GPIO 34):
void setup() {
[Link](115200);
delay(3000);
analogReadResolution(12); //da 9 a 12
}

void loop() {
int v = analogRead(34);
[Link](v);
delay(500);
}

Listato 3.24 – Lettura di un pin analogico.

La tensione rilevata sarà convertita in un numero tra 0 e 4095 e stampata sul


Serial Monitor. Potete anche aprire il Serial Plotter per visualizzare
graficamente l’andamento della grandezza. Il Serial Plotter è simile al Serial
Monitor e può essere aperto cliccando sul pulsante posto in alto a destra
sulla toolbar di Arduino.
Figura 3.24 – Utilizzo del Serial Plotter per visualizzare l’andamento dei segnali. Per aprire il Serial
Plotter utilizzate il pulsante sulla toolbar posto a sinistra rispetto a quello del Serial Monitor.

Uscite analogiche
Il modulo ESP32 dispone anche di due uscite analogiche (DAC – Digital to
Analogic Converter) utilizzabili per generare segnali di controllo da 0 a 3,3
V. I DAC purtroppo sono solo a 8 bit e generano 256 livelli, il che non li
rende molto adatti per generare segnali musicali. Nel caso servisse una
precisione maggiore possiamo collegare dei DAC esterni che utilizzano i
bus I2C o SPI con maggiore risoluzione (12 o 16 bit).
Tabella 3.6 – Mappatura dei pin analogici di uscita.

Pin digitale Canale DAC


GPIO25 DAC1

GPIO26 DAC2

Figura 3.25 – Posizione dei pin utilizzabili come uscite analogiche (DAC).
La funzione da utilizzare per produrre un segnale analogico è dacWrite() a
cui indicheremo il pin digitale da utilizzare e il valore da impostare (da 0 a
255):
dacWrite(gpio_pin, valore);

Ripetere istruzioni con il ciclo for


Un altro elemento fondamentale dei linguaggi di programmazione è la
possibilità di ripetere delle operazioni, così da far svolgere alla macchina
compiti ripetitivi al posto nostro. I linguaggi di programmazione prevedono
in genere due tipi di ripetizioni: cicli con ripetizioni o iterazioni predefinite,
oppure cicli che proseguono fino al verificarsi di una condizione.
Per ripetere una o più istruzioni per un numero predefinito di volte
utilizzeremo il ciclo for(), altrimenti, nel caso non si sappia quante volte
sia necessario proseguire con le ripetizioni, useremo l’istruzione while().
Se scriviamo un programma per stampare cento etichette, utilizzeremo un
ciclo for(), mentre se dobbiamo controllare una temperatura e attivare
un’azione quando supera un certo valore, sarà più indicato un ciclo while()
perché non sappiamo in anticipo quale sarà l’andamento della temperatura.

Figura 3.26 – Schema a blocchi (non formale) di un ciclo for che esegue dieci ripetizioni. All’inizio
del ciclo è necessario fornire una variabile (in questo caso indicata con i) che faccia da contatore. I
cicli for possono eseguire diversi tipi di “conteggi”.

Il ciclo for() è composto da:


• una variabile utilizzata per contare le ripetizioni, impostata a un certo
valore iniziale;
• un’espressione che indica quante ripetizioni effettuare;
• una regola per incrementare la variabile;
• il blocco di istruzioni da ripetere.

Nel linguaggio C/C++ l’inizializzazione della variabile, il limite da


raggiungere e la regola d’incremento sono raccolte all’interno di due
parentesi tonde come indicato di seguito:
for (variabile; limite; incremento) {
//istruzioni da ripetere
}

Per un conteggio fino a 10 possiamo scrivere:


for (int i = 0; i < 10; i = i + 1) {
[Link]("i: "); [Link](i);
}

Il codice usa una [Link]() per stampare sul Serial Monitor il


valore della variabile i. La variabile è stata creata direttamente all’interno
delle parentesi di definizione del ciclo for. L’incremento della variabile è
indicato con l’espressione:
i = i + 1

L’espressione significa: “prendi il valore di i e sommagli uno, quindi


riassegna il risultato a i”. I programmatori preferiscono però utilizzare la
forma abbreviata per l’incremento con l’operatore ++:
i++

Ecco un esempio di ciclo for più compatto:


for (int i = 0; i < 10; i++) {
[Link]("i: ");
[Link](i);
}

Per vedere il ciclo in azione possiamo aggiungerlo a uno sketch di base. Se


vogliamo eseguirlo una sola volta lo metteremo nel setup():
void setup() {
[Link](115200);
for (int i = 0; i < 10; i++) {
[Link]("i: ");
[Link](i);
}
}

void loop() {}

Listato 3.25 – Utilizzo di un ciclo for per stampare una sequenza di numeri.

La variabile i può anche essere una variabile preesistente e non è


obbligatorio definirla all’interno del ciclo for, per esempio possiamo
riutilizzarne una preesistente:
int i = 0;

void setup() {
[Link](115200);

for (i = 0; i < 10; i++) {


[Link]("i: "); [Link](i);
}
for (i = 10; i >= 0; i--) {
[Link]("i: "); [Link](i);
}
}
void loop() {}

Listato 3.26 – Utilizzo di due cicli for per realizzare un conteggio in avanti e poi all’indietro.

Nell’esempio precedente troviamo un secondo esempio di ciclo for, in cui


decrementiamo la variabile realizzando un conteggio alla rovescia. La
variabile è impostata inizialmente a 10 e poi il suo valore è decrementato di
uno a ogni passaggio con la scrittura abbreviata: i--. Il conteggio può
procedere anche a passi maggiori di uno, basta semplicemente modificare
l’operazione di incremento. Provate per esempio a eseguire il seguente
ciclo:
for (int i = 0; i < 10; i = i + 2) {
[Link]("i: "); [Link](i);
}

Generare segnali analogici


Proviamo a combinare i cicli for con la generazione di un livello analogico
per generare dei segnali triangolari. Possiamo poi visualizzare il segnale
generato collegando un LED in serie con una resistenza da 100 Ω al pin 25
oppure con un oscilloscopio. Utilizzando un ciclo for con 256 passi (da 0 a
255) incrementeremo il valore di un contatore i che passeremo a
dacWrite():

for (int i = 0; i < 256; i++) {


dacWrite(25, i);
delay(1);
}
Per rallentare un po’ il segnale generato inseriremo anche un delay di 1
millisecondo. Questo ritardo “rallenterà” la forma d’onda definendo anche
la sua frequenza. Potete creare forme d’onda più rapide sostituendo a
delay(), delayMicroseconds(). Il primo ciclo for() crea la “salita”, poi
dovremo riportare il segnale a “0” con un secondo ciclo che parte da 255 e
scende fino a 0:
for (int i = 255; i >= 0; i--) {
dacWrite(25, i);
delay(1);
}

I due cicli for() sono inseriti nel loop() così che siano ripetuti all’infinito.
Ecco il codice completo:
void setup() {}

void loop() {
for (int i = 0; i < 256; i++) {
dacWrite(25, i);
delay(1);
}
for (int i = 255; i >= 0; i--) {
dacWrite(25, i);
delay(1);
}
}

Listato 3.27 – Generare un’onda triangolare con i DAC della scheda ESP32.

Figura 3.27 – L’onda “triangolare” visualizzata con un oscilloscopio.

Per generare un’onda sinusoidale possiamo utilizzare un ciclo for() ma la


tecnica è differente. Prima di iniziare salveremo i valori della sinusoide in
un array che poi leggeremo continuamente all’interno del loop() dello
sketch. Il codice genera quindi una forma d’onda sinusoidale tramite
l’utilizzo di una tabella precalcolata contenente i valori della sinusoide.
Questa tecnica consente di generare una forma d’onda continua e fluida sul
pin DAC1 dell’ESP32. Il valore del campione corrente viene letto dall’array
e scritto sul pin DAC1 a intervalli regolari per generare la forma d’onda
sinusoidale desiderata. Possiamo impiegare questa tecnica per generare
forme d’onda di qualsiasi tipo, popolando opportunamente l’array iniziale
che nel nostro caso ha 256 campioni:
int sinusoide[256];

Questo array verrà utilizzato per memorizzare i valori della forma d’onda
sinusoidale. Calcoliamo il passo dell’angolo per ogni campione della forma
d’onda sinusoidale, quindi prenderemo il valore 2 * PI (che rappresenta un
ciclo completo, cioè 360° ma espressi in gradi radianti) diviso per 256, il
numero di campioni desiderato:
float passo = (2.0 * PI) / 256.0;

Utilizziamo un ciclo for() per calcolare il valore dei campioni da inserire


nell’array:
for (int i = 0; i < 256; i++){}

All’interno del ciclo calcolo l’angolo corrispondente a ciascun campione


moltiplicando il passo per l’indice corrente del ciclo:
for (int i = 0; i < 256; i++){
float angolo = passo * (float)i;
}

L’indice è di tipo intero, mentre l’angolo è un numero con virgola. Per


questo motivo, al fine di effettuare calcoli corretti e con i decimali,
convertiamo il contatore in un numero con virgola utilizzando la scrittura
(type cast):
(float)i

A questo punto, avendo l’angolo possiamo calcolare il valore assunto dalla


funzione sin(). Il seno è compreso tra –1 e 1, mentre noi vogliamo
amplificarlo così cha vada da 0 a 255. Se lo moltiplicassimo semplicemente
per 127 ci ritroveremo un numero che va da –127 a 127 e che manderebbe
in errore dacWrite(). Dobbiamo quindi aggiungere un offset di 128 unità
così che i campioni della sinusoide stiano esattamente tra 0 e 255:
for (int i = 0; i < 256; i++){
float angolo = passo * (float)i;
float val = (sin(angolo) * 127.0) + 128.0;
}
Inseriamo quindi il valore calcolato nell’array:
for (int i = 0; i < 256; i++){
float angolo = passo * (float)i;
float val = (sin(angolo) * 127.0) + 128.0;
sinusoide[i] = val;
}

Nel loop() inseriamo un secondo ciclo for() da 0 a 255 che legge i valori
dell’array e li applica a dacWrite():
void loop() {
for (int i = 0; i < 256; i++) {
dacWrite(25, sinusoide[i]);
delay(1);
}
}

Nel ciclo for() abbiamo un breve ritardo di 1 millisecondo tra ogni


campione per ottenere la frequenza desiderata della forma d’onda
sinusoidale. Il segnale è prelevabile dal pin 25 dove possiamo collegare un
LED con resistenza in serie da 100 Ω per poter vedere l’effetto prodotto
oppure un oscilloscopio. Ecco il codice completo:
int sinusoide[256];

void setup() {
[Link](115200);
delay(1000);

float passo = (2.0 * PI) / 256.0;


for (int i = 0; i < 256; i++) {
float angolo = passo * (float)i;
//[Link](angolo);
float val = (sin(angolo) * 127.0) + 128.0;
sinusoide[i] = val;
//[Link](val);
}
}

void loop() {
for (int i = 0; i < 256; i++) {
dacWrite(25, sinusoide[i]);
delay(1);
}
}

Listato 3.28 – Generare un’onda sinusoidale con i DAC della scheda ESP32.

Figura 3.28 – L’onda sinusoidale visualizzata con un oscilloscopio.

Ripetere senza fine: il ciclo while


A volte abbiamo la necessità di ripetere delle istruzioni fino a che è
necessario oppure fino al verificarsi di un evento. Un ciclo for non è adatto
perché prevede che il numero di ripetizioni sia predefinito. Possiamo
utilizzare allora un ciclo while(), cioè una struttura di controllo utilizzata
per ripetere un blocco di codice fino a quando una specifica espressione
risulta falsa. L’espressione è valutata e, se è vera, il blocco di codice è
eseguito. Dopo ogni esecuzione, l’espressione è nuovamente valutata e, se
risulta ancora vera, il blocco di codice viene eseguito di nuovo, e così via,
fino a quando la condizione diventa falsa:
while (espressione) {
//codice da ripetere
}

Se non abbiamo un blocco di codice ma solo un’istruzione possiamo


omettere le parentesi graffe e usare la forma più compatta:
while (espressione) istruzione_da_ripetere;

Quando si utilizza un ciclo while c’è sempre il rischio di rimanere


intrappolati e di non uscire mai. A volte questa situazione è voluta…
possiamo realizzare un ciclo che non termina mai inserendo un’espressione
sempre vera:
while (true);

Inserendo questa istruzione in un punto del codice ne bloccheremo


l’esecuzione. Questa è una tecnica usata dai programmatori in fase di test
del codice, per inserire un blocco. Normalmente invece cercheremo di
assicurarci che qualcosa possa intervenire per modificare il valore
dell’espressione controllata e quindi far terminare il ciclo. L’uscita del ciclo
è quindi determinata dal codice inserito nel suo corpo. A volte la condizione
di termine dipende da eventi esterni (la lettura di un sensore o altro) che
però saranno sempre letti o controllati all’interno del corpo del while().

Figura 3.29 – Diagramma di flusso di un ciclo while: prima di entrare nel ciclo viene eseguito un test
e se ha esito positivo il programma inizia a eseguire e ripetere le istruzioni che compongono il corpo
del ciclo fino a che la condizione di test è vera.

Possiamo usare un ciclo while anche come alternativa a un for, per eseguire
un conteggio. Utilizzeremo una variabile da usare come contatore e sarà
nostra cura incrementarne il valore. Ecco un semplice esempio per stampare
numeri da 0 a 9:
int i = 0;
while (i < 10){
[Link](i);
i++;
}

L’implementazione e il controllo del ciclo sono lasciati interamente a noi ed


è possibile che qualcosa vada storto e che per qualche motivo il ciclo non si
interrompa mai. Vediamo un esempio: immaginate di voler modificare il
ciclo che conteggia da 0 a 9 e di voler stampare al posto del numero 5, la
parola “five”. Modifichiamo il ciclo in questo modo:
int i = 0;
while (i < 10) {
if (i == 5) {
[Link]("five");
} else {
[Link](i);
i++;
}
delay(1000);
}

Per stampare la parola, abbiamo inserito un if() che controlla il valore di i


e se questo è pari a 5, allora stampa una parola al posto del numero. Negli
altri casi stampiamo il numero e incrementiamo il contatore. Purtroppo
abbiamo commesso un errore e il ciclo continuerà a stampare sempre e solo
“five”! Infatti inizialmente, fino a che i vale meno di 5, il suo valore cresce
(saremo sempre nell’else dell’if). Quando e se i vale 5, stamperemo la
parola ma non abbiamo nessun incremento della variabile e quindi
resteremo bloccati per sempre nel ciclo. Prestate attenzione a queste
situazioni! Se usate un while per esempio per tentare la connessione a una
rete, prevedete sempre un numero massimo di tentativi o un tempo di time-
out!

Do while
Il C prevede anche il ciclo do-while, una struttura di controllo simile al
ciclo while, ma con una differenza importante: il blocco di codice viene
eseguito almeno una volta, prima di valutare la condizione di uscita. Il
corpo del ciclo è quindi eseguito e solo alla fine abbiamo il test della
condizione. Se l’espressione fornita è vera, allora proseguiremo con la
prossima iterazione. Ecco la sintassi del ciclo:
do {
// Codice da ripetere
} while (espressione);

Ecco un esempio di utilizzo del ciclo do-while in cui attendiamo un


carattere dalla seriale e lo stampiamo. Se il carattere è una x, allora il ciclo
termina:
char c;
do {
if ([Link]()) {
c = [Link]();
[Link](c);
}
} while (c != 'x');
[Link]("uscito");

Il ciclo do-while è particolarmente utile in situazioni in cui si desidera


eseguire un’operazione almeno una volta, indipendentemente dalla
condizione di uscita.

Figura 3.30 – Diagramma di flusso di un ciclo do-while: il corpo del ciclo è eseguito completamente
almeno una volta prima di valutare l’espressione di controllo ed eventualmente ripetere il ciclo.

Il loop() come ciclo


Ogni sketch contiene un ciclo che è ripetuto all’infinito. Questo serve per
mantenere “attivo” il vostro codice nel tempo. Il loop() non ha tempi di
esecuzione garantiti e il tempo necessario per essere eseguito di nuovo
dipende solamente dal codice presente al suo interno, per questo si cerca di
evitare situazioni bloccanti (come l’uso di delay()) così che il nostro
software sia molto reattivo.
A volte fa comodo usare il loop come un vero e proprio ciclo per
svolgere operazioni ripetitive. L’esempio più comune è nella realizzazione
del classico “blink” dove il LED lampeggia sfruttando la ripetizione offerta
dal loop().

Continue e break
break e continue sono due istruzioni che potrebbero essere utili quando
lavoriamo con dei cicli (for, while o do-while). Queste istruzioni sono
utilizzate per modificare il flusso di esecuzione all’interno di cicli o
strutture di controllo in C.
Quando utilizziamo l’istruzione continue all’interno di un ciclo, il
controllo salta immediatamente alla successiva iterazione, ignorando il
codice restante all’interno del blocco di codice. In altre parole, se
l’istruzione continue viene eseguita, il codice successivo all’istruzione
continue all’interno del ciclo verrà ignorato e il ciclo passerà all’iterazione
successiva. Ecco un esempio con un ciclo for in cui saltiamo una delle
iterazioni:
for (int i = 0; i < 10; i = i + 2) {
if (i == 3) continue;
[Link]("i: "); [Link](i);
}

In questo esempio, un ciclo for esegue un conteggio da 0 a 9 stampando i


numeri sul Serial Monitor. Quando il contatore i assume il valore 3, viene
eseguita l’istruzione continue, che fa saltare l’iterazione corrente. Di
conseguenza, la riga di codice successiva alla continue non verrà eseguita
e il ciclo passerà all’iterazione successiva. Questo significa che il valore 3
non verrà stampato sulla porta seriale.
L’istruzione break posta all’interno di un ciclo fa terminare
immediatamente le ripetizioni: il ciclo si interrompe e il controllo passa a
eseguire le istruzioni successive al blocco di controllo. Ecco un esempio di
utilizzo di break per uscire da un ciclo:
char c;
while(true) {
if ([Link]()) {
c = [Link]();
[Link](c);
if (c == 'x') break;
}
}
[Link]("uscito");

In questo esempio leggiamo i caratteri provenienti dalla seriale utilizzando


un ciclo while che non ha mai termine. Se il carattere ricevuto è una x,
viene eseguita l’istruzione break, che fa uscire immediatamente dal ciclo
while. In questo modo, il programma passerà alle istruzioni successive al
blocco di controllo del ciclo.
break e continue si usano solitamente per gestire situazioni impreviste o
errori che si verificano all’interno di un ciclo.

Pulse Width Modulation


I segnali PWM (Pulse Width Modulation), in italiano modulazione a
larghezza di impulso, sono un particolare tipo di segnali digitali, spesso
impiegati per controllare la potenza fornita a motori elettrici o la luminosità
dei LED. Si possono confondere con segnali analogici, ma sono in realtà
una sequenza periodica di impulsi digitali, in cui la larghezza dell’impulso
(pulse width) viene variata. In un’onda quadra perfetta il segnale si ripete
con una certa frequenza e può assumere solo due valori: per metà del tempo
è alto e per l’altra metà è basso e quindi ha un duty cycle del 50%.
Modificando i tempi acceso/spento, otteniamo un duty cycle che può andare
dallo 0% (segnale assente) fino al 100% (segnale “tutto acceso”). Quando il
duty cycle assume un valore intermedio, anche il valore medio del segnale
si modificherà. Ci accorgiamo dell’effetto applicando un PWM a un LED:
al LED arriverà sempre un segnale pari a 0 o 3,3 V e con una certa
frequenza. Modificando il duty cycle vedremo il LED più o meno luminoso
ma in effetti il LED sarà acceso e spento molto rapidamente tanto che
l’occhio non si accorge di questi “sfarfallii” e percepirà il valore medio del
segnale applicato.
La frequenza del segnale PWM, ovvero il numero di volte che si ripete il
periodo in un secondo, determina la velocità di aggiornamento del segnale e
può influenzare l’applicazione specifica. Frequenze più elevate tendono a
ridurre il flicker (sfarfallio) e a fornire una risposta più rapida, ma possono
richiedere più risorse di elaborazione.
Sulla scheda ESP32 possiamo prelevare un segnale PWM da ben 21 pin.
Il generatore PWM interno ha 16 canali indipendenti che possono essere
collegati a uno o più pin in varie configurazioni. Per generare un segnale
PWM sulla scheda ESP32 possiamo utilizzare la libreria “ledc” di Arduino
che troviamo già inclusa nell’ambiente di sviluppo.
I pin GPIO da cui possiamo prelevare un segnale PWM sono: 1, 3, 2, 4,
5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33. A differenza
di Arduino dove l’utilizzo del PWM è immediato, su ESP32 è necessaria
una semplice configurazione, infatti prima di tutto bisogna scegliere quale
canale utilizzare (da 0 a 15), che frequenza impostare e il numero di bit (o
la risoluzione).

Figura 3.31 – Pin PWM di una ESP32.

Su Arduino abbiamo solo 256 livelli che corrispondono a 8 bit ma con


ESP32 possiamo selezionare la risoluzione da 1 a 16 bit! Ecco la sintassi
dell’istruzione di configurazione:
ledcSetup(canale_pwm, frequenza, risoluzione_in_bit);

La frequenza è espressa in Hz: valori tipici per controllare LED e motori


sono tra 1000 e 5000. Una volta configurato il generatore lo assoceremo al
pin desiderato (uno o più contemporaneamente) utilizzando:
ledcAttachPin(pin_gpio, canale_pwm);

Infine potremo modificare il duty cycle con un’istruzione simile ad analog-


Write():

ledcWrite(canale_pwm, duty_cycle);

Osservate che stiamo controllando il canale e non il singolo pin! Il valore da


assegnare al duty cycle dipende dalla risoluzione impostata: con 8 bit sarà
da 0 a 255, con 12 andremo da 0 a 4095 e così via. Colleghiamo due LED
con resistore da 100 Ω ai pin 4 e 5 della scheda e proviamo a controllarli
con il seguente codice:
void setup() {
int ch = 0;
int freq = 1000;
int res = 8;
ledcSetup(ch, freq, res);

int ch2 = 1;
int freq2 = 3000;
int res2 = 8;
ledcSetup(ch2, freq2, res2);

int pin1 = 4;
ledcAttachPin(pin1, ch);

int pin2 = 5;
ledcAttachPin(pin2, ch2);
ledcWrite(1, 10);
}

void loop() {
for (int i = 0; i < 256; i++) {
ledcWrite(0, i);
delay(5);
}
for (int i = 255; i >= 0; i--) {
ledcWrite(0, i);
delay(5);
}
}

Listato 3.29 – Generare segnali PWM con ESP32.

Figura 3.32 – Schema elettrico per verificare i segnali PWM con due LED collegati tramite resistori
da 100 Ω ai pin 4 e 5 di una scheda ESP32.

Nel setup() dello sketch impostiamo il PWM. Utilizzeremo due canali. Per
il primo impostiamo una frequenza di 1000 Hz e usiamo 8 bit:
int ch = 0;
int freq = 1000;
int res = 8;
ledcSetup(ch, freq, res);
Il secondo canale avrà una frequenza di 3000 Hz e sempre 8 bit:
int ch2 = 1;
int freq2 = 3000;
int res2 = 8;
ledcSetup(ch2, freq2, res2);

Quindi associamo il pin 4 al primo canale PWM:


int pin1 = 4;
ledcAttachPin(pin1, ch);

Il secondo canale è collegato al pin 5:


int pin2 = 5;
ledcAttachPin(pin2, ch2);

Sempre nel setup() impostiamo il duty cycle a 10 per il canale 1 così che il
LED sul pin 5 possa accendersi:
ledcWrite(1, 10);

Vedremo quindi un LED debolmente illuminato. La luminosità del secondo


LED è controllata nel loop(), utilizzando due cicli for() che aumentano e
diminuiscono il duty cycle così che il LED possa accendersi e spegnersi
gradualmente:
for (int i = 0; i < 256; i++) {
ledcWrite(0, i);
delay(5);
}
for (int i = 255; i >= 0; i--) {
ledcWrite(0, i);
delay(5);
}

Ricordatevi che la disponibilità dei canali PWM e dei relativi pin possono
variare a seconda del modello specifico di scheda ESP32. Consultate
sempre la documentazione ufficiale o il pinout del vostro modello per avere
informazioni circa i pin supportati.

Touch pin
Una delle caratteristiche abbastanza esotiche della scheda ESP32 è la
possibilità di utilizzare nativamente alcuni dei suoi pin come ingressi touch
senza aggiungere particolare hardware. Per utilizzare i contatti capacitivi è
sufficiente collegare un jumper a uno di questi pin. Volendo realizzare
qualcosa di più efficiente potreste collegare al jumper una piccola
placchetta metallica.
I sensori touch capacitivi funzionano sfruttando la capacità elettrica tra di
noi e il sensore.
La rilevazione capacitiva si basa sul fatto che gli oggetti conduttivi (come
il nostro corpo) hanno una capacità intrinseca che può essere misurata.
Quando tocchiamo un sensore touch capacitivo, creiamo una variazione nel
campo elettrico del sensore. Misurando questa variazione di capacità
possiamo interpretare se c’è stato un contatto. I sensori capacitivi
dell’ESP32 sono una decina e possono essere utilizzati per realizzare
controlli touch con il vantaggio di non avere contatti meccanici e di poter
essere posti al di sotto di scocche e pannelli (non metallici). Ogni pin è
identificato da un numero e sono collegati ai pin GPIO come riportato nella
seguente tabella.
Tabella 3.7 – Mappatura pin con touch sulla scheda ESP32 DevKitC (schede differenti
potrebbero presentare diverse configurazioni).

Pin Touch Pin GPIO


0 4

1 0 (non accessibile)

2 2

3 15

4 13

5 12

6 14

7 27

8 32

9 33

I pin di tipo touch sono anche indicati con il termine “canali touch” e sono
immediatamente rilevabili utilizzando il comando:
touchRead(numero_pin_touch);

Il parametro da fornire a touchRead() è il numero del pin touch e non il pin


GPIO. L’istruzione restituirà un valore intero, e quindi una lettura analogica
proporzionale al campo elettrico rilevato.
Per identificare un tocco dovremo quindi verificare se la lettura supera
una certa soglia.
Quindi conviene prima fare delle prove utilizzando uno sketch che legga
il sensore e che stampi il valore di soglia. Collegate un jumper al pin GPIO2
che corrisponde al canale touch 2 (vedi Figura 3.34), quindi caricate il
seguente sketch:
void setup() {
[Link](115200);
delay(1000);
}

void loop() {
int v = touchRead(2);
[Link](v);
delay(200);
}

Listato 3.30 – Esempio di utilizzo di un touch pin.

Dopo aver compilato e caricato lo sketch, aprendo il Serial Monitor


potremo leggere il valore letto dal sensore. Provate ad avvicinare il dito e
annotatevi i valori massimi e minimi della lettura. Quindi scegliete una
soglia che sia posta a metà. Per esempio se senza dito leggete 1000 e con il
dito appoggiato leggete 100, potreste scegliere una soglia pari a 500.
Utilizzate un if() per discriminare il tocco. Collegate un LED con
resistore al pin 4 come riportato nello schema di Figura 3.34 e provate il
seguente sketch (dopo aver modificato il valore della soglia coerentemente
con il vostro esperimento):
int soglia = 500;

void setup() {
[Link](115200);
delay(1000);
[Link]("Touch LED");

//imposto il pin 4 come uscita


pinMode(4, OUTPUT);
}

void loop() {
int v = touchRead(2);
if (v < soglia) {
digitalWrite(4, HIGH);
} else {
digitalWrite(4, LOW);
}
}

Listato 3.31 – Controllare un LED con un pin touch.

Figura 3.33 – Posizione dei touch pin su una scheda ESP32.

Figura 3.34 – Schema elettrico per utilizzare un touch pin collegato sul pin 2 per accendere un LED
sul pin 4. Il sensore touch può essere realizzato fissando una piccola piastrina di metallo a un jumper
con una saldatura oppure con un piccolo rettangolo di alluminio per alimenti.

Questo listato purtroppo in alcune situazioni potrebbe non funzionare


correttamente perché il valore della soglia è fisso. Spostando il circuito o
variando le condizioni esterne (anche atmosferiche) il valore di soglia
potrebbe non essere più adatto. Possiamo renderlo dinamico facendo,
all’accensione, una serie di letture del valore ricavato dal sensore e
calcolando una media con cui determinare la soglia. Creiamo una variabile
valmedio di tipo intero e poi leggiamo per dieci volte il valore rilevato dal
touch pin, sommandolo a valmedio:
int valmedio = 0;
for (int i = 0; i < 10; i++) {
valmedio += touchRead(2);
}

All’uscita del ciclo, valmedio contiene la somma di dieci letture, dividendo


il valore per 10 otterremo il valore medio rilevato:
valmedio = valmedio / 10;

Possiamo decidere di impostare la soglia all’85% del valore rilevato:


soglia = valmedio * 0.85;

Purtroppo sia soglia che valmedio sono variabili intere e moltiplicandole


per un numero con virgola perderemmo alcune parti ottenendo letture non
corrette. Convertiamo quindi valmedio in un float usando un type cast a
float:
soglia = (float)valmedio * 0.85;

Poi trasformiamo tutto il risultato di nuovo in un intero:


soglia = (int)((float)valmedio * 0.85);

Ecco lo sketch completo:


int soglia = 0;

void setup() {
[Link](115200);
delay(1000);
[Link]("Touch test");

pinMode(4, OUTPUT);

int valmedio = 0;
for (int i = 0; i < 10; i++) {
valmedio += touchRead(2);
}
valmedio = valmedio / 10;
soglia = (int)((float)valmedio * 0.85);
[Link]("vmedio: ");[Link](valmedio);
[Link]("soglia: ");[Link](soglia);
}

void loop() {
int v = touchRead(2);
if (v < soglia) {
digitalWrite(4, HIGH);
} else {
digitalWrite(4, LOW);
}
}

Listato 3.32 – Controllare un LED con un pin touch con calcolo automatico della soglia.
1
Chiaramente questo tipo di comportamento si potrebbe ottenere anche senza un microcontrollore.
Sapresti realizzare il circuito?
2
Per maggiori dettagli si veda Paolo Aliverti, Elettronica per Maker. Guida completa, LSWR.
4

Coding like a “PRO”

Introduciamo alcuni argomenti avanzati di


programmazione come puntatori, funzioni, strutture
ed enumerazioni. Impariamo a utilizzare gli sketch a
stati e alcune tecniche per programmare da esperti,
rilevando fronti di salita e discesa o applicando il
debouncing ai pulsanti.

Puntatori
Abbiamo detto che una variabile in C è una specie di cassetto in cui possiamo memorizzare
delle informazioni.
Il cassetto ha un nome di comodo che ci permette di richiamarlo per leggere il valore
contenuto o per modificarlo. Quando creiamo una variabile di tipo intero usiamo questa
scrittura:
int n = 123;

Il tipo di dati utilizzato (int) è necessario per poter riservare la quantità di memoria adatta: per
un tipo int serviranno 4 byte. La variabile è mantenuta nella memoria del microcontrollore.
Per poter verificare l’occupazione di memoria di una variabile possiamo usare sizeof():
void setup() {
[Link](115200);
int n = 123;
[Link]("Valore di n: %d, dimensioni: %d\n", n, sizeof(n));
}

void loop() {}

Listato 4.1 – Uso di sizeof() per verificare il numero di byte occupati da una variabile.
Utilizzando le variabili non ci preoccupiamo di dove sia effettivamente mantenuta
l’informazione all’interno della memoria. In effetti possiamo immaginare la memoria come un
grande nastro quadrettato in cui ogni riquadro ha un indirizzo, necessario per poter accedere.
La prima casella ha indirizzo 0, la seconda 1 e così via. Gli indirizzi sono solitamente espressi
in formato esadecimale. Nei programmi più semplici raramente ci serve sapere qual è
l’indirizzo di memoria di una cella.
Il linguaggio C prevede la possibilità di trattare anche con gli indirizzi delle variabili.
Possiamo ricavare l’indirizzo di una variabile anteponendo il carattere & al suo nome:
&n

L’indirizzo non è altro che un numero e quindi può essere memorizzato a sua volta in una
variabile che chiameremo puntatore. Il tipo di questa variabile è speciale. Per creare un
puntatore per la variabile n scriveremo:
int *ptr;

Abbiamo il tipo della variabile int seguito dal carattere speciale * e poi dal nome che
vogliamo dare al puntatore. Potremo poi assegnare l’indirizzo di una variabile di tipo int al
puntatore con:
ptr = &v;

Quando ho un indirizzo di memoria posso leggere il suo contenuto, cioè il valore presente
all’indirizzo, scrivendo:
int valore = *ptr;

Per modificare il valore nella posizione di memoria scriveremo:


*ptr = 456;

Ecco un esempio completo:


void setup() {
[Link](115200);

//una variabile
int n = 123;

//puntatore a variabile di tipo int


int *ptr;

//ricavo l'indirizzo di n e lo assegno al puntatore


ptr = &n;

//stampo il contenuto puntato dal ptr


[Link]("Valore puntato: %d indirizzo: %p\n", *ptr, ptr);

//modifico il valore:
*ptr = 456;

//stampo il contenuto puntato dal ptr


[Link]("Valore modificato: %d indirizzo: %p\n", *ptr, ptr);
}

void loop() {}

Listato 4.2 – Esempio completo con i puntatori.

I puntatori offrono un metodo per accedere a porzioni di memoria e modificarle


presupponendo la conoscenza della posizione fisica delle informazioni. Per chi si occupa di
hardware potrebbe anche essere sensato, ma non lo è per un programmatore. I linguaggi di
programmazione servono per astrarre l’hardware e semplificare la scrittura del codice e per
non dover trattare con argomenti critici come la posizione dei dati nella memoria. Tuttavia in
alcuni casi è utile potersi scambiare l’indirizzo di un’informazione per permettere a un codice
che non ha la visibilità diretta sulla variabile di modificarla. Vedremo qualche esempio con i
parametri delle funzioni. Uno dei motivi per cui i puntatori incutono timore è la possibilità di
incappare in gravi errori, infatti trattando direttamente con posizioni di memoria potrebbe
capitare di andare a scrivere in indirizzi riservati o speciali e di combinare qualche pasticcio.

Array
Se una variabile può essere paragonata a un cassettino con applicata un’etichetta, un array è un
cassettino con nome e scomparti. Per accedere alle informazioni contenute utilizzeremo quindi
“il nome del cassettino” e un numero (indice) per specificare lo scomparto. Gli array in C/C++
hanno una dimensione finita, cioè un numero di scomparti deciso nel momento della loro
creazione. Per creare un array indicheremo quindi il tipo di dato che lo compone, il nome e la
dimensione:
int buf[10];

In questo modo abbiamo riservato dieci celle di memoria che possono contenere degli interi.
Possiamo creare un array fornendo subito il suo contenuto, se fosse già noto con:
int val[] = {1,3,5,7,9};

Per leggere un valore dell’array sarà sufficiente specificare la sua posizione con l’indice
racchiuso tra due parentesi quadre. L’indice parte sempre da 0. Se l’array ha dieci valori
l’indice andrà da 0 a 9. Per estrarre il primo elemento scriveremo:
buff[0]

Non sono effettuati controlli sull’indice e se specificate un valore superiore alle dimensioni
dell’array, leggerete da una posizione di memoria attigua qualsiasi cosa sia contenuto. Fate
attenzione!
Per assegnare un valore a un elemento dell’array ci comporteremo come se fosse una
comune variabile:
buff[1] = 123;

Di seguito riportiamo un esempio completo:


void setup() {
[Link](115200);
//un array di interi
int buff[10];

//array inizializzato
int val[] = {1,3,5,7,9};

//leggo un valore
[Link]("Valore: %d \n", val[0]);
[Link]("Valore: %d \n", val[1]);
[Link]("Valore: %d \n", val[2]);

//assegno un valore
buff[0] = 12;
[Link]("Valore: %d \n", buff[0]);

void loop() {}

Listato 4.3 – Esempio di utilizzo degli array.

Gli array ci permettono di automatizzare le elaborazioni e sono fondamentali nella


programmazione. Solitamente li troviamo combinati con dei cicli che ne scorrono i valori e li
modificano. Utilizzando l’array non inizializzato:
int buff[10];

possiamo popolarlo con un ciclo for() con indice i che va da 0 a 9. La variabile i del ciclo
può essere direttamente utilizzata come indice per l’array. Inseriamo nelle celle dei numeri pari
ottenuti moltiplicando i per due:
for (int i = 0; i < 10; i++) {
buff[i] = i*2;
}

Verifichiamo i valori inseriti con un secondo ciclo for():


for (int i = 0; i < 10; i++) {
[Link]("%d) Valore: %d \n", i, buff[i]);
}

Ecco il listato completo:


void setup() {
[Link](115200);

//un array di interi


int buff[10];

//array inizializzato con numeri pari


for (int i = 0; i < 10; i++) {
buff[i] = i*2;
}

//stampo l'array:
for (int i = 0; i < 10; i++) {
[Link]("%d) Valore: %d \n", i, buff[i]);
}
}

void loop() {}

Listato 4.4 – Popolare un array con ciclo for.

Array di caratteri
Un tipo particolare di array sono quelli composti da caratteri che sono usati nel C per
contenere testi. Con Arduino potete utilizzare anche gli oggetti String che offrono maggiore
flessibilità. Gli array di caratteri sono ancora molto utilizzati e vale la pena spendere qualche
parola per comprendere come funzionino. Possiamo definire un array vuoto ma specificando la
lunghezza:
char str[20];

In questo modo abbiamo a disposizione 20 celle per inserire un testo. Il grosso problema degli
array di caratteri è che hanno una lunghezza fissa e quindi il testo deve occupare al massimo
20 celle, meno una in cui si inserirà il carattere di fine stringa \0 o terminatore. Se non
vogliamo contare i caratteri della stringa possiamo lasciare il compito al compilatore scrivendo
semplicemente:
char msg = "Hello World!";

Questa stringa avrà quindi dodici caratteri più il terminatore. Come per gli array possiamo
accedere a singoli caratteri, sia per leggerli che per modificarli. Per modificare un carattere
scriveremo:
msg[6] = 'w';

Fate attenzione quando specificate dei caratteri singoli a utilizzare le virgolette singole e non
doppie. Per leggere un carattere e magari stamparlo sul Serial Monitor scriveremo:
[Link](msg[0]);

Ci sono operazioni che paiono scontate utilizzando gli oggetti stringa come per esempio la
concatenazione. Con gli array è necessario fare più attenzione e controllare le dimensioni.
Immaginiamo di avere due array di caratteri:
char str[20] = "Hello";
char b[10] = " World!";

Il primo array ha una dimensione maggiore del secondo e lo useremo come contenitore. Nella
prima stringa abbiamo cinque caratteri più il terminatore. La seconda stringa contiene sette
caratteri più il terminatore e quindi può essere contenuta nella prima. Per unire le stringhe
useremo quindi il comando:
strcat(destinazione, sorgente);

Questa funzione aggiunge alla stringa destinazione il contenuto di sorgente purché vi sia
sufficiente spazio:
strcat(str, b);

Possiamo verificare il funzionamento di strcat() con un semplice sketch:


char str[20] = "Hello";
char b[10] = " World!";

void setup() {
[Link](115200);
strcat(str, b);
[Link](str);
}

void loop(){}

Listato 4.5 – Esempio di codice per concatenare due array di caratteri.

Vediamo una variazione dell’esempio precedente in cui introduciamo altre due importanti
funzioni per lavorare con gli array di caratteri. Spesso capita di preparare una stringa vuota e di
poter assegnare il contenuto in un secondo momento:
char str[20];

Purtroppo non è possibile fare un’assegnazione diretta come se fosse una comune variabile:
str = "hello world!";

Per inserire una sequenza di caratteri nell’array serve una funzione speciale chiamata:
strcpy(destinazione, sorgente);

La funzione copia i caratteri da sorgente a destinazione. Quindi per popolare una stringa con
un testo scriveremo:
strcpy(str, "hello world!");

Non sempre la memoria è sgombra e quindi lavorando con le stringhe potremmo avere la
necessità di ripulire l’area di memoria occupata, per evitare di trovarvi caratteri spurii. Una
buona pratica è quella di chiamare la funzione:
memset(stringa, carattere_di_riempimento, ripetizioni);

La funzione inserisce nella stringa il carattere di riempimento fornito per il numero di


ripetizioni indicato. Possiamo assicurarci che una stringa sia vuota con:
memset(str, '\0', sizeof(str));

Usiamo sizeof() per far calcolare in automatico il numero di caratteri.


Vediamo un esempio completo per concatenare due stringhe con le nuove funzioni che
abbiamo incontrato:
char str[20];

void setup(){
[Link](115200);
memset(str, '\0', sizeof(str));
strcpy(str, "Hello");
strcat(str, " World!");

[Link](str);

void loop(){}

Listato 4.6 – Esempio di codice per concatenare due array di caratteri con memset e strcpy.

Funzioni
Scrivendo sketch, vi sarete accorti che all’inizio si tende a mettere tutto il codice nel setup() e
nel loop() che in breve tempo diventano ingestibili. Tutto il codice è scritto di seguito con
numerose ramificazioni date dagli if presenti ed è difficile capire cosa accade. In più potreste
avere delle istruzioni ripetute.
Per fare un po’ di ordine è necessario introdurre le funzioni che ci permettono di raccogliere
linee di codice che si occupano di un certo compito e “battezzarle”. Inoltre con le funzioni
possiamo evitare di duplicare le istruzioni, pratica molto pericolosa per via degli errori che può
generare. In breve le funzioni ci aiutano a organizzare il codice e a evitare inutili ridondanze.
Le funzioni possono essere utilizzate per eseguire operazioni complesse, elaborare dati,
effettuare calcoli e altro ancora. Vengono definite con un nome univoco, seguite da parentesi
tonde (), possono accettare zero o più parametri e restituire un valore. Gli elementi
fondamentali di una funzione in linguaggio C sono i seguenti:
• nome della funzione: è un nome, univoco (non può essere già stato usato all’interno del
programma), che viene utilizzato per chiamare la funzione;
• parametri: sono i dati che riceve in ingresso la funzione. I parametri possono essere
opzionali;
• tipo di ritorno: indica il tipo di dati che la funzione restituisce come risultato. Può essere
void se la funzione non restituisce alcun valore. Il valore è restituito utilizzando la parola
chiave return;
• corpo della funzione: contiene il blocco di istruzioni che definiscono le operazioni che la
funzione deve eseguire. Il corpo della funzione è racchiuso tra parentesi graffe {}.

Ecco un esempio che mostra i quattro elementi fondamentali di una funzione:


int somma(int a, int b) {
int risultato = a + b;
return risultato;
}

In questo esempio, la funzione si chiama somma, ha due parametri di tipo int (a e b), restituisce
un valore di tipo int, e calcola la somma dei due parametri restituendo il risultato. La funzione
può essere richiamata semplicemente scrivendo il suo nome e fornendo i parametri che
possono essere delle costanti o forniti direttamente o delle variabili:
int n = 12;
int res = somma(n, 100);
Una funzione potrebbe non restituire nulla. In questo caso al posto del valore di ritorno
troveremo al parola chiave void:
void setup() {
//corpo della funzione
}

Quando creiamo una funzione all’interno di uno sketch è preferibile, per motivi di visibilità,
porre all’inizio dello sketch la sua dichiarazione o firma, cioè una riga in cui si riporta il nome
della funzione, il tipo di ritorno e poi i parametri richiesti. L’implementazione può essere poi
messa in fondo allo sketch o anche su un file a parte. In questo modo il compilatore è a
conoscenza dell’esistenza della funzione e viene avvisato che l’implementazione è posta da
qualche altra parte. Ecco un esempio per la funzione somma:
//dichiarazione delle funzioni
int somma(int a, int b);

void setup(){
[Link](115200);

int a = 10;
int b = 20;
int r = somma(a, b);
[Link]("r: ");
[Link](r);

void loop(){}

//implementazione delle funzioni


int somma(int a, int b){
int res = a + b;
return res;
}

Listato 4.7 – Esempio di funzione con dichiarazione e implementazione.

Eseguendo il codice nel Serial Monitor dovremmo leggere il risultato della somma:
r: 30

Quando passiamo dei parametri a una funzione definita come abbiamo appena visto, la
funzione riceverà una copia dei valori forniti. Richiamando la funzione somma definita poche
righe fa scriveremo:
int n = 12;
int m = 100;
int res = somma(n, m);

Durante l’esecuzione del programma, quando la CPU giungerà alla chiamata a funzione,
recupererà i valori dei parametri (in questo caso n che vale 12 e m che vale 100) e li copierà in
qualche posto sicuro. La funzione somma lavorerà quindi su delle copie delle variabili passate.
Quindi una funzione non può modificare una variabile passata come parametro.
Passaggio valori per indirizzo
In alcuni casi vorremmo che la funzione possa modificare direttamente la variabile fornita. Per
poter lavorare direttamente e modificare un parametro dovremo passare alla funzione
l’indirizzo della variabile:
void f(int *p);

void setup(){
[Link](115200);

int v = 10;
[Link]("v: ");
[Link](v);

f(&v);

[Link]("v: ");
[Link](v);

void loop(){}

void f(int *p) {


*p = *p * 10;
}

Listato 4.8 – Esempio di funzione con dichiarazione e implementazione.

Eseguendo il codice, nel Serial Monitor dovremmo leggere il valore della variabile v prima e
dopo la chiamata della funzione:
v: 10
v: 100

Possiamo osservare che la funzione ha effettivamente modificato il valore della variabile


passata come parametro. Utilizzeremo il passaggio per indirizzo quando vogliamo che la
funzioni modifichi effettivamente il valore della variabile originale oppure nel caso ci servano
più valori di ritorno. Un esempio frequente è quello in cui la funzione legge un sensore e
questo fornisce più risultati, per esempio temperatura, umidità e pressione atmosferica. Come
possiamo restituire tre valori? Una soluzione è quella di passare alla funzione tre variabili per
riferimento, così che possa modificarle direttamente, come riportato in questo frammento di
codice:
void setup(){
[Link](9600);

float temp, press, hum;

readSensor(&temp, &press, &hum);

[Link]("t: ");
[Link](temp);
[Link]("p: ");
[Link](press);
[Link]("h: ");
[Link](hum);
}

void loop(){}

void readSensor (float *t, float *p, float *h) {


*t = [Link]();
*h = [Link]();
*p = [Link]();
}

Un’altra possibile soluzione è quella di creare una struttura, cioè un contenitore per più
variabili, che contenga i tre valori. Nella funzione si valorizzerà la struttura che poi sarà
restituita con return.

Enumerazioni
Le enumerazioni permettono di creare insiemi di costanti associate a numeri interi. Per creare
una enumerazione useremo la parola chiave enum seguita dal nome che vogliamo dare al nuovo
insieme e quindi dai valori:
enum giorni {lun, mar, mer, gio, ven, sab, dom};

Il nome dato all’insieme è come se fosse un nuovo tipo dati. Per creare una variabile che possa
assumere i valori indicati nell’enumerazione dovremo scrivere:
enum giorni myWeekDay;

Quindi enum giorni è il tipo di dato e myWeekDay è il nome della variabile che poi useremo nel
codice. È anche possibile creare tutto in una sola volta accodando il nome della variabile alla
dichiarazione dell’enumerazione:
enum giorni {lun, mar, mer, gio, ven, sab, dom} myWeekDay;

La variabile myWeekDay può assumere valori presi dall’enumerazione e quindi potremo


scrivere:
myWeekDay = mar;

Implicitamente i nomi delle costanti che abbiamo definito sono associati a dei valori interi ma
questo è un dettaglio solitamente irrilevante. Utilizziamo le enum per rendere il codice più
leggibile e comprensibile.
Vediamo un semplice esempio dove utilizziamo una enum che prevede quattro livelli di
luminosità:
enum livello_pwm {spento, basso, medio, alto} lev;

Un trimmer collegato al pin 34 della ESP32 fornirà un valore analogico che useremo per
impostare la variabile lev. Nel loop() leggeremo il valore di lev per impostare il livello di
luminosità di un LED utilizzando un segnale PWM. Il circuito necessario al nostro
esperimento necessita di un LED collegato con una resistenza da 100 Ω sul pin 4 e di un
trimmer da 10 kΩ sul pin 34 (si veda la Figura 4.1 per lo schema elettrico).
Figura 4.1 – Schema elettrico per comandare un LED in PWM utilizzando un trimmer e valori presi da una enumerazione.

Ecco qui di seguito il listato completo:


enum livello_pwm {spento, basso, medio, alto} lev;

void setup() {
[Link](115200);
[Link]("enum");
int ch = 0;
int freq = 1000;
int res = 8;
ledcSetup(ch, freq, res);
ledcAttachPin(4, ch);

analogReadResolution(10); //0-1023
}

void loop() {
int v = analogRead(34);
[Link](v);
delay(300);
if (v < 200) lev = spento;
else if (v < 400) lev = basso;
else if (v < 800) lev = medio;
else lev = alto;

if (lev == spento) {
[Link]("off");
ledcWrite(0, 0);
}
if (lev == basso) {
ledcWrite(0, 70);
[Link]("basso");
}
if (lev == medio) {
ledcWrite(0, 140);
[Link]("medio");
}
if (lev == alto) {
ledcWrite(0, 255);
[Link]("on");
}
}

Listato 4.9 – Esempio di utilizzo delle enumerazioni per impostare la luminosità di un LED.

All’inizio del listato creiamo l’enumerazione e una variabile lev che possa contenerne i valori:
enum livello_pwm {spento, basso, medio, alto} lev;

Nel setup(), prima di tutto impostiamo la porta seriale così da poter leggere i valori del
trimmer nel Serial Monitor e capire se il circuito è funzionante:
void setup() {
[Link](115200);
[Link]("enum");

Proseguiamo configurando canale PWM a 1000 Hz con una risoluzione di 8 bit così che possa
essere controllato con valori che vanno da 0 a 255. Associamo poi il canale al pin 4 a cui
avremo collegato il LED:
int ch = 0;
int freq = 1000;
int res = 8;
ledcSetup(ch, freq, res);
ledcAttachPin(4, ch);

Prepariamo anche l’ADC impostando una risoluzione a 10 bit (valori tra 0 e 1023):
analogReadResolution(10);

Nel loop() leggiamo il valore sul pin analogico 34. Possiamo intervenire sul valore agendo sul
trimmer con un cacciavite:
void loop() {
int v = analogRead(34);
[Link](v);
delay(300);

Il valore impostato è anche stampato sul Serial Monitor. Con un if-else leggiamo il valore
analogico e assegniamo la costante alla variabile lev prendendola dalla enumerazione:
if (v < 200) lev = spento;
else if (v < 400) lev = basso;
else if (v < 800) lev = medio;
else lev = alto;

Successivamente rileggiamo il valore della variabile lev confrontandola con quelli


dell’enumerazione e accendiamo il LED con vari valori di luminosità:
if (lev == spento) {
[Link]("off");
ledcWrite(0, 0);
}
if (lev == basso) {
ledcWrite(0, 70);
[Link]("basso");
}
if (lev == medio) {
ledcWrite(0, 140);
[Link]("medio");
}
if (lev == alto) {
ledcWrite(0, 255);
[Link]("on");
}

L’esempio può apparire banale ma ci è servito per capire come trattare le costanti dichiarate
all’interno di un’enumerazione.

Strutture
Con le strutture possiamo creare dei nuovi tipi di dati “aggregati”, combinando più
informazioni di diverso tipo. Possiamo usare una struttura per creare un nuovo tipo di dato che
possa memorizzare degli indirizzi in modo più confortevole, oppure per raggruppare tutte le
letture fornite da un sensore o lo stato di una nostra applicazione. Per definire una struttura
utilizzeremo la parola chiave struct seguita dal nome del nuovo tipo dati e quindi dagli
elementi che la compongono. Vediamo un esempio per creare una struttura indirizzo:
struct indirizzo {
char nome[20];
char cognome [20];
char via[30];
char citta[30];
int cap;
char provincia[2];
} ;

Per creare una variabile con il nuovo tipo indirizzo scriveremo:


struct indirizzo casa;
struct indirizzo lavoro;

È possibile dare la definizione di struttura e creare subito una variabile aggiungendo alla fine il
nome desiderato. Ecco un esempio dove creiamo la struttura di tipo struct indirizzo e poi
istanziamo anche una variabile myaddr che utilizza la struttura:
struct indirizzo {
char nome[20];
char cognome [20];
char via[30];
char citta[30];
int cap;
char provincia[2];
} myaddr;

Per assegnare valori ai membri della struttura useremo la notazione puntata:


[Link] = 12345;

Purtroppo per gli array di caratteri non possiamo fare un’assegnazione diretta ma dobbiamo
copiare il valore utilizzando la funzione strncpy() fornendo la destinazione, la stringa da cui
prendere i valori e il numero di caratteri da copiare:
strncpy([Link], "Milano", 7);

Se invece che passare direttamente un array di caratteri utilizziamo una variabile array,
possiamo far calcolare il numero di caratteri con sizeof():
char paese[] = "Milano";
strncpy([Link], paese, sizeof(paese));

Possiamo invece utilizzare i campi contenuti nella struttura senza particolari accorgimenti,
richiamandoli sempre con la notazione puntata. Possiamo stampare alcuni campi così:
[Link]("citta: %s e CAP: ( %d )\n", [Link], [Link]);

Ecco un esempio completo:


struct indirizzo {
char nome[20];
char via[30];
int cap;
} addr;

void setup() {
[Link](115200);

[Link] = 12345;
strncpy([Link], "Milano", 7);

char strada[] = "Via Mazzini, 12";


strncpy([Link], strada, sizeof(strada));

[Link]("%s a %s ( %d )\n", [Link], [Link], [Link]);


}

void loop() {
// put your main code here, to run repeatedly:
}

Listato 4.10 – Esempio di utilizzo delle strutture con ESP32.

Possiamo passare delle strutture come parametri a una funzione trattandole come se fossero
comuni variabili. Immaginiamo di utilizzare ancora la struttura indirizzo definita poco fa,
volendola passare a una funzione scriveremo:
void funzioneConStruttura(struct indirizzo addr) {
[Link] = 55667;
}

All’interno del corpo della funzione utilizzeremo i membri della struttura con la notazione
puntata. Per richiamare la funzione passeremo la struttura senza particolari accorgimenti:
funzioneConStruttura(addr);

Macchine a stati
Una delle maggiori difficoltà che incontrano i principianti nello sviluppare codice per sistemi
embedded con ESP32 o Arduino sta nel combinare varie funzioni e comportamenti all’interno
dello sketch. Nello sketch base ereditato dal framework di Arduino utilizziamo solitamente il
loop() per inserire tutto il codice che ci serve. Spesso sfuggono alcuni elementi:
• il loop() si ripete costantemente a rapida velocità (prima possibile);
• il tempo in cui si sta nel loop() dipende dal codice che vi inseriamo;
• non è buona cosa bloccare il loop() con delay o attese;
• non sempre un programma è costituito da una semplice sequenza di operazioni ed è
necessario eseguire cose simultaneamente.

Per chi è agli inizi ed è poco esperto è difficile scrivere un codice che sia sempre reattivo e che
possa funzionare senza problemi. In particolare l’ultimo punto segnalato è critico perché
spesso è necessario verificare costantemente un sensore, un pulsante o qualche altro evento e
nel caso questo avvenga deve essere gestito. Una tecnica adottata per questi casi sono le
interrupt che però possono rendere il codice più complicato e meno leggibile (tratteremo tra
poco anche le interrupt).
È raro che un programma sia una semplice sequenza di operazioni da ripetere. Spesso
dobbiamo realizzare dei comportamenti che prevedono il mantenimento di uno stato:
potremmo progettare un sistema che sta in attesa della pressione di un tasto e quando il tasto
viene premuto, allora attiverà certe funzioni che rimarranno attive fino a che non accade
qualche altro tipo di evento o interazione. Detta così sembra facile e si potrebbe pensare di
gestire tutto con degli if-then. Nella pratica però si incontrano numerose complicazioni che ci
costringono a inserire più if all’interno di if. In pochi attimi il codice diventa un groviglio
inestricabile e impossibile da interpretare.
Esiste una soluzione a questa situazione e, senza inventarci nulla, la possiamo copiare dal
mondo dell’automazione industriale che ci pensò già attorno agli anni ’80 del secolo scorso
introducendo il linguaggio Grafcet (a sua volta derivato dalle Reti di Petri). Ogni programma o
processo può sempre essere diviso in più stati. In ogni stato possiamo definire precisamente
una o più azioni da svolgere. Alcune azioni possono anche perdurare più stati o avere regole di
temporizzazione. Il passaggio da uno stato all’altro avviene secondo regole di transizione che
verificano se ci siano le condizioni per il cambio. Con questo sistema implementiamo una
macchina a stati.

Controllo per porta o cancello


Immaginiamo di voler controllare una porta o un cancello azionato da un motore elettrico che
può ruotare in due sensi, per aprire o chiudere. Per comandare la porta utilizzeremo due
pulsanti e per capire se la porta è aperta o chiusa ci affideremo a due fine corsa che non sono
altro che dei contatti. Quando la porta sarà completamente aperta toccherà un fine corsa che
farà capire al nostro sketch in che posizione è la porta e lo stesso nel caso della chiusura. Non
introduciamo nessun tipo di sicurezza come per esempio delle fotocellule per non complicare
troppo le cose. La porta avrà inizialmente due stati: aperto o chiuso. Possiamo rappresentare
gli stati con dei cerchi e tracciare degli archi per indicare le possibilità. La Figura 4.2 illustra la
situazione iniziale.
Figura 4.2 – La porta può assumere solo due stati: aperto o chiuso. Gli stati sono rappresentati con dei cerchi e sono legati
uno con l’altro da delle frecce.

Proviamo a immaginare cosa accade nei due stati:


• nello stato “chiuso” attendiamo che qualcuno prema il pulsante di apertura per avviare il
motore fino a che non raggiunge il primo finecorsa;
• nello stato “aperto” attendiamo che qualcuno prema il pulsante di chiusura per avviare il
motore fino a che non raggiunge il secondo finecorsa.

Dovremo quindi scrivere un codice che possa effettuare queste azioni. Riflettendoci meglio
possiamo individuare delle situazioni intermedie che possono semplificare il compito della
scrittura del software. Oltre ai due stati “statici” abbiamo anche due momenti in cui la porta è
in movimento. Individuiamo quattro stati:
• chiuso: porta chiusa e in attesa della pressione del pulsante;
• apertura: il motore è attivo e apre la porta fino a che non incontra il primo finecorsa;
• aperta: porta aperta in attesa che qualcuno prema il pulsante di chiusura;
• chiusura: il motore è attivo e chiude la porta fino a che non incontra il secondo finecorsa.

Nella Figura 4.3 abbiamo contrassegnato gli stati con un numero univoco che useremo poi nel
codice. L’arco che va da uno stato all’altro è una freccia e può essere percorso solo in un verso.
A metà dell’arco osserviamo una barretta che rappresenta la transizione, cioè la condizione che
rende possibile il passaggio da uno stato all’altro. Passeremo dallo stato 0 all’1 quando
qualcuno premerà il pulsante di apertura. La transizione ha un nome univoco, T0, e ne
troviamo una su ogni arco, per regolare i passaggi di stato.

Figura 4.3 – La porta può assumere quattro stati e il passaggio da uno all’altro avviene all’attivazione di una particolare
condizione (transizione).
Proviamo a costruire un semplice circuito che ci permetta di verificare il funzionamento di
quanto abbiamo descritto. Il circuito sarà molto semplice ma non potrà veramente controllare
un motore collegato a una porta. Ci serve solo per comprendere le dinamiche e lo sviluppo “a
stati”. Il motore sarà rappresentato con due LED.
Colleghiamo quindi due LED ai pin 4 e 5. I LED indicheranno il senso di rotazione del
motore. Colleghiamo poi il pulsante di apertura (BT1) al pin 16 e quello di chiusura al pin 17
(BT2).
Come fine corsa useremo altri due pulsati collegati pin 18 e 19. I pulsanti e i finecorsa
saranno configurati con le resistenze interne di pull up e quindi possiamo collegarli
direttamente a GND. Lo schema elettrico è riportato in Figura 4.4.

Figura 4.4 – Schema elettrico per verificare il funzionamento dello sketch a stati per il controllo della porta.

Il codice che scriveremo avrà quattro momenti ben precisi che possiamo considerare come
indipendenti. È come se in ogni stato potessimo caricare sulla scheda un solo sketch che si
occupa di una cosa ben precisa. Nello stato di partenza (porta chiusa – stato 0) nel loop()
dovremo controllare solo se qualcuno preme il pulsante BT1 e nel caso avviare il motore e
quindi passare nello stato successivo.
Nello stato di apertura dovremmo immaginare di avere nel loop() il codice che verifica
solo se il fine corsa è stato premuto e se si disattivi il motore prima di passare allo stato
successivo ecc.. Ogni stato può e deve essere trattato come un caso separato. È come se
avessimo tante “schede” con il codice da inserire nel loop stato per stato. Come possiamo
attivare queste schede? Impostiamo lo sketch a stati! Creiamo una variabile di stato che ci
servirà per sapere in quale stato ci troviamo:
int stato = 0;

poi nel loop() creiamo una struttura switch che controlla la variabile stato. Lo switch avrà
tanti case quanti sono gli stati previsti. Immaginiamo di creare quattro funzioni, una per ogni
stato, e di chiamarle loop0(), loop1(), loop2(), loop3():
int stato = 0;

void setup() {
}

void loop() {
switch (stato) {
case 0:
loop0();
break;
case 1:
loop1();
break;
case 2:
loop2();
break;
case 3:
loop3();
break;
}
}

void loop0() {}
void loop1() {}
void loop2() {}
void loop3() {}

Listato 4.11 – Sketch di base con la struttura a stati.

All’interno di ogni loop() inseriremo il codice che vogliamo eseguire in ogni stato e non
dovremo dimenticarci di aggiungere la transizione, cioè una condizione che ci faccia passare
allo stato successivo, impostando il valore di stato. Le funzioni per i loop() possono avere
nomi più significativi. Proviamo ad assegnare loro dei nomi che ricordino gli stati del nostro
esempio:
void chiuso(){

}
void apertura(){

}
void aperto(){

}
void chiusura(){

Aggiungiamo nel setup() le impostazioni dei pin che intendiamo usare, ricordandoci di
utilizzare INPUT_PULLUP per finecorsa e pulsanti. Ecco come potrebbe apparire la prima
versione del nostro codice:
int stato = 0;

void setup() {
//led direzione "motore"
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);

pinMode(16, INPUT_PULLUP); //BT apre


pinMode(17, INPUT_PULLUP); //BT chiude
pinMode(18, INPUT_PULLUP); //FC1
pinMode(19, INPUT_PULLUP); //FC2
}
void loop() {
switch(stato) {
case 0:
chiuso();
break;
case 1:
apertura();
break;
case 2:
aperto();
break;
case 3:
chiusura();
break;
}
}

void chiuso(){

}
void apertura(){

}
void aperto(){

}
void chiusura(){

Listato 4.12 – Sketch di base con la struttura a stati per l’esercizio della porta.

Ora proviamo a tradurre in codice il comportamento che desideriamo attuare nei quattro stati
che abbiamo identificato. Nello stato 0, “chiuso”, attendiamo che qualcuno prema il pulsante
di apertura della porta. Il pulsante è configurato con INPUT_PULLUP e quindi sul pin 16
leggeremo sempre un segnale HIGH. Nel caso qualcuno prema il pulsante, allora avremo un
LOW. Per fare meno confusione conviene “girare” il valore dello stato così che leggeremo
HIGH solo quando il tasto è premuto. Per invertire la lettura di digitalRead() anteponiamo
un punto esclamativo ! alla digitalRead(), quindi accenderemo il LED corrispondente al
movimento di apertura del motore e poi passeremo allo stato successivo impostando stato a 1.
Conviene aggiungere un piccolo delay nella transizione per darci il tempo di sollevare il dito
dal pulsante. Ecco il risultato:
void chiuso(){
if (!digitalRead(16)) {
[Link]("Apre");
//accende il LED "avanti" (o attiva il motore)
digitalWrite(4, HIGH);
delay(300);
stato = 1;
}
}

Lo stato 1 corrisponde all’apertura della porta. Il motore (pin 4) è stato attivato nella
transizione precedente e quindi resta attivo fino a che staremo in questo stato in cui non
faremo nulla se non attendere che si attivi il finecorsa collegato al pin 18. Anche il finecorsa è
collegato in INPUT_PULLUP e quindi lo leggeremo anteponendo la negazione ! a
digitalRead(). Alla pressione del finecorsa spegneremo il motore (pin 4) e passeremo allo
stato successivo (2):
void apertura(){
//continuo ad aprire fino a che non incontro il fine corsa
if (!digitalRead(18)) {
[Link]("FC1");
//spegne il LED "avanti"
digitalWrite(4, LOW);

delay(300);
stato = 2;
}
}

Lo stato 3 è quello in cui la porta è aperta. Qui attendiamo la pressione del pulsante di chiusura
per poi attivare il motore in senso inverso (pin 5) e passare allo stato 3. Se volete esercitarvi
provate a inserire un timer così che dopo un certo tempo la porta si chiuda automaticamente!
void aperto(){
//attende pressione del pulsante di chiusura
if (!digitalRead(17)) {
[Link]("Chiude");
//accende il LED "indietro" (o attiva il motore)
digitalWrite(5, HIGH);
delay(300);
stato = 3;
}
}

Infine il codice da eseguire durante la fase di chiusura sarà il seguente:


void chiusura(){
//continuo a chiudere fino a che non incontro il fine corsa
if (!digitalRead(19)) {
[Link]("Chiusura");
//spegne il LED "indietro"
digitalWrite(5, LOW);

delay(300);
stato = 0;
}
}

Attenderemo l’attivazione del finecorsa sul pin 19 per spegnere il motore e passare allo stato 0.
Ecco il codice completo:
int stato = 0;

void setup() {
[Link](115200);

//led direzione "motore"


pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
pinMode(16, INPUT_PULLUP); //BT apre
pinMode(17, INPUT_PULLUP); //BT chiude
pinMode(18, INPUT_PULLUP); //FC1
pinMode(19, INPUT_PULLUP); //FC2
}
void loop() {
switch(stato) {
case 0:
chiuso();
break;
case 1:
apertura();
break;
case 2:
aperto();
break;
case 3:
chiusura();
break;
}
}

void chiuso(){
//attende pressione del pulsante di apertura
if (!digitalRead(16)) {
[Link]("Apre");
//accende il LED "avanti" (o attiva il motore)
digitalWrite(4, HIGH);

delay(300);
stato = 1;
}
}
void apertura(){
//continuo ad aprire fino a che non incontro il fine corsa
if (!digitalRead(18)) {
[Link]("apertura");
//spegne il LED "avanti"
digitalWrite(4, LOW);

delay(300);
stato = 2;
}
}
void aperto(){
//attende pressione del pulsante di chiusura
if (!digitalRead(17)) {
[Link]("Chiude");
//accende il LED "indietro" (o attiva il motore)
digitalWrite(5, HIGH);

delay(300);
stato = 3;
}
}
void chiusura(){
//continuo a chiudere fino a che non incontro il fine corsa
if (!digitalRead(19)) {
[Link]("Chiusura");
//spegne il LED "indietro"
digitalWrite(5, LOW);

delay(300);
stato = 0;
}
}

Listato 4.13 – Sketch a stati per il controllo di una porta motorizzata.

Gli sketch a stati permettono anche di modificare il codice con semplicità in quanto le varie
funzioni e azioni sono suddivise in piccole porzioni facilmente comprensibili. Ogni stato
solitamente implementa una semplice funzionalità e costituisce un piccolo blocco che può
essere combinato o concatenato con il codice di altri stati. Avendo sotto mano il diagramma
degli stati del nostro sketch ci possiamo accorgere che spesso per fare una modifica è
sufficiente modificare il modo in cui gli stati sono collegati.
Immaginiamo di voler aggiungere alla nostra porta automatica una nuova funzione così che
mentre la porta è in apertura sia possibile premere il tasto di chiusura. La modifica è possibile
e basterà aggiungere un nuovo arco che collega lo stato 1 con il 3 (vedi Figura 4.5). L’arco sarà
legato alla transizione che rileva lo stato del pulsante di chiusura.

Figura 4.5 – Diagramma degli stati per la porta automatica a cui abbiamo aggiunto la possibilità di rilevare la pressione del
tasto “chiudi” anche durante l’apertura.

Possiamo modificare il codice intervenendo sulla funzione che implementa lo stato “Apre” e
aggiungendo un controllo sul pulsante di chiusura:
void apertura(){
//continuo ad aprire fino a che non incontro il fine corsa
if (!digitalRead(18)) {
[Link]("apertura");
//spegne il LED "avanti"
digitalWrite(4, LOW);

delay(300);
stato = 2;
}
//continuo ad aprire fino a che non premono il tasto chiudi
if (!digitalRead(17)) {
[Link]("chiude in corsa");
//spegne il LED "avanti"
digitalWrite(4, LOW);
//accende il LED "indietro"
digitalWrite(5, HIGH);

delay(300);
stato = 3;
}
}

Se il pulsante di chiusura (pin 17) è premuto, allora spegneremo il LED corrispondente al


motore di apertura, accenderemo il LED per segnalare il movimento di chiusura e passeremo
allo stato 3. Ecco il codice completo:
int stato = 0;

void setup() {
[Link](115200);

//led direzione "motore"


pinMode(4, OUTPUT);
pinMode(5, OUTPUT);

pinMode(16, INPUT_PULLUP); //BT apre


pinMode(17, INPUT_PULLUP); //BT chiude
pinMode(18, INPUT_PULLUP); //FC1
pinMode(19, INPUT_PULLUP); //FC2
}

void loop() {
switch(stato) {
case 0:
chiuso();
break;
case 1:
apertura();
break;
case 2:
aperto();
break;
case 3:
chiusura();
break;
}
}

void chiuso(){
//attende pressione del pulsante di apertura
if (!digitalRead(16)) {
[Link]("Apre");
//accende il LED "avanti" (o attiva il motore)
digitalWrite(4, HIGH);

delay(300);
stato = 1;
}
}
void apertura(){
//continuo ad aprire fino a che non incontro il fine corsa
if (!digitalRead(18)) {
[Link]("apertura");
//spegne il LED "avanti"
digitalWrite(4, LOW);

delay(300);
stato = 2;
}
//continuo ad aprire fino a che non premono il tasto chiudi
if (!digitalRead(17)) {
[Link]("chiude in corsa");
//spegne il LED "avanti"
digitalWrite(4, LOW);
//accende il LED "indietro"
digitalWrite(5, HIGH);

delay(300);
stato = 3;
}
}
void aperto(){
//attende pressione del pulsante di chiusura
if (!digitalRead(17)) {
[Link]("Chiude");
//accende il LED "indietro" (o attiva il motore)
digitalWrite(5, HIGH);

delay(300);
stato = 3;
}
}
void chiusura(){
//continuo a chiudere fino a che non incontro il fine corsa
if (!digitalRead(19)) {
[Link]("Chiusura");
//spegne il LED "indietro"
digitalWrite(5, LOW);

delay(300);
stato = 0;
}
}

Listato 4.14 – Sketch a stati per il controllo di una porta motorizzata con possibilità di arrestare l’apertura della porta.

Come avete visto l’approccio “a stati” è molto potente e permette di semplificare notevolmente
lo sviluppo di applicazioni complesse. Provate a esercitarvi modificando l’esempio proposto:
• aggiungete la possibilità di aprire la porta mentre è in fase di chiusura;
• si può fare in modo che la porta resti aperta un certo tempo e poi si chiuda da sola?
• come possiamo aggiungere un LED di segnalazione lampeggiante che sia attivo quando la
porta è in movimento?
• si possono aggiungere fotocellule di sicurezza? Come saranno gestite dal software?
Interrupt
Immaginate di essere assorti e concentrati a programmare la vostra scheda ESP32. A un certo
punto suonano alla porta: nonostante il vostro lavoro sia importante, abbandonate la vostra
postazione per correre a vedere chi ha suonato il campanello. È solo il postino che vi lascia
una raccomandata e se ne va. Il bollo dell’auto da pagare. Ci penserete settimana prossima,
intanto tornate al computer e riprendete il codice da dove lo avete lasciato. Questa scenetta
quotidiana ci permette di comprendere le interrupt, un meccanismo con cui i microcontrollori
possono interrompere il normale flusso di esecuzione del programma e rispondere
immediatamente a un evento esterno. L’esecuzione del programma principale impegna la CPU
che deve eseguire una lunga sequenza di operazioni. Se nel loop() abbiamo previsto la lettura
di alcuni ingressi, probabilmente non riusciremo ad avere letture tempestive fino a che non
giungeremo alle istruzioni in oggetto. Anche se il loop() è eseguito in pochi millisecondi,
potremmo comunque perdere degli eventi che richiedono pochi microsecondi! Per questo sono
utili le interrupt che possono intercettare segnali rapidi e improvvisi anche se la CPU è
impegnata.
I moduli ESP32 offrono 32 linee di interrupt che possono essere utilizzate per intercettare
eventi come la pressione di un pulsante o il segnale proveniente da un sensore (per esempio da
degli encoder rotativi). Le linee di interrupt dell’ESP32 sono associate a specifici pin GPIO.
Abbiamo due tipi di interrupt:
• interrupt hardware: si attivano in risposta a un evento esterno come l’azione su un pin o
l’arrivo di un segnale;
• interrupt software: si attivano in risposta a un evento legato a timer interni o al
raggiungimento di certe condizioni (per esempio fine conteggi, overflow di registri ecc.).

Se vogliamo utilizzare le interrupt per rispondere a un evento “esterno” dobbiamo decidere a


che tipo di situazione siamo interessati, cioè a quale variazione del segnale, e poi scrivere una
funzione che sarà richiamata nel caso l’evento si presenti. La funzione prende il nome di
“Interrupt Service Routine” (ISR) (o anche handler dell’interrupt) e va scritta con qualche
piccola precauzione. Per collegare la funzione di risposta all’interrupt utilizzeremo la
funzione:
attachInterrupt(pin, funzione_di_risposta, tipo_di_evento);

fornendo tre argomenti:


• pin: il numero del pin a cui vogliamo collegare l’interrupt (su ESP32 possiamo usare tutti i
pin);
• funzione_di_risposta: è il nome della funzione ISR che sarà richiamata in seguito all’evento
a cui siamo interessati;
• tipo_di_evento: indica a che tipo di variazione del segnale siamo interessati e quindi la
condizione scatenante dell’interrupt che indicheremo utilizzando specifiche costanti
predefinite.

Per impostare la modalità di attivazione dell’interrupt abbiamo cinque possibilità:


• LOW: attiva l’interrupt ogni volta che il pin è al livello LOW;
• HIGH: attiva l’interrupt quando il pin è al livello HIGH;
• CHANGE: si attiva a ogni cambio di valore del pin;
• FALLING: si attiva se il pin passa da HIGH a LOW;
• RISING: si attiva se il pin passa da LOW a HIGH.

L’Interrupt Service Routine (ISR) è la funzione che viene richiamata ogni volta che si verifica
un evento sul pin a cui abbiamo collegato l’interrupt. Queste funzioni hanno qualche
particolarità:
• non ricevono parametri;
• non restituiscono nulla;
• devono essere brevi e veloci;
• devono essere precedute dall’attributo IRAM_ATTR.

Vediamo un esempio di sintassi:


void IRAM_ATTR azione_da_compiere() {
//codice da eseguire
}

La cosa più particolare che notiamo è l’attributo IRAM_ATTR che fa sì che il codice compilato
sarà mantenuto nella RAM e non nella memoria flash così che sia molto più rapido da
eseguire.
In un certo momento potremmo voler disattivare l’interrupt. È sufficiente richiamare la
funzione:
detachInterrupt(pin);

indicando il pin da scollegare.


Per comprendere il funzionamento delle interrupt costruiamo un semplice circuito
combinando un pulsante con resistore di pull down da 10 kΩ collegato sul pin 2. Lo schema
del circuito è riportato in Figura 4.6.

Figura 4.6 – Schema elettrico per circuito di prova delle interrupt.

In questo esercizio incrementeremo una variabile usata per conteggiare il numero di pressioni
sul tasto collegato. La variabile utilizzata all’interno del codice della ISR per conteggiare gli
eventi deve avere accesso rapido. Per questo motivo la dichiareremo di tipo volatile, che
significa che sarà mantenuta su un registro della CPU:
int volatile count = 0;

Creiamo la funzione ISR e utilizziamola per incrementare il valore della variabile count:
void IRAM_ATTR conteggio() {
count++;
}

Nel setup() collegheremo il pin 2 alla ISR utilizzando attachInterrupt(). Siamo interessati
solo ai fronti di salita del segnale e quindi specificheremo la modalità RISING:
attachInterrupt(2, conteggio, RISING);

Nel loop() dello sketch possiamo stampare il valore di count. Possiamo provare a
visualizzare quante volte il tasto viene premuto in un secondo. Creiamo un timer non
bloccante:
if ((millis() - t1) >= 1000) {

t1 = millis();
}

Nel corpo del “timer” stampiamo il valore di count, lo azzeriamo e reimpostiamo il timer così
che possa scattare di nuovo:
if ((millis() - t1) >= 1000) {
[Link]("count: ");
[Link](count);
count = 0;
t1 = millis();
}

Ecco il codice completo:


int volatile count = 0;

unsigned long t1;

void IRAM_ATTR conteggio() {


count++;
}

void setup() {
[Link](115200);
delay(1000);
[Link]("interrupt test");

attachInterrupt(2, conteggio, RISING);


t1 = millis();
}

void loop() {
if ((millis() - t1) >= 1000) {
[Link]("count: ");
[Link](count);
count = 0;
t1 = millis();
}
}

Listato 4.15 – Sketch a stati per l’utilizzo delle interrupt.

Carichiamo lo sketch sulla ESP32 e poi apriamo il Serial Monitor per leggere i valori rilevati:
premendo il tasto dovrebbe comparire il numero di clic al secondo.

Debouncing
Quando vogliamo utilizzare un pulsante lo colleghiamo alla ESP32 e leggiamo semplicemente
il livello del pin a cui è collegato con digitalRead(). Sembra tutto semplice ma questo
approccio può dare parecchi problemi, infatti i pulsanti sono oggetti elettro-meccanici e ogni
volta che li azioniamo i loro contatti rimbalzano, producendo segnali spuri che sono comunque
rilevati da un microcontrollore.
I rimbalzi possono richiamare il codice più volte creando malfunzionamenti o presentando
comportamenti incomprensibili (vedi Figura 4.7).
Le tecniche hardware e software adottate per limitare o ridurre i rimbalzi sono chiamate
“debouncing”. Qui vedremo solo degli approcci di tipo software.

Figura 4.7 – Il tracciato A rappresenta il segnale ideale prodotto da un pulsante premuto e rilasciato; il tracciato B è quello
prodotto da un pulsante reale: al rilascio del contatto possono verificarsi rimbalzi che generano segnali spuri. Nella realtà è
possibile trovare rimbalzi sia alla pressione che al rilascio del tasto.

Debouncing con delay


La tecnica più semplice utilizza un delay() di alcune centinaia di millisecondi per dare il
tempo ai disturbi di cessare. La durata dei rimbalzi è di solito di qualche decina di
millisecondi. Una pausa di 200 o 300 millisecondi è sufficiente ad annullare ogni effetto
negativo, purtroppo le delay() bloccano completamente lo sketch e non sono sempre
raccomandabili.
Per prototipi semplici e senza particolari criticità possiamo tranquillamente utilizzare questa
tecnica. Per verificarne il funzionamento realizziamo un semplicissimo circuito collegando un
LED con resistenza da 100 Ω al pin 4 di una ESP32 e un pulsante con resistenza di pull down
da 10 kΩ sul pin 16 (vedi Figura 4.8).
Figura 4.8 – Schema del circuito di prova per attenuare gli effetti del debouncing.

Prima di tutto proviamo a vedere cosa accade se non si prendono contromisure. Scriviamo uno
sketch che faccia cambiare stato al LED ogni volta che si preme il pulsante. Definiamo la
funzione dei pin nel setup:
pinMode(4, OUTPUT);
pinMode(16, INPUT);

Può far comodo attivare anche la porta seriale così da poter inviare un messaggio a ogni clic
del pulsante:
[Link](115200);
delay(1000);
[Link]("Debouncing con delay");

Nel loop() controlliamo lo stato del pin 16:


if (digitalRead(16)) {
}

A ogni pressione del tasto scriviamo un messaggio e invertiamo lo stato del pin 4. Senza usare
variabili possiamo impostare il pin 4 leggendo direttamente il suo valore con digitalRead() e
invertendolo con il punto esclamativo !. In questo modo quando il LED è spento, leggendo lo
stato del pin con digitalRead() otterremo LOW che sarà invertito dalla negazione,
impostando il pin ad HIGH e quindi accendendo il LED. Se il LED è acceso, accadrà
esattamente il contrario:
if (digitalRead(16)) {
[Link]("clic");
digitalWrite(4, !digitalRead(4));
}

Ecco il codice completo:


void setup() {
[Link](115200);
delay(1000);
[Link]("Debouncing con delay");

pinMode(4, OUTPUT);
pinMode(16, INPUT);
}

void loop() {
if (digitalRead(16)) {
[Link]("clic");
digitalWrite(4, !digitalRead(4));
}
}

Listato 4.16 – Provate a premere il pulsante per cambiare lo stato del LED!

Provate a caricarlo e premete il pulsante. Vi accorgerete che il LED a volte cambierà stato ma
a volte no. Il comportamento è molto frustrante e dà l’impressione che le cose non funzionino.
Purtroppo l’istante in cui il tasto è premuto dura parecchie centinaia di millisecondi e il codice
all’interno dell’if() è eseguito centinaia di volte! Alzando il dito non sappiamo in che stato il
LED sarà e il controllo è lasciato completamente al caso!
La prima semplice soluzione a questo grave problema è mettere un piccolo delay all’interno
dell’if(), subito dopo aver cambiato lo stato del LED. L’utilizzatore avrà il tempo di
osservare il cambio di stato del LED e di sollevare il suo dito, lasciando anche cessare
eventuali rimbalzi:
if (digitalRead(16)) {
[Link]("clic");
digitalWrite(4, !digitalRead(4));
delay(300);
}

Ecco il codice completo:


void setup() {
[Link](115200);
delay(1000);
[Link]("Debouncing con delay");

pinMode(4, OUTPUT);
pinMode(16, INPUT);
}

void loop() {
if (digitalRead(16)) {
[Link]("clic");
digitalWrite(4, !digitalRead(4));
delay(300);
}
}

Listato 4.17 – Un semplice delay() può eliminare i problemi con i pulsanti.

Provate a caricare il codice e a premere il tasto. Questa volta il LED cambierà stato senza
problemi e tutto sarà più stabile. Purtroppo non sempre possiamo utilizzare dei delay() perché
bloccano completamente l’esecuzione dello sketch e spesso poche centinaia di millisecondi
sono un’eternità!
Debouncing non bloccante
Se vogliamo trovare una soluzione ai rimbalzi che non sia bloccante dobbiamo scrivere un
codice più complesso. Per annullare i rimbalzi dovremo contare il tempo che trascorre tra una
lettura del pulsante e la successiva (generata, magari, da un rimbalzo). Tutti i segnali ricevuti
prima del tempo prefissato saranno ignorati perché immaginiamo siano spuri. Utilizzeremo
ancora il circuito di Figura 4.8 e cercheremo di migliorare il codice che abbiamo scritto fino a
qui. Prima di tutto creiamo una variabile per tenere traccia del tempo che passa:
unsigned long t = 0;

Nel setup() configureremo i pin come ingressi e uscite secondo le connessioni dello schema
di Figura 4.8. Intercetteremo la pressione del tasto con un if(), all’interno del corpo del quale
realizzeremo un timer non bloccante:
void loop() {
if (digitalRead(16)) {
//qui il timer
}
}

Il timer farà in modo che la prima volta che premiamo il pulsante, salveremo il tempo corrente,
fornito da millis(), nella variabile t. Confronteremo poi il tempo corrente con quello
memorizzato dentro a t e se superiore alla soglia preimpostata (1000 ms), cambieremo lo stato
del LED e salveremo il nuovo valore di millis() in t:
void loop() {
if (digitalRead(16)) {
if ((millis()-t) > 1000) {
digitalWrite(4, !digitalRead(4));
t = millis();
}
}
}

Mille millisecondi sono parecchi ma servono per vedere chiaramente l’effetto del codice. Se
proverete a premere il tasto prima che sia passato un secondo, non accadrà nulla. Ecco qui di
seguito il codice completo:
unsigned long t = 0;

void setup() {
[Link](115200);
delay(1000);
[Link]("Debouncing");

pinMode(4, OUTPUT);
pinMode(16, INPUT);
}

void loop() {
if (digitalRead(16)) {
if ((millis()-t) > 1000) {
digitalWrite(4, !digitalRead(4));
t = millis();
}
}
}

Listato 4.18 – Codice per implementare il debouncing senza utilizzare delay().

Fronti di salita e discesa


La pressione di un tasto può sembrare apparentemente un evento semplice ma dilatando la
scala del tempo possiamo individuare vari momenti:
1. il tasto è a riposo;
2. il tasto viene premuto e il contatto si chiude;
3. il tasto è premuto e il suo contatto è chiuso;
4. il dito che preme il pulsante si solleva;
5. il tasto torna nello stato di riposo con il suo contatto aperto.

Questi eventi sono molto rapidi ma un microcontrollore è abbastanza rapido da rilevarli;


poterli gestire fa molto comodo e spesso permette di eliminare parecchi problemi dovuti, per
esempio, ai rimbalzi. Solitamente leggiamo lo stato di un pulsante con una semplice
digitalRead(), senza preoccuparci di questi dettagli. Per alcune applicazioni particolari,
come per esempio l’individuazione di clic multipli o dei tempi di pressione, è fondamentale
individuare le varie fasi dell’azionamento.

Figura 4.9 – Diagramma temporale degli azionamenti e dello stato delle variabili.

Utilizziamo di nuovo il semplice circuito di Figura 4.8 con un pulsante collegato al pin 16 e un
LED sul pin 4. Il pin a cui è collegato il pulsante registrerà sempre un segnale LOW fino a che
non appoggeremo un dito sul tasto. Il passaggio del segnale dallo stato LOW a quello HIGH si
definisce il fronte di salita: è un segnale molto rapido con una forma “a ginocchio”. L’istante
in cui il segnale passa dallo stato HIGH a quello LOW si definisce “fronte di discesa”. Non
esiste una funzione che possa rilevare questi due passaggi (a meno di utilizzare le interrupt) ma
possiamo farlo utilizzando alcune semplici variabili.
Per rilevare i passaggi di stato del segnale serve una variabile che memorizzi lo stato del
segnale letto sul pin digitale. Possiamo definire quindi una variabile tasto di tipo int o bool.
La variabile è direttamente collegata allo stato del pulsante perché prende il valore dalla
digitalRead(). Quando il tasto è premuto assume immediatamente (o nel tempo permesso dal
loop()) il valore HIGH, mentre torna a LOW non appena il tasto è rilasciato. Oltre alla
variabile tasto serve una seconda variabile di stato che memorizzi lo stato precedente del
tasto. Chiamiamo questa variabile prev_tasto:
int tasto, prev_tasto;

Inizialmente sia tasto che prev_tasto sono impostate a LOW. Appena premiamo il tasto, la
variabile tasto passa a HIGH. Ci troviamo nella situazione in cui prev_tasto vale LOW e
tasto è HIGH. Questa condizione individua il fronte di salita e la possiamo individuare con un
if. Al termine di ogni iterazione del loop dobbiamo copiare il valore di tasto in prev_tasto:
void loop() {
tasto = digitalRead(16);

//qui ragiono sui valori di tasto e prev_tasto

//salvo il valore di stato in prev_tasto


prev_tasto = tasto;
}

Immaginate che l’azione sia molto lenta e che il vostro dito sia sempre premuto sul tasto. Fino
a che è in questa situazione, sia tasto che prev_tasto valgono HIGH. Nel momento in cui
sollevate il dito, la variabile tasto passa immediatamente a LOW. All’interno dello stesso
ciclo di loop() ci troviamo con prev_tasto a HIGH e tasto a LOW. Con un secondo if possiamo
individuare questa situazione che corrisponde al fronte di discesa del tasto!
Figura 4.10 – Schema a blocchi dell’algoritmo utilizzato per individuare i fronti di salita e discesa.

Lo sketch completo che permette di individuare i fronti di salita e discesa di un tasto è il


seguente:
int tasto, prev_tasto;

void setup() {

[Link](115200);
delay(1000);
[Link]("FDS FDD");

pinMode(4, OUTPUT);
pinMode(16, INPUT);
}

void loop() {
tasto = digitalRead(16);

//individuo il fronte di salita


if (!prev_tasto && tasto) {
[Link]("off-on - salita");
}
//individuo il fronte di discesa
if (prev_tasto && !tasto) {
[Link]("on-off - discesa");
}
//salvo il valore di stato in prev_stato
prev_tasto = tasto;
}

Listato 4.19 – Sketch di base per rilevare il fronte di salita e di discesa di un pulsante.

Le variabili di stato sono definite all’inizio dello sketch, esternamente a loop() e setup() e
sono di tipo booleano (bool). I due momenti sono individuati con delle semplici if che
controllano il valore di tasto e prev_tasto. Prima della fine del loop() troviamo l’istruzione
di copia del valore attuale di tasto all’interno di prev_tasto, fondamentale per poter fare un
confronto tra il valore corrente di stato e quello assunto nella iterazione precedente.
Possiamo sfruttare i fronti di salita e di discesa per leggere la pressione o il rilascio di un
tasto in modo preciso. Spesso si utilizza questo metodo come tecnica alternativa di
debouncing. Lavorando con fronti di salita e discesa possiamo implementare anche altre azioni
interessanti.

Misurare il tempo di pressione di un pulsante


Potrebbe essere necessario attivare una certa azione solo dopo alcuni secondi che premiamo un
pulsante. Una “pressione lunga” attiva procedure particolari come lo spegnimento di un
dispositivo, il reset o l’attivazione di una modalità speciale. La pressione lunga del tasto non è
un comportamento “naturale” e con questo modo si proteggono delle azioni speciali, così che
la loro attivazione sia fatta in modo consapevole.
Per attivare un’azione solo dopo alcuni istanti che il tasto è premuto dobbiamo conteggiare
il tempo che passa dal momento in cui incontriamo il fronte di salita del segnale. Abbiamo
imparato a individuare i fronti di salita e discesa, vediamo come collegare un timer al codice
già scritto. Per il nostro esercizio useremo lo schema di Figura 4.8 con un pulsante collegato al
pin 16 e un LED sul pin 4. Utilizziamo lo sketch 4.19 come punto di partenza. Per misurare il
tempo serve una variabile in cui salvare il numero di millisecondi rilevati da millis() nel
momento dell’attivazione del fronte di salita. All’inizio dello sketch inseriamo una variabile
t0:

unsigned long t0;

Individuiamo nel loop() l’if() che individua il fronte di salita e aggiungiamo l’istruzione che
salva il tempo corrente in t0:
//individuo il fronte di salita
if (!prev_tasto && tasto) {
[Link]("FDS: inizio conteggio");
t0 = millis();
}

Nel momento in cui il tasto è rilasciato, e quindi nel codice che rileva il fronte di discesa,
calcoliamo la differenza tra il tempo corrente fornito da millis() e il tempo memorizzato in
t0:
long tempo_trascorso = millis() - t0;

Se il tempo trascorso è superiore a un valore predefinito (per esempio 2000 ms) allora
attiveremo la funzione speciale e cambieremo lo stato del LED:
if (tempo_trascorso > 2000) {
[Link]("Azione Speciale attivata!");
digitalWrite(4, !digitalRead(4));
}

Ecco lo sketch completo:


int tasto, prev_tasto;
unsigned long t0;

void setup() {
[Link](115200);
delay(1000);
[Link]("FDS FDD");

pinMode(4, OUTPUT);
pinMode(16, INPUT);
}

void loop() {
tasto = digitalRead(16);

//individuo il fronte di salita


if (!prev_tasto && tasto) {
[Link]("FDS: inizio conteggio");
t0 = millis();
}
//individuo il fronte di discesa
if (prev_tasto && !tasto) {
[Link]("FDD: fine conteggio");
long tempo_trascorso = millis() - t0;
[Link]("tempo pressione: %d\n", tempo_trascorso);

if (tempo_trascorso > 2000) {


[Link]("Azione Speciale attivata!");
digitalWrite(4, !digitalRead(4));
}
}
//salvo il valore di stato in prev_stato
prev_tasto = tasto;
}

Listato 4.20 – Sketch per individuare una pressione “lunga” sul tasto e attivare un’azione speciale.

Clic multipli su un pulsante


Utilizzando i fronti di salita e di discesa di un segnale possiamo contare il numero di clic e
scatenare delle azioni. Possiamo implementare un comportamento simile al doppio clic del
mouse: premendo per un certo numero di volte un pulsante, entro un certo tempo, attiveremo
un’azione.
Il conteggio dei “clic” inizia la prima volta che si preme il pulsante. Dobbiamo “aprire una
finestra temporale” in cui il codice conteggia le successive pressioni. Alla chiusura della
“finestra temporale” conteremo il numero di clic e decideremo cosa fare. È importante rilevare
i clic solo nel momento del rilascio del tasto, cioè in corrispondenza del fronte di discesa: in
questo modo, se un tasto è tenuto premuto, non accade nulla e non si incrementano i clic.
Utilizzeremo ancora lo schema di Figura 4.8 con un pulsante sul pin 16 e un LED sul pin 4.
Partiremo dallo sketch 4.19 per la rilevazione dei fronti di salita e discesa.
Aggiungiamo una variabile conteggio che ci servirà per aprire e chiudere la finestra:
bool conteggio; //variabile di controllo della finestra di conteggio

Servirà anche una variabile per contare il numero di clic e una per misurare il tempo della
finestra:
unsigned long t0; //variabile per salvare il tempo di apertura della finestra
int click = 0; //numero di click del tasto

La prima volta che premeremo il tasto (fronte di salita), la variabile conteggio sarà impostata
al valore true per aprire la finestra. Per impedire di rieseguire il codice a ogni successiva
pressione del tasto, lo si protegge con un if che fa modificare il valore solo se la variabile
conteggio è a false.
All’apertura della finestra salviamo anche il numero corrente di millisecondi millis() in
una variabile t0:
t0 = millis();

e azzeriamo il numero di clic:


click = 0;

Stampiamo anche un messaggio sul Serial Monitor. Ecco il codice di rilevazione del fronte di
salita a cui sono state aggiunte (in grassetto) le istruzioni di apertura della finestra:
//individuo il fronte di salita
if (!prev_tasto && tasto) {
[Link]("FDS");
prev_tasto = tasto;

//se il conteggio non è ancora attivo, lo faccio partire (apertura finestra)


if (!conteggio) {
conteggio = true;
//salvo il tempo iniziale
t0 = millis();
[Link]("start conteggio");
//azzero i click
click = 0;
}
}

La chiusura della finestra dipende solo dal tempo che scorre. Dopo un certo numero di
millisecondi dal primo clic, richiuderemo la finestra, fermeremo il conteggio e potremo
contare i clic rilevati. Il tempo di apertura è impostato a 2000 millisecondi (un valore
abbastanza elevato). La misura del tempo di apertura della finestra si ottiene confrontando
l’istante attuale con l’istante di apertura salvato dentro a t0:
millis() - t0

Quindi in fondo al loop possiamo aggiungere un nuovo if che, quando il tempo di apertura
della finestra è passato, la richiude riportando la variabile conteggio al valore false e
conteggiando il numero di clic:
//chiusura della finestra da 2s
if (conteggio && ((millis() - t0) > 2000) ){
[Link]("fine conteggio");
conteggio = false;

[Link]("click: %d\n", click);


[Link]("----------");
}

Contiamo a questo punto il numero di clic e se questi sono tre, allora cambiamo stato al LED:
if (click == 3) digitalWrite(4, !digitalRead(4));

Dove rileviamo i clic? I clic sono incrementati solo nel momento in cui si rilascia il pulsante,
cioè in corrispondenza del fronte di discesa del segnale. Per conteggiare i click basta
incrementare il valore della variabile click. Ecco come modificare il codice di rilevazione del
fronte di discesa (in grassetto):
//individuo il fronte di discesa
if (prev_tasto && !tasto) {
[Link]("FDD: fine conteggio");
prev_tasto = tasto;
click++;

Ecco il codice completo dello sketch:


int tasto, prev_tasto;
unsigned long t0; //variabile per salvare il tempo di apertura della finestra
bool conteggio; //variabile di controllo della finestra di conteggio
int click = 0; //numero di click del tasto
void setup() {
[Link](115200);
delay(1000);
[Link]("FDS FDD");

pinMode(4, OUTPUT);
pinMode(16, INPUT);
}

void loop() {
tasto = digitalRead(16);

//individuo il fronte di salita


if (!prev_tasto && tasto) {
[Link]("FDS");
prev_tasto = tasto;

//se il conteggio non è ancora attivo, lo faccio partire (apertura finestra)


if (!conteggio) {
conteggio = true;
//salvo il tempo iniziale
t0 = millis();
[Link]("start conteggio");
//azzero i click
click = 0;
}
}

//individuo il fronte di discesa


if (prev_tasto && !tasto) {
[Link]("FDD: fine conteggio");
prev_tasto = tasto;
click++;
}
//chiusura della finestra da 2s
if (conteggio && ((millis() - t0) > 2000) ){
[Link]("fine conteggio");
conteggio = false;

[Link]("click: %d\n", click);


[Link]("----------");
if (click == 3) digitalWrite(4, !digitalRead(4));
}
}

Listato 4.21 – Codice per rilevare un triplo clic del pulsante e cambiare lo stato di un LED.
5

Hardware & periferiche

Approfondiamo la nostra conoscenza sull’utilizzo dei pin


GPIO aggiungendo transistor e MOSFET. Scopriamo
come utilizzare le interfacce I²C e SPI per collegare vari
tipi di sensori e display. Impariamo ad attivare lo sleep
mode della scheda per risparmiare energia.

Controllare correnti con i transistor


Non possiamo pilotare carichi e dispositivi che richiedono correnti superiori a qualche decina di
milliampere collegandoli direttamente ai pin dell’ESP32: è necessario utilizzare qualche componente
elettronico che possa controllare correnti maggiori. Per questi scopi utilizziamo dei transistor (bipolari
o MOSFET). I transistor si comportano come degli interruttori controllati dalla corrente ed evitano
che il microcontrollore si danneggi.
Il modello più comune di transistor è quello bipolare e può controllare una corrente di forte
intensità tramite una debole. La corrente di pilotaggio raggiunge il terminale chiamato “base” e
determina l’intensità della corrente principale che scorre tra i terminali definiti “collettore” ed
“emettitore”. I transistor bipolari esistono in due varianti: NPN e PNP a seconda della loro
organizzazione interna. Alcuni modelli molto comuni, tutti di tipo NPN, sono: 2N2222, 2N3904,
BC237.
Figura 5.1 – Alcuni modelli di transistor bipolari in vari formati e loro simbolo.

I transistor MOSFET appartengono alla famiglia dei transistor FET e hanno tre terminali chiamati
gate, source e drain. Li possiamo usare come interruttori controllati dalla tensione (i bipolari sono
controllati in corrente!).
Applicando una tensione al gate è possibile modificare il flusso della corrente che circola tra drain
e source (chiamato anche canale). Il gate è elettricamente isolato dal canale tra drain e source e forma
una specie di condensatore. Per questo motivo sono molto sensibili all’elettricità statica e vanno
maneggiati con cautela per evitare di danneggiarli con un semplice tocco.
Ci sono due tecnologie di fabbricazione chiamate enhanchment e depletion; per ogni tecnologia
esistono due varianti: a canale N o a canale P.
Solitamente utilizziamo MOSFET enhanchment a canale N come i 2N7000, gli IRF520 o meglio
ancora IRF3708 o IRLZ44N che sono di tipo “logic level” e quindi più adatti a essere pilotati dai 3,3
V dell’ESP32.
Per avere maggiori informazioni su questi dispositivi vi consiglio di consultare un testo specifico o
il mio libro Elettronica per Maker o Elettronica trucchi e segreti. Per trovare le caratteristiche e la
piedinatura dei transistor, cercate i loro datasheet su Internet.
Figura 5.2 – Alcuni MOSFET e i loro simboli.

Accendere una lampada o una striscia di LED


Proviamo a controllare una lampada a 12 V oppure una striscia a LED tramite un transistor. Questi
dispositivi presentano due problemi:
• sono alimentati a 12 V (tensione ben superiore ai 3,3 V ottenibili sui pin dell’ESP32);
• richiedono correnti superiori a 50 mA.

Decidiamo di usare un transistor 2N2222 di tipo NPN. Fate attenzione perché nella Figura 5.3 è
indicata la piedinatura più comune, ma potrebbero capitare dei modelli con piedinatura differente.
Esiste un modello 2N2222A con i terminali disposti con un ordine differente! Il transistor sarà
controllato dalla corrente di base (B), fornita da un pin digitale del modulo ESP32. Il transistor è
controllato in corrente e quindi collegheremo una resistenza da un centinaio di ohm alla sua base
(vedi Figura 5.3). Collegheremo un carico (una lampada o dei LED) al collettore del transistor. Il
carico può essere alimentato a una tensione superiore a quella usata comunemente dai pin del modulo
ESP32. In questo esempio useremo un alimentatore o una batteria da 12 V. Il negativo della batteria
va collegato al piedino GND di ESP32.
Quando non circola corrente nella base del transistor, anche tra emettitore (E) e collettore (C) non
scorrerà corrente e la lampada sarà spenta. Facendo scorrere una corrente nella base del transistor,
anche tra emettitore e collettore inizierà a scorrere corrente. Se la corrente di base raggiunge un valore
di pochi milliampere, il transistor andrà in saturazione e si comporterà (quasi) come un interruttore
chiuso: la corrente tra collettore ed emettitore sarà massima e sarà determinata dalla legge di Ohm.
Ogni transistor è caratterizzato da un parametro “Ic max” che indica la massima corrente che può
scorrere tra emettitore e collettore. Questo valore dipende dal modello di transistor, non va superato
per non danneggiare il dispositivo, e varia da qualche centinaia di milliampere fino a decine di
ampere.
Figura 5.3 – ESP32 pilota una lampada a incandescenza o dei LED a 12 V, utilizzando un transistor bipolare NPN, di tipo 2N2222.

Per verificare il funzionamento del circuito possiamo utilizzare uno sketch che fa lampeggiare il pin
4:
void setup() {
pinMode(4, OUTPUT);
}

void loop() {
digitalWrite(4, !digitalRead(4));
delay(1000);
}

Listato 5.1 – Codice per accendere e spegnere a intermittenza (blink) una striscia a LED o una lampadina.

Pilotare un relè con un transistor


Per controllare correnti di valore elevato o dispositivi alimentati a 220 V, che un transistor non
potrebbe gestire, è raccomandabile utilizzare un relè, cioè un interruttore elettromeccanico che separa
galvanicamente ESP32 dall’oggetto che si desidera pilotare.
I relè hanno la forma di una piccola scatoletta con dei piedini che fuoriescono dal lato inferiore. Al
loro interno trova posto un’elettrocalamita che serve per spostare i contatti di un interruttore o di un
deviatore. A seconda del modello, il “contatto” può sopportare correnti di vari ampere (anche più di
10 o 15) e tensioni fino a 220 V. Queste informazioni sono spesso riportate sul corpo del relè, assieme
alla tensione della bobina che nei modelli più comuni è di 5, 12 o 24 V.

Fate molta attenzione quando lavorate con la tensione di rete: uno shock elettrico
NOTA può essere anche mortale. Non lavorate mai su apparecchi o circuiti alimentati e se
non siete esperti, o in caso di dubbio, fatevi aiutare da un professionista.

Il relè non può essere connesso direttamente a un pin, ma serve un transistor di pilotaggio. Lo schema
elettrico è riportato nella Figura 5.4 dove utilizziamo un transistor NPN di tipo 2N2222. In parallelo
alla bobina del relè è necessario aggiungere un diodo di protezione (per esempio modello 1N4005 o
1N4004) per evitare che al rilascio del relè, delle correnti parassite circolino in senso inverso e
danneggino il transistor.
Figura 5.4 – ESP32 collegato a un relè a 12 V, che controlla una lampada a 220 V.

Quando alimentate il relè con una tensione differente da quella fornita da ESP32, per esempio a 12 V,
attenetevi alle seguenti regole:
• tenere le differenti alimentazioni separate tra di loro: mai unire il pin 3V3 o 5 V di ESP32 con i 12
V richiesti dal relè;
• unite le masse: il GND di ESP32 va collegato al negativo di alimentazione dei 12 V necessari al
relè.

Per verificare il funzionamento dei circuiti presentati utilizzate lo sketch 5.1 (blink).

Utilizzare un MOSFET
Per controllare correnti di valore elevato è possibile utilizzare, in alternativa ai transistor bipolari, dei
transistor MOSFET. Il funzionamento è simile a quello del transistor bipolare solo che per controllare
la corrente che lo attraversa applicheremo una tensione al suo terminale di controllo (gate), per questo
un transistor MOSFET si può usare come interruttore controllato da una tensione (il transistor BJT è
controllato da una corrente).
Per accendere una lampada o una striscia a LED a 12 V dovete procurarvi un MOSFET
enhanchment a canale N, come il modello 2N7000, l’IRF520 o meglio l’IRLZ44.
Per controllare il dispositivo si utilizza il pin di gate (G), su cui è necessario applicare una tensione.
Il comportamento del MOSFET dipende dalla tensione applicata tra gate e source: VGS.
Provate a realizzare il circuito illustrato in Figura 5.5: il pin quattro di ESP32 è collegato al gate del
MOSFET, il drain (D) invece è a GND e il source (S) alla lampada o al carico alimentato a 12 V.
Quando si applica una tensione sul gate, il transistor MOSFET va in conduzione e la lampada si
accenderà. Quando il gate è a 0 V, il transistor non conduce.
Nello schema di Figura 5.5 è stata aggiunta una resistenza da 10 kΩ tra il gate e GND per rendere
più stabile il MOSFET in assenza di segnale.
Per verificare il funzionamento dei circuiti presentati utilizzate lo sketch 5.1 (blink).
Figura 5.5 – Pilotaggio di un carico a 12 V con ESP32 e un MOSFET IRLZ44N.

Controllare la velocità di un motore


Un motore elettrico in corrente continua (c.c.) può ruotare utilizzando un sistema di magneti ed
elettromagneti. Come tutti i dispositivi elettromagnetici richiede correnti troppo intense per i pin di
una ESP32 e inoltre sono spesso necessarie tensioni di alimentazione superiori a 3,3 V. Il motore in
c.c. si può mettere in rotazione semplicemente alimentandolo. Possiamo invertire la direzione di
rotazione invertendo i fili mentre per variare la velocità possiamo utilizzare un segnale PWM
generato da ESP32 e applicato con un MOSFET che interrompe l’alimentazione del motore più volte
in un secondo.
Lo schema di collegamento è riportato in Figura 5.6. Il MOSFET utilizzato è un IRLZ44N. La
resistenza da 10 kΩ serve per tenere il gate “ancorato” a GND e dare maggior stabilità al circuito. Il
motore elettrico genera correnti parassite e indesiderate che possono danneggiare il transistor di
pilotaggio, per questa ragione ai capi del motore è collegato un diodo (per esempio un 1N4007 o
1N4005) per annullarle.
Per controllare la velocità, il gate va collegato a un pin adatto a generare un segnale PWM (per
esempio il pin numero 4). Ho ipotizzato che il motore sia da 12 V, verificate bene i parametri del
vostro prima di collegarlo al circuito, eventualmente modificando la tensione di alimentazione.
Prima di tutto si deve impostare il canale nel setup(), cioè la frequenza del segnale e la sua
risoluzione. Utilizzeremo il canale 0 per generare un segnale alla frequenza di 1 kHz a 8 bit così da
poter utilizzare i valori da 0 a 255:
void setup() {
int ch = 0;
int freq = 1000;
int res = 8;
ledcSetup(ch, freq, res);

Colleghiamo poi il canale al pin 4:


int pin1 = 4;
ledcAttachPin(pin1, ch);

Nel loop(), modificando il duty cycle del canale varieremo la velocità del motore. Facciamo crescere
il duty cycle utilizzando un ciclo for() con un piccolo delay per rallentarlo quanto basta:
for (int i = 0; i < 256; i++) {
ledcWrite(0, i);
delay(5);
}

Ecco lo sketch completo:


void setup() {
int ch = 0;
int freq = 1000;
int res = 8;
ledcSetup(ch, freq, res);

int pin1 = 4;
ledcAttachPin(pin1, ch);
}

void loop() {
for (int i = 0; i < 256; i++) {
ledcWrite(0, i);
delay(5);
}
for (int i = 255; i >= 0; i--) {
ledcWrite(0, i);
delay(5);
}
}

Listato 5.2 – Codice per variare la velocità di un motore utilizzando un segnale PWM.

Figura 5.6 - Schema di collegamento a ESP32 di un motore a 12 V pilotato con un segnale PWM tramite un transistor MOSFET.

Questo tipo di controllo è molto semplice e non è possibile invertire il moto del motore, cosa per cui è
più adatto un dispositivo chiamato ponte ad H, realizzato con quattro transistor. Trovate maggiori
informazioni sull’utilizzo dei ponti ad H nei miei video su Arduino:
• ponte ad H con Arduino ([Link]
• ponte ad H con MOSFET ([Link]
• ponte ad H con L298 ([Link]

Utilizzare un servomotore
Un servomotore è un dispositivo elettronico che può far ruotare un perno di un certo numero di gradi.
Il suo albero, a differenza di un motore c.c., non ruota in continuazione ma mantiene la posizione
impostata. Questi oggetti sono di solito utilizzati per il controllo dello sterzo, di alettoni o timoni di
piccoli modelli radiocomandati. La rotazione non è completa ma arriva a un massimo di 180° / 270° a
seconda del modello. All’interno del servomotore trovano posto:
• un motore in corrente continua;
• un potenziometro: usato per rilevare la posizione del motore;
• un gruppo d’ingranaggi: per collegare potenziometro e motore e per aumentare la “potenza
meccanica” del dispositivo demoltiplicando il motore;
• un piccolo circuito di pilotaggio: che riceve il segnale di pilotaggio, aziona il motore e rileva la sua
posizione leggendo il potenziometro.

Dal servomotore fuoriescono tre fili: il rosso e il nero servono per l’alimentazione, il terzo filo, giallo
o arancione, serve per fornire il segnale di pilotaggio.
Questi dispositivi sono capaci di sviluppare anche forze notevoli, che si misurano come “coppia“ e
quindi sono espresse in “kgm”. Un servo con una coppia di 2 kgm è in grado di sollevare un peso di
due chilogrammi collegato a un’asta lunga un metro connessa al suo perno. Se riduciamo la distanza
dell’asta a mezzo metro, riusciremo a sollevare quattro chilogrammi a cinquanta centimetri.

Figura 5.7 – Un servomotore e il suo simbolo elettrico.

Per impostare la posizione del servomotore serve un particolare segnale digitale con precise
temporizzazioni dell’ordine dei millisecondi. I servomotori a 5 V possono essere collegati
direttamente a un modulo ESP32 senza bisogno di aggiungere transistor di pilotaggio perché
prendono l’alimentazione dal pin 5 V della scheda. Al filo di pilotaggio applicheremo un segnale
digitale preso direttamente da un pin. La potenza (e quindi la corrente) necessaria al movimento del
motore è presa dai due fili di alimentazione che non gravano sul microcontrollore.
Lo schema di collegamento è molto semplice: collegate il filo rosso ai 5 V, il filo nero a GND e il
terzo filo (arancione o giallo) a un pin di tipo PWM (pin consigliati sono 2, 4, 12-19, 21-23, 25-27,
32-33). Se il vostro servomotore utilizza una tensione differente, per esempio a 12 V, alimentatelo
separatamente e abbiate solo cura di collegare il negativo di alimentazione con il pin GND di ESP32.
Figura 5.8 – Schema di collegamento di un servomotore a 5 V a ESP32.

Installare una libreria per il servomotore


Il segnale di pilotaggio per un servomotore può essere facilmente prodotto con un’apposita libreria da
aggiungere ad Arduino IDE. Per aggiungere la libreria è sufficiente cliccare l’icona “Library
Manager” posta sulla toolbar verticale di sinistra. Nel pannello di ricerca delle librerie scrivete:
“ESP32Servo” e attendete che compaiano i risultati (vedi Figura 5.9). Installate quindi “ESP32Servo
by Kevin Harrington, John K. Bennet” cliccando il pulsante nel riquadro. Attendete che la libreria sia
installata, quindi procedete con la scrittura del codice.

Figura 5.9 – Per installare una libreria dovete aprire il pannello del Library Manager cliccando sulla corrispondente icona sulla toolbar
laterale.

Potete installare una libreria anche passando dal menu principale dell’applicazione:

Sketch > Include Library > Manage Library


A volte le librerie sono disponibili come file .zip. La libreria ESP32Servo, per esempio, è reperibile
anche sul sito GitHub all’indirizzo: [Link] Aprendo il link con un
browser possiamo visitare la pagina della libreria dove troviamo i file sorgenti, alcuni esempi e le
istruzioni di utilizzo (Figura 5.10). Nella pagina campeggia un pulsante di colore verde con la scritta
“Code”: premiamolo per accedere al download del file compresso delle librerie (Figura 5.11).
Apparirà un menu con varie voci tra cui “Download ZIP” che cliccheremo per scaricare il file
compresso (.zip). Salvate il file dove preferite, quindi in Arduino IDE selezionate la voce del menu:
Sketch > Include Library > Add .zip Library. Scegliete il file che avete appena scaricato e chiudete la
finestra. Se non appaiono messaggi d’errore, la libreria dovrebbe essere stata installata correttamente
e dovreste vederla tra le voci del menu: Sketch > Include Library.

Figura 5.10 – Home page del sito GitHub utilizzato da molti sviluppatori di codice per mantenere e distribuire librerie e sorgenti in vari
linguaggi di programmazione.
Figura 5.11 – Premendo il pulsante verde “Code” potremo scaricare il file compresso con le librerie e gli esempi per Arduino IDE.

ESP32Servo
La libreria ESP32Servo mette a disposizione l’oggetto Servo con cui possiamo controllare un
servomotore senza doverci preoccupare dei dettagli a basso livello dei segnali digitali. La libreria
deve essere aggiunta all’inizio dello sketch con la direttiva:
#include <ESP32Servo.h>

Ora possiamo creare un oggetto software che rappresenti il nostro servomotore. Chiamiamolo
myservo scrivendo la seguente riga di codice:

Servo myservo;

Ora potremo interagire con l’oggetto myservo per azionare il servomotore. Nel setup() dello sketch
specifichiamo a quale pin è collegato il servomotore:
setup() {
[Link](4);
}

La libreria offre numerose opzioni per interagire con il servomotore e con i segnali di controllo. Se
siete curiosi, visitate il sito su GitHub. Spostiamo il dispositivo indicando l’angolo di apertura con:
[Link](10);

A seconda del tipo di hardware possiamo indicare un numero che va da 0 a 180, massimo 270 gradi.
Nel loop() realizziamo due cicli for che impostano progressivamente l’angolo del servomotore da 0 a
180 gradi e viceversa. Dopo ogni istruzione di write() è bene inserire un piccolo delay() per dare il
tempo al servomotore di arrivare in posizione. Ecco il codice completo:
#include <ESP32Servo.h>

Servo myservo;

void setup() {
//servomotore collegato al pin 4
[Link](4);
}

void loop() {
for (int pos = 0; pos <= 180; pos += 1) {
[Link](pos);
delay(15);
}

for (int pos = 180; pos >= 0; pos -= 1) {


[Link](pos);
delay(15);
}
}

Listato 5.3 – Codice per attivare un servomotore facendolo ruotare da 0 a 180 gradi e ritorno.

I²C
ESP32 è equipaggiato con due linee di comunicazione I²C (Inter Integrated Circuit1), un diffuso e
molto pratico protocollo per lo scambio dati introdotto nel 1982 da Philips, per far comunicare tra di
loro diversi circuiti integrati. L’ I²C prevede uno o più “master” e uno o più “slave” che condividono
due linee, che formano un bus, chiamate:
• SDA (Serial Data);
• SCL (Serial Clock).

Ogni dispositivo collegato al bus ha un suo indirizzo univoco. Gli indirizzi sono rappresentati da 7 bit
e quindi abbiamo 128 possibilità, meno 12 che sono riservati: su una linea I²C possiamo quindi
collegare fino a 116 dispositivi. Quando un “master” vuole comunicare con un circuito periferico
annuncia con quale dispositivo vuole dialogare, così che si prepari alla comunicazione. La velocità di
trasmissione è di 100 kB/s ma è possibile spingersi fino a 400 kB/s.
Le linee del bus andrebbero sempre collegate con una resistenza di pull up verso il positivo di
alimentazione. Le resistenza si usano da 4,7 kΩ per alimentazioni a 5 V e da 2,2 kΩ con 3,3 V. Le
resistenze non sono mai montate sulle schede ESP32 ma spesso le troviamo sulle breakout board che
utilizziamo. È sempre bene verificare gli schemi elettrici e i datasheet dei componenti prima di
utilizzarli, così da evitare di incappare in malfunzionamenti. Le due interfacce I²C del modulo ESP32
sono chiamate I2C0 e I2C1. Con la libreria Wire.h di Arduino stiamo utilizzando la porta I2C0 e le
due linee sono così assegnate:
• SDA – pin 21;
• SCL – pin 22.
Figura 5.12 – Posizione dei pin SDA e SCL.

Per utilizzare il protocollo I²C è necessario includere la libreria Wire.h all’inizio dello sketch:
#include <Wire.h>

Per inizializzare l’interfaccia richiameremo il metodo begin():


[Link]();

Su ESP32 abbiamo la possibilità di modificare l’assegnazione dei pin SDA e SCL e nel caso
volessimo utilizzare posizioni differenti da quelle preassegnate le specificheremo così:
#define I2C_SDA 33
#define I2C_SCL 32
void setup() {
[Link](I2C_SDA, I2C_SCL);

La seconda porta I²C (I2C1) non è accessibile a meno di utilizzare l’oggetto TwoWire (che appartiene
sempre alla libreria Wire.h). Quando creiamo una istanza di TwoWire dobbiamo indicare a quale delle
due porte deve far riferimento:
• 0 per la I2C0;
• 1 per la I2C1.

Per attivare entrambe le porte scriveremo:


TwoWire I2C_0 = TwoWire(0);
TwoWire I2C_1 = TwoWire(1);
Nel setup() inizializzeremo le due interfacce con begin(), passando il numero dei pin che
desideriamo usare e la velocità di trasmissione (100.000 = 100 kB/s):
#include <Wire.h>

TwoWire I2C_0 = TwoWire(0);


TwoWire I2C_1 = TwoWire(1);

#define SDA_0 21
#define SCL_0 22

#define SDA_1 18
#define SCL_1 19

void setup() {
I2C_0.begin(SDA_0 , SCL_0 , 100000);
I2C_1.begin(SDA_1 , SCL_1 , 100000);
}

Trasmettere byte tra master e slave


La trasmissione su un bus I²C presuppone la presenza di uno (o anche più) master e degli slave. Con
ESP32 possiamo realizzare sia un master sia uno slave con dei semplici sketch e verificare il processo
di trasmissione dati con il master che fa una richiesta a uno slave che risponde.
Prima di tutto realizziamo un circuito con due schede ESP32. Le schede vanno collegate come
illustrato in Figura 5.13, portando le linee SDA e SCL da un modulo all’altro. Collegate tra di loro
anche il GND e l’alimentazione (se la scheda slave non è alimentata via USB). Le linee SDA e SCL
sono collegate tramite un resistore di pull up da 2,2 kΩ ai 3,3 V.

Figura 5.13 – Collegamento di due ESP32 di cui una agisce come master e l’altra come slave.

Iniziamo a programmare la scheda master aprendo un nuovo sketch e aggiungendo la libreria Wire:
#include <Wire.h>

Anche se i pin predefiniti per I²C sono il 21 e il 22, definiamoli esplicitamente utilizzando l’oggetto
TwoWire:

TwoWire i2c = TwoWire(0);


#define SDA_0 21
#define SCL_0 22
Nel setup() dello sketch configuriamo la porta seriale a 115.200 baud (simboli al secondo),
stampiamo un messaggio iniziale e assegniamo i pin e la velocità all’oggetto i2c:
void setup() {
[Link](115200);
[Link]("I2C MASTER");
[Link](SDA_0 , SCL_0 , 100000);
}

Ora passiamo nel loop() dove invieremo una richiesta per 12 byte allo slave all’indirizzo 7:
void loop() {
[Link](7, 12); //richiede 12 byte al device numero 7

La linea I²C si comporta come una linea seriale e una volta inviata la richiesta allo slave ci mettiamo
in ascolto, attendendo i dati con un ciclo while:
while ([Link]()) {
}

Non appena i dati saranno disponibili li possiamo leggere uno a uno con il metodo read() per poi
stamparli sul Serial Monitor:
while ([Link]()) {
char c = [Link]();
[Link](c);
}

Ricevuti i 12 byte andiamo a capo e stampiamo una riga di trattini per evidenziare i dati appena
ricevuti.
[Link]("\n------------");

Attendiamo mezzo secondo prima di inviare una nuova richiesta:


delay(500);

Ecco lo sketch completo:


#include <Wire.h>

TwoWire i2c = TwoWire(0);


#define SDA_0 21
#define SCL_0 22

void setup() {
[Link](115200);
[Link]("I2C MASTER");
[Link](SDA_0 , SCL_0 , 100000);
}
void loop() {
[Link](7, 12); //richiede 12 byte al device numero 7

while ([Link]()) {
char c = [Link]();
[Link](c);
}
[Link]("\n------------");

delay(500);
}
Listato 5.4 – Master I²C che richiede una sequenza di caratteri allo slave all’indirizzo 7.

Collegate la scheda ESP32 “master” al vostro computer con un cavo USB e caricate lo sketch 5.4, poi
scollegate la scheda mentre vi occupate della programmazione dello slave. Per lo slave apriamo un
nuovo sketch e inseriamo subito la direttiva per la libreria:
#include <Wire.h>

Creiamo l’oggetto i2c per gestire la porta di comunicazione:


TwoWire i2c = TwoWire(0);
#define SDA_0 21
#define SCL_0 22

Nel setup() inizializziamo la porta I²C specificando però anche l’indirizzo che assumerà lo slave (7
nel nostro caso). Il numero è stato scelto casualmente:
void setup() {
[Link](7, SDA_0 , SCL_0 , 100000); //setup come slave rispondente all'indirizzo 7

Colleghiamo poi la funzione trasmettiDati() che sarà richiamata ogni volta che il master farà una
richiesta allo slave. La funzione si occuperà di trasmettere i dati richiesti e la definiremo tra poco:
[Link](trasmettiDati);

Il loop() è vuoto:
void loop() {}

Infine definiamo la funzione che gestirà la richiesta del master, rispondendo con un semplice array di
caratteri:
void trasmettiDati() {
[Link]("hello world!");
}

Ecco il codice completo:


#include <Wire.h>

TwoWire i2c = TwoWire(0);


#define SDA_0 21
#define SCL_0 22

void setup() {
[Link](7, SDA_0 , SCL_0 , 100000); //setup come slave rispondente all'indirizzo 7
[Link](trasmettiDati);
}

void loop() {}

void trasmettiDati() {
[Link]("hello world!");
}

Listato 5.5 – Slave I²C che invia una sequenza di caratteri al master, quando richiesto.

Collegate il modulo ESP32 “slave” e programmatelo. Poi scollegate il cavo USB dallo slave e
ricollegatelo al master, quindi riconfigurate la porta seriale del master da Arduino IDE come se
voleste riprogrammarlo, infine aprite il Serial Monitor su cui dovreste vedere arrivare i dati ricevuti
dallo slave:
hello world!
------------
hello world!
------------

Collegare il sensore di temperatura e pressione BMP280


Proviamo ora a collegare un vero dispositivo I²C al modulo ESP32. Utilizziamo per esempio il chip
BMP280, prodotto da Bosch, che può fornire misure di temperatura e umidità. Il circuito integrato è
molto piccolo e per essere utilizzato deve essere saldato su un circuito stampato.
Possiamo in alternativa procurarci una breakout board pronta all’uso, di quelle utilizzate anche con
Arduino2 e visibile in Figura 5.14. Il collegamento del sensore con ESP32 è molto semplice (Figura
5.15): collegate i pin 21 (SDA) e 22 (SCL) di ESP32 ai corrispettivi terminali SDA e SCL del
sensore. Aggiungete poi il collegamento a GND e prendete l’alimentazione (pin VCC) dai 3,3 V del
modulo ESP32.
Non dimenticatevi di aggiungere i due resistori di pull up da 2,2 kΩ sulle linee I²C. Si è spesso
tentati di usare il bus I²C come se fosse una linea seriale e di utilizzare lunghi cavi “volanti” per fare i
collegamenti: cercate di limitare le distanze perché non è raccomandabile utilizzare cavi più lunghi di
una quindicina di centimetri.
Se dovete collegare sensori e dispositivi posti a decine di centimetri o metri di distanza è preferibile
utilizzare altre tecnologie meno soggette ai disturbi (per esempio RS-485, una seriale che funziona
come un bus).

Figura 5.14 – Il sensore di temperatura BMP280 montato su una pratica breakout board.

Volendo aggiungere altri dispositivi sul bus I²C, una volta aggiunta una coppia di resistori di pull up
(devono essercene solo due su tutto il bus e non una coppia per ogni dispositivo!), basterà
semplicemente collegare gli oggetti desiderati (ognuno con un diverso indirizzo) a SDA e SCL.
Figura 5.15 – Collegamento del sensore di temperatura BMP280 alla porta I²C di una ESP32.

Per ricavare i dati dal sensore BMP280 dobbiamo interrogare i suoi registri inviando specifiche
richieste.
La descrizione dei registri e delle risposte ottenibili si può trovare nel datasheet3 del componente. Il
compito non è mai semplice e dovendo affrontare un nuovo sensore mettete in conto di perdere alcune
giornate per decifrare le comunicazioni!
Fortunatamente la comunità di maker e sviluppatori legata al mondo Arduino è molto prolifica e
troviamo librerie per qualsiasi oggetto elettronico desideriamo utilizzare.
Apriamo Arduino IDE, creiamo un nuovo sketch e salviamolo con un nome a piacere. Per cercare
una libreria per BMP280 clicchiamo l’icona “Library Manager” posta sulla toolbar verticale di
sinistra.
Nel pannello di ricerca delle librerie scrivete “BMP280” e attendete che compaiano i risultati (vedi
Figura 5.16).
Potete scegliere la libreria che preferite. Io ho utilizzato la BMX280MI. Attendete che la libreria
sia installata, quindi procedete con la scrittura del codice. Potete trovare maggiori informazioni sulla
libreria appena installata visitando l’indirizzo web:
[Link]
Figura 5.16 – Installazione della libreria per BMP280.

All’inizio dello sketch includiamo la libreria con la direttiva:


#include <BMx280I2C.h>

Per utilizzare il sensore definiamo un oggetto di tipo BMx280I2C e con un nome a piacere (per
esempio bmx280). Passeremo come parametro l’indirizzo I²C del sensore. L’indirizzo si può ricavare
dal datasheet o con lo sketch I2CScanner che presenteremo più avanti:
#define I2C_ADDRESS 0x76
BMx280I2C bmx280(I2C_ADDRESS);

Nel setup() configuriamo la porta seriale e la libreria per l’I²C:


void setup() {
[Link](115200);
[Link]();

Aprendo gli esempi installati con la libreria possiamo trovare vari sketch da cui apprendere il
funzionamento del software. Possiamo copiare parte del codice dall’esempio: BMX280_I2C. Come
prima cosa richiamiamo il metodo begin() del nostro oggetto per inizializzarlo. Nel caso qualcosa
non funzioni leggeremo un messaggio nel Serial Monitor e l’esecuzione del programma si bloccherà:
if (![Link]()) {
[Link]("begin() fallito: verifica connessioni e indirizzo!");
while (1);
}

Il sensore non esegue letture se prima non è resettato e poi impostato correttamente:
[Link]();
[Link](BMx280MI::OSRS_P_x16);
[Link](BMx280MI::OSRS_T_x16);

Possiamo ora dedicarci al loop() dove prima avvieremo una lettura dei dati e poi attenderemo i
valori. Per avviare la lettura della temperatura e della pressione utilizzeremo la seguente chiamata:
[Link]();

Il metodo measure() restituisce un boolean che ci permette di capire se ci sono stati problemi
nell’avviare la lettura. Possiamo sfruttarlo con un if():
void loop() {
if (![Link]()) {
[Link]("Non riesco ad avviare la misura… forse è in corso?");
return;
}

Il metodo hasValue() ci dice quando i valori sono pronti per essere letti. Attendiamo che tutto sia
pronto con un ciclo do-while() che ricontrolla la presenza o disponibilità dei dati ogni 100 ms:
do {
delay(100);
} while (![Link]());

Quando i dati sono arrivati, stampiamoli sul Serial Monitor:


[Link]("Pressure: "); [Link]([Link]());
[Link]("Temperature: "); [Link]([Link]());

Inseriamo poi una pausa prima di richiedere una nuova lettura dati:
delay(1000);

Ecco il codice completo:


#include <Arduino.h>
#include <Wire.h>

#include <BMx280I2C.h>
#define I2C_ADDRESS 0x76
BMx280I2C bmx280(I2C_ADDRESS);

void setup() {

[Link](115200);
[Link]();

if (![Link]()) {
[Link]("begin() fallito: verifica connessioni e indirizzo!");
while (1);
}

[Link]();
[Link](BMx280MI::OSRS_P_x16);
[Link](BMx280MI::OSRS_T_x16);
}

void loop() {
if (![Link]()) {
[Link]("Non riesco ad avviare la misura… forse è in corso?");
return;
}

do {
delay(100);
} while (![Link]());
[Link]("Pressure: "); [Link]([Link]());
[Link]("Temperature: "); [Link]([Link]());

delay(1000);
}

Listato 5.6 – Sketch per leggere la temperatura con il sensore BMP280.

Caricando lo sketch e aprendo il Serial Monitor dovreste veder apparire le letture:


Temperature: 30.71
Pressure: 100556.00
Temperature: 30.73
Pressure: 100556.00
...

I²C Scanner
Anche se due fili sono semplici da gestire, potrebbe capitare di avere qualche problema con i
dispositivi I²C. I problemi più frequenti sono:
• aver invertito la linea SDA con SCL;
• utilizzo di jumper difettosi o presenza di falsi contatti;
• mancanza di adeguate resistenze di pull up;
• indirizzo errato;
• più dispositivi utilizzano lo stesso indirizzo.

Prima di perdere ore per capire cosa non funziona conviene caricare sul modulo ESP32 lo sketch “i2c
scanner” sviluppato da Nick Gammon ([Link] Il
programma effettua una scansione della linea I²C e riporta gli indirizzi degli oggetti collegati così che
si possa fare una rapida verifica di eventuali problemi nell’hardware. Riporto qui di seguito lo sketch
completo, che trovate però anche nel materiale allegato al libro (Listato 5.7):
// i2c_scanner
#include <Wire.h>
void setup() {
[Link]();

[Link](115200);
while (!Serial); // Leonardo: wait for serial monitor
[Link]("\nI2C Scanner");
}

void loop() {
byte error, address;
int nDevices;
[Link]("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++ )
{
// The i2c_scanner uses the return value of
// the [Link] to see if
// a device did acknowledge to the address.
[Link](address);
error = [Link]();
if (error == 0)
{
[Link]("I2C device found at address 0x");
if (address<16)
[Link]("0");
[Link](address,HEX);
[Link](" !");
nDevices++;
}
else if (error==4)
{
[Link]("Unknown error at address 0x");
if (address<16)
[Link]("0");
[Link](address,HEX);
}
}
if (nDevices == 0)
[Link]("No I2C devices found\n");
else
[Link]("done\n");
delay(5000); // wait 5 seconds for next scan
}

Listato 5.7 – Il listato I2CScanner verifica la presenza di dispositivi sulla linea I²C e ne stampa l’indirizzo.

Una volta caricato lo sketch, se qualche dispositivo è collegato al bus I²C dovreste veder apparire
messaggi simili ai seguenti, prodotti da un BMP820:
Scanning...
I2C device found at address 0x76 !
done

Scanning...
I2C device found at address 0x76 !
done

Quando due dispositivi I²C hanno lo stesso indirizzo e sono collegati allo stesso bus, abbiamo un
conflitto di indirizzi. Trattandosi di hardware potrebbe non essere sempre semplice risolvere questo
tipo di problemi. I costruttori dei dispositivi cercano però di lasciare sempre dei pin da collegare a
GND o alla tensione di alimentazione con cui modificare l’indirizzo I²C. A volte è sufficiente lasciare
una goccia di stagno o chiudere un ponticello per modificare l’indirizzo. Quando questo non è
possibile possiamo sfruttare il doppio bus I²C del nostro modulo ESP32 mettendo due dispositivi su
due bus differenti. Se i dispositivi con lo stesso indirizzo sono più di due possiamo utilizzare un I²C
multiplexer come il chip TCA9548A che permette di far coesistere fino a 8 dispositivi con lo stesso
indirizzo sullo stesso bus!
Ecco un mio video che parla dell’utilizzo di un modulo con TCA9548A con Arduino:
[Link]
Figura 5.17 – Alcune memorie EEPROM I2C dotate di ponticelli (A0, A1 e A2) per modificare l’indirizzo.

Figura 5.18 – Il modulo TCA9548A crea 8 bus I2C che possono essere utilizzati per far convivere dispositivi con lo stesso indirizzo.
Anche il TCA9548A ha la possibilità di modificare il proprio indirizzo base agendo sui pin A0, A1 e A2 (collegandoli a massa o alla
tensione di alimentazione).

Collegare un display I2C


I display I²C sono ideali per visualizzare informazioni e interagire con l’utilizzatore dei nostri
progetti. Molti display tradizionali di tipo LCD richiedono complessi cablaggi che impegnano svariati
pin del microcontrollore. Impiegando un display OLED tipo il modello SSD1306 saranno necessari
solo due fili (quelli del bus I²C) oltre alle alimentazioni. I display I²C non hanno grandi dimensioni e
solitamente sono monocromatici, però sono grafici, il che li rende ideali per creare disegni,
diagrammi e indicatori. È possibile utilizzare anche alcuni display LCD come i modelli da 16 x 2
caratteri applicando un apposito adattatore per il bus I²C (non tutti i display ne sono forniti) (Figura
5.19-3). Il collegamento dei display a ESP32 è immediato: collegate direttamente i pin SDA e SCL a
quelli del display, eventualmente aggiungendo le resistenze di pull up da 2,2 kΩ.
Figura 5.19 – Alcuni display I²C: display SSD1306 da 128x64 pixel (1), display SSD1306 da 128x32 pixel (2), display LCD 16x2 con
adattatore I²C (3).

Proviamo a collegare un display SSD al sensore di temperatura e umidità BMP280. Lo schema di


connessione è riportato in Figura 5.20 e come potete notare i due dispositivi condividono lo stesso bus
I²C. Vogliamo realizzare un termometro digitale che visualizza la temperatura rilevata dal sensore sul
display OLED.

Figura 5.20 – Connessione di 2 dispositivi.

Per utilizzare il display è necessario installare una libreria utilizzando il Library Manager di Arduino
IDE (dal menu: Sketch > Include Library > Manage Library). Cercando la parola chiave “SSD1306”
vedrete apparire varie possibilità: io ho scelto di installare la libreria “Adafruit SSD1306”. Una volta
installata la libreria vi consiglio di dare una sbirciata agli esempi che vengono aggiunti all’ambiente
di sviluppo. Trovate gli esempi sotto la voce di menu: File > Examples > Adafruits SSD1306.
Probabilmente vi verrà proposto di installare anche la libreria “Adafruit GFX”. Accettate e
aggiungetela perché è necessaria per poter utilizzare la grafica del display.
Figura 5.21 – Installazione della libreria SSD1306.

Per utilizzare il sensore di temperatura e il display serve la libreria Wire.h che aggiungiamo all’inizio
dello sketch:
#include <Wire.h>

Aggiungiamo le librerie per il sensore di temperatura:


#include <BMx280I2C.h>
#define I2C_ADDRESS 0x76
BMx280I2C bmx280(I2C_ADDRESS);

Quindi le librerie per il display SSD1306:


#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Il display utilizzato ha una dimensione di 128x64 pixel e risponde all’indirizzo I²C 0x3C. Non tutti i
display hanno questo indirizzo, a volte alcuni cloni sono assegnati allo 0x3D. In caso di dubbi
verificate con lo sketch I2CScanner. Alcuni modelli di display hanno anche un pin di reset che deve
essere collegato a un pin del microcontrollore; nel caso il vostro modello avesse solo quattro pin,
indicate come pin di reset il numero -1:
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Nel setup() inseriamo il codice per inizializzare il display. L’oggetto display ha un metodo begin()
che restituisce un boolean utile per capire se ci sono problemi con i cablaggi o l’hardware:
if(![Link](SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
[Link]("Verifica i collegamenti del display!");
while(1); //stop
}

Volendo scrivere dei testi specifichiamo le dimensioni, il colore e i font da utilizzare:


[Link](1);
[Link](SSD1306_WHITE);
display.cp437(true);

Ripuliamo lo schermo:
[Link]();

e aggiorniamolo con:
[Link]();

Chiameremo il metodo display() ogni volta che disegneremo qualcosa sul display, per farlo
apparire. Nel loop() visualizziamo la temperatura pulendo lo schermo, spostando il cursore nella
posizione 1,1 (le coordinate x e y) e stampando quanto necessario con print():
[Link]();
[Link](1, 1);
[Link]([Link]());
[Link](" C");

La temperatura rilevata nella nostra stanza sarà un numero tra 0 e 100. La convertiamo in un intero
per poterla visualizzare graficamente.
int x = (int)constrain([Link](), 0, 100);

Tracciamo un rettangolo vuoto usando drawRect() con lo spigolo superiore sinistro nel punto (0,20),
largo 100 pixel e alto 20:
[Link](0, 20, 100, 20, SSD1306_WHITE);

Disegnamo una barra grafica (un rettangolo riempito di bianco) con fill-Rect(), specificando il
punto di inizio (0,20), l’altezza di 20 pixel e la lunghezza pari alla variabile x, calcolata in precedenza
e legata alla temperatura:
[Link](0, 20, x, 20, SSD1306_WHITE);

Infine aggiorniamo lo schermo:


[Link]();

Ecco lo sketch completo:


#include <Arduino.h>
#include <Wire.h>

#include <BMx280I2C.h>
#define I2C_ADDRESS 0x76
BMx280I2C bmx280(I2C_ADDRESS);

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
[Link](115200);
[Link]();

if (![Link]()) {
[Link]("Errore hardware BMx280");
while (1);
}

[Link]();
[Link](BMx280MI::OSRS_P_x16);
[Link](BMx280MI::OSRS_T_x16);

if(![Link](SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
[Link]("Verifica i collegamenti del display!");
while(1); //stop
}

//inizializza il display
[Link](1);
[Link](SSD1306_WHITE);
display.cp437(true);

[Link]();
[Link]();
}

void loop() {
if (![Link]()) {
[Link]("could not start measurement, is a measurement already running?");
return;
}

do {
delay(100);
} while (![Link]());

//visualizza la temperatura
[Link]();
[Link](1, 1);
[Link]([Link]());
[Link](" C");
int x = (int)constrain([Link](), 0, 100);
[Link](0, 20, 100, 20, SSD1306_WHITE);
[Link](0, 20, x, 20, SSD1306_WHITE);
[Link]();

[Link]("Pressure: "); [Link]([Link]());


[Link]("Temperature: "); [Link]([Link]());

delay(1000);
}

Listato 5.8 – Visualizziamo la temperatura rilevata dal sensore BMP280 sul display SSD1306.
Figura 5.22 – Foto del prototipo con ESP32 collegato a un sensore BMP280 e display OLED SSD1306.

SPI
L’interfaccia SPI (Serial Peripheral Interface) è un altro protocollo molto diffuso per collegare
dispositivi al modulo ESP32. Anche con SPI abbiamo un master e uno o più slave ma a differenza
del’I²C sono necessarie più linee per la comunicazione:
• MOSI (Master Out Slave In) o SDI (Serial Data In): usata per inviare dati dal master allo slave;
• MISO (Master In Slave Out) o SDO (Serial Data Out): usata dal master per ricevere dati dallo slave
selezionato;
• SCK (Serial Clock): il segnale di sincronizzazione;
• CS (Chip Select) o SS (Slave Select), cioè la linea per attivare uno slave (di solito lo slave si attiva
ponendo la linea a LOW). Serve una linea per ogni slave da controllare;
• GND: tutti i dispositivi collegati devono condividere anche la massa (GND).

Il protocollo SPI richiede un clock per trasferire i dati e la comunicazione avviene in due direzioni ma
a pilotare le operazioni provvede sempre e solo il master che di solito è il microcontrollore e attiva
ogni singolo slave utilizzando una linea dedicata.
SPI permette di raggiungere maggiori velocità di comunicazione perché la trasmissione può
avvenire in entrambe le direzioni, contemporaneamente. Il clock condiviso, fornito dal master,
permette di sincronizzare tutti i dispositivi tra di loro.
Uno svantaggio di SPI è che se si hanno più slave, per ognuno di essi è necessario “portare un filo”
dal master ai dispositivi controllati.
Esiste uno schema di collegamento che prevede che la linea MISO sia comune a tutti gli slave, ci
sia un solo SS per tutti e tutti i dispositivi siano collegati in catena (daisy-chain). I dati in questo caso
passeranno attraverso tutti i dispositivi slave.
Figura 5.23 – Schema di collegamento di una periferica alla porta SPI (VSPI) di ESP32.

L’ESP32 ha ben quattro interfacce SPI indipendenti (SPI0, SPI1, SPI2 e SPI3) anche se solo due sono
disponibili per i nostri progetti perché SPI0 e SPI1 sono utilizzate per comunicare con la memoria
flash interna. Le porta SPI2 è anche indicata con la sigla HSPI mentre SPI3 è anche indicata come
VSPI. È preferibile non collegare sulle singole porte più di tre dispositivi per volta. Come per
l’interfaccia I²C i pin preassegnati delle porte possono essere riconfigurati da software. La tabella
seguente riporta i pin GPIO predefiniti per le porte HSPI e VSPI.
Tabella 5.1 – Assegnazione dei pin GPIO alle porte SPI.4

HSPI VSPI
MISO o SDO 12 19
MOSI o SDI 13 23

SCK 14 18
CS o SS 15 5

I pin GPIO predefiniti della porta SPI sono assegnati a quattro costanti che possiamo utilizzare
direttamente. I nomi delle costanti sono: MISO, MOSI, SCK, SS. Il valore delle costanti può essere
stampato sul Serial Monitor per essere certi delle assegnazioni:
void setup() {
[Link](115200);
[Link]("MOSI\tMISO\tSCK\tSS");
[Link]("%d\t%d\t%d\t%d\n", MOSI, MISO, SCK, SS);
}

void loop() {}

Listato 5.9 – Codice per visualizzare i pin SPI predefiniti.

Dopo aver caricato lo sketch 5.9, aprite il Serial Monitor per leggere l’assegnazione dei pin:
MOSI MISO SCK SS
23 19 18 5

Utilizzare la porta SPI non è complicato con ESP32 grazie alla libreria SPI.h, che dovete ricordarvi di
includere all’inizio dello sketch:
#include <SPI.h>
La porta si inizializza con:
[Link]();

Se utilizzate dispositivi SPI provvisti di libreria, potrebbe essere che sia la libreria stessa a provvedere
alla configurazione dei pin e della porta. Purtroppo non c’è una regola e può capitare di tutto. Il
metodo begin() permette di specificare esplicitamente i pin da assegnare alla porta SPI:
#define SCK 25
#define MISO 26
#define MOSI 32
#define SS 33
[Link](SCK, MISO, MOSI, SS);

Se non esiste una libreria specifica per il dispositivo SPI che vogliamo utilizzare dobbiamo
trasmettere e inviare dati attraverso la porta. Prima di tutto si deve abilitare lo slave portando a LOW
la linea CS e quindi è possibile trasmettere dei byte con:
[Link](byte b);

Al termine della trasmissione la si deve chiudere con:


[Link]();

È possibile modificare alcuni parametri di una trasmissione SPI per variare tempi e forma dei segnali
generati. Le configurazioni si specificano come parametri del metodo [Link]().
Per maggiori informazioni consultate la pagina di riferimento della libreria
([Link] oppure l’ottima guida di sparkfun
([Link]

Collegare più dispositivi a una porta SPI


Il collegamento di più dispositivi slave a una porta SPI è possibile ma richiede un “filo” aggiuntivo
per ogni dispositivo. Il “filo” serve al master per selezionare con quale dispositivo vuole comunicare e
può essere realizzato utilizzando uno dei pin del microcontrollore. Un esempio di cablaggio è
raffigurato in Figura 5.24 dove utilizziamo come CS/SS i pin 5 e 6. I pin devono essere impostati nel
codice come uscite:
#define CS_1 5
#define CS_2 6
pinMode(CS_1, OUTPUT);
pinMode(CS_2, OUTPUT);

Per dialogare con un dispositivo il master deve “abbassare” il relativo pin di CS. Se il pin è a livello
alto, il dispositivo è disabilitato:
digitalWrite(CS_1, HIGH); // disabilita il primo device
digitalWrite(CS_2, LOW); // abilita il secondo device
Figura 5.24 – Collegamento di più dispositivi SPI.

Utilizzare HSPI e VSPI


Con ESP32 abbiamo a disposizione due interfacce SPI (VSPI e HSPI) che possiamo utilizzare
contemporaneamente. Le due interfacce devono essere configurate indipendentemente utilizzando
l’oggetto SPIClass:
vspi = new SPIClass(VSPI);
hspi = new SPIClass(HSPI);

Chiamando il metodo begin() le inizializziamo assegnandole ai pin standard:


[Link]();
[Link]();

È possibile modificare i pin preassegnati indicando esplicitamente nel begin(), come parametri, i
nuovi pin da utilizzare. I pin possono essere specificati come numeri oppure utilizzando delle costanti
che rendono il codice più leggibile:
[Link](VSPI_CLK, VSPI_MISO, VSPI_MOSI, VSPI_SS);
[Link](HSPI_CLK, HSPI_MISO, HSPI_MOSI, HSPI_SS);

È buona norma anche configurare i pin SS/CS come uscite:


pinMode(VSPI_SS, OUTPUT);
pinMode(HSPI_SS, OUTPUT);

Collegare una SD Card via SPI


Se abbiamo necessità di memorizzare in modo permanente dati per il nostro modulo ESP32 possiamo
utilizzare una scheda di memoria SD (o micro SD). Questi dispositivi di memorizzazione di massa
sono economici e molto immediati da utilizzare perché possiamo collegarli direttamente ai pin di
ESP32. Potete utilizzare un adattatore a cui saldare direttamente i fili che vanno al microcontrollore
oppure cercare una breakout board adatta (che lavori a 3,3 V) (Figura 5.25). Le schede di memoria
SD utilizzano l’interfaccia SPI ed esistono delle librerie per facilitare le operazioni di accesso,
scrittura e lettura dei file.
Figura 5.25 – In Figura : (1) slitta per micro SD Card modificata per poter funzionare a 3,3 V e a 5 V, (2) slitta per SD Card
autocostruita, (3) adattatore per SD Card e (4) micro SD Card.

Per il collegamento della scheda SD tramite SPI possiamo utilizzare i pin predefiniti:
• contatto DAT3/CD su CS (GPIO 5);
• contatto CMD su MOSI (GPIO 23);
• contatto DAT0 su MISO (GPIO 19);
• contatto CLK su SCK (GPIO 18);
• contatto VDD ai 3,3 V di alimentazione;
• contatto VSS al GND.

È consigliabile aggiungere un piccolo condensatore in poliestere da 100 nF tra VDD e VSS per
eliminare eventuali disturbi.

Figura 5.26 – Una SD Card o micro CD card può essere collegata direttamente a un modulo ESP32 rispettando i collegamenti indicati
in figura.

Preparate la scheda di memoria formattandola con un formato FAT16 o FAT32. Il file system sarà di
tipo un po’ “vecchio” e i file potranno avere dei nomi con solo otto caratteri più tre per l’estensione
così da garantire la massima compatibilità. Includiamo le librerie necessarie:
#include <SPI.h>
#include "SD.h"
#include "FS.h"

Nel setup() configuriamo la porta SPI indicando il pin di CS come parametro nel metodo begin().
L’istruzione [Link]() può ricevere una serie di parametri corrispondenti ai pin da utilizzare per la
porta SPI. Se utilizzate i pin standard dedicati alla SPI, non è necessario fornire alcun parametro:
if (![Link](5)) {
[Link]("Errore SD");
while(1);
}
[Link]("SD: ok");

Se l’inizializzazione non va a buon fine o se ci sono errori con la scheda di memoria leggeremo un
messaggio sul Serial Monitor e lo sketch si interromperà: ricontrollate i cablaggi e verificate la
leggibilità della scheda.
Per creare e aprire un file in scrittura utilizziamo [Link](), fornendo il nome del file e il tipo di
accesso desiderato. Useremo:
• FILE_WRITE per creare un file in scrittura (ed eventualmente cancellarlo se esiste già);
• FILE_READ per leggerne il contenuto;
• FILE_APPEND per aprire un file esistente in scrittura e aggiungere i nuovi dati “in coda” a quanto
presente.
File f = [Link]("/[Link]", FILE_WRITE);

Ricordatevi di anteporre al nome del file il simbolo / che indica la root della struttura di cartelle. Se il
file è aperto senza errori possiamo provare a scrivere dei valori presi da un pin analogico (GPIO 34) a
cui possiamo collegare un trimmer da 10 kΩ. La scrittura dei dati avviene con i metodi print(),
println() e write(). Al termine delle scritture ricordatevi sempre di chiudere il file con close():

if (f) {
[Link]("SD: writing...");
[Link]("lettura sensore: ");
int v = analogRead(34);
[Link](v);
[Link]();
[Link]("SD: write ok");
} else {
[Link]("Errore apertura file");
}

Il file system fornito da ESP32 è molto semplice e quindi non è possibile lavorare su più di un file per
volta!
Per la lettura del file, usate open () indicando solo il nome del file: preceduto da / e dalla clausola
FILE_READ:

f = [Link]("/[Link]", FILE_READ);

La lettura del contenuto si fa verificando che ci siano caratteri disponibili:


while ([Link]()) {
}

e quindi leggendo i dati con [Link]() e stampandoli sul Serial Monitor:


[Link]([Link]());

I byte letti appariranno anche nel Serial Monitor. Ricordatevi sempre di chiudere il file con:
[Link]();

Ecco il codice completo per leggere e scrivere dati. Provate a modificare la modalità di scrittura
utilizzando FILE_APPEND invece che FILE_WRITE. Cosa accade?
#include <SPI.h>
#include "SD.h"
#include "FS.h"

void setup() {
[Link](115200);
delay(1000);
[Link]("Test SPI SDCard");

if (![Link](5)) {
[Link]("Errore SD");
while(1);
}
[Link]("SD: ok");

File f = [Link]("/[Link]", FILE_WRITE);


if (f) {
[Link]("SD: writing...");
[Link]("lettura sensore: ");
int v = analogRead(34);
[Link](v);
[Link]();
[Link]("SD: write ok");
} else {
[Link]("Errore apertura file");
}

f = [Link]("/[Link]", FILE_READ);
if (f) {
[Link]("\nSD: reading...");
while ([Link]()) {
[Link]([Link]());
}
[Link]();
[Link]("SD: read ok");
} else {
[Link]("Errore apertura file");
}

void loop() {}

Listato 5.10 – Esempio di scrittura e lettura file su SD Card.

SPIFFS: un’alternativa alle SD Card


I moduli ESP32 dotati di flash memory offrono la possibilità di utilizzare parte della memoria come
spazio disco per le applicazioni dell’utente. Preparando opportunamente la flash potremo utilizzarla
per salvare file e dati in modo permanente. A oggi sono due i sistemi utilizzati per svolgere queste
operazioni: SPIFFS e LittleFS. Entrambi richiedono (purtroppo) l’utilizzo del vecchio ambiente di
programmazione per Arduino (versione 1.8) perché non funzionano ancora con il nuovo IDE (luglio
2023). Anche la flash memory dell’ESP32 è collegata al microcontrollore via porta SPI e queste
librerie creano un file system a basso impatto (light weight) e semplice da utilizzare per creare,
leggere e scrivere file (ma non directory!).
Vedremo SPIFFS anche se al momento non è più mantenuto perché è comunque utilizzato da altre
librerie per il web che lo sfruttano per memorizzare pagine HTML a bordo del modulo ESP32. Ecco
la procedura di installazione per SPIFFS (per LittleFS è simile):
1. scaricate e installate l’ambiente di programmazione Arduino 1.8. Lo trovate scorrendo la pagina
[Link] alla sezione “Legacy IDE (1.8.X)”. Scaricatelo e installatelo. I
due programmi possono coesistere e condivideranno la cartella degli sketch;
2. per utilizzare SPIFFS dovete scaricare un plugin da aggiungere all’IDE. Trovate il plugin a questo
indirizzo: [Link]
3. scaricate il file “[Link]” e salvatelo sul vostro computer;
4. ora dovete trovare la posizione della cartella “sketchbook” di Arduino. Aprite le preferenze di
Arduino 1.8 (Figura 5.27) e individuate la voce “Sketchbook location”. Il percorso riportato è
quello in cui Arduino salva tutti gli sketch;
5. aprite una finestra “File Explorer” e portatevi nella cartella degli sketch. Sul mio mac la cartella si
trova in: /Users/paoloaliverti/Documents/arduino. Con Windows avrete un percorso simile;
6. create una nuova cartella e chiamatela “tools”;
7. spostate il file che avete scaricato, “[Link]”, dentro la cartella “tools” e scompattatelo;
8. chiudete Arduino e riapritelo;
9. Se l’installazione è riuscita, nel menu “tools” (o “strumenti”) selezionando la vostra scheda
ESP32, dovreste vedere la nuova voce “ESP32 Sketch Data Upload” (Figura 5.28).

Figura 5.27 – Pannello delle preferenze di Arduino 1.8.


Figura 5.28 – Nel menu “tools”, dopo l’installazione del plugin apparirà la nuova voce “ESP32 Sketch Data Upload”.

Per caricare dei file nella flash dobbiamo creare la struttura del file system. L’operazione è molto
semplice:
1. create un progetto vuoto (File > New);
2. salvatelo con nome a vostra scelta;
3. aprite la cartella dello sketch navigando sul disco del computer oppure, dal menu “Sketch”,
scegliete la voce “Show Sketch Folder” (Figura 5.29);

Figura 5.29 – Per aprire la cartella dello sketch, dal menu “Sketch”, scegliete la voce “Show Sketch Folder”.

4. all’interno della cartella dello sketch create una cartella chiama “data”;
5. all’interno della cartella “data” create un file di testo a vostro piacimento. Io ho inserito un file
chiamato “[Link]” con una riga di testo: “hello spiffs”. Tutto quello che è contenuto nella cartella
data farà parte dell’ “immagine del disco” che sarà creata nella memoria flash;
dopo aver inserito i file che vi servono (non esagerate con le dimensioni! Lo spazio massimo
6.
disponibile è di qualche MB), collegate la scheda ESP32 al computer e scegliete la voce “Tools” >
“ESP32 Sketch Data Upload” e attendete il caricamento dei file.

Una volta caricati i file a bordo della ESP32 possiamo leggerli e scriverli a piacere utilizzando la
libreria SPIFFS.h che includeremo all’inizio del nostro sketch:
#include "SPIFFS.h"

Nel setup() inizializziamo il file system SPIFFS chiamando il metodo begin() e nel caso qualcosa
vada storto ci fermiamo:
if (![Link](true)) {
[Link]("spiffs error");
while(1);
}

Le operazioni di accesso ai file sono le medesime di quelle utilizzate per le schede SD. L’unica
differenza è nell’apertura del file che avviene chiamando metodi sulla classe SPIFFS:
File file = [Link]("/[Link]");

Se il file esiste possiamo leggerlo:


if (file) {
//lettura del file
}

Per la lettura utilizzeremo un ciclo while che richiama il metodo available(). Al termine delle
operazioni non dimentichiamoci di chiudere sempre il file con il metodo close():
if (file) {
while ([Link]()) {
[Link]([Link]());
}
[Link]();
} else {
[Link]("file non trovato");
}

Possiamo aprire un file in scrittura con open() e passando il carattere w (write). Se il file non esiste
sarà creato, mentre se è presente sarà completamente sovrascritto:
file = [Link]("/[Link]", "w");

Se vogliamo semplicemente aggiungere dati a un file esistente, utilizziamo l’attributo a (append),


utile per realizzare dei file di log:
file = [Link]("/[Link]", "a");

Ecco qui di seguito un esempio completo:


#include "SPIFFS.h"
void setup() {
[Link](115200);
delay(1000);
[Link]("spiffs test");

if (![Link](true)) {
[Link]("spiffs error");
while(1);
}

File file = [Link]("/[Link]");


if (file) {
while ([Link]()) {
[Link]([Link]());
}
[Link]();
} else {
[Link]("file non trovato");
}

//test scrittura
file = [Link]("/[Link]", "a"); //"w" sostituisce
if (file) {
[Link]("test scrittura");
[Link]();
} else {
[Link]("file non trovato 2");
}

file = [Link]("/[Link]");
if (file) {
while ([Link]()) {
[Link]([Link]());
}
[Link]();
} else {
[Link]("file non trovato");
}
}

void loop() {}

Listato 5.11 – Esempio per leggere un file con SPIFFS.

Real Time Clock (RTC)


Abbiamo imparato a creare file utilizzando SPIFFS o le schede SD, su cui spesso salviamo dati
creando dei file di log. Un file di log solitamente tiene traccia di un evento riportando anche la data e
l’ora a cui è avvenuto il fatto.
Mentre un computer comune è dotato di un orologio con batteria per mantenere data e ora, sui
microcontrollori queste informazioni non sono quasi mai disponibili e se servissero si devono
realizzare con circuiti specifici chiamati Real Time Clock, spesso esterni e dotati di batteria. I
moderni moduli come ESP32 prevendono però un RTC interno che può tenere traccia delle
informazioni temporali. Il circuito di clock interno funziona anche quando la scheda è messa in
modalità risparmio energetico a condizione che sia sempre alimentata, magari con una piccola batteria
tampone. Possiamo utilizzare l’RTC interno di ESP32 installando la libreria ESP32Time.
Figura 5.30 – Installazione della libreria ESP32Time.

Dopo averla installata aggiungiamola al nostro sketch:


#include <ESP32Time.h>

Creiamo una istanza dell’oggetto ESP32Time, indicando il fuso orario in secondi. Per Roma dobbiamo
aggiungere due ore e quindi 3600 secondi per 1:
ESP32Time rtc(3600); //GMT+1 (Roma)

Nel setup() impostiamo l’ora iniziale fornendo secondi, minuti, ore, giorno, mese e anno:
[Link](00, 30, 16, 30, 8, 2023); // 16:30:00 del 30/08/2023

In questo modo l’ora sarà “cablata” nel codice e spegnendo la scheda imposterete sempre l’ora a
partire dall’istante che avete configurato. Per aggiornare l’ora in modo dinamico è possibile sfruttare
dei servizi di rete (per esempio NTP) da richiamare per sincronizzare l’RTC e avere sempre l’ora
corretta.
ESP32Time fornisce vari metodi per accedere alle informazioni temporali (date un’occhiata agli
esempi installati con la libreria). Per stampare una data “formattata” utilizziamo getTime()
passandogli un template:
[Link]([Link]("%F %H:%M:%S"));

Ecco un esempio completo:


#include <ESP32Time.h>

ESP32Time rtc(3600); // GMT+1 (Roma)

void setup() {
[Link](115200);
[Link](00, 30, 16, 30, 8, 2023); // 16:30:00 del 30/08/2023
}

void loop() {
[Link]([Link]("%F %H:%M:%S"));
delay(1000);
}

Listato 5.12 – Codice per utilizzare l’RTC interno di ESP32 e stampare l’ora corretta.

Caricando lo sketch 5.12 e aprendo il Serial Monitor osserveremo la data e l’ora impostate:
2023-08-30 18:31:49
2023-08-30 18:31:50
2023-08-30 18:31:51
2023-08-30 18:31:52
2023-08-30 18:31:53

Sleep mode
Se il vostro progetto prevede un’alimentazione a batteria, dovrete sicuramente utilizzare le modalità
di sleep dell’ESP32. Un’applicazione IoT tipica che utilizza alcune periferiche e magari si collega
tramite WiFi a delle reti richiede decine o centinaia di milliampere di corrente. Alimentandolo a
batteria, il dispositivo resterà acceso qualche ora o al massimo qualche giorno. Per prolungare la vita
della carica della batteria possiamo spegnere quello che non serve e risvegliare il microprocessore
solo al verificarsi di certi eventi o periodicamente risparmiando corrente. ESP32 prevede cinque
livelli di attività a seconda di quante funzioni sono attive contemporaneamente:
• Active Mode
• Modem Sleep Mode
• Light Sleep Mode
• Deep Sleep Mode
• Hibernation Mode

Vediamo in dettaglio le varie modalità di risparmio energetico, tenendo presente che i valori di
corrente indicati sono quelli del solo modulo ESP32. Se al vostro circuito sono collegati altri
dispositivi dovrete conteggiare anche i loro consumi ed eventualmente valutare la possibilità di
mettere in sleep pure loro.

Active Mode
Durante l’utilizzo normale del modulo tutte le periferiche interne sono accese e abbiamo il massimo
consumo energetico. Con il modulo WiFi acceso e in trasmissione arriviamo a oltre 200 mA di
consumo e se anche il Bluetooth è attivo, si possono registrare brevi picchi di quasi 1 A di corrente!
Quando il WiFi è in ricezione i consumi scendono a un centinaio di mA.

Modem Sleep Mode


Spegnendo il modulo WiFi e Bluetooth siamo in Modem Sleep Mode. La CPU è pienamente attiva ed
è come se il modulo ESP fosse offline. Con la sola CPU attiva il consumo di corrente scende a 20
mA. Se fosse necessario riattivare la connettività si possono utilizzare tecniche che prevedono brevi
risve-gli periodici.
È possibile spegnere direttamente il WiFi utilizzando le funzioni messe a disposizione dalla libreria
WiFi.h (si veda il prossimo capitolo). Prima di spegnere il modulo è bene terminare eventuali
connessioni esistenti con:
[Link](true);

e poi procedere con lo spegnimento:


[Link](WIFI_OFF);

Ci si potrà ricollegare alla rete riattivando a mano il WiFi e ristabilendo la connessione.


La connessione WiFi può essere anche gestita in modo più automatico. Nel caso sia necessario
mantenere la connessione ma si voglia risparmiare potenza, si potrà limitare il consumo energetico
del solo modulo WiFi mettendolo in sleep. Il modulo manterrà la connessione alla rete con un minimo
di pacchetti scambiati e se necessario si riattiverà automaticamente. In questo caso, dopo la prima
connessione alla rete si richiamerà:
[Link](true);

Ci sono tre diversi livelli di impostazione del comportamento del modulo WiFi ma non sono
direttamente accessibili con le librerie messe a disposizione in Arduino IDE. Anche il modulo
Bluetooth ha un impatto sui consumi energetici, anche se molto più limitato. Possiamo spegnerlo
quando non necessario con:
btStop();

per riattivarlo con:


btStart();

Per aumentare l’efficacia dello sleep e ridurre i consumi può essere utile anche rallentare la velocità
della CPU che solitamente gira alla massima frequenza (240 MHz). Utilizzando:
setCpuFrequencyMhz(40);

possiamo scegliere a che velocità lavorare: più bassa è la frequenza e minore è il consumo. Non
possiamo inserire valori a piacere ma solo quelli previsti e cioè: 240, 160, 80, 40, 20 e 10.

Light Sleep Mode


La modalità Light Sleep Mode è simile alla precedente con i modem spenti e la CPU “rallentata”. Il
segnale di clock che viene fornito solo ai circuiti interni che ne necessitano. La CPU è messa in pausa
a intervalli regolari salvando lo stato in RAM. Il consumo di corrente scende a circa 1 mA. Vediamo
un esempio di come impostare questa modalità di risparmio energetico. Prepariamo uno sketch vuoto
dove configuriamo la porta seriale e scriviamo un messaggio iniziale. Attendiamo quattro secondi
così da avere il tempo di aprire il Serial Monitor. Prima di attivare la modalità di sleep, impostiamo un
risveglio a tempo, dopo quattro secondi:
esp_sleep_enable_timer_wakeup(SLEEP_DURATION);

La variabile SLEEP_DURATION è definita in microsecondi nel seguente modo:


const uint32_t SLEEP_DURATION = 4 * 1000000; // 4s

Avendo configurato un metodo per risvegliare la scheda possiamo attivare la modalità Light Sleep:
esp_light_sleep_start();
Dopo quattro secondi il timer impostato risveglierà la scheda stampando un nuovo messaggio. Ecco
qui di seguito il codice completo:
const uint32_t SLEEP_DURATION = 4 * 1000000; // 4s

void setup() {
[Link](115200);
[Link]("light sleep");

delay(4000);
[Link]("sleep now");
delay(100);

esp_sleep_enable_timer_wakeup(SLEEP_DURATION);
esp_light_sleep_start();

[Link]("Wakeup");
}

void loop() {}

Listato 5.13 – Esempio di attivazione del Light Sleep Mode.

Deep Sleep Mode


Nella modalità Deep Sleep la CPU, gran parte della RAM e tutte le periferiche integrate sono
disabilitate e solo le componenti legate al modulo RTC sono attive. Il consumo di corrente scende
sotto al milliampere. Il coprocessore (ULP – Ultra Low Power) è attivo e può rispondere a eventi
esterni o a un timer interno così da risvegliare il sistema periodicamente e risparmiare la carica della
batteria.
Se è necessario svolgere operazioni in questa modalità si deve utilizzare la memoria RTC perché
quella principale è disabilitata. Per creare variabili nell’area RTC si antepone RTC_DATA_ATTR alla
dichiarazione della variabile:
RTC_DATA_ATTR int nome_della_variabile = 0;

Durante la modalità Deep Sleep solo alcuni pin possono essere utilizzati e sono quelli collegati al
processore ULP. Anche i pin touch possono essere utilizzati. La Figura 5.31 riporta i pin e il loro
nome.

Figura 5.31 – Posizione e nome dei pin GPIO e touch utilizzabili con l’ULP Processor in Deep Sleep Mode.

L’attivazione del Deep Sleep Mode si fa con l’istruzione:


esp_deep_sleep_start();

Prima di eseguire questa istruzione si deve sempre definire il metodo di riattivazione (ne parleremo in
dettaglio tra poco). Vediamo un esempio che utilizza un timer per il risveglio ogni quattro secondi. Il
timer si imposta con:
esp_sleep_enable_timer_wakeup(SLEEP_DURATION);

Ecco l’esempio completo:


const uint32_t SLEEP_DURATION = 4 * 1000000; // 4s

void setup() {
[Link](115200);
[Link]("deep sleep");
delay(4000);
[Link]("sleep now");
delay(100);

esp_sleep_enable_timer_wakeup(SLEEP_DURATION);
esp_deep_sleep_start();

[Link]("Wakeup");
}
void loop() {}

Listato 5.14 – Esempio con Deep Sleep.

Dopo averlo caricato, aprite il Serial Monitor dove potrete osservare che a ogni risveglio la scheda si
riavvierà completamente, come se fosse stata resettata: il loop() non è mai eseguito. Tenete presente
questo comportamento nella realizzazione dei vostri progetti.

Hibernation Mode
Per risparmiare ancora più energia e prolungare la durata della batteria è possibile spingersi fino alla
modalità Hibernation Mode, dove resta attivo solo il timer RTC e abbiamo la possibilità di riattivare il
modulo ESP32 utilizzando delle interrupt su specifici pin. In Hibernation Mode il modulo consuma
2,5 μA.
Per attivare la modalità Hibernation non abbiamo un comando specifico ma è necessario spegnere
uno a uno i dispositivi interni al modulo ESP32. Possiamo quindi modificare il Listato 5.13
aggiungendo le istruzioni per spegnere i pin GPIO e il processore ULP:
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);

Disattiviamo poi le memorie RAM (standard e RTC):


esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);

E infine il clock RTC:


esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);

Ecco l’esempio completo:


const uint32_t SLEEP_DURATION = 4 * 1000000; // 4s

void setup() {
[Link](115200);
[Link]("hibernate");

delay(4000);
[Link]("hibernate now");
delay(100);

esp_sleep_enable_timer_wakeup(SLEEP_DURATION);

esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);

esp_deep_sleep_start();
//qui non arriverà mai
[Link]("Wakeup");
}

void loop() {}

Listato 5.15 – Esempio di utilizzo della modalità Hibernate per il risparmio energetico.

Caricando lo sketch sulla scheda potrete osservare che il modulo va in Hibernation per risvegliarsi
dopo alcuni secondi per effetto di un timer interno. Al risveglio abbiamo un riavvio completo della
scheda, come se l’avessimo resettata. Il codice viene quindi eseguito da capo e non raggiungeremo
mai il loop()!

Mettere a dormire ESP32 e metodi per il risveglio


Ora che abbiamo illustrato le varie possibilità per risparmiare energia con la nostra scheda vediamo
come scrivere alcuni sketch dimostrativi. Prima di attivare lo sleep è sempre raccomandabile decidere
come avverrà il risveglio. Abbiamo tre possibilità:
• risveglio a tempo (con timer);
• risveglio da segnale esterno;
• risveglio con pin touch.

Risveglio con timer


Quando il clock RTC è attivo possiamo impostare un timer che si attivi e risvegli la scheda dallo
sleep. In questo modo la scheda si riattiverà periodicamente permettendo lo svolgimento di operazioni
ripetitive (per esempio data logger) o per controllare lo stato di sensori o per inviare segnalazioni. Il
comando per impostare il timer per il risveglio è semplice:
esp_sleep_enable_timer_wakeup(tempo_risveglio)

Il parametro tempo_risveglio serve per specificare ogni quanti microsecondi il timer dovrà scattare
(1 s = 1.000.000 µs).
Creiamo un nuovo sketch dove inseriamo una variabile che incrementeremo a ogni risveglio.
Utilizzando la modalità Deep Sleep, la variabile sarà dichiarata così:
RTC_DATA_ATTR int count = 0;

Nel setup() configuriamo la porta seriale:


void setup() {
[Link](115200);
delay(1000);

Incrementiamo la variabile count e stampiamone il valore:


count++;
[Link]("\ncount: ");
[Link](count);

Possiamo intercettare la ragione del risveglio utilizzando la funzione


esp_sleep_get_wakeup_cause() e analizzando quanto restituito:
esp_sleep_wakeup_cause_t causa;
causa = esp_sleep_get_wakeup_cause();
Il valore della variabile causa può essere esaminato con una switch() per mettere in atto specifici
comportamenti. Possiamo intercettare se l’evento del risveglio è stato:
• un segnale su un pin: ESP_SLEEP_WAKEUP_EXT0;
• un timer: ESP_SLEEP_WAKEUP_TIMER;
• l’attivazione di un touch pin: ESP_SLEEP_WAKEUP_TOUCHPAD.
switch (causa){
case ESP_SLEEP_WAKEUP_EXT0:
[Link]("Button");
break;
case ESP_SLEEP_WAKEUP_TIMER:
[Link]("timer");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
[Link]("Touch");
break;
default:
[Link]("??");
}

Ora possiamo configurare il timer per il risveglio della scheda. Scegliamo di riattivarla dopo 5 s
(5.000.000 µs):
esp_sleep_enable_timer_wakeup(5000000);

Richiamiamo il Deep Sleep:


esp_deep_sleep_start();

Ecco il codice completo:


RTC_DATA_ATTR int count = 0;
void setup() {
[Link](115200);
delay(1000);

count++;
[Link]("\ncount: ");
[Link](count);

esp_sleep_wakeup_cause_t causa;
causa = esp_sleep_get_wakeup_cause();
switch (causa){
case ESP_SLEEP_WAKEUP_EXT0:
[Link]("Button");
break;
case ESP_SLEEP_WAKEUP_TIMER:
[Link]("timer");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
[Link]("Touch");

break;
default:
[Link]("??");
}

esp_sleep_enable_timer_wakeup(5000000);

[Link]("Sto per andare in sleep...");


delay(1000);
esp_deep_sleep_start();
}

void loop() {} //non eseguito

Listato 5.16 – Esempio di risveglio dallo stato Deep Sleep con timer a 5 s.

Anche in questo caso, caricando lo sketch() e osservando il Serial Monitor, potrete vedere che, dopo
il risveglio programmato, la scheda si riavvierà completamente. I loop() non sarà mai eseguito.
Tenetene conto nei vostri progetti. Di fatto è come se il loop() fosse realizzato dalla sequenza sleep-
risveglio ciclando sul setup().

Risveglio con segnale esterno


Possiamo risvegliare il modulo ESP32 in Deep Sleep utilizzando uno dei pin collegati al processore
ULP. I pin sono riportati in Figura 5.31. La condizione di risveglio va sempre impostata prima di
entrare in modalità Sleep e per utilizzare un cambio di segnale su uno dei pin GPIO utilizzeremo la
funzione:
esp_sleep_enable_ext0_wakeup(GPIO_PIN, LIVELLO_LOGICO);

I parametri sono il numero del pin come indicato sulla scheda e il livello logico che ci aspettiamo. Se
colleghiamo un pulsante con resistore di pull down sul pin GPIO 14 (che corrisponde al pin
RTC_GPIO16), come in Figura 5.32, per risvegliare la scheda, scriveremo:
esp_sleep_enable_ext0_wakeup(GPIO_NUM_14, 1);

Premendo il tasto mentre la scheda è in sleep, il livello sul pin passerà da LOW (0) a HIGH (1) e
quindi riattiveremo ESP32.

Figura 5.32 – Pulsante collegato al pin 14, utilizzato per risvegliare la scheda dallo stato di sleep.

Qui di seguito riporto un esempio completo basato sul Listato 5.16 in cui risvegliamo la scheda dallo
sleep alla pressione del tasto:
RTC_DATA_ATTR int count = 0;

void setup() {
[Link](115200);
delay(1000);

count++;
[Link]("\ncount: ");
[Link](count);

esp_sleep_wakeup_cause_t causa;
causa = esp_sleep_get_wakeup_cause();
switch (causa){
case ESP_SLEEP_WAKEUP_EXT0:
[Link]("Button");
break;
case ESP_SLEEP_WAKEUP_TIMER:
[Link]("timer");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
[Link]("Touch");
break;
default:
[Link]("??");
}

esp_sleep_enable_ext0_wakeup(GPIO_NUM_14, 1);

[Link]("Sto per andare in sleep...");


delay(1000);
esp_deep_sleep_start();

void loop() {}

Listato 5.17 – Esempio di risveglio dallo stato Deep Sleep in seguito alla pressione di un tasto.

Caricando lo sketch e aprendo il Serial Monitor potrete osservare che la scheda si mette in sleep.
Premete il tasto per risvegliarla. Potrete leggere il valore aggiornato di count e la ragione del
risveglio. Ovviamente è possibile combinare più meccanismi di wake-up.
Se fosse necessario controllare più pin contemporaneamente dobbiamo utilizzare un secondo
sistema chiamato ext1. Prima di tutto dobbiamo creare una maschera in cui indicare quali pin
vogliamo utilizzare. La maschera di bit è un numero esadecimale da 5 byte che permette di
controllare fino a 40 pin (anche se la scheda arriva a 35). La maschera di bit può essere rappresentata
in binario usando solo 0 e 1:
00000000 00000000 00000000 00000000 00000000

Lo zero più a destra corrisponde al pin GPIO 0, mentre quello più a sinistra si riferisce al pin GPIO39
(che non esiste). Se volessimo monitorare lo stato del pin 2 scriveremmo una maschera fatta così:
00000000 00000000 00000000 00000000 00000100

Abbiamo impostato a 1 il terzo bit da destra che corrisponde al pin GPIO2. Possiamo attivare più bit e
per risvegliare la scheda se arriva un segnale sui pin 2, 4 e 14 comporremo una maschera fatta così:
00000000 00000000 00000000 01000000 00010100

Per specificare la maschera useremo una codifica esadecimale. Possiamo ottenere rapidamente la
codifica considerando 4 bit per volta e trasformandoli nella relativa cifra esadecimale secondo la
Tabella 5.2.
Dividiamo la nostra maschera di bit in gruppi di 4 cifre:
0000 0000 0000 0000 0000 0000 0100 0000 0001 0100

E poi convertiamo, sempre usando la Tabella 5.2:


0 0 0 0 0 0 4 0 1 4

E compattando:
0x0000004014

Definiamo all’inizio dello sketch la maschera nel seguente modo:


#define BUTTON_PIN_BITMASK 0x0000004014

Tabella 5.2 – Conversione da bit a hex.

Binario Esadecimale
0000 0
0001 1

0010 2
0011 3

0100 4
0101 5

0110 6
0111 7

1000 8
1001 9

1010 A
1011 B
1100 C

1101 D
1110 E

1111 F

Per configurare i pin per il risveglio useremo la funzione esp_sleep_enable_ext1_wakeup() che


necessita, come parametri, della maschera di bit e della modalità di risveglio che può essere:
• ESP_EXT1_WAKEUP_ANY_HIGH: per riattivare la scheda se un qualsiasi pin indicato nella maschera
passa a livello alto;
• ESP_EXT1_WAKEUP_ALL_LOW: se tutti i pin indicati nella maschera sono a livello basso.

Possiamo quindi, per esempio, scrivere:


esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,ESP_EXT1_WAKEUP_ANY_HIGH);

Attiveremo poi il Deep Sleep come di consueto.

Ecco un esempio completo:


#define BUTTON_PIN_BITMASK 0x0000004014

RTC_DATA_ATTR int count = 0;

void setup() {
[Link](115200);
delay(1000);

count++;
[Link]("\ncount: ");
[Link](count);

esp_sleep_wakeup_cause_t causa;
causa = esp_sleep_get_wakeup_cause();
switch (causa){
case ESP_SLEEP_WAKEUP_EXT0:
[Link]("Button ext0");
break;
case ESP_SLEEP_WAKEUP_EXT1:
[Link]("Button ext1");
break;
case ESP_SLEEP_WAKEUP_TIMER:
[Link]("timer");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
[Link]("Touch");
break;
default:
[Link]("??");
}

esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,ESP_EXT1_WAKEUP_ANY_HIGH);

[Link]("Sto per andare in sleep...");


delay(1000);
esp_deep_sleep_start();

void loop() {}

Listato 5.18 – Risveglio con azione su un pin con il metodo ext1.

Risveglio con touch pad


Possiamo risvegliare la nostra ESP32 anche utilizzando i touch pin. Questo è utile se utilizziamo
questi pin per realizzare pulsanti a sfioramento per interagire con il nostro dispositivo che in questo
modo può essere prontamente riattivato non appena un utente interagisce con i pulsanti.
Per fare un esperimento colleghiamo un piccolo quadrato di alluminio o di metallo al pin GPIO2
della scheda. Il pin GPIO2 corrisponde al Touch Pin 2 (i pin touch abilitati sono riportati in Figura
5.31).

Figura 5.33 – Collegamento del pin Touch T2 per risvegliare la scheda dallo Sleep.

Per abilitare il risveglio con i pin touch utilizzeremo la funzione touchAttachInterrupt() fornendo
tre parametri:
• il nome del pin touch (per esempio T2);
• una funzione da richiamare in seguito al tocco;
• il livello di soglia o di sensibilità (più il valore è alto e maggiore è la sensibilità).

Ecco come collegare il pin GPIO2 con un livello di soglia pari a 40:
touchAttachInterrupt(T2, toccato, 40);

Aggiungete gli altri pin che potrebbero servirvi e poi abilitate il risveglio con touch pin invocando:
esp_sleep_enable_touchpad_wakeup();

Infine attivate la modalità di sleep:


esp_deep_sleep_start();

Al risveglio potete sapere quale dei pin ha scatenato il risveglio esaminando il valore ritornato da
esp_sleep_get_touchpad_wakeup_status():

touch_pad_t pins = esp_sleep_get_touchpad_wakeup_status();

La variabile pins contiene un numero intero corrispondente al pin sfiorato. Ecco un esempio
completo:
RTC_DATA_ATTR int count = 0;
void toccato() {}
void setup() {
[Link](115200);
delay(1000);

count++;
[Link]("\ncount: ");
[Link](count);
esp_sleep_wakeup_cause_t causa;
causa = esp_sleep_get_wakeup_cause();
switch (causa){
case ESP_SLEEP_WAKEUP_EXT0:
[Link]("Button");
break;
case ESP_SLEEP_WAKEUP_TIMER:
[Link]("timer");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
[Link]("Touch");

break;
default:
[Link]("??");
}

touch_pad_t pins = esp_sleep_get_touchpad_wakeup_status();


switch (pins){
case 0: [Link]("GPIO 4"); break;
case 1: [Link]("GPIO 0"); break;
case 2: [Link]("GPIO 2"); break;
case 3: [Link]("GPIO 15"); break;
case 4: [Link]("GPIO 13"); break;
case 5: [Link]("GPIO 12"); break;
case 6: [Link]("GPIO 14"); break;
case 7: [Link]("GPIO 27"); break;
case 8: [Link]("GPIO 33"); break;
case 9: [Link]("GPIO 32"); break;
default: [Link]("GPIO ??");
}

touchAttachInterrupt(T2, toccato, 40);


esp_sleep_enable_touchpad_wakeup();
[Link]("Sto per andare in sleep...");
delay(1000);
esp_deep_sleep_start();

void loop() {}

Listato 5.19 – Esempio di risveglio con touch pad.

EEPROM
La ESP32 non ha una EEPROM interna per salvare dati in modo permanente ma utilizza la flash
memory in modo trasparente al programmatore. Su Arduino utilizziamo la libreria EEPROM.h che
possiamo continuare a utilizzare anche per ESP32 con la sola differenza che i dati saranno salvati su
flash senza che noi ce ne accorgiamo. In pratica utilizzeremo una EEPROM simulata da 512 byte.
L’uso della EEPROM può essere utile se utilizziamo l’Hibernation Mode della scheda perché è
l’unico sistema che abbiamo per memorizzare o scambiare informazioni se la RAM non è attiva.
Prima di utilizzare la EEPROM dovremo inizializzare l’area di memoria necessaria indicando il
numero di byte da riservare:
[Link](dimensione_eeprom_desiderata);

Potremo poi scrivere un byte (un numero da 0 a 255) in ogni indirizzo. La scrittura si esegue con:
[Link](indirizzo, dato);

e deve sempre essere seguita dalla chiamata a:


[Link]();

per confermare il salvataggio. Per recuperare un byte dalla EEPROM da uno specifico indirizzo
scriveremo:
uint8_t valore = [Link](indirizzo);

dove uint8_t è un sinonimo del tipo byte senza segno.


Colleghiamo un LED al pin 25 e un pulsante con resistenza di pull down al pin 23, come illustrato
in Figura 5.34, e apriamo un nuovo sketch. Proveremo a memorizzare lo stato del LED, impostato con
il pulsante, così che sia mantenuto anche se spegniamo la scheda.

Figura 5.34 – Schema elettrico per verificare il funzionamento di EEPROM con un LED e un pulsante.

All’inizio dello sketch includiamo la libreria EEPROM e creiamo una costante per definire quanti
byte ci serviranno:
#include <EEPROM.h>
#define EEPROM_SIZE 10

Nel setup() configuriamo la dimensione della EEPROM:


[Link](EEPROM_SIZE);

Impostiamo anche la funzione dei pin 23 e 25:


pinMode(23, INPUT);
pinMode(25, OUTPUT);

Impostiamo lo stato del LED leggendolo dalla EEPROM all’indirizzo 0. Inizialmente il valore sarà
pari a 0:
stato_led = [Link](0);
digitalWrite(25, stato_led);

Nel loop() rileviamo la pressione del pulsante per modificare lo stato del LED. È importante lavorare
sul fronte di discesa perché utilizzando solo lo stato, le scritture nelle EEPROM sarebbero milioni a
ogni pressione del tasto e in breve tempo la danneggeremmo. Per rilevare il fronte di discesa usiamo
due variabili e uno sketch come abbiamo imparato a fare nel Capitolo 4. Usiamo quindi un if() per
intercettare il fronte di discesa:
if (pv_bt && !bt) { //FDD
}

All’interno dell’if() invertiamo lo stato del pulsante, salvando nella variabile il suo valore “negato”
con l’operatore !:
stato_led = !stato_led;

Accendiamo o spegniamo il LED applicando la variabile di stato:


digitalWrite(25, stato_led);

e infine salviamo il nuovo valore dello stato all’indirizzo 0 della EEPROM:


[Link](0, stato_led);
[Link]();

Qui di seguito riporto lo sketch completo. Caricatelo e provate a modificare lo stato del LED, quindi
spegnete la scheda ESP32 scollegandola del tutto e riaccendetela: il LED dovrebbe aver mantenuto lo
stato da voi impostato:
#include <EEPROM.h>

#define EEPROM_SIZE 10

int stato_led;
int bt, pv_bt;

void setup() {
[Link](115200);
[Link](EEPROM_SIZE);

pinMode(23, INPUT);
pinMode(25, OUTPUT);

stato_led = [Link](0);
digitalWrite(25, stato_led);
}

void loop() {
bt = digitalRead(23);

if (pv_bt && !bt) { //FDD


[Link]("BT");
stato_led = !stato_led;
digitalWrite(25, stato_led);
[Link](0, stato_led);
[Link]();
}
pv_bt = bt;
}

Listato 5.20 – Esempio di utilizzo della libreria EEPROM con ESP32.

Utilizzare due CPU


La CPU utilizzata dal modulo ESP32 è Dual Core, il che significa che abbiamo due processori che
possono essere utilizzati indipendentemente e contemporaneamente. I due processori sono identificati
da un numero: 0 o 1. Solitamente quando carichiamo uno sketch è il core 0 a eseguirlo. Possiamo
conoscere su quale dei due core il codice è in esecuzione esaminando il risultato della funzione:
int core_attuale = xPortGetCoreID();

Per eseguire due “listati” in parallelo creeremo dei task e li assegneremo al core desiderato. I codici
condividono memoria e variabili ed è facile creare situazioni di stallo se non si è molto esperti.
L’approccio dei task è preso da FreeRTOS, implementato e integrato nel framework di sviluppo di
ESP32. Possiamo creare quindi dei task, collegati a una funzione a nostro piacere e a uno dei due
core. La funzione associata al task è simile al loop(), solo che richiede un parametro particolare
(vParameters):
void loop1(void * vParameters) {
//codice da eseguire
}

L’assegnazione della funzione/task a uno dei core si fa chiamando nel setup() la funzione
xTaskCreatePinnedToCore() e indicando alcuni parametri:
• il nome della funzione da utilizzare come task;
• il nome che desideriamo dare al task;
• la dimensione dello stack di memoria assegnato al task;
• eventuali parametri;
• la priorità del task;
• un puntatore all’oggetto task;
• il numero del core a cui collegarlo.

Possiamo creare anche più di due task ma per non esagerare ora faremo lampeggiare due LED in
simultanea con due task associati ai due core del processore. I LED li collegheremo ai pin 15 e 16
come riportato in Figura 5.35.

Figura 5.35 – Schema di collegamento di due LED ai pin 15 e 16.

All’inizio dello sketch definiamo i due task che ci servono:


TaskHandle_t t1;
TaskHandle_t t2;

Nel setup() dello sketch assegniamo ai task due funzioni che chiameremo loop1() e loop2():
xTaskCreatePinnedToCore(loop1, "loop1", 10000, NULL, 1, &t1, 0);
xTaskCreatePinnedToCore(loop2, "loop2", 10000, NULL, 1, &t2, 1);

Le due funzioni avranno questo aspetto:


void loop1(void * vParameters) {
}
void loop2(void * vParameters) {
}

Appena entrati nella funzione stampiamo il numero del core assegnato:


void loop1(void * vParameters) {
[Link]("LOOP1 in esecuzione su..");
[Link](xPortGetCoreID());
}

Se vogliamo tenere il task in esecuzione dobbiamo creare un ciclo infinito con un while:
void loop1(void * vParameters) {
[Link]("LOOP1 in esecuzione su..");
[Link](xPortGetCoreID());
while(true) {
//loop del task
}
}
All’interno del loop possiamo accendere e spegnere un LED. Ecco il codice per un esempio completo:
TaskHandle_t t1;
TaskHandle_t t2;

void setup() {
[Link](115200);
delay(1000);
[Link]("multicore");
[Link]("core: ");
[Link](xPortGetCoreID());

xTaskCreatePinnedToCore(loop1, "loop1", 10000, NULL, 1, &t1, 0);


xTaskCreatePinnedToCore(loop2, "loop2", 10000, NULL, 1, &t2, 1);
}

void loop() {}

void loop1(void * vParameters) {


[Link]("LOOP1 in esecuzione su..");
[Link](xPortGetCoreID());
pinMode(15, OUTPUT);
while(true) {
digitalWrite(15, HIGH);
delay(500);
digitalWrite(15, LOW);
delay(500);
}
}

void loop2(void * vParameters) {


[Link]("LOOP2 in esecuzione su..");
[Link](xPortGetCoreID());

pinMode(16, OUTPUT);
while(true) {
digitalWrite(16, HIGH);
delay(100);
digitalWrite(16, LOW);
delay(500);
}
}

Listato 5.21 – Sketch che utilizza due CPU contemporaneamente per far lampeggiare dei LED.

Potete trovare approfondimenti cercando su Google informazioni e tutorial su FreeRTOS.

1
A volte chiamata anche TWI (Two Wire Interface), cioè interfaccia a due fili.
2
Le breakout board per Arduino e ESP32 sono in gran parte compatibili. Potete cercare un BMP280 montato su breakout board per
Arduino e utilizzarlo senza problemi. Verificate sempre se i segnali sono a 3,3 V e alimentate questi moduli sempre a 3,3 V a meno che
non sia diversamente specificato dal produttore.
3
Il datasheet è “il foglio di istruzioni” di un componente elettronico. Per trovare un datasheet del sensore BMP280 digitate nel vostro
motore di ricerca preferito: “BMP280 datasheet pdf”.
4
Schede differenti potrebbero avere assegnazioni dei pin differenti da quella riportata per il modulo di prototipazione ESP32 DevKitC.
6

Trasmettere e ricevere dati

ESP32 offre molte possibilità per trasmettere dati. In questo


capitolo vedremo come utilizzare WiFi e connessioni di rete per
trasmettere e ricevere dati con http, realizzare server e servizi di
rete. Decodificheremo informazioni in JSON e tratteremo di
TCP, UDP e MQTT. ESP32 dispone anche di un modulo
Bluetooth e BLE oltre che di un sistema proprietario di
comunicazione chiamato ESPNow.

WiFi
Le schede che utilizzano i moduli ESP32 devono la loro celebrità e diffusione principalmente alla loro connettività
WiFi. Sin da subito sono apparse come dispositivi per collegare un progetto in rete. Tutte le funzioni e gli oggetti
software necessari per gestire la connessione WiFi sono raccolti nella libreria WiFi.h che includeremo sempre
all’inizio di uno sketch “connesso”. I passi da seguire per collegarsi a una rete WiFi sono i seguenti:
1. avviare una richiesta di connessione a una rete esistente;
2. attendere la connessione;
3. svolgere le operazioni necessarie sulla rete (chiamare un servizio, inviare dati…);
4. disconnettersi.

Tradotto in codice significa includere in un nuovo sketch la libreria WiFi.h:


#include <WiFi.h>

Nel setup() configureremo la porta seriale per poter avere informazioni su quello che accade:
void setup() {
[Link](115200);
delay(1000);
[Link]("Simple WiFi");

Avviamo una richiesta di connessione a una rete esistente di cui conosciamo il nome (SSID) e la password:
[Link](MY_WIFI_SSID, MY_WIFI_PASS);

Leggiamo lo stato della connessione con [Link]() e attendiamo fino a che il collegamento non si è stabilito
e nell’attesa stampiamo una serie di punti:
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
Lo stato della rete può assumere diversi valori che riporto nella Tabella 6.1. La funzione status() può essere
richiamata in qualsiasi momento per conoscere lo stato della rete.
Tabella 6.1 – Stati della rete WiFi.

Nome Stato Descrizione


WL_IDLE_STATUS In attesa di connessione dopo il begin()
WL_NO_SSID_AVAIL Non ci sono reti visibili

WL_SCAN_COMPLETED La scansione delle reti è terminata

WL_CONNECTED Connesso alla rete


WL_CONNECT_FAILED Non è stato possibile collegarsi alla rete

WL_CONNECTION_LOST La connessione è stata persa

WL_DISCONNECTED Disconnesso dalla rete

A questo punto possiamo ricavare alcune informazioni sulla connessione come l’indirizzo IP che ci è stato
assegnato o il livello del segnale:
[Link]("IP: "); [Link]([Link]());
[Link]("Segnale (RRSI): "); [Link]([Link]());

A questo punto potremo svolgere le operazioni desiderate come chiamare un servizio per ottenere informazioni o
inviare dati a un server e quindi al termine di tutto (se necessario), scollegarci chiamando:
[Link]();

Ecco un esempio completo:


#include <WiFi.h>

void setup() {
[Link](115200);
delay(1000);
[Link]("Simple WiFi Connection");

[Link]("nome_rete_wifi", "password");
[Link]("mi connetto...");
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");

[Link]("IP: "); [Link]([Link]());


[Link]("Segnale (RRSI): "); [Link]([Link]());

delay(5000);

//mi scollego
[Link]();
[Link]("disconnesso");
}

void loop() {}

Listato 6.1 – Connessione a una rete WiFi.

Station e Access Point Mode


Il modulo WiFi integrato a ESP32 può funzionare in tre modalità. Nell’esempio appena affrontato lo abbiamo
utilizzato nella modalità “client”, usato per collegarci a una rete esistente.
Questo tipo di comportamento è quello predefinito e non è necessario specificare nulla a livello di codice. Le
modalità previste sono:
• Station: ESP32 si collega a una rete esistente;
• Access Point: ESP32 genera una rete WiFi a cui altri client possono collegarsi;
• Mixed: genera un access point e permette di collegarsi a una rete esistente con un client.

La modalità di funzionamento si deve definire all’inizio dello sketch utilizzando il comando [Link]()
passandogli un opportuno parametro:
[Link](WIFI_STA); //station mode
[Link](WIFI_AP); //access point
[Link](WIFI_AP_STA); //mixed mode – station + access point

Quando il modulo funziona in modalità Station potrebbe far comodo conoscere quali reti siano visibili prima di
tentare la connessione. ESP32 permette di eseguire la scansione delle reti WiFi visibili.

Cercare una rete


Il modulo ESP32 può effettuare una ricerca delle reti WiFi presenti per eventualmente scegliere a quale
connettersi. Per avviare una ricerca il modulo WiFi deve essere in modalità Station e non deve essere connesso a
nessuna altra rete, per questo nel setup() inseriamo queste righe di codice:
[Link](WIFI_STA);
[Link]();

La ricerca si avvia con [Link]() che restituisce il numero di reti trovate. Conviene salvare il risultato
per poi procedere all’estrazione dei risultati:
int reti_trovate = [Link]();

Conoscendo in anticipo il numero di risultati trovati, utilizziamo un ciclo for per estrarre le informazioni su SSID e
RSSI:
for (int i = 0; i < reti_trovate; ++i) {
[Link]([Link](i));
[Link](" p: ");
[Link]([Link](i));
[Link]("[dB] ");
delay(10);
}

Ecco il codice completo:


#include "WiFi.h"

void setup() {
[Link](115200);
[Link](WIFI_STA);
[Link]();
delay(100);
}

void loop() {
int reti_trovate = [Link]();
if (reti_trovate == 0) {
[Link]("nessuna rete disponibile");
} else {
[Link]("trovate %d reti\n", reti_trovate);
for (int i = 0; i < reti_trovate; ++i) {
// Print SSID and RSSI for each network found
[Link]([Link](i));
[Link](" p: ");
[Link]([Link](i));
[Link]("[dB] ");
delay(10);
}
}
delay(5000);
}

Listato 6.2 – Codice per eseguire una scansione delle reti visibili.
Caricando il codice sulla scheda e aprendo il Serial Monitor potrete leggere l’elenco delle reti WiFi rilevate. Ecco
cosa appare a me:
trovate 4 reti
[Link] p: -84[dB]
WebPocket-31D4 p: -85[dB]
WRG-G p: -85[dB]
Emaxy_FRITZ!Box 7530 PH_2 p: -86[dB]

Gestire la connessione
Gestire la connessione WiFi di un piccolo dispositivo, soprattutto se alimentato a batteria o di tipo mobile, può
essere complesso perché è facile perdere la connessione WiFi. Possiamo monitorare lo stato della connessione
chiamando [Link]() e quindi valutare cosa fare nel caso la connessione sia assente.
La soluzione più semplice consiste nel richiamare:
[Link]();

che proverà a ristabilire la connessione precedentemente stabilita, utilizzando la password e l’SSID utilizzati.
Possiamo provare a impostare uno sketch che verifichi la connessione periodicamente con un timer non bloccante
e, nel caso venga persa, effettui la riconnessione. Ecco qui di seguito lo sketch:
#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete_wifi"
#define MY_WIFI_PASS "password"

unsigned long t1;

void setup() {
[Link](115200);

[Link](WIFI_STA);

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

[Link]("\n");
[Link]("IP: ");
[Link]([Link]());

t1 = millis();
}

void loop() {
if ((millis() - t1) > 10000){
[Link]("test rete?");
if ([Link]() != WL_CONNECTED) {
[Link]("Mi riconnetto...");
[Link]();
[Link]();
delay(3000);
}
t1 = millis();
}

Listato 6.3 – Gestione della connessione con riconnessione automatica.

La parte cruciale è il timer non bloccante creato utilizzando una variabile t1 per memorizzare il tempo:
unsigned long t1;

Le operazioni di connessione possono richiedere alcuni secondi quindi il timer è impostato all’uscita del setup()
con:
t1 = millis();

Nel loop() ogni dieci secondi verificheremo lo stato della rete e prima di uscire reimpostiamo il timer:
if ((millis() - t1) > 10000){
//qui verifico la rete

//reimposto il timer
t1 = millis();
}

Verifichiamo lo stato della rete con [Link]() e, se rileviamo che è diverso da WL_CONNECTED, scolleghiamoci
e ricolleghiamoci:
if ([Link]() != WL_CONNECTED) {
[Link]("Mi riconnetto...");
[Link]();
[Link]();
delay(3000);
}

WiFiMulti
La libreria WiFiMulti permette di registrare più reti WiFi così che nel caso la connessione attuale si perda, verrà
automaticamente tentata la connessione a una delle altre reti fornite. L’impiego di WiFiMulti non è molto
complicato e come prima cosa includeremo la libreria nel nostro sketch:
#include <WiFiMulti.h>

Quindi dichiariamo un oggetto WiFiMulti:


WiFiMulti wifimulti;

Creiamo alcune coppie di costanti con le credenziali di accesso delle reti che abbiamo a disposizione.
Immaginiamo di avere accesso a tre reti:
#define WIFI1_SSID "nome_rete_principale"
#define WIFI1_PASS "password_principale"
#define WIFI2_SSID "nome_rete_secondaria"
#define WIFI2_PASS "password_secondaria"
#define WIFI3_SSID "nome_rete_3"
#define WIFI3_PASS "password_3"

Con il metodo addAP() aggiungiamo le credenziali a WiFiMulti:


[Link](WIFI1_SSID, WIFI1_PASS);
[Link](WIFI2_SSID, WIFI2_PASS);
[Link](WIFI3_SSID, WIFI3_PASS);

Impostiamo il modulo WiFi in modalità Station:


[Link](WIFI_STA);

La connessione alla rete si farà poi con il metodo run() di WiFiMulti che selezionerà automaticamente la rete con
il segnale più alto:
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

Richiamando periodicamente run() possiamo verificare se la connessione è attiva e, nel caso fosse persa,
automaticamente sarà selezionata una nuova rete tra quelle indicate. Per richiamare run() all’interno del loop()
utilizziamo un timer non bloccante:
if ((millis()-t1) > 5000) {
[Link]("verifico connessione...");
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}
t1 = millis();
}

Ecco il codice completo:


#include <WiFi.h>
#include <WiFiMulti.h>

WiFiMulti wifimulti;

#define WIFI1_SSID "nome_rete_principale"


#define WIFI1_PASS "password_principale"
#define WIFI2_SSID "nome_rete_secondaria"
#define WIFI2_PASS "password_secondaria"
#define WIFI3_SSID "nome_rete_3"
#define WIFI3_PASS "password_3"
unsigned long t1;

void setup() {
[Link](115200);
delay(2000);
[Link]("WiFi Multi test");

[Link](WIFI1_SSID, WIFI1_PASS);
[Link](WIFI2_SSID, WIFI2_PASS);
[Link](WIFI3_SSID, WIFI3_PASS);

[Link](WIFI_STA);

while ([Link]() != WL_CONNECTED) {


[Link](".");
delay(1000);
}

[Link]("\n");
[Link]("connesso con IP: ");
[Link]([Link]());

t1 = millis();
}

void loop() {
if ((millis()-t1) > 5000) {
[Link]("verifico connessione...");
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

t1 = millis();
}

Listato 6.4 – Esempio completo per l’utilizzo di WiFiMulti.

Eventi
Il modulo WiFi di ESP32 in combinazione con la libreria WiFi.h offre un potente approccio per la gestione della
connessione a una rete: la possibilità di ricevere degli eventi e quindi di richiamare del codice in risposta a quanto
accaduto. Il meccanismo ricorda quello delle interrupt ed evita, per esempio, di continuare a eseguire del codice
per rilevare lo stato della connessione.
Gli eventi rilevabili sono numerosi, ecco una lista delle costanti definite nella libreria e una breve descrizione:
• ARDUINO_EVENT_WIFI_READY: WiFi pronto all’utilizzo;
• ARDUINO_EVENT_WIFI_SCAN_DONE: ricerca reti terminata;
• ARDUINO_EVENT_WIFI_STA_START: avvio modalità Station;
• ARDUINO_EVENT_WIFI_STA_STOP: stop modalità Station;
• ARDUINO_EVENT_WIFI_STA_CONNECTED: connessione avvenuta (modalità Station);
• ARDUINO_EVENT_WIFI_STA_DISCONNECTED: disconnessa dalla rete (modalità Station);
• ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: la rete a cui è connessa ESP32 ha cambiato la modalità di
autenticazione;
• ARDUINO_EVENT_WIFI_STA_GOT_IP: è stato assegnato un indirizzo IP;
• ARDUINO_EVENT_WIFI_STA_LOST_IP: la Station ha perso l’indirizzo IP;
• ARDUINO_EVENT_WIFI_AP_START: access point avviato;
• ARDUINO_EVENT_WIFI_AP_STOP: access point disattivato;
• ARDUINO_EVENT_WIFI_AP_STACONNECTED: un client si è connesso all’access point;
• ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: un client si è disconnesso dall’access point;
• ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: è stato assegnato un IP al client collegato all’access point;
• ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: è stato ricevuto un pacchetto “sonda” dall’interfaccia Access Point;
• ARDUINO_EVENT_WIFI_AP_GOT_IP6: Interfaccia Access Point richiede indirizzo IP6;
• ARDUINO_EVENT_WIFI_STA_GOT_IP6: Interfaccia Station riceve un indirizzo IP6.

A seconda del comportamento desiderato o che vogliamo intercettare, registreremo una nostra funzione,
collegandola all’evento. Le funzioni possono avere un nome a scelta ma devono ricevere come parametri
WiFiEvent_t1 e WiFiEventInfo_t:2

void onConnect(WiFiEvent_t e, WiFiEventInfo_t einfo){


[Link]("connesso");
}

Per collegare la funzione all’evento useremo la chiamata alla funzione onEvent() indicando il nome della funzione
che vogliamo utilizzare e l’evento:
[Link](onConnect, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);

Ecco un esempio completo in cui scriviamo dei messaggi alla connessione e all’assegnazione di un indirizzo IP:
void onConnect(WiFiEvent_t e, WiFiEventInfo_t einfo){
[Link]("connesso");
}

void onIP(WiFiEvent_t e, WiFiEventInfo_t einfo){


[Link]("ip:");
[Link](einfo.got_ip.ip_info.[Link]);
}

void setup() {
[Link](115200);
delay(2000);
[Link]("WiFi test");

[Link](true);

[Link](onConnect, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
[Link](onIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);

[Link](WIFI_STA);

[Link]("nome rete wifi", "password");


while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

[Link]("\n");
[Link]("IP: ");
[Link]([Link]());

[Link](WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);

}
void loop() {

Listato 6.5 – Esempio di utilizzo degli eventi durante la connessione a una rete.

Il meccanismo degli eventi può essere utilizzato per intercettare una disconnessione dalla rete WiFi senza doverne
continuamente verificare lo stato con [Link]().

Impostare i parametri di rete


Un dispositivo connesso a una rete ha sempre un indirizzo (IP) che permette di identificarlo in modo univoco. Gli
indirizzi possono essere assegnati automaticamente (dal sistema DHCP) oppure, occasionalmente, potremmo avere
la necessità di indicare manualmente i parametri di connessione utilizzando una chiamata a [Link]() e
fornendo, nell’ordine:
• indirizzo IP della scheda;
• indirizzo IP del gateway;
• il tipo di classe di indirizzi (subnet mask);
• l’indirizzo IP del DNS (Domain Network Server) primario (facoltativo);
• l’indirizzo IP del secondario (facoltativo).

Questi indirizzi si definiscono utilizzando una speciale classe chiamata IPAddress a cui passeremo i quattro
numeri che compongono l’indirizzo IP. Ecco un esempio:
IPAddress ip_scheda(192, 168, 1, 21);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
IPAddress DNS1(8, 8, 8, 8);
IPAddress DNS2(8, 8, 4, 4);

Creati gli indirizzi li possiamo passare al [Link]():


[Link](ip_scheda, gateway, subnet, DNS1, DNS2);

La funzione config() restituisce un boolean e in caso di mancata configurazione possiamo prendere


provvedimenti.
Una volta che la scheda è collegata in rete possiamo interrogare la classe WiFi per ottenere informazioni sulla
configurazione attuale:
[Link]();
[Link]();
[Link]();
[Link]();

Ogni dispositivo di rete ha anche un identificativo univoco legato all’hardware, chiamato MAC Address. Possiamo
ottenere il MAC delle nostra scheda con:
[Link]();

WiFiClient per leggere una pagina web


Una volta che siamo collegati a una rete WiFi possiamo inviare o ricevere pacchetti con vari servizi. Possiamo
leggere una pagina web collegandoci a un server web. I server per il web comunicano utilizzando il protocollo http
che non è molto complesso da gestire. Le operazioni saranno svolte utilizzando uno speciale oggetto di tipo
WiFiClient, preso dalla libreria WiFi.h, che istanzieremo non appena stabilita la connessione:

WiFiClient client;

Per ottenere una pagina web con l’HTTPClient:


1. stabiliremo una connessione con il server indicando il suo URL e la porta del servizio esposto (per i server web
la porta standard è la 80);
2. stabilita la connessione richiederemo la pagina o la risorsa a cui siamo interessati con il comando GET;
3. attendiamo la risposta del server;
4. leggiamo i byte della risposta e stampiamoli o interpretiamoli.

Ci connetteremo a un server utilizzando il metodo connect() e specificando l’URL e la porta a cui collegarci.
Connect restituisce un booleano che ci permette di capire se la connessione è avvenuta oppure no:
if (![Link]("[Link]", 80)) {
[Link]("non connesso");
while(true); //mi blocco per sempre!
} else {
//invio la richiesta GET
}

Per inviare la richiesta GET al server invieremo delle stringhe utilizzando l’oggetto client. Come prima cosa
invieremo il comando GET seguito dalla pagina o risorsa che desideriamo ricevere e dalla versione del protocollo
da utilizzare:
[Link]("GET /[Link] HTTP/1.1");

Poi specifichiamo l’host che nel nostro caso è identico al server. Uno stesso server può ospitare vari sidi/domini
che condividono lo stesso indirizzo IP, quindi quando richiediamo una pagina dobbiamo anche indicare a quale
sito/dominio ci riferiamo:
[Link]("Host: [Link]");

Chiudiamo la connessione e inseriamo una riga vuota.


[Link]("Connection: close");
[Link]("");

Ora attendiamo la risposta del server verificando se siano disponibili dei caratteri per il client:
while ([Link]() == 0) {
}

Per non rischiare di attendere all’infinito, inseriamo un timeout creando una variabile t1 di tipo unsigned long
prima di entrare in attesa. Se la differenza tra il tempo corrente, dato da millis(), e il valore in t1 è superiore a
5000 ms, allora stampiamo un messaggio, blocchiamo il client e interrompiamo per sempre lo script:
unsigned long t1 = millis();
while ([Link]() == 0) {
if ((millis() - t1) > 5000) {
[Link]("timeout");
[Link]();
while(true);
}
}

Ora siamo sicuri che ci siano dati in arrivo e quindi li possiamo leggere con readStringUntil() e stampare
direttamente sulla seriale:
while ([Link]()) {
String line = [Link]('\r');
[Link](line);

Infine terminiamo le operazioni del client e scolleghiamoci dal server:


[Link]();

Ecco il listato completo:


#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

void setup() {
[Link](115200);
delay(1000);
[Link]("Simple WiFi Client");
[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");[Link]([Link]());

delay(1000);

WiFiClient client;
if (![Link]("[Link]", 80)) {
[Link]("non connesso");
while(true);
} else {

[Link]("GET /[Link] HTTP/1.1");


[Link]("Host: [Link]");
[Link]("Connection: close");
[Link]("");

unsigned long t1 = millis();


while ([Link]() == 0) {
if ((millis() - t1) > 5000) {
[Link]("timeout");
[Link]();
while(true);
}
}

//ricezione dati
while ([Link]()) {
String line = [Link]('\r');
[Link](line);
}

[Link]();
}
[Link]();
}

void loop() {}

Listato 6.6 – Lettura di una pagina web utilizzando la connessione WiFi.

Modificate le credenziali di accesso alla rete WiFi e caricate lo sketch sulla scheda ESP32. Aprendo il Serial
Monitor dovreste leggere qualcosa di simile a quanto riportato qui di seguito:
Simple WiFi Client
....
connesso
IP: [Link]
HTTP/1.1 200 OK
Server: aruba-proxy
Date: Tue, 12 Sep 2023 07:13:32 GMT
Content-Type: text/plain
Content-Length: 370
Connection: close
Vary: Accept-Encoding
Last-Modified: Mon, 24 Feb 2020 15:52:00 GMT
Accept-Ranges: bytes
X-ServerName: [Link]
_ _ _ _ _ _ _ _ _
| | | | | | | | | | | | | | | |
| |_| | ___| | | ___ | | | | ___ _ __| | __| | |
| _ |/ _ \ | |/ _ \ | |/\| |/ _ \| '__| |/ _' | |
| | | | __/ | | (_) | \ /\ / (_) | | | | (_| |_|
\_| |_/\___|_|_|\___/ \/ \/ \___/|_| |_|\__,_(_)

Il server risponde con una sequenza di linee tra cui vediamo anche il codice di risposta 2003 che corrisponde al
caso in cui la risorsa è stata trovata e la richiesta è corretta.
La pagina che abbiamo chiamato è raggiungibile all’indirizzo: [Link]
Possiamo vedere il contenuto del file [Link] inserendo il link nella barra indirizzi di un browser che
contatterà il server inviando un comando GET. Possiamo chiamare semplici servizi web utilizzando questa
modalità e magari aggiungendo dei parametri alla nostra richiesta. I parametri di una GET sono “appiccicati”
all’indirizzo e iniziano sempre con un ? seguito da varie coppie nome-valore. Le coppie sono separate tra di loro
dal carattere & mentre tra il nome e il valore troviamo un segno =. Ecco un esempio di chiamata GET con
parametri:

[Link]

Provate a inserirlo in un browser per vedere cosa accade!

GET con HTTPClient


Utilizzando la classe HTTPClient possiamo semplificare le chiamate su web. Per utilizzare il client evoluto,
includiamo la libreria all’inizio dello sketch:
#include <HTTPClient.h>

Stabiliamo la connessione alla rete WiFi come di consueto e poi creiamo un oggetto HTTPClient:
HTTPClient http;

Prepariamo una stringa contenente l’indirizzo della risorsa web da interrogare aggiungendo anche i parametri in
GET:
String addr = "[Link]

Prepariamo il client http comunicandogli l’indirizzo da chiamare:


[Link](addr.c_str());

Alcuni server potrebbero richiedere una login e password che possiamo fornire con:
[Link]("SERVER_LOGIN", "SERVER_PASSWORD");

Avviamo quindi la richiesta GET() e riceviamo il codice di risposta http:


int code = [Link]();

Se il codice è pari a 200 significa che la richiesta è corretta ed è stata accolta, quindi possiamo leggere la risposta
del server salvandola in una stringa:
String risposta = [Link]();
[Link](risposta);

L’utilizzo di HTTPClient semplifica notevolmente le operazioni in http. Ecco il codice di un esempio completo:
#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"
#include <HTTPClient.h>

unsigned long t1;

void setup() {
[Link](115200);
delay(1000);
[Link]("Simple WiFi Client");

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");[Link]([Link]());

t1 = millis();
}

void loop() {
if ((millis() - t1) > 10000) {
if ([Link]()== WL_CONNECTED) {
HTTPClient http;

String addr = "[Link]


[Link](addr.c_str());
//[Link]("SERVER_LOGIN", "SERVER_PASSWORD");

int code = [Link]();


[Link](code);

if (code == 200) {
String risposta = [Link]();
[Link](risposta);
} else {
[Link]("Errore HTTP: ");
[Link](code);
}
[Link]();
}
t1 = millis();
}
}

Listato 6.7 – Esempio di chiamata GET effettuata con HTTPClient.

Invio dati in POST


I dati inviati con il metodo GET sono in chiaro e fanno parte dell’indirizzo. Se si vuole una maggior discrezione
possiamo nasconderli all’interno dell’intestazione della richiesta così che non siano immediatamente visibili. Per
inviare i dati includendoli nell’header della richiesta http utilizzeremo il metodo POST. Una volta ottenuta la
connessione alla rete WiFi possiamo utilizzare l’HTTPClient per mandare i dati ma in questo caso per la sua
inizializzazione sarà necessario creare anche un oggetto WiFiClient:
WiFiClient client;
HTTPClient http;
String addr = "[Link]
[Link](client, addr);

Nel metodo begin() abbiamo impostato l’indirizzo del servizio a cui manderemo i dati in POST. Fate attenzione
perché WiFiClient supporta solo server che rispondono in http e non in https (per cui sono necessarie maggiori
complicazioni).
Aggiungiamo alla richiesta da inviare una riga di intestazione (header) per avvisare che saranno presenti dei
parametri:
[Link]("Content-Type", "application/x-www-form-urlencoded");

Infine inviamo i dati con una POST:


String dati = "temp=23.4&hum=45&colr=red";
int code = [Link](dati);

Ecco il codice completo di uno sketch per inviare dati a un server con una POST:
#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

#include <HTTPClient.h>
unsigned long t1;

void setup() {
[Link](115200);
delay(1000);
[Link]("Simple WiFi Client");

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");[Link]([Link]());

t1 = millis();
}

void loop() {
if ((millis() - t1) > 10000) {
if ([Link]()== WL_CONNECTED) {
WiFiClient client;
HTTPClient http;

String addr = "[Link]


[Link](client, addr);
//[Link]("SERVER_LOGIN", "SERVER_PASSWORD");

[Link]("Content-Type", "application/x-www-form-urlencoded");
String dati = "temp=23.4&hum=45&colr=red";
int code = [Link](dati);
[Link](code);

[Link]();
}
t1 = millis();
}
}

Listato 6.8 – Esempio di chiamata a un servizio con invio dati in POST.

Inviare dati in JSON


JSON è un formato per rappresentare dati molto utilizzato e un grande numero di servizi web lo ha adottato per la
sua semplicità. Un altro formato spesso usato per rappresentare informazioni sul web è XML, che però richiede
maggiori capacità di elaborazione. JSON è più snello e comprensibile e quindi nei progetti con Arduino e ESP32 è
il preferito.
JSON può rappresentare dati complessi, organizzati in oggetti o entità. Possiamo raccogliere una serie di
informazioni racchiudendole in una lista all’interno di una coppia di parentesi graffe. Le informazioni saranno
sempre rappresentate da una coppia nome-valore divisa da un segno :. I testi sono sempre racchiusi tra virgolette
doppie ":
{ "temp":23.4, "hum":45, "color":"red"}

Una rappresentazione più corretta prevede che ci siano oggetti con proprietà e non solo liste di parametri. I
parametri raccolti da un sensore potrebbero essere organizzati in una entità scritta così:
{
"meteo-sensor":
{
"temp":23.4,
"hum":45,
"color":"red"
}
}
JSON permette anche di rappresentare array usando le parentesi quadre. Vi invito ad approfondire il tema su
Internet (per esempio sul sito di [Link] [Link]
Per inviare dati in JSON possiamo semplicemente scriverli in una stringa che poi invieremo in POST con un
HTTPClient. Per avvisare il server della ricezione dei dati in formato JSON daremo il comando:

[Link]("Content-Type", "application/json");

Potremo semplicemente inviare una stringa di testo con il JSON generato avendo la cura di anteporre alle
virgolette il caratetre di escape \:
int httpResponseCode = [Link]("{\"meteo-sensor\": { \"temp\":23.4, \"hum\":45, \"color\":\"red

Per la ricezione e generazione automatica di dati in JSON conviene però affidarsi alla libreria ArduinoJson che
possiamo installare utilizzando il library manager. La versione attuale è la 6. Includiamo la libreria all’inizio dello
sketch:
#include <ArduinoJson.h>

Per la creazione del JSON creiamo un oggetto DynamicJsonDocument specificando la dimensione massima in byte:
DynamicJsonDocument doc(1024);

Possiamo aggiungere un’entità principale chiamata sensor al documento con:


JsonObject sensor = [Link]("sensor");

Aggiungeremo poi le proprietà dell’entità con:


sensor["temp"] = 24.8;
sensor["hum"] = 40;
sensor["color"] = "red";

Possiamo anche aggiungere un array, inserendolo nell’oggetto sensor e poi indicando tutti gli elementi che
servono:
JsonArray coords = [Link]("coords");
[Link](45.12340);
[Link](7.34456);

Per visualizzare sul Serial Monitor la stringa JSON generata utilizziamo uno dei due metodi disponibili (il secondo
formatta i campi rendendo tutto più leggibile):
serializeJson(doc, Serial);
serializeJsonPretty(doc, Serial);

Aprendo il Serial Monitor vedremo apparire:


{
"sensor": {
"temp": 24.8,
"hum": 40,
"color": "red",
"coords": [
45.1234,
7.34456
]
}
}

Se invece vogliamo salvare il JSON generato in una stringa scriveremo:


String output;
serializeJson(doc, output);
[Link](output);

Ecco il codice di un esempio completo:


#include <ArduinoJson.h>
void setup() {
[Link](115200);

DynamicJsonDocument doc(1024);
JsonObject sensor = [Link]("sensor");
sensor["temp"] = 24.8;
sensor["hum"] = 40;
sensor["color"] = "red";

JsonArray coords = [Link]("coords");


[Link](45.12340);
[Link](7.34456);

serializeJson(doc, Serial);
[Link]();

serializeJsonPretty(doc, Serial);
[Link]();

String output;
serializeJson(doc, output);
[Link](output);
}

void loop() {}

Listato 6.9 – Esempio di codice per la generazione di dati in formato JSON utilizzando la libreria ArduinoJson.

Ricevere dati in formato JSON


Se il servizio che interroghiamo invia dati in formato JSON possiamo facilmente estrarre le informazioni che ci
servono utilizzando il parser di Arduino-Json. Otterremo un oggetto “ramificato” che corrisponde alla struttura dati
ricevuta, contenente tutte le coppie nome-valore valorizzate. Prendiamo come punto di partenza il Listato 6.7 in
cui abbiamo realizzato una chiamata GET con l’oggetto HTTPClient. Possiamo chiamare un servizio online che
pubblica dati in formato JSON come il servizio meteo di OpenWeatherMap ([Link] Per
poter utilizzare i servizi (o le API – Access Point Interface) del sito è necessario registrarsi e poi fare richiesta di
una chiave. Una volta registrati, eseguite l’accesso al sito e nel menu a discesa presente in corrispondenza del
vostro nome (vedi Figura 6.1) scegliete la voce “My API keys”. Nella sezione “API Keys” del vostro profilo
potrete creare una nuova chiave di accesso (key) da utilizzare a ogni chiamata verso i servizi del sito web. Potete
provare a interrogare i servizi anche con un semplice browser web chiamando il seguente indirizzo (dopo aver
sostituito la vostra API key di fianco a APPID):
[Link]
Nella finestra del browser potrete leggere la risposta in JSON del servizio:
{"coord":{"lon":9.1895,"lat":45.4643},"weather":[{"id":802,"main":"Clouds",
"description":"scattered clouds","icon":"03d"}],"base":"stations","main":
{"temp":297.47,"feels_like":297.65,"temp_min":294.38,"temp_max":300.09,
"pressure":1015,"humidity":65},"visibility":10000,"wind":{"speed":3.6,
"deg":10},"clouds":{"all":40},"dt":1694622522,"sys":{"type":2,"id":2012644,
"country":"IT","sunrise":1694581124,"sunset":1694626814},"timezone":7200,
"id":3173435,"name":"Milan","cod":200}

Figura 6.1 – Il sito OpenWeatherMap offre servizi gratuiti per le previsioni del tempo. I dati sono ottenibili richiamando i servizi pubblicati sul sito e
restituiscono un risultato in JSON.

Figura 6.2 – Per poter interrogare i servizi è necessario generare una chiave (key) da utilizzare a ogni chiamata.

Possiamo effettuare la chiamata direttamente dalla ESP32 utilizzando un HTTPClient e un WiFiClient:


WiFiClient client;
HTTPClient http;

Prepariamo una stringa con l’indirizzo da chiamare (ricordatevi di sostituire la vostra key di fianco a APPID):
String addr = "[Link]

Inizializziamo il client http:


[Link](client, addr);

Avviamo la richiesta GET e salviamo il codice HTTP di ritorno:


int code = [Link]();

Prepariamo una stringa con all’interno un JSON “vuoto” così che in caso di errori il parser non vada in errore e poi
salviamo la risposta del server all’interno della stringa:
String json = "{}";
json = [Link]();

Instanziamo un documento json dinamico da 1024 byte (dipende dalla quantità di informazioni che ci invia il
server) e avviamo la decodifica:
DynamicJsonDocument doc(1024);
deserializeJson(doc, json);

Se tutto è andato bene possiamo iniziare a “navigare” nella struttura dati che è stata creata:
JsonObject obj = [Link]<JsonObject>();

La temperatura, espressa in gradi Kelvin, è posta all’interno dell’elemento main. Possiamo provare a estrarla e poi
a stamparla sul Serial Monitor, convertendola in gradi centigradi:4
double t = (double)obj["main"]["temp"];
[Link]("Temperature: ");
[Link]((t - 273.15));

Ecco il codice completo:


#include <ArduinoJson.h>
#include <WiFi.h>

#define MY_WIFI_SSID "nome_rete"


#define MY_WIFI_PASS "password"

#include <HTTPClient.h>

unsigned long t1;

void setup() {
[Link](115200);
delay(1000);
[Link]("Simple WiFi Client");
[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");[Link]([Link]());

t1 = millis();
}

void loop() {
if ((millis() - t1) > 10000) {
if ([Link]()== WL_CONNECTED) {
WiFiClient client;
HTTPClient http;
String addr = "[Link] IT&APPID=0c051000000

[Link](client, addr);
int code = [Link]();
String json = "{}";
[Link]("HTTP Response code: ");
[Link](code);

if (code > 0) {
json = [Link]();
[Link](json);
DynamicJsonDocument doc(1024);
deserializeJson(doc, json);
JsonObject obj = [Link]<JsonObject>();

double t = (double)obj["main"]["temp"];
[Link]("Temperature: ");
[Link]((t - 273.15));
}
[Link]();
}
t1 = millis();
}
}

Listato 6.10 – Codice per interrogare il servizio di OpenWeatherMap che fornisce le previsioni del tempo in formato JSON.

Caricando lo sketch sulla scheda ESP32 nel Serial Monitor dovremmo leggere qualcosa di simile:
Simple WiFi Client
..
connesso
IP: [Link]
HTTP Response code: 200
{"coord":{"lon":9.1895,"lat":45.4643},"weather":[{"id":802,"main":"Clouds",
"description":"scattered clouds","icon":"03d"}],"base":"stations",
"main":{"temp":296.24,"feels_like":296.43,"temp_min":293.14,"temp_max":
299.54,"pressure":1016,"humidity":70},"visibility":10000,"wind":{"speed":
3.6,"deg":350},"clouds":{"all":40},"dt":1694602629,"sys":{"type":
2,"id":20368,"country":"IT","sunrise":1694581124,"sunset":1694626814},
"timezone":7200,"id":3173435,"name":"Milan","cod":200}
Temperature: 23.09

L’ultima riga riporta la temperatura ricavata dal JSON.

Chiamate in HTTPS
La maggioranza dei siti Internet e dei servizi utilizza oggi HTTPS, cioè la versione sicura di HTTP in cui i dati
sono criptati con una tecnologia SSL/TLS. Tutte le informazioni scambiate con il server saranno quindi protette da
un sistema di crittografia, in più, per essere sicuri che il server a cui ci colleghiamo sia veramente quello
dichiarato, si utilizzano dei certificati.
Ogni server HTTPS pubblica un suo certificato che possiamo scaricare e che utilizzeremo per iniziare lo
scambio dati. Per collegarci a un server HTTPS con ESP32 dobbiamo ottenere il suo certificato. I server che
lavorano in HTTPS si possono riconoscere perché, visitandoli con un browser, di fianco all’indirizzo possiamo
vedere un piccolo lucchetto (con Chrome). Cliccando sul lucchetto possiamo ispezionare se la connessione è sicura
e accedere al certificato.
Per scaricare il certificato con Chrome:
1. aprite l’indirizzo che vi interessa per esempio [Link]
2. di fianco alla barra dell’indirizzo cliccate sul piccolo lucchetto (Figura 6.3 - 1) per aprire una finestra
informativa;
3. cliccate di fianco alla scritta “Connection is secure” (Figura 6.3 - 2) per aprire la finestra Security;
4. nella finestra Security cliccate di fianco a “Certificate is Valid” (Figura 6.3 - 3) per aprire la finestra del
certificato;
5. nella finestra del certificato, selezionate il pannello “Details” (Figura 6.4 - 1);
6. cliccate sulla prima riga (Figura 6.4 - 2), quella con la parola “root”;
7. esportate il certificato premendo il pulsante “Export” (Figura 6.4 - 3);
8. salvate il certificato in una cartella a piacere.
Figura 6.3 – Per accedere al certificato del server, su Chrome, di fianco alla barra dell’indirizzo cliccate sul piccolo lucchetto (1). Se il sito ha un
certificato https valido leggerete la voce “Connection is secure”. Cliccate di fianco alla scritta (2) e nella nuova finestra cliccate di nuovo di fianco a
“Certificate is valid” per aprire la finestra del certificato.

Figura 6.4 – Nella finestra del certificato, selezionate il pannello “Details” (1), cliccate sulla prima riga (2), quella con la parola “root”, e quindi
esportate il certificato premendo il pulsante “Export” (3).

Aprite il certificato con un editor di testo e copiate tutto il contenuto facendo attenzione a non modificare nulla:
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
rqXRfboQnoZsG4q5WTP468SQvvG5
-----END CERTIFICATE-----

Incollate il contenuto in un nuovo sketch di Arduino IDE subito dopo la dichiarazione di un array di caratteri.
Aggiungete anche le virgolette all’inizio e alla fine di ogni riga, il carattere di “a capo” e il carattere escape \ a fine
riga:
const char* root_ca = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n" \
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n" \
"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n" \
"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n" \
"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n" \
"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n" \
"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n" \
"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n" \
"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n" \
"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n" \
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n" \
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n" \
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n" \
"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n" \
"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n" \
"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n" \
"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n" \
"rqXRfboQnoZsG4q5WTP468SQvvG5\n" \
"-----END CERTIFICATE-----\n";

Così abbiamo inserito tutto il certificato in una variabile all’interno dello sketch. Modifichiamo lo sketch 6.7
aggiungendo l’include per la libreria WiFiClient-Secure:
#include <HTTPClient.h>
#include <WiFiClientSecure.h>

Effettuiamo la connessione al WiFi come di consueto, poi nel punto in cui dovremo fare la chiamata al server
creiamo due oggetti client:
HTTPClient https;
WiFiClientSecure *client = new WiFiClientSecure;

Assegniamo al WiFiClientSecure il certificato:


client->setCACert(root_ca);

Eseguiamo la chiamata al servizio o alla pagina sotto https inizializzando l’HTTPClient anche con il
WiFiClientSecure:

String addr = "[Link]


[Link](*client, addr.c_str());

Possiamo poi procedere con la GET() come di consueto:


int code = [Link]();
[Link](code);
if (code == 200) {
String risposta = [Link]();
[Link](risposta);
}

Ecco il codice completo:


#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
const char* root_ca = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n" \
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n" \
"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n" \
"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n" \
"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n" \
"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n" \
"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n" \
"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n" \
"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n" \
"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n" \
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n" \
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n" \
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n" \
"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n" \
"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n" \
"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n" \
"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n" \
"rqXRfboQnoZsG4q5WTP468SQvvG5\n" \
"-----END CERTIFICATE-----\n";
unsigned long t1;

void setup() {
[Link](115200);
delay(1000);
[Link]("Simple HTTPS Call");

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");
[Link]([Link]());

t1 = millis();
}

void loop() {
if ((millis() - t1) > 5000) {
if ([Link]() == WL_CONNECTED) {
HTTPClient https;
WiFiClientSecure *client = new WiFiClientSecure;
client->setCACert(root_ca);

String addr = "[Link]


[Link](*client, addr.c_str());

int code = [Link]();


[Link](code);

if (code == 200) {
String risposta = [Link]();
[Link](risposta);
} else {
[Link]("Errore HTTP: ");
[Link](code);
}
[Link]();
}
t1 = millis();
}
}

Listato 6.11 – Esempio per chiamare una pagina o un servizio su un server https.

HTTP Server
Una volta che ESP32 è connessa a una rete può funzionare anche da server invece che da client e pubblicare
pagine e servizi accessibili da altri dispositivi. Per realizzare un semplice server che riceve richieste http e risponde
con una pagina statica o dinamica possiamo utilizzare l’oggetto WiFiServer che resta in ascolto delle richieste e
crea un WiFiClient per la loro gestione. La classe WiFiServer è inclusa in WiFi.h e quindi possiamo creare subito
un’istanza del server indicando la porta di ascolto (che per l’http è di solito la 80):
#include <WiFi.h>
WiFiServer server(80);

Effettuiamo la connessione alla rete WiFi e assicuriamoci di stampare l’indirizzo assegnato alla scheda una volta
avvenuta la connessione, così da sapere dove trovare il server:
[Link]("IP: ");
[Link]([Link]());

Prima di abbandonare il setup() avviamo il server con:


[Link]();

Nel loop() interroghiamo continuamente il server per verificare se sia arrivata qualche richiesta con:
WiFiClient client = [Link]();

Nel caso sia giunta una richiesta da un client, l’oggetto client sarà valorizzato e potremo iniziare a interagire:
if (client) {
[Link]("new req");

//gestisco la richiesta

[Link]();
[Link]("client scollegato");
}

Il client invierà al server una sequenza di caratteri che dovremo combinare in una linea per poi, eventualmente,
analizzarne il contenuto. Prepariamo una stringa per comporre la linea e creiamo un ciclo while() che resterà
attivo fino a che il client sarà collegato al server:
String line = "";
while ([Link]()) {
}

Nel ciclo verifico se ci siano caratteri in arrivo dal client e li estraggo uno per uno con [Link]():
if ([Link]()) {
char c = [Link]();
}

Se vogliamo analizzare la richiesta effettuata dal browser stampiamo sul Serial Monitor i caratteri così come
arrivano:
[Link](c);

I caratteri possono essere:


• fine linea (\n): indica che la linea è terminata e possiamo esaminarla per poi fornire una risposta. Prima di
terminare svuotiamo la stringa line per poter ricominciare da capo;
• ritorno di carrello (\r): che possiamo ignorare;
• un carattere numerico, alfanumerico e comunque diverso da \n e \r: che accoderemo nella stringa line.

Considerato che il carattere (\r) sarà sempre ignorato possiamo usare un if() con due semplici casi per elaborare
i caratteri ricevuti:
if (c == '\n') {
//elaboro la linea
//svuota la linea
line = "";
} else if (c != '\r') {
//accodo caratteri
line += c;
}

Inserendo in un browser l’indirizzo del server (come: [Link] riceveremo una richiesta GET in cui si
richiede la risorsa principale del sito o la pagina index (/) ed è qualcosa simile a:
GET / HTTP/1.1
Host: [Link]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1

La sequenza si conclude sempre con una riga vuota che possiamo intercettare conteggiando i caratteri presenti in
line:

if ([Link]() == 0) {
//invio la risposta
}

Nel nostro codice molto semplificato non analizziamo il tipo di risorsa richiesto e la risposta è sempre identica. La
risposta per il browser deve contenere alcuni messaggi di intestazione e il codice http seguito da una riga vuota:
[Link]("HTTP/1.1 200 OK");
[Link]("Content-type:text/html");
[Link]("");

infine possiamo inviare il codice HTML della pagina da visualizzare:


[Link]("<html>");
[Link]("<head><title>Hello ESP32</title></head>");
[Link]("<body>");
[Link]("<h1>Hello ESP32</h1>");
[Link]("</body>");
[Link]("</html>");
Disconnettiamo il client uscendo dal ciclo while() con:
break;

Ecco il codice completo:


#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

WiFiServer server(80);

void setup() {
[Link](115200);
delay(2000);
[Link]("Simple Server");

[Link](true);
[Link](WIFI_STA);
[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

[Link]("\n");
[Link]("IP: ");
[Link]([Link]());

[Link]();
}

void loop() {
WiFiClient client = [Link]();
if (client) {
[Link]("new req");
String line = "";
while ([Link]()) {
if ([Link]()) {
char c = [Link]();
//[Link](c);
if (c == '\n') {
if ([Link]() == 0) {
[Link]("HTTP/1.1 200 OK");
[Link]("Content-type:text/html");
[Link]("");
[Link]("<html>");
[Link]("<head><title>Hello ESP32</title></head>");
[Link]("<body>");
[Link]("<h1>Hello ESP32</h1>");
[Link]("</body>");
[Link]("</html>");
break;
}
line = "";

} else if (c != '\r') {
line += c;
}

}
}
[Link]();
[Link]("client scollegato");
}
}

Listato 6.12 – Listato per implementare un semplice server http che risponde con una pagina statica.

Una volta caricato il Listato 6.12 su una ESP32 e connesso alla rete WiFi, sul Serial Monitor leggerete l’indirizzo
dell’IP assegnato alla scheda, per esempio [Link]. Aprite un browser e inseritelo nella barra degli indirizzi
per aprire la pagina web pubblicata da ESP32.

Figura 6.5 – La pagina web di ESP32 visualizzata su un browser.

Servire una pagina dinamica


Utilizzando come punto di partenza il codice del Listato 6.12 possiamo creare una pagina web che si aggiorna
automaticamente e mostra lo stato dei pin o degli ingressi analogici. Colleghiamo un trimmer da 10 kΩ al pin 34
della scheda ESP32 come illustrato in Figura 6.6.

Figura 6.6 – Schema di collegamento di un trimmer a ESP32 per visualizzare il valore su una pagina HTML.

Nello sketch 6.12 modifichiamo solamente il codice inviato dalla scheda al browser. Per ricaricare in automatico la
pagina HTML possiamo inserire una direttiva nell’intestazione che richiede il refresh del contenuto ogni due
secondi. Individuiamo il punto in cui inizia l’invio del codice HTML e inseriamo il tag meta appropriato:
[Link]("<html>");
[Link]("<head><title>ESP32</title>");
[Link]("<meta http-equiv=\"refresh\" content='2;url=[Link]
[Link]([Link]());

[Link]("/'>");

Nel tag per il refresh dobbiamo inserire l’indirizzo del server che possiamo ottenere automaticamente con il
metodo [Link](). Nel corpo della pagina aggiungiamo un tag di tipo <p> e subito di seguito leggiamo il
valore presente sul pin 34 della scheda e stampiamolo con un println():
[Link]("<body>");
[Link]("<h1>ESP32</h1>");
[Link]("<p>ANALOG in:");
int v = analogRead(34);
[Link](v);
[Link]("</p>");
[Link]("</body>");
[Link]("</html>");

Carichiamo il codice modificato sulla scheda e apriamo il Serial Monitor per conoscere l’indirizzo IP del server.
Inseriamo l’indirizzo in un browser per aprire la pagina HTML che inizierà a ricaricarsi ogni due secondi.
Muovendo il trimmer dovreste vedere il valore analogico cambiare.

Figura 6.7 – Aprendo un browser all’indirizzo del server potremo veder apparire la pagina dinamica che riporta il livello presente sul pin 34.
Qui di seguito trovate il codice completo:
#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

WiFiServer server(80);

void setup() {
[Link](115200);
delay(2000);
[Link]("Simple Server");

[Link](true);
[Link](WIFI_STA);
[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

[Link]("\n");
[Link]("IP: ");
[Link]([Link]());
[Link]();
}

void loop() {
WiFiClient client = [Link]();
if (client) {
[Link]("new req");
String line = "";
while ([Link]()) {
if ([Link]()) {
char c = [Link]();
//[Link](c);
if (c == '\n') {
if ([Link]() == 0) {
[Link]("HTTP/1.1 200 OK");
[Link]("Content-type:text/html");
[Link]("");
[Link]("<html>");
[Link]("<head><title>ESP32</title>");
[Link]("<meta http-equiv=\"refresh\" content='2;url=[Link]
[Link]([Link]());
[Link]("/'>");
[Link]("</head>");
[Link]("<body>");
[Link]("<h1>ESP32</h1>");
[Link]("<p>ANALOG in:");
int v = analogRead(34);
[Link](v);
[Link]("</p>");
[Link]("</body>");
[Link]("</html>");
break;
}
line = "";

} else if (c != '\r') {
line += c;
}

}
}
[Link]();
[Link]("client scollegato");
}
}

Listato 6.13 – Codice per pubblicare una pagina dinamica con ESP32.
Controllo di un LED da pagina HTML
Possiamo interagire con le pagine dinamiche codificando azioni che prendono la forma di link. Collegando un
LED alla scheda ESP32 possiamo accenderlo o spegnerlo cliccando su dei link riportati sulla pagina web
pubblicata dal server. Colleghiamo un LED sul pin 25 come illustrato in Figura 6.8. Utilizziamo il Listato 6.7
come punto di partenza. Per controllare il LED utilizzeremo due link /ledon e /ledoff che saranno individuati
nelle GET inviate dal browser al server.

Figura 6.8 – Schema di collegamento di un LED al pin 25 di ESP32 per controllarlo da una pagina HTML.

Nel setup() configuriamo il pin 25 per collegare il LED e impostiamolo nello stato LOW:
pinMode(25, OUTPUT);

Individuiamo il punto in cui identifichiamo il carattere di fine linea (\n):


if (c == '\n') {

Qui, all’interno della stringa line, troviamo la richiesta che proviene dal browser. Quando clicchiamo sul link
/ledon o /ledoff, riceveremo una GET con l’indirizzo. Individuiamo la presenza dell’indirizzo che ci interessa
utilizzando il metodo indexOf dell’oggetto stringa:
if ([Link]("GET /ledon") >= 0) {
digitalWrite(25, HIGH);
}
if ([Link]("GET /ledoff") >= 0) {
digitalWrite(25, LOW);
}

Chiamando l’indirizzo /ledon accenderemo il LED utilizzando una chiamata a digitaWrite(); useremo invece
una chiamata a /ledoff per spegnerlo. Nel codice che compone la pagina riportiamo lo stato del LED:
[Link]("<body>");
[Link]("<h1>Hello ESP32</h1>");
[Link]("<p>Stato LED:");
[Link](digitalRead(25));
[Link]("</p>");

Creiamo poi un link “dinamico”, differente a seconda dello stato del pin, così che se il pin 25 è nello stato basso il
link chiamerà /ledon per accendere il LED e viceversa:
if (digitalRead(25)) {
[Link]("<a href='[Link]
[Link]([Link]());
[Link]("/ledoff'>OFF</a>");
} else {
[Link]("<a href='[Link]
[Link]([Link]());
[Link]("/ledon'>ON</a>");
}

Trovate il listato completo riportato qui di seguito. Caricatelo sulla vostra ESP32, verificate quale sia l’indirizzo
del server e quindi aprite la pagina con un browser (Figura 6.9): cliccando sui link riportati nella pagina potrete
accendere e spegnere il LED.
Figura 6.9 – Aprendo un browser all’indirizzo del server potremo veder apparire la pagina dinamica con cui controllare il LED collegato sul pin 25.

#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

WiFiServer server(80);

void setup() {
pinMode(25, OUTPUT);

[Link](115200);
delay(2000);
[Link]("HTTP LED");

[Link](true);
[Link](WIFI_STA);
[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

[Link]("\n");
[Link]("IP: ");
[Link]([Link]());

[Link]();
}

void loop() {
WiFiClient client = [Link]();
if (client) {
String line = "";
while ([Link]()) {
if ([Link]()) {
char c = [Link]();
//[Link](c);
if (c == '\n') {

if ([Link]("GET /ledon") >= 0) {


digitalWrite(25, HIGH);
}
if ([Link]("GET /ledoff") >= 0) {
digitalWrite(25, LOW);
}

if ([Link]() == 0) {
[Link]("HTTP/1.1 200 OK");
[Link]("Content-type:text/html");
[Link]("");
[Link]("<html>");
[Link]("<head><title>Hello LED ESP32</title>");
[Link]("</head>");
[Link]("<body>");
[Link]("<h1>Hello ESP32</h1>");
[Link]("<p>Stato LED:");
[Link](digitalRead(25));
if (digitalRead(25)) {
[Link]("</p>");
[Link]("<a href='[Link]
[Link]([Link]());
[Link]("/ledoff'>OFF</a>");
} else {
[Link]("</p>");
[Link]("<a href='[Link]
[Link]([Link]());
[Link]("/ledon'>ON</a>");
}
[Link]("</body>");
[Link]("</body>");
[Link]("</html>");
break;
}
line = "";

} else if (c != '\r') {
line += c;
}

}
}
[Link]();
[Link]("client scollegato");
}
}

Listato 6.14 – Codice completo per controllare lo stato di un LED da una pagina HTML.

Web Server con più pagine


Il framework di ESP32 include un oggetto WebServer che può essere molto utile per sviluppare applicazioni che
prevedono più di una pagina web. Partendo dallo sketch 6.7 aggiungiamo la libreria WebServer.h e creiamo il
corrispondente oggetto indicando di ascoltare sulla porta 80:
#include <WebServer.h>
WebServer server(80);

Colleghiamo un LED al pin 25 (vedi schema di Figura 6.8) e nel setup() configuriamo il pin come uscita,
impostandolo nello stato LOW:
pinMode(25, OUTPUT);
digitalWrite(25, LOW);

Ora creiamo dei “percorsi” e colleghiamoli a delle funzioni che aggiungeremo in coda allo sketch:
[Link]("/", pageIndex);
[Link]("/home", pageIndex);
[Link]("/on", ledOn);
[Link]("/off", ledOff);
[Link](pageError);

Richiamando da browser la “risorsa” /home, il codice a bordo di ESP32 eseguirà il codice della funzione
pageIndex(). Le funzioni necessarie si possono definire come riportato di seguito:

void pageIndex(){
}
void ledOn() {
}
void ledOff() {
}
void pageError() {
}

Avviamo il server con:


[Link]();

Per semplificare l’individuazione del server sulla rete possiamo attivare un DSN associando alla scheda un nome
personalizzato da usare al posto dell’indirizzo IP. Aggiungiamo all’inizio dello sketch la libreria:
#include <ESPmDNS.h>

Immaginiamo di voler chiamare la nostra scheda myesp32. Dopo aver stabilito la connessione alla rete WiFi
avviamo il servizio MDNS fornendo il nome desiderato:
if ([Link]("myesp32")) {
[Link]("[Link]
} else {
[Link]("Error DNS");
[Link]("IP: ");
[Link]([Link]());
}

Se l’inizializzazione non andasse a buon fine potremo comunque trovare la scheda tramite l’indirizzo IP, altrimenti
basterà inserire nel browser l’indirizzo [Link]
Nel loop() sarà sufficiente inserire il seguente codice così che il server possa ascoltare e gestire tutte le
chiamata in arrivo:
void loop() {
[Link]();
delay(2);
}

Ora possiamo dedicarci al codice da inserire nelle funzioni di risposta alle chiamate. La pagina principale (root o
index) del server risponde all’indirizzo / e la risposta sarà fornita dalla funzione pageIndex(). L’oggetto server
può fornire l’URI, cioè il nome della risorsa cercata e altre informazioni come i parametri inviati e il loro numero.
Possiamo raccogliere queste informazioni per poi restituirle con [Link](), indicando anche il codice http di
risposta e il tipo di contenuto (text/plain o text/html):
void pageIndex() {
String str = "Hello Web Server ESP32\n";
str += "URI: ";
str += [Link]();
str += " ARGS: ";
str += [Link]();
str += "\n";

for (int i = 0; i < [Link](); i++) {


str += [Link](i);
str += "=";
str += [Link](i);
str += "\n";
}

[Link](200, "text/plain", str);


}

Per accendere il LED chiameremo l’indirizzo /on e risponderà la funzione ledOn(), inviando un semplice OK e il
codice HTTP 200:
void ledOn() {
digitalWrite(25, HIGH);
[Link](200, "text/plain", "OK");
}

Spegniamo il LED con /off che chiamerà la funzione ledOff():


void ledOff() {
digitalWrite(25, LOW);
[Link](200, "text/plain", "OK");
}

In caso di errore chiameremo pageError() che risponderà con un messaggio di pagina non trovata:
void pageError() {
String str = "Pagina non trovata\n";
[Link](404, "text/plain", str);
}

Ecco il codice completo del server multi pagina, caricatelo e poi provate i vari “servizi” esposti con un browser.
Gli indirizzi da chiamare sono:
• [Link]
• [Link]
• [Link]
• [Link]

Provate anche a chiamare la pagina “index” fornendo dei parametri:

[Link]
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WebServer.h>

#define MY_WIFI_SSID "nome_rete"


#define MY_WIFI_PASS "password"

WebServer server(80);

void setup() {
pinMode(25, OUTPUT);
digitalWrite(25, LOW);

[Link](115200);
delay(2000);
[Link]("Web Server");

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
[Link](".");
delay(1000);
}

[Link]("\n");

if ([Link]("myesp32")) {
[Link]("[Link]
} else {
[Link]("Error DNS");
[Link]("IP: ");
[Link]([Link]());
}

[Link]("/", pageIndex);
[Link]("/home", pageIndex);
[Link]("/on", ledOn);
[Link]("/off", ledOff);
[Link](pageError);

[Link]();
}

void loop() {
[Link]();
delay(2);
}

void pageIndex() {
String str = "Hello Web Server ESP32\n";
str += "URI: ";
str += [Link]();
str += " ARGS: ";
str += [Link]();
str += "\n";

for (int i = 0; i < [Link](); i++) {


str += [Link](i);
str += "=";
str += [Link](i);
str += "\n";
}

[Link](200, "text/plain", str);


}
void ledOn() {
digitalWrite(25, HIGH);
[Link](200, "text/plain", "OK");
}
void ledOff() {
digitalWrite(25, LOW);
[Link](200, "text/plain", "OK");
}

void pageError() {
String str = "Pagina non trovata\n";
[Link](404, "text/plain", str);
}

Listato 6.15 – Esempio completo per un server multi pagina su ESP32.

Web Server con SPIFFS


Abbiamo imparato a utilizzare SPIFFS per salvare dati sulla flash. Possiamo sfruttare questo meccanismo per
salvare pagine HTML da pubblicare con un web server. La creazione di un mini sito web, in questo modo, è
notevolmente agevolata perché il codice HTML sarà scritto nei file che copieremo sulla flash memory del
dispositivo. Possiamo pubblicare pagine statiche oppure renderle dinamiche modificando il testo presentato. Prima
di tutto creiamo un nuovo sketch, salviamolo e quindi creiamo una cartella “data” come presentato nel Capitolo 5
(pag. 199). Nella cartella data creiamo due file di testo che chiameremo [Link] e [Link]. Verificate che
prendano l’estensione .html e non .txt. Riporto qui di seguito il contenuto dei file.

file [Link]
<!doctype html>
<html>
<head>
<title>SPIFFS Web Server</title>
</head>
<body>
<h1>SPIFFS Server</h1>
<p>Hello <strong>world!</strong></p>
</body>
</html>

file [Link]
<!doctype html>
<html>
<head>
<title>SPIFFS Web Server</title>
</head>
<body>
<h1>SPIFFS MAIN PAGE</h1>
<p>Main page....</p>
</body>
</html>

Una volta creati i file, create l’immagine del file system e caricatelo su ESP32 come spiegato a pag. 199.
Per realizzare un server con SPIFFS servono librerie speciali che possiamo scaricare a questi link:
• [Link]
• [Link]

Per scaricare le librerie da GitHub fate clic sul pulsante “Code” e quindi su “Download Zip”. Scaricate e installate
le librerie come file compressi “ZIP”.

Figura 6.10 – Per scaricare le librerie da GitHub: fate clic sul pulsante “Code” e quindi su “Download Zip”.
Partiamo da uno sketch di base che effettui la connessione WiFi (Listato 6.1) e includiamo le librerie necessarie.
Per utilizzare il server con SPIFFS aggiungeremo in cima allo sketch:
#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"

Creiamo l’oggetto AsyncWebServer mettendolo in ascolto sulla porta 80:


AsyncWebServer server(80);

Nel setup() non dimentichiamoci di inizializzare SPIFFS con:


if (![Link](true)) {
[Link]("SPIFFS KO");
while(1);

Se l’immagine del file system non è stata caricata correttamente su ESP32, lo sketch si bloccherà.
Nel setup(), dopo aver stabilito la connessione WiFi creiamo i legami tra i percorsi e le pagine presenti su
“disco SPIFFS”:
[Link]("/", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);

});

Aggiungiamo anche dei legami espliciti con le pagine html:


[Link]("/[Link]", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/[Link]", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});

Tra i vari parametri passati abbiamo processor che è il nome di una speciale funzione che serve per rendere
dinamiche le pagine. Nel nostro caso restituirà sempre una stringa vuota:
String processor (const String& par) {
return String();
}

Avviamo il server:
[Link]();

Il loop() è completamente vuoto perché il server gira in autonomia grazie al secondo core della CPU di ESP32:
#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

#include <ESPAsyncWebServer.h>
#include "SPIFFS.h"

AsyncWebServer server(80);

void setup() {
[Link](115200);
delay(1000);
[Link]("SPIFFS webserver");

if (![Link](true)) {
[Link]("SPIFFS KO");
while(1);
}

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");[Link]([Link]());
[Link]("/", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/[Link]", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/[Link]", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]();
}

void loop() {}

String processor (const String& par) {


return String();
}

Listato 6.16 – Codice per realizzare un server con SPIFFS e pagine HTML “statiche”.

Dopo aver caricato lo sketch su ESP32, aprite il Serial Monitor e ricavate l’indirizzo IP del server, quindi provate a
richiamare le pagine presenti.

Server SPIFFS dinamico


Il server che abbiamo realizzato invia le pagine al browser così come sono. Modificando la funzione processor()
possiamo creare contenuti dinamici. Possiamo controllare lo stato di un LED collegato a un pin (per esempio pin
25), come in Figura 6.8. Ricordiamoci di configurare il pin come uscita all’inizio del setup():
void setup() {
pinMode(25, OUTPUT);
digitalWrite(25, LOW);

Aggiungiamo due nuove “azioni” che rispondano ai percorsi /on e /off. Possiamo aggiungere direttamente nelle
dichiarazioni il comando per accendere e spegnere il LED:
[Link]("/on", HTTP_GET, [](AsyncWebServerRequest *req){
digitalWrite(25, HIGH);
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/off", HTTP_GET, [](AsyncWebServerRequest *req){
digitalWrite(25, LOW);
req->send(SPIFFS, "/[Link]", String(), false, processor);
});

Modifichiamo la pagina [Link] inserendo dei link che richiamino /on e /off e aggiungiamo un paragrafo in
cui visualizzeremo dinamicamente un’informazione. All’interno del paragrafo inseriamo il “segnaposto”
%STATO_LED%:

<!doctype html>
<html>
<head>
<title>SPIFFS Web Server Dinamico</title>
</head>
<body>
<h1>Accendi LED</h1>
<p>STATO LED: <strong>%STATO_LED%</strong></p>
<p><a href='/on'><button>ACCENDI</button></a></p>
<p><a href='/off'><button>SPEGNI</button></a></p>
</body>
</html>

Quando il browser richiederà la pagina [Link] al server, il processore inizierà a leggerla fino a incontrare il
segnaposto %STATO_LED%. All’interno della funzione processor() possiamo intercettare STATO_LED (senza %) e
sostituirlo con le informazioni che desideriamo far apparire. Possiamo analizzare lo stato del pin 25 e quindi far
apparire “ON” o “OFF” di conseguenza:
String processor (const String& par) {
if (par == "STATO_LED") {
if (digitalRead(25)) {
return "ON";
} else {
return "OFF";
}
}
return String();
}

Qui di seguito il listato completo:


#include <WiFi.h>
#define MY_WIFI_SSID "nome_rete"
#define MY_WIFI_PASS "password"

AsyncWebServer server(80);

void setup() {
pinMode(25, OUTPUT);
digitalWrite(25, LOW);

[Link](115200);
delay(1000);
[Link]("SPIFFS web");
if (![Link](true)) {
[Link]("SPIFFS KO");
while(1);
}

[Link](MY_WIFI_SSID, MY_WIFI_PASS);
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
[Link]("IP: ");[Link]([Link]());

[Link]("/", HTTP_GET, [](AsyncWebServerRequest *req){


req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/[Link]", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/[Link]", HTTP_GET, [](AsyncWebServerRequest *req){
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/on", HTTP_GET, [](AsyncWebServerRequest *req){
digitalWrite(25, HIGH);
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]("/off", HTTP_GET, [](AsyncWebServerRequest *req){
digitalWrite(25, LOW);
req->send(SPIFFS, "/[Link]", String(), false, processor);
});
[Link]();
}

void loop() {}

String processor (const String& par) {


if (par == "STATO_LED") {
if (digitalRead(25)) {
return "ON";
} else {
return "OFF";
}
}
return String();
}

Listato 6.17 – Web server dinamico con SPIFFS.

Access Point
Il modulo WiFi di ESP32 può funzionare anche in modalità Access Point, generando una rete a cui altri dispositivi
si possono collegare. Questa funzionalità è utilizzata di frequente assieme all’oggetto WebServer per creare pagine
di configurazione dell’hardware, oppure per impostare parametri inziali.
Per impostare un server con Access Point prendiamo come punto di partenza lo sketch 6.12. Questa volta
dovremo dare noi le impostazioni di rete e quindi dovremo definire l’indirizzo IP della scheda, la maschera di rete
e il gateway. Inseriamo dunque queste informazioni all’inizio dello sketch scegliendo come indirizzo per la scheda
[Link]:
IPAddress local_ip(192,168,1,20);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

Nel setup() configuriamo la porta seriale e poi selezioniamo la modalità Access Point:
[Link](WIFI_AP);

Scegliamo il nome della rete e una password:


[Link]("esp32wifi", "123Stella");

Avviamo la rete WiFi:


[Link](local_ip, gateway, subnet);

Ecco di seguito lo sketch completo:


#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);

IPAddress local_ip(192,168,1,20);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

void setup() {
[Link](115200);
delay(2000);
[Link]("AP Server");

[Link](WIFI_AP);
[Link]("esp32wifi", "123Stella");
[Link](local_ip, gateway, subnet);
delay(1000);

[Link]("/", pageIndex);
[Link](pageError);

[Link]();
}

void loop() {
[Link]();
delay(2);
}

void pageIndex() {
String str = "Hello Web Server ESP32\n";
[Link](200, "text/plain", str);
}
void pageError() {
String str = "Pagina non trovata\n";
[Link](404, "text/plain", str);
}

Listato 6.18 – Access Point per ESP32.

Provate a cercare la rete WiFi con SSID esp32wifi con il vostro smartphone e quindi connettetevi. Inserite la
password 123Stella e poi aprite un browser e visitate l’indirizzo [Link] Dovrebbe apparirvi la
pagina index pubblicata dal server.

Altri protocolli
Fino a qui abbiamo sperimentato con l’invio e la ricezione di informazioni via HTTP, il più diffuso protocollo su
Internet. HTTP nasce per visualizzare pagine web ma è anche utilizzato per realizzare servizi web “lightweight”
(leggeri). Non è raro incontrare pagine dinamiche in php o altri linguaggi dinamici per il web che, se interrogate,
rispondono con semplici sequenze di dati. Realizzare “servizi” di questo tipo è rapido e semplice. Esistono
problemi legati alla sicurezza che dovreste tenere in considerazione: fino a che richiedete o comunicate dati sulla
temperatura di una stanza non ci sono particolari rischi, ma se i vostri servizi trattano dati sensibili o informazioni
personali o di pagamento, sarebbe meglio prendere qualche precauzione in più, criptando i dati, aggiungendo delle
autenticazioni e qualche sicurezza. Valutate sempre di volta in volta quale deve essere il grado di protezione da
utilizzare: più meccanismi introducete e più complicata diventa la gestione. Implementare certi servizi su un
dispositivo embedded potrebbe essere veramente complesso!

REST
I servizi RESTful,5 o semplicemente REST, sono molto utilizzati per lo scambio di dati su Internet e sfruttano i
server http e il relativo protocollo. Per richiedere una risorsa a un server solitamente utilizziamo una GET seguita
dal nome della risorsa e altri comandi per meglio definire la richiesta. Le chiamate GET possono includere anche
parametri da inviare al server. Un server che espone servizi REST permette di svolgere alcune operazioni sui dati
che detiene utilizzando i metodi HTTP:
• GET: per ottenere informazioni su una risorsa o su una entità;
• POST: per creare una nuova entità;
• PUT: per le modifiche;
• DELETE: per eliminare informazioni.

Le risorse sono di solito individuate da percorsi (path) implementati con gli indirizzi web stessi, il che permette di
creare una certa gerarchia e di organizzare le informazioni. Potremo per esempio avere informazioni su un
impiegato con una chiamata GET simile a questa:
/office/users/user?id=123

Per ottenere tutti gli impiegati potremo invece chiamare una GET simile a:
/office/users/

Per implementare servizi di tipo REST esistono varie possibilità. Il metodo più semplice e rapido è quello di
utilizzare php con un database. Trovate svariati tutorial in rete per implementare da zero alcuni semplici servizi. È
possibile anche utilizzare Python con framework come Django ([Link] o Flask
([Link]
Spesso i dati complessi sono scambiati utilizzando JSON, che abbiamo trattato nelle pagine precedenti. Per
scambiare dati con servizi RESTful possiamo utilizzare i vari metodi presentati nelle pagine precedenti oppure si
può sperimentare qualche libreria (cercate REST Client). ESP32 è abbastanza potente da ospitare un web server e
quindi può essere utilizzata anche come server REST implementando le opportune chiamate e sfruttando la libreria
WebServer.h.

MQTT
Per le applicazioni IoT il protocollo6 preferito è MQTT (Message Queuing Telemetry Transport). In MQTT
abbiamo dei publisher che pubblicano dati e dei client (subscriber) “interessati” a questi messaggi che si
sottoscrivono per poterli ricevere. Tra publisher e subscriber c’è un server (broker) che fa in modo che i messaggi
siano ricevuti e trasmessi ai diretti interessati. Publisher e subscriber devono sempre comunicare attraverso il
server che fa da broker. MQTT è adatto a IoT perché è un protocollo “leggero” che occupa poche risorse e quindi
adatto a dispositivi hardware con poche risorse. Inoltre MQTT è adatto nel caso in cui ci siano numerosi
dispositivi.
È possibile realizzare un server MQTT con Eclipse Mosquitto ([Link] Potete installare il server
sul vostro computer, su un server o anche su un piccolo RaspberryPI ([Link] Per qualche
rapido test possiamo anche appoggiarci a broker MQTT gratuiti7 come Hive MQ
([Link] Per implementare publisher e subscriber con ESP32 è consigliabile
utilizzare librerie come Pub Sub Client (per Arduino ma funzionante anche con ESP32)
([Link]
Nel mio canale YouTube trovate un esempio per Arduino:
• Subscriber: [Link]
• Publisher: [Link]

TCP
Un protocollo di rete molto comune e utilizzato per sviluppare molti altri servizi è TCP (Transmission Control
Protocol). Con TCP possiamo trasmettere in modo affidabile pacchetti di dati molto semplici tra due computer.
Prima di trasmettere dati, i due dispositivi devono stabilire una connessione che rimane attiva anche se non c’è uno
scambio di dati e per questo si definisce TCP “orientato alla connessione”. Al termine della comunicazione la
connessione deve essere chiusa. I pacchetti sono poi trasmessi in modo affidabile, cioè abbiamo la garanzia che
arrivino nell’ordine corretto e che arrivino sempre a destinazione. Il destinatario infatti invia una conferma quando
riceve un pacchetto (ACK o acknowledgement). La trasmissione dei pacchetti avviene nelle due direzioni,
contemporaneamente (full-duplex). Il protocollo prevede l’utilizzo di porte per condividere la connessione tra
diverse applicazioni. Per esempio un servizio HTTP utilizza la porta 80, mentre l’FTP per il trasferimento dei file
occupa la porta 21.
In rare occasioni o per sviluppare applicazioni particolari e nuovi servizi potrebbe essere necessario sviluppare
del codice per ESP32 che lavori con i pacchetti TCP. L’oggetto software necessario per comunicare via TCP è
WiFiClient presente nella libreria WiFi.h.

Client TCP
Per realizzare un client creeremo un’istanza di WiFiClient e avvieremo la connessione con connect(), fornendo
l’indirizzo del server e la porta da utilizzare:
WiFiClient client;
[Link]("[Link]", 80);

Il metodo connect() restituisce un valore booleano che vale true se la connessione viene stabilita. Una volta che
la connessione è attiva possiamo inviare dati come su una seriale utilizzando println(), o write() per inviare
byte:
[Link]("hello");
[Link](123);

Il client può anche ricevere dati, quando disponibili, con il metodo read():
while ([Link]() == 0) {
char ch = [Link]();
}

Al termine delle operazioni chiuderemo la connessione con stop():


[Link]();

Ecco un mio video su Arduino e i client TCP: [Link]


TCP Server
Per creare un server TCP su una porta specifica utilizzeremo la classe WiFi-Server della libreria WiFi.h:
WiFiServer server(80);
[Link]();

Una volta che il server è inizializzato possiamo creare un loop in cui attendere l’arrivo di richieste da parte di
qualche client. Solitamente utilizziamo il loop() di Arduino:
void loop() {
WiFiClient client = [Link]();
if (client) {
//ricevo i dati dal client
[Link]();
}
}

Quando arriva una richiesta, il server crea un client e può iniziare a ricevere dati fino a che il client non si scollega.
Per questo creiamo un ciclo while() che resta attivo fino a che il client è collegato:
void loop() {
WiFiClient client = [Link]();
if (client) {
while ([Link]()) {
//ricevo i dati dal client
char c = [Link]();
}
[Link]();
}
}

All’interno del ciclo while() ricevo i dati se sono disponibili utilizzando read(). È possibile inviare risposte al
client con i metodi write() e println().
Ecco un mio video su Arduino e i server TCP: [Link]

UDP
Il protocollo UDP (User Datagram Protocol) è simile a TCP ma ha caratteristiche differenti che lo rendono adatto a
diversi tipi di applicazioni dove l’affidabilità non è rilevante: infatti con il protocollo UDP non abbiamo la
garanzia che un pacchetto giunga a destinazione. Questa semplificazione offre una maggiore velocità, inoltre per la
comunicazione non è necessario creare una connessione esplicita: se qualcuno è in ascolto riceverà i dati,
altrimenti andranno persi. L’UDP è usato dal servizio DNS e da servizi di streaming.

UDP Client
Per utilizzare UDP con ESP32 includeremo nello sketch la libreria WiFiUdp.h e creeremo un oggetto WiFiUDP:
#include <WiFiUdp.h>
WiFiUDP udp;

Prima di tutto stabiliremo una connessione WiFi come già sappiamo fare e poi impostermo la porta UDP su cui
vogliamo trasmettere:
[Link](1234);

Per trasmettere dati a un server UDP dobbiamo conoscere il suo indirizzo IP e la porta di ascolto (che sarà
presumibilmente la 1234). Non dovremo stabilire una connessione ma semplicemente preparare un pacchetto con:
[Link]("[Link]", 1234);

Compiliamo il messaggio con write(), preparando un array di byte, inserendo alcuni caratteri e indicando il
numero di dati da trasmettere:
uint8_t buffer[50] = "hello UDP!";
[Link](buffer, 10);
La trasmissione effettiva si ha con:
[Link]();

Ecco un esempio completo:


#include <WiFi.h>
#include <WiFiUdp.h>

WiFiUDP udp;

void setup() {
[Link](115200);
delay(1000);
[Link]("UDP Client");

[Link]("reelco_2_4", "cardu450IX");
[Link]("mi connetto...");
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}

[Link]("\nconnesso");

[Link]("IP: "); [Link]([Link]());

delay(1000);
[Link](1234);
}

void loop() {
uint8_t buffer[50] = "hello UDP!";
[Link]("[Link]", 1234);
[Link](buffer, 10);
[Link]();

delay(1000);
}

Listato 6.19 – Codice per un client UDP.

UDP Server
Possiamo realizzare un server UDP utilizzando WiFiUDP:
WiFiUDP udp;

In questo caso sarebbe preferibile instaurare una connessione WiFi specificando l’indirizzo IP così che i client
possano trovare facilmente il server. Nell’esempio precedente (Listato 6.19) il client trasmette verso l’indirizzo IP
[Link]. Verifichiamo che sulla nostra rete l’indirizzo sia libero e poi configuriamo la connessione indicando
esplicitamente tutti i parametri:
IPAddress ip_scheda(192, 168, 1, 111);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
IPAddress DNS1(8, 8, 8, 8);
IPAddress DNS2(8, 8, 4, 4);

e poi nel setup():


void setup() {
[Link](ip_scheda, gateway, subnet, DNS1, DNS2);
[Link]("nomerete", "password");

Assegniamo all’oggetto udp la porta desiderata (1234):


[Link](1234);

Prepariamo un buffer per la ricezione dei byte trasmessi dal client UDP:
uint8_t buffer[50];

Nel loop() verifichiamo se ci sono dati in arrivo:


void loop() {
[Link]();

Leggiamo i dati con read() che riempirà il buffer. Stampiamo quanto ricevuto con una println():
if([Link](buffer, 50) > 0){
[Link]("UDP: ");
[Link]((char *)buffer);
}

Ripuliamo il buffer prima di ricevere un nuovo pacchetto:


memset(buffer, 0, 50);

Qui di seguito trovate il codice completo del server UDP:


#include <WiFi.h>
#include <WiFiUdp.h>

IPAddress ip_scheda(192, 168, 1, 111);


IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
IPAddress DNS1(8, 8, 8, 8);
IPAddress DNS2(8, 8, 4, 4);

WiFiUDP udp;

void setup() {
[Link](115200);
delay(1000);
[Link]("UDP Server");

[Link](ip_scheda, gateway, subnet, DNS1, DNS2);

[Link]("reelco_2_4", "cardu450IX");
[Link]("mi connetto...");
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");

[Link]("IP: "); [Link]([Link]());

delay(1000);
[Link](1234);
}

uint8_t buffer[50];
void loop() {
[Link]();
if([Link](buffer, 50) > 0){
[Link]("UDP: ");
[Link]((char *)buffer);
}
memset(buffer, 0, 50);
}

Listato 6.20 – Codice per un server UDP.

Caricando il codice del client e del server su due schede ESP32 vedrete apparire sul Serial Monitor del Server
UDP i messaggi ricevuti:
UDP Server
mi connetto...
..
connesso
IP: [Link]
UDP: hello UDP!
UDP: hello UDP!
UDP: hello UDP!

Ethernet
Il modulo ESP32 non ha una porta Ethernet ma è possibile aggiungerne una utilizzando un hardware esterno come
le breakout board che utilizzano il chipset Wiznet W5500. Queste schede comunicano solitamente utilizzando SPI.
Per alcuni progetti che ho realizzato ho collegato un modulo Wiznet a ESP32 e l’ho programmato usando la
libreria Ethernet.h8 presa dal framework di Arduino:
#include <Ethernet.h>

Per la gestione della connessione si utilizzerà un client Ethernet:


EthernetClient client;

Prima di poter utilizzare l’hardware è necessario configurarlo indicando a quale pin di ESP32 è stato collegato il
CS. Il pin si indica come parametro del metodo begin():
[Link](5);

Le schede Wiznet W5500 hanno un pin di RESET che va posto a livello basso per inizializzare l’hardware.
Possiamo utilizzare una sequenza di digital-Write():
pinMode(RESET_P, OUTPUT);
digitalWrite(RESET_P, HIGH);
delay(250);
digitalWrite(RESET_P, LOW);
delay(50);
digitalWrite(RESET_P, HIGH);

Ogni scheda di rete deve avere un suo MAC address, solitamente riportato con un’etichetta sul circuito stampato.
Comunicate il MAC address della scheda al momento della connessione. Ipotizzando di collegarsi a una rete con
DHCP chiameremo begin() in questo modo:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
[Link](mac);

Una volta ottenuto un indirizzo IP possiamo utilizzare i servizi di rete desiderati per ricevere o inviare dati.

Bluetooth
Il modulo ESP32 incorpora anche un ricetrasmettitore Bluetooth il cui utilizzo è semplice grazie alla libreria
BluetoothSerial.h, inclusa nel framework di sviluppo perché si comporta come una porta seriale. Per verificare il
funzionamento servirà un telefono dotato di Bluetooth9 su cui installeremo un’app per inviare e ricevere caratteri.
Il programma che scriveremo per ESP32 serve per scambiare testi con il telefono. Il modulo ESP32 verrà visto dal
telefono come un dispositivo Bluetooth.
Iniziamo creando un nuovo sketch in cui includiamo la libreria:
#include "BluetoothSerial.h"

Quindi creiamo un oggetto BluetoothSerial:


BluetoothSerial bt;

Nel setup() configuriamo la porta seriale per poter scrivere sul Serial Monitor:
void setup() {
[Link](115200);

e diamo un nome al dispositivo Bluetooth. Questo nome sarà quello che poi dovremo cercare con il nostro
telefono:
[Link]("ESP32-Bt");

Nel loop() attenderemo dati provenienti dal Serial Monitor con il metodo available() di Serial, per ritrasmetterli
subito sulla connessione Bluetooth:
if ([Link]()) {
[Link]([Link]());
}

Viceversa, ogni byte ricevuto sulla linea Bluetooth sarà ritrasmesso sulla seriale:
if ([Link]()) {
[Link]([Link]());
}

Aggiungiamo infine un piccolo ritardo per dare tempo ai buffer di svuotarsi. Qui di seguito c’è il codice completo
da caricare sulla scheda:
#include "BluetoothSerial.h"
BluetoothSerial bt;
void setup() {
[Link](115200);
delay(1000);
[Link]("BT terminal");

[Link]("ESP32-Bt");

void loop() {
if ([Link]()) {
[Link]([Link]());
}
if ([Link]()) {
[Link]([Link]());
}
delay(20);
}

Listato 6.21 – Terminale Bluetooth con ESP32.

Collegate la scheda ESP32 al vostro computer e caricate il programma riportato nel Listato 6.21. Tenete la scheda
collegata con il cavo al computer e aprite il Serial Monitor di Arduino IDE, quindi seguite questi passaggi:
1. installate su uno smartphone Android dotato di Bluetooth, l’App “Serial Bluetooth Terminal”, reperibile
gratuitamente nel Play Store;
2. entrate nelle configurazioni di sistema di Android, quindi attivate il Bluetooth nella sezione dedicata (vedi
Figura 6.11 A);
3. una volta acceso il Bluetooth, avviate la ricerca di dispositivi e dopo qualche istante dovreste vedere apparire la
voce “ESP322-Bt”;
4. associate il telefono al dispositivo;
5. aprite l’applicazione “Serial Bluetooth Terminal” (Figura 6.11 B);
6. toccate le tre righe orizzontali del menu (in alto a sinistra) e selezionate “Devices” (Figura 6.11 C – punto 2);
7. cercate nel pannello “Bluetooth Classic” della sezione “Devices” il dispositivo “ESP322-Bt” (Figura 6.11 D –
punto 3) e selezionatelo;
8. chiudete il pannello “Devices”;
9. nella finestra principale cliccate sull’icona per connettere il dispositivo (Figura 6.11 E – punto 4);
10. scrivete qualcosa nella casella testuale posta sul lato inferiore dell’App e inviatela premendo la freccia (Figura
6.11 F);
11. nel Serial Monitor di Arduino IDE dovreste veder arrivare le stringhe inserite nell’App:
BT terminal
hello bt
how are you??

12. scrivendo un messaggio nel Serial Monitor, dovrebbe apparire nella finestra dell’app.
Figura 6.11 – Utilizzo dell’applicazione Bluetooth Serial Terminal con ESP32: (A) attivate il Bluetooth e associate il dispositivo ESP32-Bt (1); (B)
schermata principale dell’app Serial Bluetooth Terminal; (C) nel menu Impostazioni dell’app scegliete la voce Devices (2); (D) selezionate il dispositivo
ESP32-Bt (3) dai dispositivi associati; (E) connettetevi al dispositivo toccando l’icona (4) nella schermata principale; (F) inviate e ricevete messaggi
dall’app al Serial Monitor di Arduino IDE.

Controllare un LED
Utilizziamo il Bluetooth per controllare un LED collegato all’ESP32. Colleghiamo un LED con resistenza da 100
Ω al pin 25 della scheda, come illustrato in Figura 6.12. Utilizzeremo l’App per Android, Serial Bluetooth
Terminal, per inviare stringhe di controllo così che quando la scheda ESP32 riceverà un “on”, accenderà il LED,
per spegnerlo con ”off”.
Figura 6.12 – Schema di collegamento di un LED su pin 25.

Apriamo un nuovo sketch, includiamo la libreria BluetoothSerial.h e creiamo un’istanza bt così come nel Listato
6.21. Nel setup() assegneremo un nome al nostro dispositivo Bluetooth e configureremo il pin GPIO 25 per
pilotare il LED:
pinMode(25, OUTPUT);
digitalWrite(25, LOW);

Questa volta il grosso del lavoro è compiuto nel loop() dove attenderemo l’arrivo di caratteri dall’oggetto bt che
si comporta come una porta seriale:
void loop() {
if ([Link]()) {
//ricevo i caratteri
}
}

Se ci sono caratteri in arrivo, li riceveremo con read() e li salveremo in una variabile:


char ch = [Link]();

Accoderemo i caratteri in una stringa che dovremo creare al di fuori di loop() e setup() così che sia a visibilità
globale. Poco prima di setup() scriviamo quindi:
String line = "";

I caratteri che riceviamo saranno numeri, lettere oppure \r o \n. Considerato che \r non ci interessa possiamo
considerare solo l’arrivo di \n che corrisponde alla fine di un comando e tutti gli altri casi diversi da \r in cui
accoderemo il carattere ricevuto alla stringa line:
if (ch == '\n') {
//esamino il contenuto di line

//svuoto line
line = "";
} else if (ch != '\r') {
//accodo i caratteri in line
line += ch;
}

Ottenuta una linea completa potremo esaminarla con degli if() e quindi accendere o spegnere il LED:
if (line == "on"){
digitalWrite(25, HIGH);
[Link]("LED ON");
} else if (line == "off"){
digitalWrite(25, LOW);
[Link]("LED OFF");
}

Dopo aver esaminato la stringa è necessario svuotarla prima che possa accogliere un nuovo comando. Qui di
seguito trovate il listato completo:
#include "BluetoothSerial.h"

BluetoothSerial bt;
String line = "";

void setup() {
[Link](115200);
delay(1000);
[Link]("BT LED");
[Link]("ESP32-Bt");

pinMode(25, OUTPUT);
digitalWrite(25, LOW);
}

void loop() {
if ([Link]()) {
char ch = [Link]();
if (ch == '\n') {
[Link]("cmd: ");
[Link](line);
if (line == "on"){
digitalWrite(25, HIGH);
[Link]("LED ON");
} else if (line == "off"){
digitalWrite(25, LOW);
[Link]("LED OFF");
}
line = "";
} else if (ch != '\r') {
line += ch;
}
}
}

Listato 6.22 – Codice per controllare un LED via Bluetooth.

Caricate il listato sulla vostra scheda ESP32 e collegatevi con lo smartphone. Aprite l’app “Serial Bluetooth
Terminal” e provate a inviare le stringhe “on” e “off” per accendere e spegnere il LED.

Inviare dati del sensore DHT11


Il sensore digitale DHT11 rileva la temperatura e l’umidità dell’aria con una buona precisione. È un sensore
digitale che trasmette i dati su un solo “filo”. Per leggere il sensore servirà la libreria SimpleDHT, reperibile nel
Library Manager di Arduino IDE. Colleghiamo il sensore come illustrato in Figura 6.13 avendo cura di aggiungere
una resistenza di pull up da 3.8 kΩ tra la linea dati e la tensione di alimentazione a 3,3 V. La linea dati del sensore
va collegata al pin GPIO 15.

Figura 6.13 – Collegamento del sensore DHT11 a ESP32.


Realizzeremo un dispositivo Bluetooth che invia periodicamente i dati di temperatura e umidità rilevati. Creiamo
un nuovo sketch includendo le librerie necessarie per il Bluetooth e per il sensore DHT11:
#include "BluetoothSerial.h"
#include <SimpleDHT.h>

Creiamo un oggetto per il Bluetooth e uno per gestire il DHT11, fornendo anche il pin a cui è collegato il sensore:
BluetoothSerial bt;
SimpleDHT11 dht11(15);

Per trasmettere i dati regolarmente, utilizzeremo un timer non bloccante, quindi creiamo una variabile per
memorizzare il tempo:
unsigned long t1;

Nel setup() configuriamo la porta seriale e il Bluetooth come di consueto, quindi nel loop() realizziamo il timer
non bloccante leggendo il tempo con millis() e facendolo scattare ogni tre secondi:
void loop() {
if ((millis()-t1) > 3000) {
//codice da ripetere

t1 = millis();
}
}

Prepariamo due variabili per ricevere i valori dal sensore DHT11:


byte temp = 0;
byte hum = 0;

Quindi ricaviamo i valori di temperatura e umidità con:


[Link](&temp, &hum, NULL);

Componiamo una stringa con i valori ricavati:


String msg = "t: " + String(temp) + "C h:" + String(hum) + "%";

e infine trasmettiamola utilizzando il Bluetooth:


[Link](msg);

Qui di seguito trovate lo sketch completo:


#include "BluetoothSerial.h"
#include <SimpleDHT.h>
BluetoothSerial bt;
SimpleDHT11 dht11(15);
unsigned long t1;
void setup() {
[Link](115200);
delay(1000);
[Link]("BT DHT11");
[Link]("ESP32-Bt");
}

void loop() {
if ((millis()-t1) > 3000) {
byte temp = 0;
byte hum = 0;
[Link](&temp, &hum, NULL);
String msg = "t: " + String(temp) + "C h:" + String(hum) + "%";
[Link](msg);
t1 = millis();
}
}
Listato 6.23 – Codice per leggere temperatura e umidità con DHT11 e inviare i dati con il Bluetooth.

Caricate il listato sulla vostra scheda ESP32 e collegatevi con lo smartphone. Aprite l’app “Serial Bluetooth
Terminal”. Non appena connessi dovreste vedere apparire la temperatura e l’umidità rilevati dal sensore DHT11.

BLE
Il “Bluetooth Low Energy” (BLE) è una variante a basso consumo energetico del Bluetooth ideale per trasmettere
piccole quantità di dati a breve distanza. Sia il Bluetooth classico sia il BLE lavorano nella banda di frequenza di
2.4 GHz organizzata in un certo numero di canali radio. A differenza del Bluetooth, che è sempre attivo, BLE è
costantemente in sleep mode per risvegliarsi all’arrivo di una connessione. Un dispositivo BLE consuma molta
meno energia rispetto al Bluetooth tradizionale (a seconda dell’attività si va da metà della potenza fino a un
centesimo!). Il BLE prevede connessioni punto a punto (come nel Bluetooth classico) ma anche trasmissioni in
broadcast e la creazione di reti mesh.10
Il BLE è adatto per applicazioni che scambiano periodicamente piccole quantità di dati, magari con circuiti
alimentati a batteria come sensori per il fitness, il monitoraggio, localizzazione all’interno di edifici, sicurezza e
domotica.
Nella connessione punto a punto, il BLE prevede due tipi di dispositivi (entrambi realizzabili con un ESP32):
• server: annuncia la sua presenza così che possa essere trovato da altri dispositivi che collegandosi possono
interagire e leggere i dati pubblicati;
• client: ricerca la presenza di server e quando li trova verifica se presentano il dato che gli interessa.

Nella modalità di trasmissione broadcast, il server trasmette a tutti i client che sono connessi, mentre nella
modalità a “rete mesh” tutti i dispositivi sono collegati tra di loro e possono ricevere e trasmettere.
Quando due dispositivi comunicano in modalità punto a punto, utilizzano il GATT (Generic ATTribute profile),
cioè una convenzione per organizzare le informazioni scambiate tra i due dispositivi. I dati e i servizi sono
organizzati in una struttura gerarchica, cioè “a cipolla” (o come le cartelle del file system di un computer). Un
profilo è una specie di contenitore al cui interno troviamo uno o più servizi. Ogni servizio contiene uno o più
elementi chiamati “caratteristiche”. Esistono servizi predefiniti per diversi tipi di dati definiti dal SIG (Bluetooth
Special Interest Group) come: livello della batteria, pressione sanguigna, frequenza cardiaca, bilancia ecc. Ogni
servizio è identificato da un identificatore “unico”, cioè una specie di indirizzo Internet (per esempio
[Link].battery_service) e da un codice numerico (per esempio 0x180F per il servizio batterie).
L’elenco dei servizi è pubblicato all’indirizzo: [Link]
All’interno di un servizio troviamo le caratteristiche, cioè contenitori per i dati. Le caratteristiche nel caso più
semplice hanno un “nome” (o descrizione) e un valore. Se necessario, per informazioni più complesse, possiamo
trovare ulteriori “descrittori” contenuti nella stessa “caratteristica”.
Una caratteristica rappresenta quindi dei dati e offre delle operazioni per interagire con questi. Operazioni
tipiche o proprietà per una caratteristica sono:
• broadcast;
• lettura;
• scrittura;
• scrittura senza necessità di conferma;
• notifica.

Una caratteristica, per chi si intende di programmazione a oggetti (OOP), è una specie di oggetto.
Perché è necessaria tutta questa complessità? Perché un servizio non è inteso come un semplice contenitore per
dati ma qualcosa di più interattivo e dotato di una “certa intelligenza”. Un servizio legato a un sensore di
temperatura avrà delle caratteristiche per leggere il valore corrente ma poi sarà anche possibile impostare una
soglia e ricevere notifiche quando la temperatura supera la soglia. Ovviamente questo non è uno standard ma
dipende dal servizio e da come è stato realizzato.
Ogni servizio, caratteristica e descrittore è identificato da un numero univoco a 128 bit, detto UUID
(Universally Unique Identifier). Ecco un UUID:

317be145-34f5-4f17-a84c-42baf6b9bd4a
Per generarne uno a piacere visitate il sito: [Link] Per alcuni servizi specifici esistono
degli UUID prefissati che hanno una forma più breve.

BLE Server
Una volta chiarito come funziona il BLE, con poche linee di codice possiamo realizzare un server BLE con una
scheda ESP32. Apriamo un nuovo sketch e includiamo tutte le librerie necessarie (incluse nel framework di
sviluppo):
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

Impostiamo gli UUID per il servizio e per la caratteristica:


#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

Creiamo un dispositivo BLE assegnandogli un nome:


BLEDevice::init("My BLE Server");

Creiamo il server:
BLEServer *pServer = BLEDevice::createServer();

e quindi il servizio:
BLEService *pService = pServer->createService(SERVICE_UUID);

Definiamo la caratteristica specificando i parametri necessari e i permessi di lettura e scrittura:


BLECharacteristic *pCharacteristic = pService->createCharacteristic
(CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |BLECharacteristic::PROPERTY_WRITE
);

Inseriamo un valore:
pCharacteristic->setValue("Hello World!");

E infine avviamo il servizio:


pService->start();

Per rendere visibile agli smartphone il servizio, attiviamo l’advertising:


BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);

Per rendere compatibile il servizio con gli iPhone è necessario impostare dei valori particolari:
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMinPreferred(0x12);

Avviamo quindi il servizio di advertising:


BLEDevice::startAdvertising();

Ecco il codice completo:


#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
void setup() {
[Link](115200);
[Link]("ESP32 - BLE Server");
BLEDevice::init("ESP32BLE Server");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
BLECharacteristic *pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setValue("Hello ESP32-BLE!");
pService->start();

BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();


pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); //compatibilita' iphone
pAdvertising->setMinPreferred(0x12); //compatibilita' iphone
BLEDevice::startAdvertising();
}

void loop() {}

Listato 6.24 – Codice per realizzare un Server BLE con ESP32.

Una volta caricato il codice su ESP32 serve uno smartphone con BLE e un’app per poter interrogare il dispositivo.
Potete installare un’app come “nRF Connect” di Nordic Semiconductor che può eseguire la scansione dei
dispositivi presenti e interrogarli. Per utilizzare l’app dovete abilitare il Bluetooth sul telefono. Aprendo nRF
Connect, nella sezione “scanner” vedrete apparire il modulo ESP32 che abbiamo battezzato “ESP32BLE Server”
(Figura 6.14 – A). Collegatevi al server e passate nel pannello “Client” dove, navigando tra le caratteristiche,
potrete leggere il messaggio pubblicato “Hello ESP32 BLE!” (Figura 6.14 – B). Per leggere il messaggio potrebbe
essere necessario “scaricarlo” esplicitamente premendo sull’icona a forma di freccia rivolta verso il basso che
troviamo di fianco alla caratteristica.
Figura 6.14 – Utilizzo dell’app nRF Connect per cercare il modulo BLE (A) e leggere il valore pubblicato tra le caratteristiche esposte (B).

Server BLE con advertising


Modifichiamo il codice del server BLE così che pubblichi un contatore aggiornato ogni secondo e invii delle
notifiche ai client. Ci collegheremo al server con uno smartphone che riceverà le notifiche e aggiornerà
automaticamente una proprietà del nostro dispositivo. Possiamo provare a utilizzare un parametro standard come il
livello della batteria del dispositivo. Questo tipo di parametri usa degli UUID predefiniti e quindi utilizzeremo il
servizio per la batteria aggiornando la caratteristica corrispondente al livello di carica. Simuleremo il livello di
carica con un contatore che si incrementerà ogni secondo.
All’inizio dello sketch includiamo le librerie necessarie e inseriamo due costanti che definiscono il servizio che
intendiamo pubblicare:
#define SERVICE_UUID "0000180F-0000-1000-8000-00805F9B34FB"
#define CHARACTERISTIC_UUID "00002A19-0000-1000-8000-00805F9B34FB"

Definiamo una variabile globale che tenga traccia del livello della batteria che simuleremo incrementandolo da 0 a
100:
uint8_t livello_batteria = 0;

Invieremo notifiche BLE sono se c’è un client collegato e quindi dovremo prestare attenzione agli eventi di
connessione e disconnessione che aggiorneranno lo stato di una variabile booleana utilizzate nel loop():
bool CONNESSO = false;

class EventiBLE: public BLEServerCallbacks {


void onConnect(BLEServer* pServer) {
CONNESSO = true;
}

void onDisconnect(BLEServer* pServer) {


CONNESSO = false;
}
};

Nel setup() inizializziamo il BLE:


BLEDevice::init("ESP32 BLE Server");

Creiamo il server:
server = BLEDevice::createServer();
server->setCallbacks(new MyServerCallbacks());

Quindi il servizio:
service = server->createService(SERVICE_UUID);

e infine la caratteristica indicando i permessi di lettura e scrittura:


caratteristica = service->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);

Abilitiamo le notifiche e avviamo il servizio:


caratteristica->addDescriptor(new BLE2902());
service->start();

Anche l’advertising deve essere esplicitamente avviato:


BLEAdvertising *advertising = server->getAdvertising();
advertising->addServiceUUID(SERVICE_UUID);
advertising->start();

Nel loop() invieremo le notifiche solo se la variabile CONNESSO ha valore True:


void loop() {
if (CONNESSO) {
//invio notifica
}
delay(1000);
}

Il livello della batteria è un semplice contatore che incrementeremo ogni secondo:


livello_batteria++;
if (livello_batteria >= 100) livello_batteria = 0;

Modificato il valore del livello batteria possiamo inviare la notifica al client:


caratteristica->setValue((uint8_t*)&livello_batteria, sizeof(livello_batteria));
caratteristica->notify();

Ecco di seguito lo sketch completo:


#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

#define SERVICE_UUID "0000180F-0000-1000-8000-00805F9B34FB"


#define CHARACTERISTIC_UUID "00002A19-0000-1000-8000-00805F9B34FB"

BLEServer *server;
BLEService *service;
BLECharacteristic *caratteristica;

bool CONNESSO = false;


uint8_t livello_batteria = 0;

class EventiBLE: public BLEServerCallbacks {


void onConnect(BLEServer* pServer) {
CONNESSO = true;
}
void onDisconnect(BLEServer* pServer) {
CONNESSO = false;
}
};

void setup() {
[Link](115200);
[Link]("ESP32 BLE Server");

// Inizializza il BLE
BLEDevice::init("ESP32 BLE Server");

// Crea il server BLE


server = BLEDevice::createServer();
server->setCallbacks(new EventiBLE());

// Crea il servizio
service = server->createService(SERVICE_UUID);

// Crea la caratteristica con il contatore


caratteristica = service->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);

// Abilita le notifiche per la caratteristica


caratteristica->addDescriptor(new BLE2902());

// Avvia il servizio
service->start();

// Avvia l'annuncio del servizio


BLEAdvertising *advertising = server->getAdvertising();
advertising->addServiceUUID(SERVICE_UUID);
advertising->start();
}

void loop() {
if (CONNESSO) {
livello_batteria++;
if (livello_batteria >= 100) livello_batteria = 0;

// Aggiorna la caratteristica e manda la notifica


caratteristica->setValue((uint8_t*)&livello_batteria, sizeof(livello_batteria));
caratteristica->notify();
}

delay(1000);
}

Listato 6.25 – Codice per realizzare un servizio BLE che pubblica un contatore che si incrementa ogni secondo.
Figura 6.15 – Utilizzo dell’app nRF Connect per cercare il modulo BLE (A) e leggere il contatore (B) pubblicato come livello della batteria (1),
reperibile tra le caratteristiche esposte.

Caricate lo sketch sul modulo ESP32 e poi cercate il dispositivo BLE con l’app “nRF Connect” (Figura 6.15 – A).
Collegatevi al dispositivo “ESP32BLE Server”, quindi andate nel pannello “client” e cercate il servizio “Battery
Service” (Figura 6.15 – B – 1). Attivate la ricezione delle notifiche cliccando sull’icona con tre frecce verso il
basso e osservate il valore della batteria variare ogni secondo.

Client BLE
Utilizziamo una scheda con ESP32 per collegarci al server BLE e leggere la caratteristica pubblicata e aggiornata.
Il codice è abbastanza complesso ma comprensibile. Lo riporto per intero qui di seguito e lo descrivo subito dopo:
#include "BLEDevice.h"

//Il nome del servizio/dispositivo BLE a cui collegarsi


#define bleServerName "ESP32 BLE Server"

//servizio batteria e livello di carica


static BLEUUID batteryServiceUUID("0000180F-0000-1000-8000-00805F9B34FB");
static BLEUUID battLevelCharacteristicUUID("00002A19-0000-1000-8000-00805F9B34FB");

//Flag di stato:
static boolean IN_CONNESSIONE = false; //il dispositivo deve connettersi

//Indirizzo del dispositivo a cui collegarsi


static BLEAddress *bleServerAddress;
//caratteristica da leggere
static BLERemoteCharacteristic* chBatteria;

//Attivazione notifiche
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};

//livello batteria
uint8_t* battery_level;
boolean UPDATED = false;

bool connectToServer(BLEAddress pAddress) {


BLEClient* pClient = BLEDevice::createClient();

pClient->connect(pAddress);
BLERemoteService* pRemoteService = pClient->getService(batteryServiceUUID);
if (pRemoteService == nullptr) {
[Link]("Servizio UUID non trovato");
return (false);
}

chBatteria = pRemoteService->getCharacteristic(battLevelCharacteristicUUID);
if (chBatteria == nullptr ) {
[Link]("Caratteristica non trovata");
return false;
}

chBatteria->registerForNotify(batteryNotifyCallback);
return true;
}
class AdvertiseReceived: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if ([Link]() == bleServerName) { //i nomi coincidono
//fermo lo scan
[Link]()->stop();
//recupero l'indirizzo del server (advertiser)
pServerAddress = new BLEAddress([Link]());
IN_CONNESSIONE = true;
[Link]("Server trovato: in connessione");
}
}
};

static void batteryNotifyCallback(


BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {

battery_level = (uint8_t*)pData;
UPDATED = true;
}

void setup() {
[Link](115200);
[Link]("BLE Client");

BLEDevice::init("");

//serve uno scanner per cercare il server


BLEScan* pBLEScan = BLEDevice::getScan();
//funzione callback per lo scanner
pBLEScan->setAdvertisedDeviceCallbacks(new AdvertiseReceived());
//avvio lo scanner per 30 secondi
pBLEScan->setActiveScan(true);
pBLEScan->start(30);
}

void loop() {
if (IN_CONNESSIONE == true) {
if (connectToServer(*bleServerAddress)) {
[Link]("Connesso al server BLE");
//richiede di essere notificato per la caratteristica che mi interessa
chBatteria->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn,
} else {
[Link]("Connessione fallita");
}
IN_CONNESSIONE = false;
}

if (UPDATED){
//ricevo valori
[Link]("batteria: ");
[Link](*battery_level);
[Link]("%");
UPDATED = false;
}
delay(1000);
}

Listato 6.26 – Codice per realizzare un client BLE che si collega a un server e legge il valore pubblicato.

Nella parte iniziale dello sketch troviamo la dichiarazione delle variabili necessarie, in particolare inseriamo gli
UUID del servizio e della caratteristica a cui siamo interessati, cioè il livello di carica della batteria pubblicato e
aggiornato dal server del Listato 6.25.
Nel setup() abbiamo la configurazione del device che risulta più semplice di quella del server a cui però
dobbiamo aggiungere il meccanismo di scansione che sarà attivo per trenta secondi, necessario per cercare il
device a cui collegarsi:
BLEDevice::init("");
//server uno scanner per cercare il server
BLEScan* pBLEScan = BLEDevice::getScan();
//funzione callback per lo scanner
pBLEScan->setAdvertisedDeviceCallbacks(new AdvertiseReceived());
//avvio lo scanner per 30 secondi
pBLEScan->setActiveScan(true);
pBLEScan->start(30);

Lo scanner utilizza un meccanismo a eventi ed è necessario specificare una classe da richiamare nel caso accada
qualcosa. Creiamo quindi la classe:
class AdvertiseReceived: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if ([Link]() == bleServerName) { //i nomi coincidono
//fermo lo scan
[Link]()->stop();
//recupero l'indirizzo del server (advertiser)
pServerAddress = new BLEAddress([Link]());
IN_CONNESSIONE = true;
[Link]("Server trovato: in connessione");
}
}
};

Questa classe risponde solo all’evento onResult(), attivato nel caso sia trovato un device. All’interno del metodo
verifichiamo se il dispositivo che ha risposto è quello a cui siamo interessati e, nel caso lo sia, ci salviamo un suo
riferimento. Nel loop() cercheremo di collegarci al server individuato e richiederemo l’attivazione delle notifiche
per la caratteristica che riporta il livello della batteria:
if (IN_CONNESSIONE == true) {
if (connectToServer(*bleServerAddress)) {
[Link]("Connesso al server BLE");
//richiede di essere notificato per la caratteristica che mi interessa
chBatteria->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn,
} else {
[Link]("Connessione fallita");
}
IN_CONNESSIONE = false;
}

La connessione al server è implementata dalla funzione connectToServer() in cui registriamo anche una funzione
di callback batteryNotifyCallback() che sarà chiamata alla ricezione di ogni valore e aggiornerà la variabile
globale che tiene traccia del livello della batteria. Se c’è un aggiornamento del dato, la variabile UPDATED è
impostata a True e nel loop() stampiamo il valore ricevuto.
Per leggere il livello della batteria dovete utilizzare quindi due ESP32: sulla prima caricate il Listato 6.25
mentre la seconda (con il Listato 6.26) funzionerà da client e stamperà sul Serial Monitor il livello di batteria
ricevuto. Se intendete usare beacon o device BLE commerciali dovete utilizzare un’app come “nRF Connect” per
cercare le caratteristiche a cui siete interessati e ricavare gli UUID che espongono da inserire nei vostri sketch.

ESPNow
Espressif ha creato un sistema di comunicazione chiamato ESPNow che lavora a 2.5 GHz, indipendente da WiFi e
Bluetooth e che possiamo utilizzare tra vari moduli ESP32 o ESP8266. ESPNow crea un rete in cui più schede
(massimo 20) possono scambiarsi messaggi brevi (250 byte) senza prima stabilire una connessione: è una specie di
bus radio ideale per piccoli progetti IoT. La portata del segnale è di qualche decina di metri. Per far comunicare i
dispositivi è necessario conoscere i loro MAC address, informazione in genere facilmente reperibile. Il protocollo
può essere utilizzato per trasmissioni unidirezionali o bidirezionali.
Sono previsti vari scenari:
• comunicazione uno a uno;
• comunicazione uno a molti;
• comunicazione molti a uno.

Ricevitore
Proviamo a realizzare un ricevitore per ESPNow. Il dispositivo riceverà una struttura dati che contiene varie
informazioni. La struttura sarà inviata da un trasmettitore con cui la condivide. Aggiungiamo alla scheda anche un
LED collegato sul pin GPIO 4 che sarà controllato da remoto (Figura 6.16).

Figura 6.16 – Schema di collegamento di un LED alla scheda ESP32.

Per utilizzare ESPNow includiamo la specifica libreria:


#include <esp_now.h>

Definiamo poi la struttura che useremo per scambiare dati con il trasmettitore. La struttura dovrà essere definita
esattamente nello stesso modo anche nel trasmettitore:
struct message {
char msg[30];
int n;
float temp;
bool led;
} myData;

Nel setup() configuriamo il pin GPIO 4 e ESPNow chiamando esp_now_init(), quindi registriamo una funzione
di callback che sarà chiamata alla ricezione dei dati:
esp_now_register_recv_cb(onDataRecv);

La funzione di callback riceve la struttura ed estrae i dati. Tra i parametri ricevuti abbiamo anche il MAC address
del server che possiamo utilizzare per verificare chi abbia inviato le informazioni. Usiamo il campo led per
controllare lo stato del pin GPIO e accendere o spegnere il LED:
void onDataRecv(const uint8_t *mac_addr, const uint8_t *dati, int len){
memcpy(&myData, dati, sizeof(myData));
[Link]("bytes: ");[Link](len);
[Link]("msg: ");[Link]([Link]);
[Link]("n: ");[Link](myData.n);
[Link]("temp: ");[Link]([Link]);
digitalWrite(4, [Link]);
}

Qui di seguito abbiamo il codice completo per un client:


#include <esp_now.h>

struct message {
char msg[30];
int n;
float temp;
bool led;
} myData;

void onDataRecv(const uint8_t *mac_addr, const uint8_t *dati, int len);

void setup() {
pinMode(4, OUTPUT);
[Link](115200);

if (esp_now_init() != ESP_OK) {
[Link]("ESP-NOW KO");
while(1);
}

esp_now_register_recv_cb(onDataRecv);
}

void loop() {}

void onDataRecv(const uint8_t *mac_addr, const uint8_t *dati, int len){


memcpy(&myData, dati, sizeof(myData));
[Link]("bytes: ");[Link](len);
[Link]("msg: ");[Link]([Link]);
[Link]("n: ");[Link](myData.n);
[Link]("temp: ");[Link]([Link]);
digitalWrite(4, [Link]);
}

Listato 6.27 – Codice per un client ESPNow.

Per collegare il ricevitore con il trasmettitore dovrete ricavare il suo MAC address che può essere ottenuto con
poche righe di codice:
#include "WiFi.h"

void setup() {
[Link](115200);
delay(2000);
[Link](WIFI_MODE_STA);
[Link]([Link]());
}
void loop() {}

Listato 6.28 – Lettura del MAC address.

Se caricate sul ricevitore il Listato 6.28 per leggere il MAC address, poi ricordatevi di ricaricare il Listato 6.27!

Trasmettitore
Il trasmettitore per ESPNow deve conoscere i MAC address dei dispositivi con cui vuole comunicare che quindi
devono essere raccolti in precedenza. Questo rende la rete un po’ rigida e poco dinamica ma offre maggiore
sicurezza. Nello sketch del trasmettitore includiamo la specifica libreria:
#include <esp_now.h>

e aggiungiamo il MAC address del ricevitore:


uint8_t rxaddr[] = {0x58, 0xBF, 0x25, 0x93, 0x27, 0x48};

Dobbiamo definire la struttura dati da inviare e questa deve essere precisamente identica a quella usata dai
ricevitori:
struct message {
char msg[30];
int n;
float temp;
bool led;
} myData;

Nel setup() inizializziamo ESPNow chiamando esp_now_init() e quindi aggiungiamo il MAC address del
ricevitore specificando il canale da usare e il livello di sicurezza:
memcpy(peerinfo.peer_addr, rxaddr, 6);
[Link] = 0;
[Link] = false;
if (esp_now_add_peer(&peerinfo) != ESP_OK) {
[Link]("ESP-NOW add peer ko");
while(1);
}

Possiamo specificare anche una funzione di callback che sia chiamata dopo ogni trasmissione dati:
esp_now_register_send_cb(onDataSend);

La funzione è così definita e ci permette di avere una conferma della ricezione della struttura:
void onDataSend(const uint8_t *mac_addr, esp_now_send_status_t status){
[Link]("TX callback");
if (status == ESP_NOW_SEND_SUCCESS) {
[Link]("Dato ricevuto");
} else {
[Link]("Dato non ricevuto");
}
}

Nel loop() modifichiamo i dati della struttura e li inviamo periodicamente con:


esp_err_t ret = esp_now_send(rxaddr, (uint8_t *) &myData, sizeof(myData));

Il campo booleano led è continuamente impostato a true e false e usato per accendere e spegnere il LED del
ricevitore:
[Link] = ![Link];

Ecco il codice completo del trasmettitore:


#include <esp_now.h>

uint8_t rxaddr[] = {0x58, 0xBF, 0x25, 0x93, 0x27, 0x48};

esp_now_peer_info_t peerinfo;

struct message {
char msg[30];
int n;
float temp;
bool led;
} myData;
void onDataSend(const uint8_t *mac_addr, esp_now_send_status_t status);

void setup() {
[Link](115200);

if (esp_now_init() != ESP_OK) {
[Link]("ESP-NOW KO");
while(1);
}

esp_now_register_send_cb(onDataSend);

memcpy(peerinfo.peer_addr, rxaddr, 6);


[Link] = 0;
[Link] = false;
if (esp_now_add_peer(&peerinfo) != ESP_OK) {
[Link]("ESP-NOW add peer ko");
while(1);
}

[Link] = LOW;
strcpy([Link], "hello world");
}

void loop() {
myData.n++;
[Link] = 12.3;
[Link] = ![Link];

esp_err_t ret = esp_now_send(rxaddr, (uint8_t *) &myData, sizeof(myData));


if (ret == ESP_OK) {
[Link]("TX ok");
} else {
[Link]("TX KO");
}
delay(2000);
}

void onDataSend(const uint8_t *mac_addr, esp_now_send_status_t status){


[Link]("TX callback");
if (status == ESP_NOW_SEND_SUCCESS) {
[Link]("Dato ricevuto");
} else {
[Link]("Dato non ricevuto");
}
}

Listato 6.29 – Codice per un trasmettitore ESPNow.

Per le connessioni bidirezionali replicheremo il codice del trasmettitore anche nel ricevitore.
È possibile trasmettere a tutti i ricevitori realizzando un broadcast e specificando come MAC address:
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

OTA: aggiornare lo sketch da WiFi


Le schede dotate di WiFi come ESP32 possono essere aggiornate da remoto con il meccanismo OTA (Over The
Air) senza usare il cavo USB.

OTA con Arduino IDE


Per sfruttare questa potenzialità utilizzeremo uno sketch di base presente tra gli esempi di Arduino IDE. Il nome
dello sketch è BasicOTA e lo potete trovare anche tra le risorse del libro (Listato 6.30). Il listato va sempre
utilizzato come base su cui aggiungere il nostro codice perché le istruzioni presenti garantiscono l’aggiornabilità
del firmware da remoto.
All’inizio del listato dovrete inserire le credenziali di accesso alla vostra rete WiFi modificando le righe:
const char* ssid = "nome rete wifi";
const char* password = "password wifi";

Aggiungete poi il vostro codice in coda al setup() e nel loop() facendo attenzione a non inserire delay() o
codice bloccante perché l’aggiornamento è garantito dalla chiamata continua a:
void loop() {
[Link]();
}

Ecco il listato completo di BasicOTA:


#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

const char* ssid = "nome rete wifi";


const char* password = "password wifi";

void setup() {
[Link](115200);
[Link]("Booting");
[Link](WIFI_STA);
[Link](ssid, password);
while ([Link]() != WL_CONNECTED) {
[Link]("Connection Failed! Rebooting...");
delay(5000);
[Link]();
}

// Port defaults to 3232


// [Link](3232);

// Hostname defaults to esp3232-[MAC]


// [Link]("myesp32");

// No authentication by default
// [Link]("admin");

// Password can be set with it's md5 value as well


// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// [Link]("21232f297a57a5a743894a0e4a801fc3");

ArduinoOTA
.onStart([]() {
String type;
if ([Link]() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";

// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using [Link]()
[Link]("Start updating " + type);
})
.onEnd([]() {
[Link]("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
[Link]("Progress: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
[Link]("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) [Link]("Auth Failed");
else if (error == OTA_BEGIN_ERROR) [Link]("Begin Failed");
else if (error == OTA_CONNECT_ERROR) [Link]("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) [Link]("Receive Failed");
else if (error == OTA_END_ERROR) [Link]("End Failed");
});

[Link]();
[Link]("Ready");
[Link]("IP address: ");
[Link]([Link]());
}

void loop() {
[Link]();
}

Listato 6.30 – BasicOTA di Arduino IDE.

Se caricate il Listato 6.30 sulla vostra ESP32, otterrete il suo codice IP. Salvate questa informazione perché è
necessaria per l’aggiornamento remoto. Il primo upload del codice si farà tramite cavo USB, ma i successivi
aggiornamenti li potrete fare selezionando, nel menu tools, invece che una porta seriale, l’indirizzo IP della scheda
(vedi Figura 6.17).

Figura 6.17 – Caricamento tramite OTA.

Premendo il pulsante di upload apparirà una finestra per inserire la password. Se non l’avete modificata nel codice
con l’istruzione:
[Link]("admin");

il suo valore predefinito è admin. Inseritelo e procedete con l’aggiornamento. In questa modalità, ovviamente il
Serial Monitor non è disponibile.
Figura 6.18 – Inserimento della password per il caricamento OTA.

OTA con Web Server integrato


Se non volete dipendere da Arduino per caricare il codice potete usare l’esempio ArduinoWebUpdater che
implementa un piccolo server web per l’aggiornamento del codice. Il listato dell’esempio base è riportato nel
Listato 6.31 e lo dovrete usare sempre come “base” di sviluppo per i vostri firmware aggiornabili da remoto. Una
volta configurato lo sketch inserendo le credenziali per l’accesso al WiFi ed eventualmente modificando il nome di
rete della scheda (di default a [Link] potete caricarlo su una ESP32.
La scheda si collegherà al WiFi e potrete accedere al web server incorporato visitando [Link] (Figura
6.19). Inserendo le credenziali predefinite (utente admin e password admin) apparirà la pagina per il caricamento
del firmware (Figura 6.20).
Figura 6.19 – Pagina di login per il web server OTA a bordo della ESP32.

Figura 6.20 – Pagina per il caricamento del firmware sulla scheda ESP32 via OTA.

Il file da caricare è il “binario” che possiamo generare salvando e compilando lo sketch, quindi selezioniamo
Sketch > Export Compiled Binary e poi Sketch > Show Sketch Folder dove troveremo un file con estensione
.bin:
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

const char* host = "esp32";


const char* ssid = "nome wifi";
const char* password = "password wifi";

WebServer server(80);

/*
* Login page
*/

const char* loginIndex =


"<form name='loginForm'>"
"<table width='20%' bgcolor='A09F9F' align='center'>"
"<tr>"
"<td colspan=2>"
"<center><font size=4><b>ESP32 Login Page</b></font></center>"
"<br>"
"</td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td>Username:</td>"
"<td><input type='text' size=25 name='userid'><br></td>"
"</tr>"
"<br>"
"<br>"
"<tr>"
"<td>Password:</td>"
"<td><input type='Password' size=25 name='pwd'><br></td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td><input type='submit' onclick='check([Link])' value='Login'></td>"
"</tr>"
"</table>"
"</form>"
"<script>"
"function check(form)"
"{"
"if([Link]=='admin' && [Link]=='admin')"
"{"
"[Link]('/serverIndex')"
"}"
"else"
"{"
" alert('Error Password or Username')/*displays error message*/"
"}"
"}"
"</script>";

/*
* Server Index Page
*/

const char* serverIndex =


"<script src='[Link]
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update'>"
"<input type='submit' value='Update'>"
"</form>"
"<div id='prg'>progress: 0%</div>"
"<script>"
"$('form').submit(function(e){"
"[Link]();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
" $.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new [Link]();"
"[Link]('progress', function(evt) {"
"if ([Link]) {"
"var per = [Link] / [Link];"
"$('#prg').html('progress: ' + [Link](per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"[Link]('success!')"
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>";

/*
* setup function
*/
void setup(void) {
[Link](115200);

// Connect to WiFi network


[Link](ssid, password);
[Link]("");

// Wait for connection


while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("");
[Link]("Connected to ");
[Link](ssid);
[Link]("IP address: ");
[Link]([Link]());

/*use mdns for host name resolution*/


if (![Link](host)) { //[Link]
[Link]("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
[Link]("mDNS responder started");
/*return index page which is stored in serverIndex */
[Link]("/", HTTP_GET, []() {
[Link]("Connection", "close");
[Link](200, "text/html", loginIndex);
});
[Link]("/serverIndex", HTTP_GET, []() {
[Link]("Connection", "close");
[Link](200, "text/html", serverIndex);
});
/*handling uploading firmware file */
[Link]("/update", HTTP_POST, []() {
[Link]("Connection", "close");
[Link](200, "text/plain", ([Link]()) ? "FAIL" : "OK");
[Link]();
}, []() {
HTTPUpload& upload = [Link]();
if ([Link] == UPLOAD_FILE_START) {
[Link]("Update: %s\n", [Link].c_str());
if (![Link](UPDATE_SIZE_UNKNOWN)) { //start with max available size
[Link](Serial);
}
} else if ([Link] == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if ([Link]([Link], [Link]) != [Link]) {
[Link](Serial);
}
} else if ([Link] == UPLOAD_FILE_END) {
if ([Link](true)) { //true to set the size to the current progress
[Link]("Update Success: %u\nRebooting...\n", [Link]);
} else {
[Link](Serial);
}
}
});
[Link]();
}

void loop(void) {
[Link]();
delay(1);
}

Listato 6.31 –OTA con web server integrato.

1
WiFiEvent_t è una struttura contenente una union definita nel file sorgente esp_event_legacy.h della libreria WiFi.h.
2
WiFiEventInfo_t è una struttura che contiene informazioni sull’evento e dipende dal tipo di evento. Troviamo una descrizione delle possibili varianti
nel file sorgente esp_event_legacy.h della libreria WiFi.h.
3
Il server risponde con il codice 404 se la pagina non è stata trovata oppure 500 se ci sono stati errori nell’esecuzione.
4
Per convertire i gradi Kelvin in Celsius sottraete 273,15.
5
Il termine REST è l’acronimo di REpresentational State Transfer.
6
Un “protocollo” è una serie di regole e convenzioni per trasmettere informazioni fra due dispositivi.
7
Utilizzando server pubblici e gratuiti fate molta attenzione al tipo di dati che scambiate! Usateli solo per prove di connettività!
8
In alcune particolari situazioni ho avuto qualche problema di compatibilità con alcune schede di rete utilizzando la libreria ufficiale le ho risolte con una
versione modificata chiamata Ethernet2.h ([Link]
9
Il telefono deve potersi collegare ai dispositivi Bluetooth “classici”. Mentre la cosa pare abbastanza scontata per i telefoni Android, potrebbe non
funzionare con iPhone e dispositivi Apple dotati di limitazioni sulle porte seriali via Bluetooth.
10
Una rete mesh è formata da vari dispositivi tutti collegati tra loro. Ogni nodo della rete può inviare, ricevere e reindirizzare i dati verso gli altri
elementi creando un collegamento multiplo tra i dispositivi. In questo modo non c’è un punto centrale di controllo e ogni nodo nella rete può comunicare
direttamente con tutti gli altri nodi raggiungibili. Le reti mesh sono spesso utilizzate in situazioni in cui è richiesta una connettività affidabile, flessibile e
scalabile. Possono essere utilizzate per estendere la copertura di una rete, migliorare la stabilità della connessione e creare una rete robusta in cui la
comunicazione può essere reindirizzata automaticamente in caso di guasti o ostacoli.
A

Rudimenti di elettronica

Come leggere le resistenze


Le resistenze al 5% si riconoscono perché hanno quattro bande colorate e
una delle strisce, quella posta a un’estremità, è di colore dorato. Per leggere
la resistenza teniamo la banda dorata alla nostra destra e poi leggiamo le
bande partendo da sinistra verso destra usando la tabella dei colori visibile
in Figura A.1 e riportati in Tabella A.1.

Figura A.1 – Lettura del valore di una resistenza.

Tabella A.1 – Codici colore per la lettura dei resistori.


Colore Valore Moltiplicatore
Argento – x 0,01

Oro – x 0,1

Nero 0 x1

Marrone 1 x 10

Rosso 2 x 100

Arancione 3 x 1000
Giallo 4 x 10.000

Verde 5 x 100.000

Blu 6 x 1.000.000

Viola 7 x 10.000.000

Grigio 8 –

Bianco 9 –

Immaginiamo di avere tra le mani una resistenza con le bande di colore


marrone, nero, giallo e oro:
1. afferriamo il componente tenendo la banda dorata alla nostra destra;
2. iniziamo a leggere le bande da sinistra verso destra;
3. la prima banda è di colore marrone;
4. annotiamo “1” su un foglio;
5. la seconda banda è nera e quindi scriviamo “0”;
6. la terza banda è gialla, quindi scriviamo “4”;
7. sul foglio avremo scritto “1 0 4”, ma questo non è ancora il valore della
resistenza;
Figura A.2 – leggiamo le bande del resistore e annotiamo su di un foglio il codice corrispondente a
ogni colore.

8. cancelliamo la terza cifra e sostituiamola con un numero di zeri pari al


valore della cifra. Eliminiamo il numero “4” e scriviamo al suo posto
quattro zeri: “0 0 0 0”;

Figura A.3 – sostituiamo la terza cifra con il corrispondente numero di zeri.

9. la resistenza vale 100.000 Ω!

Al posto di scrivere 100.000 Ω gli elettronici preferiscono però usare “100


kΩ”. I numeri fino a 999 si scrivono senza abbreviazioni. I numeri sopra le
migliaia si scrivono con una “k” che sta per kilo:
1 kΩ = 1000 Ω
10 kΩ = 10.000 Ω
100 kΩ = 100.000 Ω.

Dopo i “k” abbiamo i mega “M”. Se abbiamo un resistore da un milione di


Ω: 1.000.000 Ω, scriveremo 1 MΩ. A volte il simbolo “k” o “M” si usa
anche come virgola! Vediamo qualche esempio che potremmo incontrare:

2k2 = 2200 Ω
4k7 = 47.000 Ω

3M3 = 3.300.000 Ω
Le resistenze al 5% sono prodotte solo in una serie limitata di valori. I
valori ammessi rispettano questa scaletta: 1 - 1,2 - 1,5 - 1,8 - 2,2 - 2,7 - 3,3 -
3,9 - 4,7 - 5 - 6 - 6,8 - 8,2 - 10. Non troveremo mai una resistenza da 20 Ω,
ma da 22 Ω o da 18 Ω. Non esistono componenti da 500 kΩ, ma da 470 kΩ
o da 560 kΩ.
Le resistenze a 5 o 6 bande si leggono con lo stesso processo: l’ultima
banda sarà sempre la tolleranza e la penultima il moltiplicatore.

Elenco dei componenti utili per un generico


laboratorio elettronico
In molti mi chiedono suggerimenti su quali componenti tenere nel cassetto.
Ecco un elenco minimo per iniziare e per avere un componente per ogni
esigenza.

Componente Quantità suggerita


ESP32 o compatibile 1

Breadboard 1 o più
Jumper per breadboard 2 mazzetti

Resistore 100 Ω 10

Resistore 270 Ω 20
Resistore 390 Ω 20

Resistore 1 kΩ 20
Resistore 4,7 kΩ 10

Resistore 10 kΩ 20
Trimmer 100 kΩ 2o3

Condensatore ceramico MLCC 100 nF/50 V 10

Condensatore elettrolitico 100 µF/10 V 5


Condensatore elettrolitico 1000 µF/10 V 2

Condensatore a film 10 nF/50 V 10

LED di vari colori 10 o più

LED RGB 1o2


Pulsanti N.O. (normalmente aperto) 5 o più

Deviatori 1o2

Relè a 5 V 1
Relè a 12 V 1

2N2222 5

2N3904 5

BC327 5
BD135 2

IRF520 2

IRL540 2
1N4005 10

1N4148 10

Diodo Zener 4,7 V 5

SN74HC595 2
SN74HC165 2

LM741 1

LM358 1
NE555 1

ULN2003 1
Display LCD 16x2 con driver Hitachi HD44780 1

Display OLED SSD1306 128x64 1

Fotoresistenza 2
Termistore 2

DHT11 sonda temperature e umidità digitale 1

DS18B20 sonda temperature digitale 1


HC-SR04 sonar ultrasonico 1

ADXL335 accelerometro analogico 1

DS1307 RTC 1

Motore cc a 5 V con driver L293D 1


Servomotore 1 o più

Motore passo-passo con driver A4988 1 o più

Trasduttore piezoelettrico 1
Altoparlante 1

SDCard reader 1

Joystick analogico 1
Esp32
Paolo Aliverti

EAN: 9791254911136

Copia data in licenza a


Carbone Giuseppe
Questa pubblicazione è stata acquistata il 14 febbraio
2024
su LSWR B2B

Codice cliente: 5159


Codice transazione: 2675
Questa pubblicazione è soggetta alla normativa sul diritto d'autore
e pertanto non è consentita la sua diffusione, copia o riproduzione
se non a uso personale

LSWR B2B
declina ogni responsabilità per ogni utilizzo del file non previsto
dalla legge

Potrebbero piacerti anche