Esp 32
Esp 32
ESP32
Guida completa
Paolo Aliverti
ESP32 | Guida completa
Collana:
ISBN 979-12-5491-112-9
eISBN 979-12-5491-113-6
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]
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
7. Rudimenti di elettronica
Come leggere le resistenze
Elenco dei componenti utili per un generico laboratorio elettronico
Introduzione
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.
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!
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.
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.
Figura 0.10 – Kit di componenti per ESP32: un’ottima soluzione per iniziare!
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 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:
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.
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).
ESP8266 vs Arduino
Una delle difficoltà maggiori che incontra chi realizza prototipi con
Arduino è quella di connetterlo in rete. I problemi sono vari:
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!
Arduino
ESP8266
UNO
RAM 2 kB Da 80 a 160 kB
Da 80 a 160
CPU 16 MHz
MHz
ESP32
• 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.
• SoC: [Link]
• moduli: [Link]
• DevKit: [Link]
NodeMCU
NodeMCU
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
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]
NodeMCU-32S
HUZZAH32
Hello ESP!
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.
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.
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
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.
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.
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.
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.
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.
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.
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);
}
1
Non tutte le schede ESP32 sono dotate di un LED di accensione!
3
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.
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.
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.
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).
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:
}
• 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() {}
per spegnerlo:
digitalWrite(25, LOW);
V=I×R
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 Ω.
void loop() {
digitalWrite(25, HIGH);
}
void loop() {}
Listato 3.4 – Accensione di un LED collegato al pin 25.
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).
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.
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];
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);
void loop() {
int v = digitalRead(23);
digitalWrite(25, v);
}
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Ω.
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.
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.
void loop() {
int v = !digitalRead(23);
digitalWrite(25, v);
}
Listato 3.9 – Codice per leggere un pulsante con resistore di pull up esterno.
void loop() {
int v = !digitalRead(23);
digitalWrite(5, v);
}
Listato 3.10 – Codice per leggere un pulsante con resistore di pull up interno.
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Ω.
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");
}
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.
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();
}
A B Risultato
falso falso falso
A B Risultato
falso falso falso
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.
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 (||).
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;
}
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.
postit1 = millis();
void setup() {
pinMode(26, OUTPUT);
void loop() {
if ((millis() – postit1) > 3000) {
digitalWrite(26, HIGH);
}
}
Listato 3.15 – Codice per creare un semplice “timer” utilizzando la funzione millis().
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);
void loop() {
if ((millis() – postit1) > 1000) {
digitalWrite(26, !digitalRead(26));
void setup() {
pinMode(24, INPUT);
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);
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);
}
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);
}
void loop() {}
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() {}
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);
int x = 36;
float y = 1.2;
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() {}
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");
[Link](true);
log_v("Verbose");
log_d("Debug");
log_i("Info");
log_w("Warning");
log_e("Error");
}
void loop() {}
Figura 3.20 – Schema elettrico di un LED collegato sul pin 25 della scheda ESP32.
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).
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:
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.
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
void loop() {
int v = analogRead(34);
[Link](v);
delay(500);
}
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.
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);
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”.
void loop() {}
Listato 3.25 – Utilizzo di un ciclo for per stampare una sequenza di numeri.
void setup() {
[Link](115200);
Listato 3.26 – Utilizzo di due cicli for per realizzare un conteggio in avanti e poi all’indietro.
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.
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;
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);
}
}
void setup() {
[Link](115200);
delay(1000);
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.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++;
}
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);
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.
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);
}
ledcWrite(canale_pwm, duty_cycle);
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);
}
}
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);
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);
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).
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);
void loop() {
int v = touchRead(2);
[Link](v);
delay(200);
}
void setup() {
[Link](115200);
delay(1000);
[Link]("Touch LED");
void loop() {
int v = touchRead(2);
if (v < soglia) {
digitalWrite(4, HIGH);
} else {
digitalWrite(4, LOW);
}
}
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.
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
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;
//una variabile
int n = 123;
//modifico il valore:
*ptr = 456;
void loop() {}
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;
//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() {}
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;
}
//stampo l'array:
for (int i = 0; i < 10; i++) {
[Link]("%d) Valore: %d \n", i, buff[i]);
}
}
void loop() {}
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);
void setup() {
[Link](115200);
strcat(str, b);
[Link](str);
}
void loop(){}
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);
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 {}.
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(){}
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(){}
Eseguendo il codice, nel Serial Monitor dovremmo leggere il valore della variabile v prima e
dopo la chiamata della funzione:
v: 10
v: 100
[Link]("t: ");
[Link](temp);
[Link]("p: ");
[Link](press);
[Link]("h: ");
[Link](hum);
}
void loop(){}
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;
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.
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;
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];
} ;
È 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;
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]);
void setup() {
[Link](115200);
[Link] = 12345;
strncpy([Link], "Milano", 7);
void loop() {
// put your main code here, to run repeatedly:
}
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.
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() {}
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);
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;
}
}
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);
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;
}
}
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;
}
}
void setup() {
[Link](115200);
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.).
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.
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);
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();
}
void setup() {
[Link](115200);
delay(1000);
[Link]("interrupt test");
void loop() {
if ((millis() - t1) >= 1000) {
[Link]("count: ");
[Link](count);
count = 0;
t1 = millis();
}
}
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.
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");
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));
}
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);
}
pinMode(4, OUTPUT);
pinMode(16, INPUT);
}
void loop() {
if (digitalRead(16)) {
[Link]("clic");
digitalWrite(4, !digitalRead(4));
delay(300);
}
}
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();
}
}
}
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);
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.
void setup() {
[Link](115200);
delay(1000);
[Link]("FDS FDD");
pinMode(4, OUTPUT);
pinMode(16, INPUT);
}
void loop() {
tasto = digitalRead(16);
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.
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));
}
void setup() {
[Link](115200);
delay(1000);
[Link]("FDS FDD");
pinMode(4, OUTPUT);
pinMode(16, INPUT);
}
void loop() {
tasto = digitalRead(16);
Listato 4.20 – Sketch per individuare una pressione “lunga” sul tasto e attivare un’azione speciale.
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();
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;
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;
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++;
pinMode(4, OUTPUT);
pinMode(16, INPUT);
}
void loop() {
tasto = digitalRead(16);
Listato 4.21 – Codice per rilevare un triplo clic del pulsante e cambiare lo stato di un LED.
5
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.
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.
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.
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);
}
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.
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.
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:
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);
}
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>
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.
#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);
}
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:
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------------");
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>
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!");
}
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!
------------
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.
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);
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]());
Inseriamo poi una pausa prima di richiedere una nuova lettura dati:
delay(1000);
#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);
}
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).
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>
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]("Verifica i collegamenti del display!");
while(1); //stop
}
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);
#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]("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]();
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() {}
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);
È 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]
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.
È 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);
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]("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);
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]("Errore SD");
while(1);
}
[Link]("SD: ok");
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() {}
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]("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]");
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");
if () {
[Link]("spiffs error");
while(1);
}
//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() {}
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"));
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.
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 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.
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() {}
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.
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);
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() {}
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);
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()!
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;
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);
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);
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().
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);
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 compattando:
0x0000004014
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
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);
void loop() {}
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();
Al risveglio potete sapere quale dei pin ha scatenato il risveglio esaminando il valore ritornato da
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]("??");
}
void loop() {}
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);
per confermare il salvataggio. Per recuperare un byte dalla EEPROM da uno specifico indirizzo
scriveremo:
uint8_t valore = [Link](indirizzo);
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
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;
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);
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.
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);
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());
void loop() {}
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.
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
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.
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.
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]();
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");
delay(5000);
//mi scollego
[Link]();
[Link]("disconnesso");
}
void loop() {}
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.
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);
}
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"
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();
}
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>
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"
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();
}
WiFiMulti wifimulti;
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);
[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();
}
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
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 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]("\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]().
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);
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 client;
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]("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]");
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);
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]("non connesso");
while(true);
} else {
//ricezione dati
while ([Link]()) {
String line = [Link]('\r');
[Link](line);
}
[Link]();
}
[Link]();
}
void loop() {}
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]
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]
Alcuni server potrebbero richiedere una login e password che possiamo fornire con:
[Link]("SERVER_LOGIN", "SERVER_PASSWORD");
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>
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;
if (code == 200) {
String risposta = [Link]();
[Link](risposta);
} else {
[Link]("Errore HTTP: ");
[Link](code);
}
[Link]();
}
t1 = millis();
}
}
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");
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;
[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();
}
}
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 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);
DynamicJsonDocument doc(1024);
JsonObject sensor = [Link]("sensor");
sensor["temp"] = 24.8;
sensor["hum"] = 40;
sensor["color"] = "red";
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.
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.
Prepariamo una stringa con l’indirizzo da chiamare (ricordatevi di sostituire la vostra key di fianco a APPID):
String addr = "[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));
#include <HTTPClient.h>
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
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;
Eseguiamo la chiamata al servizio o alla pagina sotto https inizializzando l’HTTPClient anche con il
WiFiClientSecure:
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);
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]());
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);
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]("");
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.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);
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]() == 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.
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() {
}
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";
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");
}
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]
[Link]
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WebServer.h>
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";
void pageError() {
String str = "Pagina non trovata\n";
[Link](404, "text/plain", str);
}
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"
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);
});
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]("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() {}
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.
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();
}
AsyncWebServer server(80);
void setup() {
pinMode(25, OUTPUT);
digitalWrite(25, LOW);
[Link](115200);
delay(1000);
[Link]("SPIFFS web");
if () {
[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]());
void loop() {}
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);
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);
}
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]();
}
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]();
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");
delay(1000);
[Link](1234);
}
void loop() {
uint8_t buffer[50] = "hello UDP!";
[Link]("[Link]", 1234);
[Link](buffer, 10);
[Link]();
delay(1000);
}
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);
Prepariamo un buffer per la ricezione dei byte trasmessi dal client UDP:
uint8_t buffer[50];
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);
}
WiFiUDP udp;
void setup() {
[Link](115200);
delay(1000);
[Link]("UDP Server");
[Link]("reelco_2_4", "cardu450IX");
[Link]("mi connetto...");
while ([Link]() != WL_CONNECTED) {
delay(500);
[Link](".");
}
[Link]("\nconnesso");
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);
}
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>
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"
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);
}
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
}
}
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;
}
}
}
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.
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();
}
}
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>
Creiamo il server:
BLEServer *pServer = BLEDevice::createServer();
e quindi il servizio:
BLEService *pService = pServer->createService(SERVICE_UUID);
Inseriamo un valore:
pCharacteristic->setValue("Hello World!");
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);
void loop() {}
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).
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;
Creiamo il server:
server = BLEDevice::createServer();
server->setCallbacks(new MyServerCallbacks());
Quindi il servizio:
service = server->createService(SERVICE_UUID);
BLEServer *server;
BLEService *service;
BLECharacteristic *caratteristica;
void setup() {
[Link](115200);
[Link]("ESP32 BLE Server");
// Inizializza il BLE
BLEDevice::init("ESP32 BLE Server");
// Crea il servizio
service = server->createService(SERVICE_UUID);
// Avvia il servizio
service->start();
void loop() {
if (CONNESSO) {
livello_batteria++;
if (livello_batteria >= 100) livello_batteria = 0;
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"
//Flag di stato:
static boolean IN_CONNESSIONE = false; //il dispositivo deve connettersi
//Attivazione notifiche
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};
//livello batteria
uint8_t* battery_level;
boolean UPDATED = false;
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");
}
}
};
battery_level = (uint8_t*)pData;
UPDATED = true;
}
void setup() {
[Link](115200);
[Link]("BLE Client");
BLEDevice::init("");
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).
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]);
}
struct message {
char msg[30];
int n;
float temp;
bool led;
} myData;
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() {}
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() {}
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>
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");
}
}
Il campo booleano led è continuamente impostato a true e false e usato per accendere e spegnere il LED del
ricevitore:
[Link] = ![Link];
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);
[Link] = LOW;
strcpy([Link], "hello world");
}
void loop() {
myData.n++;
[Link] = 12.3;
[Link] = ![Link];
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};
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]();
}
void setup() {
[Link](115200);
[Link]("Booting");
[Link](WIFI_STA);
[Link](ssid, password);
while ([Link]() != WL_CONNECTED) {
[Link]("Connection Failed! Rebooting...");
delay(5000);
[Link]();
}
// No authentication by default
// [Link]("admin");
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]();
}
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).
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.
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>
WebServer server(80);
/*
* Login page
*/
/*
* Server Index Page
*/
/*
* setup function
*/
void setup(void) {
[Link](115200);
void loop(void) {
[Link]();
delay(1);
}
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
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 –
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.
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
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
SN74HC595 2
SN74HC165 2
LM741 1
LM358 1
NE555 1
ULN2003 1
Display LCD 16x2 con driver Hitachi HD44780 1
Fotoresistenza 2
Termistore 2
DS1307 RTC 1
Trasduttore piezoelettrico 1
Altoparlante 1
SDCard reader 1
Joystick analogico 1
Esp32
Paolo Aliverti
EAN: 9791254911136
LSWR B2B
declina ogni responsabilità per ogni utilizzo del file non previsto
dalla legge