Quantcast
Channel: Tutorials – lucadentella.it
Viewing all 84 articles
Browse latest View live

Interruttore a tre posizioni e Arduino

$
0
0

Con il piccolo tutorial di oggi vi mostrerò come è possibile, utilizzando un solo PIN analogico di Arduino, leggere la posizione di un interruttore a tre posizioni.

Un interruttore a tre posizioni (a volte definito ON-OFF-ON o SPTT) presenta 3 PIN: il comune (centrale) e i due di uscita. Quando la levetta è in alto o in basso, il PIN comune è connesso ad uno dei due PIN di uscita; quando la levetta è in posizione centrale, il PIN comune non è connesso a nulla.

Utilizzando due sole resistenze, possiamo leggere la posizione della levetta con un PIN analogico di Arduino:

3sbutton-1

Per capire il significato dello schema sopra riportato, verifichiamo cosa succede nelle tre posizioni della levetta:

  • quando la levetta è in posizione centrale (2), il PIN AN0 è collegato a 5V tramite R1 (pull-up): il valore letto da Arduino sarà circa 1023
  • quando la levetta è in alto (3), il PIN AN0 è collegato al centro di un partitore resistivo formato da R1 ed R2. Essendo le due resistenze di pari valore, la tensione vista da AN0 sarà circa 2.5V e quindi il valore letto da Arduino sarà circa 512
  • quando la levetta è in basso (1), il PIN AN0 è collegato direttamente a massa; il valore letto da Arduino sarà circa 0.
     

Lo sketch sarà allora molto semplice:

#define BUTTON_PIN A0
int previousState;
 
void setup() {
 
  Serial.begin(9600);
  previousState = 0;
}
 
void loop() {
 
  int analogValue = analogRead(BUTTON_PIN);
 
  int actualState;
  if(analogValue < 100) actualState = 1;
  else if(analogValue < 900) actualState = 3;
  else actualState = 2;
 
  if(previousState != actualState) {
 
    previousState = actualState;
    Serial.print("Button state: ");
    Serial.println(actualState);
  }
}

Lo sketch utilizza una soglia di tolleranza sui valori letti.

Demo

Vista la semplicità del circuito, l’ho realizzato su una millefori:

3sbutton-4 3sbutton-3

3sbutton-2


Raspberry Pi Zero, audio output via I2S

$
0
0

Pi Zero è una piccola scheda – parte della famiglia Raspberry – pensata per applicazioni embedded. Il suo bassissimo costo (circa 10€ per la versione W, con connettività wifi) e la capacità di eseguire diversi sistemi operativi la rende la scelta ideale per diverse applicazioni (media center, datalogger…).

L’acquisto di un Raspberry Pi Zero può essere talvolta difficoltoso… è infatti spesso esaurito sui diversi negozi online. Può aiutare il sito ThePiLocator che mostra in tempo reale la disponibilità delle diverse schede Raspberry presso gli store ufficiali.

La scheda Pi Zero purtroppo non offre un connettore audio dedicato: normalmente infatti l’output audio avviene tramite il connettore HDMI:

pizero-i2s08

Sebbene tale scelta vada benissimo per utilizzi quali media center (audio e video sono riprodotti dal televisore collegato via HDMI), non è comoda per applicazioni embedded che hanno la necessità di riprodurre solo file audio (ad esempio per una voce guida…). Anche se esistono dei dispositivi in grado di estrarre il flusso audio dallo stream HDMI, tali dispositivi sono spesso costosi e di grandi dimensioni.

Una soluzione molto semplice, spiegata benissimo in un tutorial di Adafruit, è quella di generare il segnale audio utilizzando due pin PWM (Pulse Width Modulation) e un filtro passa-basso realizzato con alcuni componenti passivi. Questa soluzione ha il vantaggio di essere economica e di facile realizzazione; di contro la qualità dell’audio prodotto non è elevatissima.

Nel tutorial di oggi voglio invece mostrarvi  come ottenere un segnale audio di alta qualità utilizzando il bus I2S.

I2S

I2S è un bus seriale per collegare diversi dispositivi audio tra loro e trasferire, in formato digitale, dei flussi audio.

Le specifiche di tale bus sono state definite da Philips nel 1986. Il bus è formato da almeno 3 linee:

  • clock (indicato come SCK – serial clock o anche come BCLK – bit clock)
  • word select (WS, a volte definito anche LRCLK – left/right clock)
  • serial data (SD, o anche SDATA…)

pizero-i2s09

Possiamo sfruttare il bus I2S per far comunicare il nostro Pi Zero con un amplificatore che accetti in ingresso un segnale digitale su tale bus:

pizero-i2s10

Cosa serve

Ho scelto di utilizzare un amplificatore I2S basato sul chip MAX98357 di Maxim. Questo chip offre in uscita 3.2W e consente di collegare direttamente un piccolo altoparlante da 4 ohm. Sia la breakout board con il chip Maxim sia l’altoparlante sono di Adafruit:

Ho acquistato entrambi da un rivenditore italiano, melopero:

pizero-i2s11 pizero-i2s12

La breakout board viene venduta con una morsettiera a due contatti (per il collegamento con l’altoparlante) e con una serie di pin per alimentazione e bus I2S; entrambi da saldare alla scheda:

pizero-i2s13 pizero-i2s14

L’altoparlante viene venduto con un connettore JST che ho eliminato per poterlo collegare alla morsettiera:

pizero-i2s15 pizero-i2s16

Collegamenti e configurazione

Il collegamento tra Raspberry e la scheda con l’amplificatore MAX98357 richiede 5 fili:

  • 5V Raspberry -> Vin
  • GND Raspberry -> GND
  • PIN18 Raspberry -> BCLK
  • PIN19 Raspberry -> LRC
  • PIN21 Raspberry -> DIN

Ecco un paio di fotografie che illustrano i collegamenti indicati sopra

pizero-i2s17 pizero-i2s18

Procediamo ora alla configurazione software, partendo da una versione recente della distribuzione Raspbian.

Apriamo con un editor di testo (ad esempio nano) il file /boot/config.txt

sudo nano /boot/config.txt

commentiamo (aggiungendo un # ad inizio riga) la voce dtparam=audio=on e inseriamo due nuove voci – dtoverlay=hifiberry-dac dtoverlay=i2s-mmap:

pizero-i2s04

creiamo quindi il nuovo file /etc/asound.conf

sudo nano /etc/asound.conf

e inseriamo il contenuto mostrato nello screenshot seguente:

pizero-i2s05

Come ultimo passo, riavviamo il nostro Raspberry.

Test e mps-youtube

Proviamo a riprodurre un file audio di esempio con il comando:

speaker-test -c2 --test=wav -w /usr/share/sounds/alsa/Front_Center.wav

Se configurazione e collegamenti sono ok, dovremmo sentire una voce pronunciare “front – center” in loop; possiamo terminare la riproduzione con CTRL-C.

Per poter riprodurre dei files mp3, abbiamo bisogno di un player. Ho scelto di utilizzare mpv:

sudo apt-get install mpv

Risentiamo la frase storica di Neil Armstrong – One small step for men, one giant leap for mainking – direttamente dal sito della Nasa:

mpv http://www.nasa.gov/mp3/590331main_ringtone_smallStep.mp3

pizero-i2s19

Vi voglio mostrare infine l’utilizzo di un programma, mps-youtube, che consente di riprodurre tracce audio da Youtube tramite una interfaccia a riga di comando. Essendo scritto in Python3, dobbiamo installare questa versione e uno dei suoi package managerpip:

sudo apt-get install python3 python3-pip

Possiamo quindi installare mps-youtube e le sue dipendenze con:

sudo pip3 install youtube-dl mps-youtube

Eseguiamo il programma con mpsyt. Per prima cosa dobbiamo indicare il player che vogliamo utilizzare (mpv):

pizero-i2s07

Quindi possiamo cercare un video, digitando la stringa di ricerca preceduta da /. In alternativa se già conosciamo l’URL di un video, possiamo riprodurlo con il comando playurl <url>:

pizero-i2s20

Disturbi nel cambio traccia

E’ possibile che nel cambio traccia o comunque all’inizio di una nuova riproduzione sentiate dei disturbi. Il motivo è ben spiegato in questo thread del forum di Adafruit (thread al quale anch’io ho contribuito): tutto è causato dal cambio di frequenza del segnale di clock del bus I2S.

Il modo più semplice per risolvere è riprodurre continuamente un segnale a frequenza fissa (es. 48KHz) di completo silenzio:

aplay -t raw -r 48000 -c 2 -f S16_LE /dev/zero &

Lanciate il comando sopra prima di eseguire mpv o qualsiasi altro player e vedrete che i disturbi scompariranno!

Demo

Ecco un filmato che mostra mps-youtube all’opera (è in lingua inglese ma sono disponibili i sottotitoli in italiano):

ESP32 (15) – mDNS

$
0
0

Quando navighiamo su Internet, il servizio DNS (Domain Name System) si occupa di “tradurre” (risolvere) i nomi nei corrispettivi indirizzi IP.

Se ad esempio digitiamo www.google.com nel nostro browser, il nostro computer interroga il server DNS – normalmente quello messo a disposizione dal nostro provider – e ottiene da questo l’indirizzo IP del server che ospita il sito di Google:

mdns-05

Una rete locale casalinga spesso non dispone di un server DNS, quindi per poter comunicare con un dispositivo è necessario conoscere il suo indirizzo IP. Sapere quale indirizzo IP ha un dispositivo è semplice se tale indirizzo viene assegnato in maniera statica oppure se il dispositivo stesso ha un display dove lo visualizza:

mdns-01

Al contrario, se l’indirizzo IP viene assegnato in maniera dinamica (ad esempio dal server DHCP del nostro router) e il dispositivo non lo visualizza, può essere complicato recuperarlo. Ci può venire in aiuto il protocollo Multicast DNS (mDNS), vediamo come utilizzarlo con il chip esp32.

mDNS

mDNS è un protocollo di rete, definito nella RFC6762, che consente la risoluzione di nomi in indirizzi IP senza la necessità della presenza di un server DNS all’interno della rete.

mDNS è il protocollo utilizzato anche quando ci colleghiamo ad un Raspberry utilizzando l’indirizzo raspberrypi.local come mostrato ad esempio nel mio tutorial relativo al collegamento ad un Raspberry Pi Zero.

Il suo funzionamento è molto semplice. Quando un dispositivo ha la necessità di conoscere l’indirizzo IP di un altro dispositivo in rete invia un pacchetto UDP multicast con la richiesta; il pacchetto, essendo multicast, raggiunge tutti i dispositivi connessi. La risposta viene inviata sempre in multicast in modo che tutti la ricevano:

mdns-02

Di default, mDNS risolve nomi che terminano con il suffisso .local.

ESP32

Il framework esp-idf include un componente che implementa il protocollo mDNS; in questo tutorial vi mostrerò come utilizzarlo per rispondere a richieste mDNS. L’esempio completo è disponibile nel mio repository Github; vediamone gli elementi principali.

Per prima cosa, via menuconfig è possibile configurare due parametri:

mdns-03

MDNS_HOSTNAME è il nome al quale risponderà il nostro chip esp32 (ad esempio “esp32“) mentre MDNS_INSTANCE è una descrizione del dispositivo (es. “ESP32 Demo Board“).

L’utilizzo del server mDNS che risponde alle queries è molto semplice. Una volta effettuata la connessione alla rete wifi (il programma riprende un mio esempio precedente) è sufficiente creare una nuova istanza del server e configurarlo con i parametri sopra definiti:

// mDNS server instance
mdns_server_t* mDNS = NULL;
[...]
// create and configure the mDNS server
ESP_ERROR_CHECK(mdns_init(TCPIP_ADAPTER_IF_STA, &amp;mDNS));
ESP_ERROR_CHECK(mdns_set_hostname(mDNS, CONFIG_MDNS_HOSTNAME));
ESP_ERROR_CHECK(mdns_set_instance(mDNS, CONFIG_MDNS_INSTANCE));

Non dimentichiamoci di includere l’header del componente all’inizio del nostro programma:

#include "mdns.h"

Tutto qui! Una volta eseguito il programma, il server mDNS rimarrà in ascolto e, alla ricezione di richieste per il nome MDNS_HOSTNAME.local, risponderà con l’indirizzo IP della scheda esp32.

mDNS per Windows e Linux

Per poter effettuare un test, il nostro PC deve essere in grado di “parlare” il protocollo mDNS.

Se stiamo utilizzando Windows, possiamo installare l’applicativo Bonjour Print Services di Apple. Sebbene pensato per effettuare discovery e configurazioni di stampanti (da qui il nome), in realtà tale servizio supporta in generale il protocollo mDNS ed è quindi la soluzione migliore in ambiente Microsoft.

mdns-04

Per Linux esiste il daemon Avahi, spesso già installato nelle distribuzioni più diffuse.

Una volta predisposto il nostro PC, possiamo provare il funzionamento del programma con un semplice ping esp32.local (sostituendo il nome che avete configurato):

mdns-06

Conclusioni

L’utilizzo del componente mDNS consente di realizzare dispositivi, basati sul chip esp32, facilmente identificabili via rete, senza necessità di dotarli di un display o di un collegamento seriale. Inoltre, è sicuramente più semplice – soprattutto per utenti non esperti – utilizzare un nome ben noto (“esp32.local”) invece che dover andare alla ricerca di uno “strano numero” (l’indirizzo IP) che può addirittura cambiare nel tempo.

In questo tutorial abbiamo visto solo come rispondere a query mDNS: questo è sicuramente l’utilizzo più comune ma non è il solo: il componente del framework consente infatti di inviare richieste o di pubblicare diversi servizi… Espressif ha reso disponibile un programma di esempio che esplora alcune di queste features.

ESP32 (16) – IFTTT

$
0
0

Dopo aver pubblicato il tutorial relativo all’invio di SMS con il chip ESP32, ho ricevuto diversi commenti tramite il sito web da parte di utenti che chiedevano un modo di inviare notifiche gratuite. Oggi vi mosterò una possibile soluzione; sarà anche l’occasione per parlarvi di IFTTT e dello sviluppo di componenti custom per il framework esp-idf.

IFTTT

IFTTT (IF This Then That – Se questo allora quello) è un servizio web che consente di realizzare applets collegando in modo semplice dei servizi secondo la logica di se accade questo, allora esegui quello.

ifttt-12

Per capire meglio di cosa si tratta, citiamo alcune applets già pronte:

  • inviami una mail se per domani è prevista pioggia
  • condividi automaticamente su Facebook le foto che posto su Instagram
  • attiva la modalità silenziosa del telefono quando arrivo al lavoro

Il vantaggio di IFTTT è proprio la grande disponibilità di servizi già pronti: da quelli che interrogano siti di previsioni meteo, a quelli che si interfacciano con i social networks più famosi, fino a servizi per dialogare con dispositivi hardware come braccialetti FitBit o i termostati Nest.

Oltre al sito web, IFTTT è disponibile via app Android e iOS: per questo tutorial andremo infatti ad utilizzare una funzionalità delle app, le notifiche.

Maker Webhooks

Tra i tanti servizi che IFTTT mette a disposizione, uno è particolarmente comodo per integrazioni con dispositivi IoT: il servizio Maker Webhooks. Come indica anche il suo nome, tale servizio consente di dialogare con le applet via web. Se aggiunto come This (trigger, ovvero fonte dell’evento che attiva l’applet), il servizio consente di inviare alla applet un evento tramite chiamata web (GET o POST), mentre se aggiunto come That (action, ovvero azione che compie l’applet), il servizio consente alla applet di effettuare una chiamata web:

ifttt-13

Il servizio Maker Webhooks sarà il primo “mattone” della nostra applet: grazie ad esso potremo attivare l’applet con una chiamata web effettuata dal chip ESP32, passando anche dei parametri opzionali.

Notifications

Il secondo servizio che ci servirà si chiama Notifications: tramite tale servizio la applet potrà inviare notifiche al nostro cellulare. Unico requisito è che sul cellulare sia installata l’app di IFTTT e sia stato effettuato il login con lo stesso account IFTTT che crea l’applet.

Applet

Vediamo come costruire l’applet. Dopo aver effettuato la registrazione al sito, clicchiamo su New Applet.

Clicchiamo su this:

ifttt-01

Inseriamo qualche lettera del nome “Maker Webhooks” per cercare il servizio, quindi clicchiamo sulla sua icona:

ifttt-02

Selezioniamo l’unico trigger che il servizio mette a disposizione:

ifttt-03

Inseriamo il nome dell’evento che attiverà l’applet. Possiamo scegliere un nome a piacere che dovremo utilizzare in seguito per effettuare la chiamata web:

ifttt-04

Clicchiamo ora su that:

ifttt-05

Cerchiamo e selezioniamo il servizio Notifications:

ifttt-06

Clicchiamo ora sull’unica action che il servizio mette a disposizione:

ifttt-07

Personalizziamo il messaggio visualizzato come notifica. Possiamo inserire anche dei parametri che saranno ricevuti, insieme al nome dell’evento, tramite la chiamata web:

ifttt-08

Terminiamo la creazione dell’applet con Finish:

ifttt-09

Chiave personale

Per poter effettuare una chiamata al servizio Maker Webhooks, dobbiamo conoscere la nostra chiave personale. Colleghiamoci alla pagina del servizio e clicchiamo su Settings:

ifttt-10

Prendiamo nota della chiave che si trova dopo /use/ nella URL del servizio:

ifttt-11

esp32_ifttt_maker component

Per semplificare l’interazione con il servizio Maker Webhooks, ho preparato un componente pronto per l’utilizzo con il framework esp-idf. Possiamo paragonare i componentilibrerie, elementi di codice riutilizzabili nei propri programmi. Sarà sufficiente infatti copiare la cartella che contiene il componente nella sottocartella components del nostro progetto per averlo a disposizione:

ifttt-14

Il componente esp32_ifttt_maker è disponibile in un repository Github e il suo utilizzo è spiegato per esteso in una pagina dedicata del mio sito.

Demo

Ho preparato un programma di esempio che invia alcune notifiche tramite applets IFTTT; il sorgente si trova su Github.

Ecco un filmato che lo mostra all’opera (disponibili i sottotitoli in italiano):

ESP32 (17) – SNTP

$
0
0

In applicazioni embedded è spesso necessario avere a disposizione un orologio quanto più preciso possibile. Pensiamo ad esempio a dispositivi che si devono attivare in un preciso istante o a logger che devono memorizzare letture di una grandezza (ad esempio la temperatura esterna) a precisi intervalli.

La soluzione spesso adottata è l’utilizzo di un chip RTC (Real Time Clock). Il problema diviene quindi come configurare il chip RTC con l’orario corrente e come tenerlo allineato. Qualche tempo fa ho pubblicato un progetto (RTCSetup) per configurare un RTC collegato ad Arduino usando l’orario del proprio PC; oggi vedremo invece come utilizzare un servizio Internet con il chip esp32.

SNTP

SNTP (Simple Network Time Protocol) è un protocollo pensato per sincronizzare l’orario dei dispositivi connessi ad Internet. L’ultima versione del protocollo (SNTPv4) è definita nella RFC 4330. Il nome “simple” (semplice) è dovuto al fatto che SNTP è una versione ridotta e semplificata del protocollo NTP (Network Time Protocol); tale protocollo infatti – per consentire elevata accuratezza – ha un funzionamento molto complesso.

Il funzionamento base di SNTP è il seguente:

- il dispositivo client contatta un server usando il protocollo UDP sulla porta 123

- il server restituisce un valore di timestamp di 64bit, dove:

  • i primi 32 bit rappresentano il numero di secondi trascorsi dal giorno 01/01/1990
  • i rimanenti 32 bit rappresentano la frazione di secondi trascorsi

sntp-00

esp-idf

In un precedente articolo vi ho parlato della libreria lwip, scelta dal framework esp-idf per gestire le comunicazioni di rete. Tale libreria include una app (sntp.c) che implementa un client SNTP.

Vediamo come utilizzarla in un nostro programma (il sorgente completo del programma è disponibile nel mio repository Github).

Per prima cosa dobbiamo includere il relativo header file:

#include "apps/sntp/sntp.h"

Quindi configuriamo il client SNTP in modo che interroghi il server (pool mode) ogni tot secondi:

sntp_setoperatingmode(SNTP_OPMODE_POLL);

In alternativa possiamo configurare il client in modalità listen only (SNTP_OPMODE_LISTENONLY). In tale modalità il client SNTP rimarrà in attesa di ricevere aggiornamenti via broadcast, senza interrogare attivamente alcun server.

sntp_opts.h

Alcuni parametri di configurazione, tra i quali l’intervallo (in millisecondi) tra una interrogazione e la successiva sono configurabili modificando il file sntp_opts.h:

sntp-01

Dobbiamo ora indicare al client SNTP quale server utilizzare. Una delle scelte più comuni è affidarsi al cluster di server pool.ntp.org. In alternativa io, vivendo in Italia, normalmente utilizzo il server SNTP messo a disposizione dall’Istituto Nazionale di Ricerca Meterologica (ntp1.inrim.it) e sincronizzato con un orologio atomico.

Nell’esempio che ho preparato, il nome del server può essere configurato via menuconfig:

sntp_setservername(0, CONFIG_SNTP_SERVER);

Terminata la configurazione, possiamo attivare il client con:

sntp_init();

Il client sarà eseguito in maniera autonoma rispetto al programma principale.

Per ottenere l’orario attuale del sistema, utilizziamo il metodo time() che aggiorna un puntatore ad una variabile di tipo time_t:

time_t now;
time(&now);

La variabile time_t rappresenta normalmente il numero di secondi trascorsi dalla data chiamata Epoch (01/01/1970). Per ottenere i vari campi (anno, mese, giorno…) possiamo utilizzare il metodo localtime_r che aggiorna una struct tm:

struct tm timeinfo;
localtime_r(&now, &timeinfo);

I campi che compongono la struct tm sono dettagliati nel file time.h:

sntp-02

Possiamo quindi capire se la prima sincronizzazione ha avuto successo verificando ad esempio l’anno:

while(timeinfo.tm_year < (2016 - 1900)) {
  printf("Time not set, waiting...\n");
  vTaskDelay(5000 / portTICK_PERIOD_MS);
  time(&now);
  localtime_r(&now, &timeinfo);
}
Molte delle funzioni a disposizione per la gestione di data e ora hanno due versioni, una “normale” e una restartable (es localtime e localtime_r). In ambiente multithread è sempre consigliato utilizzare le funzioni con suffisso _r che garantiscono la possibilità di essere eseguite in simultanea (reentrancy) non utilizzando buffer in comune.

Avendo a disposizione la struct tm, possiamo utilizzare il metodo strftime() per formattare una stringa secondo un formato scelto. I placeholders disponibili sono molti e descritti nella manpage di tale metodo. Di seguito alcuni esempi:

char buffer[100];
// es. 08/05/2017 15:10:34
strftime(buffer, sizeof(buffer), "%d/%m/%Y %H:%M:%S", &timeinfo);
// es. Monday, 08 May 2017 
strftime(buffer, sizeof(buffer), "%A, %d %B %Y", &timeinfo);
// es. Today is day 128 of year 2017 
strftime(buffer, sizeof(buffer), "Today is day %j %Y", &timeinfo);

Fuso orario

I server SNTP restituiscono l’orario secondo il fuso orario UTC (Tempo coordinato universale). E’ possibile applicare un proprio fuso orario modificando la variabile di ambiente TZ, con il metodo:

int setenv(const char *name, const char *value, int overwrite);

name indica il nome della variabile mentre value il valore che tale variabile deve assumere. Se overwrite è > 0 il metodo setenv aggiorna la variabile se già esiste.

Una volta aggiornato il valore della variabile TZ, è possibile inizializzare la funzionalità di timezone con il comando tzset().

La variabile TZ può assumere due tipi di valori, a seconda che si voglia gestire o meno l’ora legale.

Nel caso più semplice (nessuna gestione dell’ora legale), il formato è std offset, dove std indica il nome del fuso orario (3 o più caratteri), mentre offset indica il tempo da aggiungere a quello locale per arrivare a UTC. Ad esempio l’Italia è nel fuso orario CET (Central European Time) che è di un’ora avanti rispetto a UTC (quindi offset -1); il comando da utilizzare è:

setenv("TZ", "CET-1", 1);

Se si vuole gestire anche l’ora legale, è possibile utilizzare il formato std offset dst [offset],start[/time],end[/time]std offset iniziali hanno lo stesso significato descritto in precedenza. dst e il secondo offset indicano il nome del fuso orario durante l’ora legale e il relativo offset (se questo secondo offset è omesso, il default è +1 rispetto all’orario normale). Infine startend indicano giorno e ora di inizio e fine dell’ora legale.

Sempre nel caso dell’Italia, durante l’ora legale si utilizza il fuso orario CEST (Central European Summer Time) che è attiva (direttiva 2008/84/CE del Parlamento Europeo) dalle 02:00 dell’ultima domenica di Marzo fino alle 03:00 dell’ultima domenica di Ottobre.

Il comando da utilizzare in questo caso è quindi:

setenv("TZ", "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", 1);

M3.5.0 indica infatti Marzo (3), ultima settimana (5), giorno domenica (0).

Su Internet si trovano siti che raccolgono i valori di TZ per i differenti fusi orari.

Gli step per formattare l’orario secondo un fuso orario sono quindi:

  • configurare la variabile TZ (setenv())
  • inizializzare la funzionalità di conversione orario (tzset())
  • aggiornare la struct tm (localtime_r())

Nel programma di esempio:

// print the actual time in Italy
printf("Actual time in Italy:\n");
localtime_r(&now, &timeinfo);
strftime(buffer, sizeof(buffer), "%d/%m/%Y %H:%M:%S", &timeinfo);
printf("%s\n", buffer);

sntp-03

ESP32 (18) – Access Point

$
0
0

In tutti gli esempi finora proposti, abbiamo utilizzato il chip esp32 in STAtion Mode, ovvero come client che si collega ad una rete wifi esistente. In un precedente articolo ho spiegato in dettaglio come interagiscono i vari componenti del framework esp-idf per stabilire la connessione e l’uso degli eventi per sincronizzare i vari tasks del nostro programma.

Oggi vedremo come configurare il chip esp32 per creare una propria rete wifi, in maniera simile ad un access point. Tale modalità prende infatti il nome di SoftAP.

Indirizzamento IP

Ogni dispositivo collegato ad una rete IP deve avere un proprio indirizzo univoco. E’ possibile assegnare l’indirizzo in maniera statica, o affidarsi ad un servizio (DHCP, Dynamic Host Configuration Protocol – Protocollo di configurazione dinamica dei dispositivi) che li assegna in maniera dinamica. Il servizio DHCP ha il vantaggio di non richiedere – a chi si collega ad una rete – di conoscere a priori la configurazione corretta da applicare (indirizzo IP, maschera di rete, gateway…); per questo normalmente il dispositivo che gestisce la rete offre anche un server DHCP:

ap-05

Anche il framework esp-idf include un server DHCP. Per poterlo rendere disponibile nella nuova rete wifi che andremo a creare dobbiamo:

  • fermare il servizio in caso sia già in esecuzione – tcpip_adapter_dhcps_stop()
  • configurare in maniera statica l’interfaccia di rete del chip esp32 – tcpip_adapter_set_ip_info()
  • avviare il servizio – tcpip_adapter_dhcps_start()
Il framework esp-idf utilizza due diverse interfacce di rete nel caso il chip esp32 sia in station mode o in SoftAP mode (TCPIP_ADAPTER_IF_STA e TCPIP_ADAPTER_IF_AP). Ponete quindi attenzione a quella che configurate in base alla modalità che volete utilizzare.

Per assegnare l’indirizzo 192.168.10.1/24 (/24 corrisponde ad una maschera di rete 255.255.255.0) ed eseguire il server DHCP dobbiamo quindi:

ESP_ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP));
tcpip_adapter_ip_info_t info;
memset(&info, 0, sizeof(info));
IP4_ADDR(&info.ip, 192, 168, 10, 1);
IP4_ADDR(&info.gw, 192, 168, 10, 1);
IP4_ADDR(&info.netmask, 255, 255, 255, 0);
ESP_ERROR_CHECK(tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info));
ESP_ERROR_CHECK(tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP));

SoftAP

Possiamo ora configurare la modalità SoftAP del chip esp32.

Per prima cosa inizializziamo l’event handler (vedremo in seguito quali eventi sono disponibili in questa modalità):

ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));

Quindi configuriamo lo stack wifi in modalità access point, con configurazione di default memorizzata nella memoria RAM:

wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&wifi_init_config));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));

I parametri di configurazione dell’access point sono disponibili nella struct wifi_ap_config_t:

ap-01

  • ssid è il nome (Service Set Identifier) della rete
  • password è la password per il collegamento alla rete (se la modalità di autenticazione la prevede, vedi sotto)
  • ssid_len è la lunghezza (in caratteri) della stringa ssid – può essere lasciata a 0 se tale stringa è null-terminated
  • channel è il canale di trasmissione
  • authmode indica la modalità di autenticazione, vedi sotto
  • ssid_hidden consente di nascondere la rete non trasmettendo il suo SSID in broadcast
  • max_connection è il numero di dispositivi collegati contemporaneamente (max 4)
  • beacon_intereval indica l’intervallo tra la trasmissione di un beacon frame e il successivo

Una volta configurati i parametri, vanno applicati con il metodo esp_wifi_set_config(). Nell’esempio pubblicato su Github, tutti i parametri sono configurabili tramite menuconfig:

ap-02

wifi_config_t ap_config = {
	.ap = {
		.ssid = CONFIG_AP_SSID,
		.password = CONFIG_AP_PASSWORD,
		.ssid_len = 0,
		.channel = CONFIG_AP_CHANNEL,
		.authmode = CONFIG_AP_AUTHMODE,
		.ssid_hidden = CONFIG_AP_SSID_HIDDEN,
		.max_connection = CONFIG_AP_MAX_CONNECTIONS,
		.beacon_interval = CONFIG_AP_BEACON_INTERVAL,			
	},
};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));

Infine avviamo lo stack wifi con:

ESP_ERROR_CHECK(esp_wifi_start());

Eventi

3 gli eventi principali che sono disponibili in modalità access point:

  • SYSTEM_EVENT_AP_START, quando lo stack ha completato il processo di avvio (iniziato con esp_wifi_start)
  • SYSTEM_EVENT_AP_STACONNECTED, quando un nuovo dispositivo si collega all’access point
  • SYSTEM_EVENT_AP_STADISCONNECTED, quando un dispositivo si disconnette

ap-03

Nell’esempio di questo post gli eventi sono notificati tramite diversi event bits:

case SYSTEM_EVENT_AP_START:
	printf("Access point started\n");
	break;
 
case SYSTEM_EVENT_AP_STACONNECTED:
	xEventGroupSetBits(event_group, STA_CONNECTED_BIT);
	break;
 
case SYSTEM_EVENT_AP_STADISCONNECTED:
	xEventGroupSetBits(event_group, STA_DISCONNECTED_BIT);
	break;

Authentication modes

La modalità SoftAP del chip esp32 supporta diverse modalità di autenticazione:

ap-04

open configura una rete wifi senza alcuna modalità di autenticazione, mentre le altre modalità implementano qualche forma di sicurezza, da quelle più “deboli” (WEP, ormai facilmente violabile) a quelle più sicure (WPA2). Personalmente utilizzo sempre la modalità WPA2_PSK che offre elevata sicurezza e autenticazione tramite password (PSK = PreShared Key, chiave di sicurezza condivisa).

Le diverse modalità di autenticazione impongono criteri di lunghezza della password. In particolare WPA e WPA2 richiedono una password da almeno 8 caratteri; se si specifica una password più corta, il metodo esp_wifi_set_config restituirà un errore.

Elenco dispositivi connessi

E’ possibile ottenere un elenco dei dispositivi connessi all’access point con il metodo esp_wifi_ap_get_sta_list() che aggiorna una struct di tipo wifi_sta_list_t:

wifi_sta_list_t wifi_sta_list;
memset(&wifi_sta_list, 0, sizeof(wifi_sta_list));
ESP_ERROR_CHECK(esp_wifi_ap_get_sta_list(&wifi_sta_list));

Per ottenere i parametri IP (ad esempio l’indirizzo) delle varie stations si può quindi utilizzare il metodo tcpip_adapter_get_sta_list():

tcpip_adapter_sta_list_t adapter_sta_list;
memset(&adapter_sta_list, 0, sizeof(adapter_sta_list));
ESP_ERROR_CHECK(tcpip_adapter_get_sta_list(&wifi_sta_list, &adapter_sta_list));
for(int i = 0; i < adapter_sta_list.num; i++) {
	tcpip_adapter_sta_info_t station = adapter_sta_list.sta[i];
	printf("%d - mac: %.2x:%.2x:%.2x:%.2x:%.2x:%.2x - IP: %s\n", i + 1,
		station.mac[0], station.mac[1], station.mac[2],
		station.mac[3], station.mac[4], station.mac[5],
		ip4addr_ntoa(&(station.ip)));
}

Demo

(sottotitoli in italiano disponibili)

Avventure robotiche

$
0
0

Su questo blog ho pubblicato diversi progetti elettronici e tutorials ma nessuno relativo ad applicazioni robotiche; per questo inizio con questo post una serie di tutorial per neofiti che spiegheranno come realizzare un piccolo robot e come programmarlo per compiere diverse funzionalità, quali evitare ostacoli, seguire una linea, ricevere comandi via radio…

robot-02

Per consentire veramente a tutti di poter seguire questi tutorial, ho scelto come base un kit veramente economico (circa 20 euro) venduto da Banggood:

robot-01

Il kit comprende:

  • uno chassis con base in acrilico, motori DC, ruote e caster (la ruota girevole frontale)
  • un Arduino Uno (clone) con il relativo cavetto USB
  • una scheda controllo motori basata sul chip L298N
  • uno shield per il collegamento di sensori
  • un sensore a ultrasuoni HC-SR04
  • un mini servocomando
  • cavi, viti…

Ecco alcune foto dell’unboxing:

air-01 air-02

air-03 air-04

air-05 air-06

air-07 air-08

ESP32 (19) – NVS

$
0
0

Nel tutorial di oggi vedremo come poter salvare informazioni in maniera permanente, in modo che si conservino anche se il chip esp32 viene resettato o gli viene tolta l’alimentazione.

NVS

NVS (Non Volatile Storage – memorizzazione non volatile) è una libreria del framework esp-idf che consente di memorizzare informazioni (rappresentate dalla coppia chiave/valore) all’interno della memoria flash, il cui contenuto non viene cancellato a fronte di un reset del chip o della mancanza di alimentazione.

Se ricordate, in un precedente articolo vi ho parlato di come viene organizzata la memoria flash esterna al chip esp32. Il compito principale di tale memoria è sicuramente quello di contenere il programma da eseguire. E’ però possibile dividere la flash in diverse partizioni: il framework propone alcune partition tables già pronte ma è anche possibile definirne di custom.

Se utilizziamo quella di default (“Single factory app, no OTA”), possiamo notare che essa contiene anche una partizione di tipo data e sottotipo nvs:

nvs-01

La dimensione di default di tale partizione è 24Kbyte.

Grazie alla libreria NVS, potremo memorizzare in tale partizione dati utili al nostro programma. I dati saranno organizzati in coppia chiave/valore, ovvero ad ogni dato (= valore) sarà associata una etichetta (= chiave), al massimo di 15 caratteri:

nvs-02

E’ possibile memorizzare diversi tipi di dati: da quelli numerici, a stringhe di testo fino a sequenze di bytes (blobbinary large object). Come vedremo la libreria mette a disposizione metodi specifici in base al tipo di dato.

Per poter utilizzare la libreria nei nostri programmi vanno inclusi i relativi headers:

#include "esp_partition.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "nvs.h"

Inizializzazione

La prima cosa da fare per poter utilizzare la partizione nvs è inizializzare la libreria, con il comando:

esp_err_t err = nvs_flash_init();

Il comando restituisce ESP_OK in caso di successo; al contrario in caso di errore viene restituito uno dei codici contenuti nel file nvs.h (vedi paragrafi successivi). In particolare, se la partizione è stata ridimensionata o modificata, è possibile che venga restituito il codice di errore ESP_ERR_NVS_NO_FREE_PAGES; tale errore può essere superato “formattando” la partizione.

Per prima cosa dobbiamo identificare la partizione nvs:

const esp_partition_t* nvs_partition = 
 esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, NULL);      
if(!nvs_partition) printf("FATAL ERROR: No NVS partition found\n");

Quindi possiamo formattarla con il comando esp_partition_erase_range():

err = (esp_partition_erase_range(nvs_partition, 0, nvs_partition->size));
if(err != ESP_OK) printf("FATAL ERROR: Unable to erase the partition\n");

Infine apriamo la partizione; possiamo farlo in modalità READONLY o READWRITE:

nvs_handle my_handle;
err = nvs_open("storage", NVS_READWRITE, &my_handle);
if (err != ESP_OK) printf("FATAL ERROR: Unable to open NVS\n");

Set – Get

Una volta aperta la partizione, è possibile memorizzare dati (set) o recuperare dati già presenti (get).

Esistono diversi metodi, in base al tipo di dato da gestire (i8 sta per integer a 8bit, u8 per integer senza segno a 8bit…):

  • nvs_set_i8(), nvs_set_u8(), nvs_set_i16(), nvs_set_u16()…
  • nvs_set_str()
  • nvs_set_blob()

Tutti i metodi set ricevono in ingresso l’handler della partizione aperta, la chiave e il dato da memorizzare (valore):

esp_err_t nvs_set_i8(nvs_handle handle, const char* key, int8_t value);

Unica eccezione il metodo nvs_set_blob() che richiede anche un parametro aggiuntivo: la lunghezza dei dati da memorizzare:

esp_err_t nvs_set_blob(nvs_handle handle, const char* key, const void* value, size_t length);

Una volta eseguito un metodo set, è necessario eseguire la commit della modifica con il metodo:

esp_err_t nvs_commit(nvs_handle handle);

Per recuperare i dati presenti nella flash esistono metodi analoghi:

  • nvs_get_i8(), nvs_get_u8(), nvs_get_i16(), nvs_get_u16()…
  • nvs_get_str()
  • nvs_get_blob()

I parametri in ingresso sono l’handler della partizione, la chiave e un puntatore alla variabile da aggiornare con il dato recuperato:

esp_err_t nvs_get_i8(nvs_handle handle, const char* key, int8_t* out_value);

Visto che non è possibile conoscere a priori la dimensione dei dati di tipo stringa o blob, possiamo utilizzare un “trucco”: eseguire una prima chiamata al metodo nvs_get_string() passando NULL come puntatore per avere la lunghezza della stringa, allocare in memoria una variabile di lunghezza adeguata e quindi chiamare nuovamente il metodo per ottenere il dato:

size_t string_size;
esp_err_t err = nvs_get_str(my_handle, parameter, NULL, &string_size);
char* value = malloc(string_size);
err = nvs_get_str(my_handle, parameter, value, &string_size);

Erase

La libreria mette a disposizione anche due metodi per cancellare il contenuto della partizione nvs:

esp_err_t nvs_erase_key(nvs_handle handle, const char* key);
esp_err_t nvs_erase_all(nvs_handle handle);

Il primo metodo è più selettivo e consente di cancellare una singola chiave mentre il secondo cancella l’intero contenuto della memoria.

Entrambi i metodi devono essere seguiti dal comando nvs_commit() come spiegato in precedenza.

Gestione errori

Tutti i metodi contenuti nella libreria nvs restituiscono un codice di errore (variabile esp_err_t).

I possibili errori sono elencati nel file nvs.h:

nvs-03

Se ad esempio stiamo utilizzando un metodo get passando una chiave non presente, otterremo l’errore ESP_ERR_NOT_FOUND. Possiamo gestire i diversi errori all’interno del nostro programma:

esp_err_t err = nvs_get_i32(my_handle, parameter, &value);
if(err == ESP_ERR_NVS_NOT_FOUND) printf("\nKey %s not found\n", parameter);

Demo

Ho preparato un programma che consente, tramite una semplice command line, di memorizzare e recuperare informazioni dalla partizione nvs. Il codice sorgente è disponibile su Github, ecco un filmato che lo mostra all’opera (sottotitoli italiani disponibili):


ESP32 (20) – Webserver

$
0
0

Uno dei progetti più popolari tra quelli inclusi nel mio tutorial sul chip enc28j60 è sicuramente WebRelay. Tale progetto consente di attivare una uscita di Arduino tramite una semplice pagina web, accessibile anche da smartphone. Oggi vedremo come eseguire WebRelay con il chip esp32; sarà l’occasione per spiegarvi come realizzare un server TCP, in particolare un web server.

Netconn API

Come ormai sapete, il framework esp-idf utilizza la libreria lwip per gestire le comunicazioni di rete. Questa libreria offre diversi livelli di astrazione: il programmatore può decidere di gestire nel proprio programma i pacchetti grezzi (raw) oppure di utilizzare componenti già pronti.

Per realizzare il nostro server TCP, utilizzeremo proprio uno di questi componenti già pronti: le Netconn API.

Il suo utilizzo per realizzare un server è molto semplice ed è schematizzato nei seguenti passi:

webrelay-01

Il metodo netconn_new() crea una nuova connessione, restituendo un puntatore a struct netconn che rappresenta la nuova connessione:

struct netconn *conn;
conn = netconn_new(NETCONN_TCP);

Il parametro passato al metodo indica il tipo di connessione… quelli più comuni sono NETCONN_TCP per una connessione con protocollo TCP e NETCONN_UDP per una con protocollo UDP.

Per utilizzare la connessione in modalità server dobbiamo quindi associarla (bind) ad una specifica porta… ad esempio un server web normalmente è in ascolto sulla porta 80 (443 se in HTTPS):

netconn_bind(conn, NULL, 80);

Il secondo parametro del metodo (NULL sopra) consente di associare la connessione anche ad uno specifico indirizzo IP e può essere utile nel caso il dispositivo abbia più interfacce di rete. Utilizzando NULL (o l’equivalente IP_ADDR_ANY) si chiede alla libreria di effettuare il bind su ogni interfaccia disponibile.

Infine possiamo mettere il programma in ascolto con il metodo listen:

netconn_listen(conn);

Gestiamo una nuova connessione

Utilizzando il metodo netconn_accept() il nostro programma può accettare una nuova connessione in ingresso:

struct netconn *newconn;
netconn_accept(conn, &newconn);

Il metodo restituisce un puntatore ad una nuova struct netconn che rappresenta la connessione stabilita con il client. Questo metodo è bloccante: il programma si ferma finché un client non effettua una richiesta di connessione.

Una volta stabilita la connessione, è possibile utilizzare i metodi netconn_recv() e netconn_write() per ricevere o inviare dati al client:

netconn_recv(struct netconn* aNetConn, struct netbuf** aNetBuf );
netconn_write(struct netconn* aNetConn, const void* aData, size_t aSize, u8_t aApiFlags);

Il metodo netconn_recv(), per ottimizzare l’utilizzo della memoria RAM, gestisce i dati tramite un buffer interno (modalità zero-copy). Per poter accedere ai dati ricevuti è quindi necessario:

  • dichiarare una variabile come puntatore a struct netbuf
  • passare l’indirizzo di tale puntatore come secondo parametro
  • utilizzare il metodo netbuf_data() per ottenere un puntatore ai dati all’interno del netbuffer e la loro lunghezza

webrelay-02

struct netbuf *inbuf;
char *buf;
u16_t buflen;
netconn_recv(conn, &inbuf);
netbuf_data(inbuf, (void**)&buf, &buflen);

Similmente il metodo netconn_write() accetta, come ultimo parametro, un flag per indicare se copiare o meno il contenuto del buffer prima di effettuare l’invio. Per risparmiare memoria è quindi possibile, se si ha la sicurezza che tale buffer non sia alterato da altri thread, indicare come flag NETCONN_NOCOPY:

netconn_write(conn, outbuff, sizeof(outbuff), NETCONN_NOCOPY);

Al termine del dialogo con il client, la connessione può essere chiusa e il buffer liberato:

netconn_close(conn);
netbuf_delete(inbuf);

HTTP server

Quanto abbiamo visto finora, può essere applicato ad un qualsiasi server TCP. Se vogliamo dialogare con un browser Internet dobbiamo “parlare” la stessa lingua, ovvero il protocollo HTTP.

Nel programma di esempio (disponibile su Github) ho quindi implementato una versione minimale di tale protocollo. Quando digitiamo nel browser un indirizzo Internet (es. www.google.com), il browser si collega al server di Google e invia una richiesta nella forma

GET <risorsa>
[...]

La richiesta può avere diversi campi ma la prima riga contiene sempre il nome della risorsa (pagina, immagine…) che si vuole ottenere. In particolare se si accede alla homepage del sito, la richiesta sarà sempicemente GET /.

Il sito pubblicato da esp32 per controllare il relay è composto da solo due pagine:

  • off.html, visualizzata quando il relay è spento
  • on.html, visualizzata quando il relay è acceso

Ogni pagina contiene una scritta (“Relay is ON|OFF“) e una immagine. L’immagine contiene un link all’altra pagina e cliccandola viene anche cambiato lo stato del relay:

webrelay-03

Il programma identifica la risorsa richiesta verificando il contenuto della richiesta con strstr():

char *first_line = strtok(buf, "\n");
if(strstr(first_line, "GET / ")) [...]
else if(strstr(first_line, "GET /on.html ")) [...]
else if(strstr(first_line, "GET /on.png ")) [...]

Un server HTTP risponde al browser indicando per prima cosa l’esito della richiesta. Se è ok, il codice inviato è 200:

HTTP/1.1 200 OK

quindi indica il media type della risorsa richiesta e infine invia la risorsa. In questo esempio, gli unici media type possibili sono:

  • text/html per le pagine HTML
  • image/png per le immagini

Contenuto statico

Un server web normalmente memorizza le risorse che compongono il sito pubblicato su un supporto esterno (memory card, disco fisso…). In casi molto semplici è possibile anche includere tutto il contenuto all’interno del programma.

In particolare nell’esempio proposto le pagine HTML e le stringhe di risposta del protocollo HTTP sono incluse come array statici:

const static char http_html_hdr[] = "HTTP/1.1 200 OK\nContent-type: text/html\n\n";
const static char http_png_hdr[] = "HTTP/1.1 200 OK\nContent-type: image/png\n\n";
const static char http_off_hml[] = "";

Per includere anche le immagini, ho sfruttato una funzionalità del framework (embedding binary data). E’ possibile indicare nel file component.mk i files da includere:

webrelay-04

All’interno del programma è possibile accedere al contenuto dei files embedded tramite appositi puntatori:

extern const uint8_t on_png_start[] asm("_binary_on_png_start");
extern const uint8_t on_png_end[]   asm("_binary_on_png_end");
extern const uint8_t off_png_start[] asm("_binary_off_png_start");
extern const uint8_t off_png_end[]   asm("_binary_off_png_end");

la sintassi è sempre _binary_filename_start|end, sostituendo il “.” con “_” nel nome del file. Avendo a disposizione i due puntatori (start ed end) è facile inviare l’intero contenuto del file con il metodo netconn_write() già spiegato:

netconn_write(conn, on_png_start, on_png_end - on_png_start, NETCONN_NOCOPY);

Demo

sottotitoli in italiano disponibili

STM32 e Arduino

$
0
0

STM32 è una famiglia di microcontrollori a 32bit prodotti da STMicroelectronics e basata su core ARM Cortex M.

La famiglia STM32 è divisa in diverse linee di microcontrollori (L0-1-4, F0-1-2…) a seconda delle caratteristiche e dell’uso a cui sono destinati:

stm32-16

Questi microcontrollori sono molto utilizzati in ambito industriale… per portare un paio di esempi sia l’orologio Pebble, sia i braccialetti Fitbit sono basati su MCU STM32.

Se vi interessate di quadricotteri o droni, sicuramente avrete sentito parlare di schede di controllo (flight control boards) F1, F3, F4… Tale nome indica proprio il microcontrollore STM32 sulle quali sono basate.

Grazie al lavoro della community stm32duino e al supporto di ST stessa, da inizio giugno i microcontrollori STM32 sono facilmente utilizzabili con l’IDE Arduino ed è inoltre possibile sfruttare gran parte delle librerie disponibili per Arduino.

Ho quindi deciso di acquistare una scheda minimale basata sul microcontrollore STM32F103C8T6 (queste schede sono spesso chiamate blue pill); vediamo come utilizzarla con Arduino.

Bootloader

Molte schede vengono vendute non programmate: la prima cosa da fare è quindi quella di programmare un bootloader, un piccolo programma che ci consentirà poi di caricare il nostro programma tramite la porta USB.

Per programmare il bootloader, ci servirà un adattatore USB -> seriale, da collegare alla scheda come segue:

  • RX -> A9
  • TX -> A10
  • VCC -> 5V
  • GND -> GND

stm32-18 stm32-19

Inoltre dobbiamo attivare la modalità programmazione, spostando il primo jumper (etichettato BOOTo) in posizione 1:

stm32-17

Dobbiamo ora scaricare il software per effettuare la programmazione e il file contenente il bootloader. Se utilizziamo Windows, possiamo scaricare il Flash Loader dal sito ufficiale di ST: una volta registrati (gratuitamente) ci sarà inviato il link per il download direttamente via mail.

Il bootloader è stato sviluppato da Roger Clark ed è disponibile sul suo repository Github. Per le schede STMF1 sono disponibili diversi binari, a seconda del pin al quale è collegato il led presente sulla scheda stessa. Nel mio caso tale pin è P13, quindi utilizzerò il file generic_boot20_pb13.bin.

Eseguiamo Demonstrator GUI e selezioniamo la porta seriale alla quale è collegato il nostro adattatore:

stm32-03

Se i collegamenti sono corretti, il software dovrebbe rilevare il microcontrollore:

stm32-04

Possiamo ora indicare il modello di microcontrollore che è presente sulla nostra scheda:

stm32-05

quindi caricare il file contenente il bootloader e procedere con la programmazione. Per sicurezza possiamo richiedere una global erase:

stm32-06

il programma procederà alla programmazione del chip e al termine ci darà un messaggio di conferma:

stm32-08

Se riportiamo il jumper BOOT0 in posizione iniziale, vedremo il led lampeggiare: questo indica che il bootloader è in esecuzione e che non c’è alcun programma memorizzato… possiamo ora passare alla configurazione dell’IDE.

Arduino IDE

Apriamo il nostro IDE e scegliamo File – Preferences. Inseriamo come Additional Boards Manager URLs la seguente:

https://github.com/stm32duino/BoardManagerFiles/raw/master/STM32/package_stm_index.json

stm32-00

Apriamo ora il Boards Manager:

stm32-01

Cerchiamo STM32F1 e installiamo il relativo Cores package:

stm32-02

Siamo quasi pronti per compilare e caricare il nostro primo programma…

Drivers

Colleghiamo la scheda di sviluppo al nostro PC via USB e verifichiamo come viene rilevata.

E’ possibile che Windows non sia in grado di riconoscerla e la visualizzi come Maple 003, in tal caso è necessario installare i relativi drivers:

stm32-09

Scarichiamo dal repository Github di Clark i seguenti files:

  • install_STM_COM_drivers.bat
  • install_drivers.bat
  • wdi-simple.exe

Eseguiamo i due .bat:

stm32-10

ora la scheda dovrebbe essere riconosciuta correttamente:

stm32-11

E’ possibile però che non venga riconosciuta anche la porta seriale associata alla scheda. Per “forzare” Windows a identificarla, possiamo programmare sulla scheda un semplice sketch che faccia uso dell’oggetto Serial. Apriamo ad esempio lo sketch blink e modifichiamolo come segue:

stm32-12

Programmiamo lo sketch selezionando BluePill F103C8 come boardSTM32duino bootloader come upload method.

Una volta programmato ed eseguito lo sketch, Windows dovrebbe rilevare anche la nuova porta COM:

stm32-14

Abbiamo terminato, ora possiamo utilizzare la scheda dall’IDE Arduino:

stm32-15

RFID e Arduino (1)

$
0
0

In questo tutorial, diviso in due parti, vedremo come utilizzare dei tag RFID (Radio-frequency Identification) con Arduino.

Nella prima parte collegheremo il lettore ad Arduino e scriveremo un semplice sketch per visualizzare l’ID del tag, mentre nella seconda parte realizzeremo un completo controllo accessi tramite tag RFID.

PN532

Ho scelto di utilizzare come lettore RFID una scheda basata sul chip PN532 di NXP. Questo chip è molto versatile: può funzionare sia come lettore/scrittore di tag, sia agire lui stesso come un tag RFID; inoltre supporta i bus di comunicazione I2C e SPI.

Adafruit ha realizzato una breakout board basata sul chip PN532 e delle librerie per Arduino che andremo ad utilizzare. In alternativa si trova su diversi store online questa scheda, che anch’io utilizzerò per questo tutorial:

rfid-03 rfid-04

Collegamenti

Come detto, il chip PN532 supporta sia I2C che SPI. Per semplicità utilizzerò il primo, collegando i pin SDA e SCL della scheda ai rispettivi pin di Arduino. Dobbiamo collegare anche il pin IRQ ad un pin digitale di Arduino (ho scelto il pin 2); attraverso questo collegamento il chip PN532 “avvisa” Arduino dell’avvenuta lettura di un tag:

rfid-06 rfid-07

Per attivare la modalità I2C, è necessario configurare i dip switch presenti sulla scheda come indicato in serigrafia:

rfid-05

Infine alimentiamo la scheda collegando i pin VCC e GND ai pin 5V e GND di Arduino.

Per completare il progetto, ho utilizzato la versione beta di un nuovo LCD shield di Lemontech. Questo shield ha la particolarità di gestire il display LCD tramite un expander I2C; inoltre tutti i pulsanti sono collegati al solo pin analogico A0. In questo modo praticamente tutti i pin di Arduino sono a disposizione per collegare altre periferiche. Il display ha indirizzo di default 0x27 (ma può essere cambiato) mentre il chip PN532 ha indirizzo 0x24, quindi non ci sono conflitti.

rfid-10 rfid-11jpg

Per consentire il collegamento dei due dispositivi al bus I2C di Arduino ho dovuto aggiungere due resistenze di pull-up da 10Kohm secondo lo schema:

rfid-13

Per semplicità le ho collocate su una mini breadboard:

rfid-08

Per indicare con un segnale acustico l’avvenuta lettura del tag, ho collegato un mini altoparlante al pin 8 di Arduino e a GND:

rfid-09

Librerie

Per poter compilare lo sketch di questo tutorial dovete installare due librerie nel vostro IDE:

  • Adafruit PN532 di Adafruit
  • hd44780 di Bill Perry

Entrambe sono disponibili nel Library Manager:

rfid-01

Sketch

Lo sketch completo è disponibile nel mio repository Github.

Per utilizzare l’LCD è necessario definire la sua dimensione (righe x colonne), l’indirizzo I2C e i pin a cui è collegato. E’ quindi possibile inizializzarlo all’interno del setup():

#define LCD_COLS      16
#define LCD_ROWS      2
hd44780_I2Cexp lcd(0x27, I2Cexp_PCF8574, 0,1,2,4,5,6,7,3,HIGH);
[...]
if(lcd.begin(LCD_COLS, LCD_ROWS) != 0) {
  Serial.println("- Unable to initialize LCD!");
  while(1);
}

Egualmente per il chip PN532 è necessario indicare il pin a cui è collegato il segnale IRQ (il RESET è opzionale) e procedere all’inizializzazione. Tramite il metodo getFirmwareVersion è possibile verificare il corretto funzionamento del chip e ottenerne la versione:

#define PN532_IRQ     2
#define PN532_RESET   3
Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);
[...]
nfc.begin();
uint32_t versiondata = nfc.getFirmwareVersion();
if(!versiondata) {
  Serial.println("- Unable to find a PN532 board");
  while(1);
}
Serial.print("- found chip PN5"); 
Serial.println((versiondata>>24) & 0xFF, HEX);

Infine dobbiamo chiamare il metodo SAMConfig() per configurare il chip in normal mode e utilizzare il pin di IRQ:

rfid-02

La lettura di un tag è molto semplice. Il metodo readPassiveTargetID restituisce true se un tag è vicino al lettore:

success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength);
if (success) {

In questo caso possiamo emettere un suono e visualizzare l’ID letto sul display:

tone(SPEAKER_PIN, TONE_FREQ, TONE_TIME);
lcd.clear();
lcd.print("Found RFID tag!");
lcd.setCursor(1,2);
lcd.print("ID: 0x");
for(int i = 0; i < uidLength; i++) {
  if(uid[i] <= 0xF) lcd.print("0");
  lcd.print(uid[i] & 0xFF, HEX);    
}

Demo

(sottotitoli in italiano disponibili)

ESP32 (22) – SPIFFS

$
0
0

In alcuni tutorial precedenti vi ho mostrato come includere elementi “esterni” al programma (immagini, certificati…) grazie alla funzionalità embedding binary data del framework. Oggi vi mostrerò invece come poter utilizzare una porzione della memoria flash per memorizzare dati, in maniera simile ad un disco fisso o una memory card.

Partizioni e file system

Avete già imparato dal mio quarto tutorial sul chip esp32 che la memoria flash collegata al chip viene suddivisa in partizioni, per poter ospitare diversi elementi e che è possibile – tramite menuconfig – configurare le diverse partizioni utilizzando dei template standard (es. “single factory app, no OTA”) o in maniera completamente custom.

L’elenco delle partizioni, la loro posizione all’interno della memoria flash e la loro dimensione sono definiti nella partition table, anch’essa memorizzata nella flash all’indirizzo 0x8000.

Possiamo definire un layout personalizzato della memoria flash creando un file csv con questo formato:

spiffs-001

Ogni riga corrisponde ad una partizione; per ogni partizione va indicato il tipo, il sottotipo, l’offset (ovvero l’indirizzo di partenza della partizione) e la dimensione. Il file csv sopra mostrato corrisponde a questo layout:

spiffs-002

Per verificare che le partizioni siano contigue, è sufficiente sommare all’offset la dimensione della partizione per trovare l’offset della successiva… ad esempio 0x9000 + 0x6000 = 0xf000.

Una partizione è semplicemente un’area di memoria: per poter memorizzarvi dei dati è necessario “organizzarli”; ad esempio abbiamo già visto l’utilizzo del componente NVS che si occupa di gestire dati nella forma “chiave-valore” nella partizione ad esso dedicato. I diversi supporti di memorizzazione (chiavette USB, dischi fissi…) organizzano i dati grazie ad un filesystem. Negli anni sono stati sviluppati moltissimi filesystem in base alle tipologie di supporti e ad esigenze specifiche… se avete un PC Windows quasi sicuramente il vostro disco fisso utilizzerà il filesystem NTFS, mentre le memory card normalmente utilizzano il filesystem FAT. Viste le caratteristiche peculiari delle memorie flash, Peter Andersson ha sviluppato un filesystem ottimizzato per tali supporti di memorizzazione, SPIFFS.

Componente

Per poter utilizzare il filesystem SPIFFS in un nostro programma, è necessario aggiungere alla cartella del progetto il componente sviluppato da Boris Lovosevic (loboris) che fa da wrapper tra la libreria SPIFFS di Peter Andersson (pellepl) e il Virtual File System (VFS) del framework esp-idf.

Scarichiamo quindi lo zip contenente il repository Github di loboris:

spiffs-01

Copiamo la cartella spiffs dallo zip all’interno della cartella components del nostro progetto:

spiffs-02

All’interno dello zip è anche presente lo strumento (mkspiffs) per poter “trasformare” una cartella del proprio PC in un file che rappresenta una partizione SPIFFS, pronto per essere caricato nella flash. Possiamo copiare la cartella mkspiffs ad esempio nella nostra home:

spiffs-04

Il componente spiffs richiede alcune variabili di configurazioni, che definiscono la geometria della partizione, ovvero come è “costruita”.

I parametri vanno aggiunti al file Kconfig.projbuild del progetto (qui vi ho spiegato la struttura di tale file) e sono i seguenti:

  • SPIFFS_BASE_ADDR, indirizzo della memoria flash dal quale inizia la partizione SPIFFS
  • SPIFFS_SIZE, dimensione in bytes della partizione
  • SPIFFS_LOG_BLOCK_SIZE, dimensione in bytes di ogni blocco di memoria
  • SPIFFS_LOG_PAGE_SIZE, dimensione in bytes di ogni pagina di memoria

Se volete approfondire la struttura interna in blocchi e pagine della partizione SPIFFS, potete leggere questo documento.

Indirizzo base e dimensioni devono essere quelle che avete configurato nel file csv che definisce il layout della memoria flash, mentre valori “buoni” per dimensione blocco e pagina sono 8192 e 256.

Nel creare il file csv, abbiamo visto che è possibile specificare la dimensione indicando ad esempio 1M per 1MB (1 Mega Bytes). Tale dimensione equivale a 1.048.576 bytes (1024 * 1024 bytes) e non come si potrebbe pensare a 1.000.000 bytes.

Prepariamo e carichiamo l’immagine

Se è necessario caricare nella partizione SPIFFS del contenuto prima che venga eseguito il programma (ad esempio immagini, files audio… che saranno poi utilizzati dal programma durante la sua esecuzione) è possibile utilizzare il tool mkspiffs per preparare un file immagine con il contenuto di una cartella del proprio computer.

Per prima cosa dobbiamo compilare mkspiffs:

cd mkspiffs/src
make

spiffs-05

Creiamo ora sul nostro PC una nuova cartella e inseriamo i files che dovranno essere memorizzati nella partizione SPIFFS. Possiamo anche creare sottocartelle… ad esempio ho creato la cartella spiffs_image nella mia home con questo contenuto:

spiffs-06

Lanciamo il seguente comando per creare il file .img che rappresenta una partizione SPIFFS con il contenuto della cartella sopra:

./mkspiffs.exe -c /home/luca/spiffs_image/ -b 8192 -p 256 
 -s 1048576 /home/luca/spiffs_image.img

Il parametro -b rappresenta il valore di LOG_BLOCK_SIZE, -p il valore di LOG_PAGE_SIZE e -s la dimensione della partizione. Il comando mkspiffs visualizza il contenuto dell’immagine creata:

spiffs-07

Ora possiamo utilizzare esptool per memorizzare il contenuto del file creato all’interno della flash:

python $IDF_PATH/components/esptool_py/esptool/esptool.py --chip esp32 
--port COM15 --baud 115200 write_flash --flash_size detect 0x180000 /home/luca/spiffs_image.img

Sostituite la porta COM con quella a cui è collegata la vostra scheda di sviluppo. Fate particolare attenzione all’indirizzo (sopra 0x180000) da cui esptool inizia a scrivere i dati: deve corrispondere a quello specificato nel file csv che definisce il layout delle diverse partizioni.

Utilizziamo spiffs in un programma

Per prima cosa includiamo nel nostro programma gli headers del wrapper SPIFFS e del componente VFS:

// VFS and SPIFFS includes
#include "esp_vfs.h"
#include "spiffs_vfs.h"

All’inizio del programma, dobbiamo registrare il filesystem SPIFFS:

vfs_spiffs_register();

Questo metodo indica al componente VFS le caratteristiche proprie della partizione SPIFFS e quali funzioni chiamare per le diverse operazioni sul filesystem. Il metodo esegue anche il mount della partizione (formattandola se necessario) in modo che sia visibile a partire dal percorso /spiffs.

Possiamo verificare che la partizione sia stata montata con:

if(spiffs_is_mounted) {...}

A questo punto, grazie al componente VFS, è possibile operare sui files e cartelle contenute nella partizione usando le funzioni standard C. Ad esempio possiamo aprire in lettura il file “readme.txt” presente nella root della partizione con:

FILE *file;
file = fopen("/spiffs/readme.txt", "r");

e leggerne il contenuto con:

int filechar;
while((filechar = fgetc(file)) != EOF)
[...]

Nel mio repository Github trovate un programma che consente di navigare all’interno di una partizione SPIFFS con comandi unix-like (cd, ls) e di visualizzare il contenuto di files (cat):

Conclusioni

La possibilità di utilizzare parte della memoria flash per memorizzare files ci consente di realizzare progetti che richiedano elementi esterni (pagine html, immagini, suoni…) senza bisogno di supporti di memorizzazione esterni al chip esp32.

ESP32, Wemos o non Wemos

$
0
0

Wemos è un produttore cinese di schede IoT, molto noto per la famiglia di prodotti D1 Mini, che comprende schede basate sul chip ESP-8266EX e shield di espansione.

Da qualche settimana Wemos ha lanciato una scheda basata sul nuovo chip ESP32, chiamata LOLIN32. Questa scheda ospita un modulo ESP-WROOM-32 e può essere alimentata tramite una batteria LiPo da una cella (1S). La scheda è in grado di ricaricare tale batteria grazie ad un circuito basato sul chip TP4054.

Ho acquistato un esemplare di LOLIN32 dallo store ufficiale di Wemos su Aliexpress. La scheda viene spedita in un sacchetto antistatico con etichetta personalizzata:

lolin-01

insieme alla scheda sono forniti anche i pin:

lolin-02 lolin-03

Curiosamente ho visto che su Aliexpress altri store vendono schede chiamate “lolin”. Ne ho acquistata una:

lolin-04 lolin-05

Sebbene questa scheda presenti il logo “wemos”, in realtà pare un clone della scheda D-duino-32 di Travis Lin: a differenza della LOLIN32 infatti questa non presenta il connettore per la batteria LiPo e il circuito di carica mentre aggiunge un display OLED da 0.96 pollici.

Ecco un confronto visivo tra le due:

lolin-06 lolin-07

Il vantaggio di acquistare una scheda Wemos originale è la disponibilità dello schema elettrico, di tutorial e di un forum di supporto… tutte cose che mancano per l’esemplare clone (come è collegato il display OLED?). Il mio consiglio quindi, se volete acquistare una scheda Wemos, è quello di passare sempre dallo store ufficiale.

Se invece vi può interessare la scheda con il display OLED, perché non acquistarla direttamente dalla pagina tindie dell’autore, così da “finanziare” il suo sviluppo?

ESP32 (23) – I2C basic

$
0
0

Nel tutorial di oggi impareremo ad interfacciare il chip esp32 a dispositivi esterni (sensori, display…) utilizzando un bus molto diffuso: il bus I2C.

I2C

I2C (si pronuncia i-quadro-c) è un bus di comunicazione seriale - inventato da Philips nel 1982 – che consente a due o più dispositivi di comunicare tra loro. I dispositivi connessi al bus si dividono in master (sono i dispositivi che “gestiscono” il bus) e in slave. Normalmente un bus ha un solo master e più slave, ma sono possibili anche topologie più complesse. Ogni dispositivo slave connesso al bus deve avere un proprio indirizzo univoco.

Sono disponibili due velocità di trasmissione: standard (100Kbit/s) e fast (400Kbit/s).

Il bus I2C richiede solo due linee di connessione tra i dispositivi:

  • SDA, Serial DAta – dove transitano i dati
  • SCLSerial CLock – dove il master genera il segnale di clock

Le due linee devono essere collegate ad una tensione di riferimento (Vdd) tramite resistenze di pull-up:

i2c-01

Per approfondire il funzionamento del bus I2C, vi consiglio l’ottimo sito www.i2c-bus.org.

esp32

Il chip esp32 offre due controller I2C, entrambi in grado di agire sia come master che come slave e di comunicare con velocità standard e fast.

I controller I2C sono collegati internamente alla matrice IO_MUX quindi, come vi ho spiegato in un precedente articolo, è possibile assegnare loro via software i diversi pin del chip (con alcune eccezioni).

Il framework esp-idf include un driver che consente di gestire tali controller ad alto livello, senza preoccuparsi di come devono essere configurati i diversi registri. Per utilizzare tale driver all’interno del proprio programma, è sufficiente includere il suo header file:

#include "driver/i2c.h"

Per prima cosa dobbiamo procedere alla configurazione del controller (port) che vogliamo utilizzare. La configurazione avviene utilizzando il metodo i2c_param_config() a cui va passato il numero del controller da configurare e una struttura i2c_config_t che contiene i diversi parametri:

esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t* i2c_conf);

Le due possibili porte sono definite in un enum all’interno del file i2c.h:

i2c-02

Anche la struttura i2c_config_t è definita nel medesimo file header:

typedef struct{
  i2c_mode_t mode
  gpio_num_t sda_io_num;
  gpio_pullup_t sda_pullup_en;
  gpio_num_t scl_io_num;
  gpio_pullup_t scl_pullup_en;
  union {
    struct {
      uint32_t clk_speed;
    } master;
    struct {
      uint8_t addr_10bit_en;
      uint16_t slave_addr;
    } slave;
  };
} i2c_config_t;

Vediamo il significato dei diversi parametri:

  • mode è la modalità di funzionamento (può essere I2C_MODE_SLAVEI2C_MODE_MASTER)
  • sda_io_numscl_io_num specificano quali pin saranno utilizzati per i segnali di SDA e SCL
  • sda_pullup_enscl_pullup_en consentono di abilitare o disabilitare le resistenze di pullup interne (possono essere GPIO_PULLUP_DISABLEGPIO_PULLUP_ENABLE)
  • master.clk_speed indica la velocità in hertz del clock se si è scelta la modalità master (100000 se standard e 400000 se fast)
  • slave.slave_addr indica l’indirizzo del dispositivo se si è scelta la modalità slave
  • slave.addr_10bit_en indica se si vuole o meno utilizzare un indirizzo esteso a 10bit (se il parametro è 0 la modalità indirizzo esteso è disabilitata)

la scelta dei pin

Il chip esp32 ci consente, tramite la matrice IO_MUX, di assegnare ai due controller I2C “quasi” tutti i suoi pin. Il driver I2C è in grado di verificare se i pin specificati in fase di configurazione del controller siano utilizzabili o meno e di segnalarcelo con un errore.

Se ad esempio vogliamo configurare il primo controller I2C in modalità master con velocità standard e utilizzare i pin 18 e 19 senza resistenze di pullpup interne scriveremo questo codice:

i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = 18;
conf.scl_io_num = 19;
conf.sda_pullup_en = GPIO_PULLUP_DISABLE;
conf.scl_pullup_en = GPIO_PULLUP_DISABLE;
conf.master.clk_speed = 100000;
i2c_param_config(I2C_NUM_0, &conf);

Una volta configurato il controller, possiamo installare il driver con il metodo i2c_driver_install():

esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, 
 size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags)

Oltre al numero del controller e alla modalità, dobbiamo specificare le dimensioni del buffer di ricezione e trasmissione (solo se in modalità slave) ed eventuali flags da usare per allocare l’interrupt (normalmente tale parametro viene lasciato a 0).

Per il controller configurato sopra, il driver sarà installato nel seguente modo:

i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0)

Master

Vediamo ora come utilizzare il controller in modalità master, per inviare comandi e leggere dati da uno slave.

Per prima cosa, dobbiamo creare un command link, ovvero un oggetto “logico” che conterrà l’elenco delle azioni da compiere per interagire con il dispositivo slave. Utilizziamo quindi il metodo i2c_cmd_link_create() che restituisce un puntatore all’handler del command link:

i2c_cmd_handle_t cmd = i2c_cmd_link_create();

Abbiamo ora a disposizione diversi metodi per aggiungere al command link diverse azioni:

  • i2c_master_start e i2c_master_stop
  • i2c_master_write e i2c_master_write_byte
  • i2c_master_read e i2c_master_read_byte

Per capire il loro significato, dobbiamo analizzare la modalità con cui il dispositivo master comunica con gli slave. Per prima cosa, il master invia sul bus il segnale di START, seguito dall’indirizzo (7bit) del dispositivo slave e da un bit che indica l’operazione richiesta (0 per WRITE, 1 per READ). Dopo ogni byte inviato (incluso il byte che rappresenta indirizzo+operazione) il dispositivo slave risponde con un bit di ACK:

i2c-04

Lato codice si traduce in:

i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (slave_addr << 1) | I2C_MASTER_WRITE, true);

Il metodo i2c_master_start() aggiunge all’handler cmd l’invio del segnale di START, mentre i2c_master_write_byte() invia sul bus un byte. Il byte inviato è composto dai 7bit dell’indirizzo (slave_addr) spostati (shiftati) a sinistra di 1bit (<< 1) e dal bit 0 (= IC2_MASTER_WRITE). Se avessi voluto effettuare una operazione di READ, avrei potuto usare la costante I2C_MASTER_READ.

L’ultimo parametro impostato a true indica al master di attendere che lo slave invii il bit di ACK.

Se l’operazione è write, a questo punto il master può inviare n bytes allo slave. Al termine dei dati, invia il segnale di STOP:

i2c_master_write(cmd, data_array, data_size, true);
i2c_master_stop(cmd);

Ho utilizzato il comando i2c_master_write che consente di inviare un array (uint8_t*) di dati. Il parametro data_size rappresenta la dimensione di tale array. In alternativa avrei potuto chiamare più volte il metodo i2c_master_write_byte usato in precedenza.

Per “eseguire” il command link, si utilizza il metodo i2c_master_cmd_begin():

i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS);

a cui va passato come parametro il numero del controller I2C, l’handler al command link e il numero massimo di ticks di attesa (il metodo è infatti bloccante; nell’esempio il metodo attende al massimo 1 secondo).

Infine è possibile liberare le risorse del command link con il metodo i2c_cmd_link_delete(cmd).

L’operazione di read è leggermente più complessa. Per prima cosa va inviato allo slave il comando che indica quale valore vogliamo leggere. Nel prossimo articolo vedremo una applicazione reale, per ora ipotizziamo che lo slave sia un sensore di temperatura con indirizzo 0x40 e che il comando misura la temperatura corrisponda al byte 0xF3.

L’invio del comando avviene come spiegato sopra:

i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (0x40 << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, (0xF3, true);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);

Terminato l’invio del comando (ed eventualmente atteso il tempo necessario perché il sensore lo esegua) è possibile leggere il risultato dal sensore creando un nuovo command link, sempre con l’indirizzo del sensore (ma con modalità READ) e inserendo una o più azioni di read (in base a quanti bytes il sensore ci restituirà):

uint8_t first_byte, second_byte;
cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (0x40 << 1) | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, &first_byte, ACK_VAL);
i2c_master_read_byte(cmd, &second_byte, NACK_VAL);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);

La differenza principale è che dopo aver letto l’ultimo byte, il master genera il segnale di NACK. Con questo segnale viene comunicato allo slave che non sono attesi ulteriori bytes e che quindi deve interrompere la trasmissione.

Le costanti per i segnali di ACK e NACK sono così definite:

#define ACK_VAL    0x0
#define NACK_VAL   0x1

Demo

Al termine di questo articolo vi voglio presentare un classico esempio dell’utilizzo della modalità master del bus I2C: uno scanner. Compito del programma è quello di analizzare il bus alla ricerca di eventuali dispositivi slave e di visualizzarne l’indirizzo.

Sono disponibili i sottotitoli in italiano

Il suo funzionamento è molto semplice, a voi il compito di comprendere il listato del programma su Github. Nel prossimo articolo vedremo invece come interfacciarsi ad un sensore I2C e ottenerne i dati.

 

ESP32 (24) – I2C un esempio pratico con sensore HTU21D

$
0
0

Nel precedente tutorial vi ho mostrato l’utilizzo del driver I2C incluso nel framework esp-idf per far comunicare il chip esp32 con dispositivi I2C. Oggi vedremo un esempio pratico: l’utilizzo di un sensore temperatura/umidità.

Il sensore

Per questo tutorial ho scelto di utilizzare il sensore HTU21D di Te Connectivity. Questo sensore offre una buona accuratezza ed è disponibile già saldato su una breakout board ad un costo di pochi euro:

htu21d-00a htu21d-00b

htu21d-00c

Sparkfun è stato il primo produttore ad offrire una board basata su tale sensore… sebbene ora il prodotto sia retired, board simili si trovano in vendita su diversi siti (es. Banggood).

Per collegarlo alla scheda di sviluppo esp32 dobbiamo per prima cosa scegliere quali pin del chip esp32 utilizzare per i segnali SDA e SCL. Oltre a tali segnali, dobbiamo collegare VDD (3.3V) e GND:

htu21d-00d htu21d-00e

htu21d-00f htu21d-00g

Datasheet e comandi

Una volta realizzati i collegamenti fisici tra la scheda di sviluppo esp32 e il sensore, dobbiamo capire come scrivere un programma che interagisce con esso. La prima cosa da fare è sicuramente leggere il datasheet del sensore (qui il documento in PDF). Non spaventatevi se aprendo il documento vedete 21 pagine di specifiche tecniche, nei prossimi paragrafi vi spiegherò quali sono le informazioni fondamentali che ci servono!

A pagina 10 inizia il capitolo relativo al protocollo di comunicazione (COMMUNICATION PROTOCOL WITH HTU21D(F) SENSOR). Subito leggiamo che il sensore offre una interfaccia I2C slave con indirizzo 0x40; appena sotto è riportata anche la tabella con i comandi disponibili:

htu21d-001

Vi sono due diverse modalità per misurare temperatura e umidità:

  • hold master
  • no hold master

Nella prima modalità, il sensore blocca il segnale di clock (SCK) durante la misurazione: in questo modo il master può inviare il comando di lettura solo quando la misurazione è effettivamente terminata. Nella seconda modalità invece il master può effettuare altre operazioni sul bus (es. interrogare un altro sensore) durante la fase di misurazione.

In modalità no hold il master, dopo aver inviato il comando di “trigger maesurement”, deve attendere che il sensore termini la misurazione prima di leggere il valore. E’ possibile verificare se la misurazione è completata inviando al sensore un comando di read e attendendo l’ACK: se questo viene ricevuto, significa che il dato è disponibile.

Il sensore restituisce il dato in forma grezza (raw) come valore a 16bit (= 2 bytes). In aggiunta al dato, viene anche restituito un valore (1 byte) di checksum; tale valore consente al master di verificare che non vi siano stati errori di trasmissione.

Nel precedente articolo abbiamo già imparato come interrogare un dispositivo slave; per leggere la temperatura dal sensore HTU21D in modalità no hold le istruzioni da utilizzare sono quindi:

// constants
#define HTU21D_ADDR			0x40
#define TRIGGER_TEMP_MEASURE_NOHOLD  	0xF3
 
// send the command
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (HTU21D_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, TRIGGER_TEMP_MEASURE_NOHOLD, true);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(_port, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);
 
// wait for the sensor (50ms)
vTaskDelay(50 / portTICK_RATE_MS);
 
// receive the answer
uint8_t msb, lsb, crc;
cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (HTU21D_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, &msb, 0x00);
i2c_master_read_byte(cmd, &lsb, 0x00);
i2c_master_read_byte(cmd, &crc, 0x01);
i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(_port, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);

Il codice di esempio attende 50ms prima di leggere il dato: dal datasheet si legge infatti che il tempo massimo di misurazione è proprio 50ms per il valore di temperatura alla massima risoluzione:

htu21d-002

Per convertire il valore raw nel valore reale di temperatura (in °C) o di umidità (in %) è possibile utilizzare le formule presenti nel datasheet:

uint16_t raw_value = ((uint16_t) msb << 8) | (uint16_t) lsb;
float temperature = (raw_value * 175.72 / 65536.0) - 46.85;
float humidity = (raw_value * 125.0 / 65536.0) - 6.0;

Risoluzione

Il sensore offre 4 diverse combinazioni di risoluzione per temperatura e umidità:

  • umidità 12bit, temperatura 14bit
  • umidità 8bit, temperatura 12bit
  • umidità 10bit, temperatura 13bit
  • umidità 11bit, temperatura 11bit

E’ possibile cambiare la risoluzione modificando il valore di un registro di configurazione, con il comando write user register. In particolare i bit del registro da modificare sono il bit 0 e il bit 7:

htu21d-003

CRC

L’algoritmo per la verifica del valore di CRC è illustrato nel datasheet.

Il polinomio utilizzato è x^8 + x^5 + x^4 + 1 che in binario diventa:

htu21d-004

A tale polinomio vanno aggiunti degli zeri in base al numero di bit del CRC:

htu21d-005

Il valore ottenuto, in esadecimale, è 0x98800.

Al dato da verificare, viene aggiunto in coda il valore di CRC ricevuto come terzo byte dal sensore:

uint32_t row = (uint32_t)value << 8;
row |= crc;

Quindi si verifica il valore del bit che corrisponde alla posizione più a sinistra del polinomio divisore: se tale bit è uguale a 1, il dato da verificare viene XORato con il divisore. Infine il divisore è spostato a destra di una posizione:

for (int i = 0 ; i < 16 ; i++) {
 if (row & (uint32_t)1 << (23 - i)) row ^= divisor;  divisor >>= 1;
}

Se il valore residuo di row è uguale a zero, il CRC è validato.

Componente

Ho sviluppato un componente per il framework esp-idf che implementa quando spiegato in questo articolo. Il componente è disponibile su Github e la documentazione sul suo utilizzo è pubblicata in una pagina ad esso dedicata.

Ecco un filmato che mostra il suo funzionamento (sono disponibili i sottotitoli in italiano):


Una Certificate Authority con OpenSSL

$
0
0

La sicurezza di molti protocolli viene implementata utilizzando certificati SSL. Normalmente tali certificati sono emessi da certificate authorities pubbliche. Se si parla di siti web (protocollo HTTPS), il browser con cui navighiamo in Internet deve riconoscere l’identità della CA che ha emesso il certificato, altrimenti ci viene mostrato un messaggio di errore:

ca-002

Una CA emette un nuovo certificato firmandolo con il proprio… ci possono essere anche più livelli di firma, ad esempio il certificato di Google (blu) è firmato da una intermediate CA (verde), il cui certificato è firmato a sua volta da una root CA (rosso):

ca-001

Perché il certificato sia ritenuto valido, il browser o il sistema operativo deve avere i certificati delle CA nei suoi certificati trusted:

ca-003

Oggi vi mostro come realizzare una CA utilizzando un software opensource, OpenSSL. Tale CA sarà utile ogni volta che ci servirà un certificato SSL ad uso interno.

Dopo aver installato l’applicativo OpenSSL, creiamo una cartella dedicata alla nuova CA (nel mio esempio MyCA).

All’interno di tale cartella creiamo alcune cartelle e files vuoti:

ca-004

Apriamo il file serial e inseriamo il valore 1000 (sarà il numero seriale del primo certificato emesso).

Prendiamo ora dal mio repository Github il file openssl.cnf e copiamolo nella cartella MyCA. Questo file contiene l’intera configurazione della CA.

Dobbiamo per prima cosa modificare il parametro dir indicando il path della cartella principale della nostra CA:

ca-005

Generiamo la chiave privata per la nostra CA. Tutti i comandi sono da lanciare all’interno della cartella MyCA:

openssl genrsa -aes256 -out private/ca.key.pem 4096

Ci verrà chiesta una password; è molto importante memorizzarla perché dovremo inserirla ogni volta che vorremo utilizzare la CA.

Passiamo ora a generare il certificato della CA, in modalità self-signed:

openssl req -config openssl.cnf -key private/ca.key.pem -new -x509 -days 3650 
 -sha256 -extensions v3_ca -out certs/ca.cert.pem

OpenSSL ci chiederà alcune informazioni, la più importante è il Common Name, ovvero il nome che identifica la nostra CA:

ca-006

Vediamo ora come generare i certificati client o server.

La generazione di un certificato parte dalla creazione di una chiave privata, che dovrà essere memorizzata sul sistema che utilizzerà il certificato (ad esempio sul server web che esporrà il sito in HTTPS). Si passa poi a creare il CSR (certificate signing request), ovvero un file che, firmato dalla CA, porterà alla generazione del certificato finale.

Il file CSR può essere fornito direttamente dall’utente; in alternativa possiamo utilizzare OpenSSL:

1. genero la chiave per un nuovo certificato server:

openssl.exe genrsa -out server.key

2. genero il file CSR:

openssl.exe req -new -config openssl.cnf -key server.key -out server.csr

Anche in questo caso ci verranno chieste alcune informazioni, tra le quali il nome (common name) del server.

Generiamo infine il certificato firmando il file CSR:

openssl.exe ca -config openssl.cnf -extensions server_cert 
 -notext -in server.csr -out server.cer

Dopo aver confermato con due Y (yes), il certificato sarà generato:

ca-007

ESP32 (25) – Display oled con U8G2

$
0
0

Se avete letto il mio post ESP32, Wemos o non Wemos sapete che ho acquistato una scheda di sviluppo, clone della D-duino-32, con un modulo ESP-WROOM-32 e un display oled da 0.96″.

Questo display, disponibile anche standalone su diversi siti web (eccolo ad esempio su Banggood) ha le seguenti caratteristiche:

  • dimensioni: 0.96 pollici
  • risoluzione: 128×64 pixels
  • controller: SSD1306 con interfaccia I2C
  • alimentazione: 3.3V – 6V

u8g2-00a u8g2-00b

Il suo utilizzo con il chip esp32 è molto semplice grazie al lavoro di olikraus e Neil Kolban. Il primo è l’autore della libreria u8g2, mentre il secondo ha sviluppato le funzioni specifiche del chip esp32 per l’hardware abstraction layer (HAL) della libreria u8g2.

u8g2, installazione

u8g2 è una fantastica libreria per display lcd monocromatici: supporta tantissimi modelli di display e controller, è facilmente portabile su nuove piattaforme e offre diversi metodi per disegnare forme geometriche, visualizzare immagini o testo con diversi fonts.

Vediamo come utilizzarla in un nostro progetto. Per prima cosa scarichiamo l’archivio contenente il repository Github della libreria:

u8g2-01

Se non esiste, creiamo la cartella components all’interno della cartella principale del nostro progetto. Scompattiamo l’archivio in tale cartella, rinominando poi la sottocartella u8g2-master in u8g2:

u8g2-02

All’interno della cartella u8g2 creiamo il file component.mk con il seguente contenuto:

u8g2-03

Dobbiamo ora scaricare i files u8g2_esp32_hal.cu8g2_esp32_hal.h dal repository di nkolban:

u8g2-04

Possiamo copiare i due files nella cartella main del nostro progetto:

u8g2-05

u8g2, configurazione

Per utilizzare la libreria u8g2 nei nostri programmi, per prima cosa includiamo il file header:

#include "u8g2_esp32_hal.h"

La configurazione di HAL avviene tramite la struct u8g2_esp32_hal_t:

typedef struct {
  gpio_num_t clk;
  gpio_num_t mosi;
  gpio_num_t sda;
  gpio_num_t scl;
  gpio_num_t cs;
  gpio_num_t reset;
  gpio_num_t dc;
} u8g2_esp32_hal_t;

La libreria supporta sia display I2C che display SPI: per questo nella struct troviamo l’indicazione dei segnali di entrambi i bus.

Possiamo definire e inizializzare la struct con i valori di default con:

u8g2_esp32_hal_t u8g2_esp32_hal = U8G2_ESP32_HAL_DEFAULT;

Il display oled della scheda di sviluppo D-duino-32 ha interfaccia I2C ed è connesso ai pin 4 (SCL) e 5 (SDA) di esp32:

u8g2_esp32_hal.sda = 5;
u8g2_esp32_hal.scl = 4;

Una volta completata la definizione dei diversi parametri della struct, possiamo utilizzare il metodo u8g2_esp32_hal_init():

u8g2_esp32_hal_init(u8g2_esp32_hal);

Passiamo ora alla configurazione della libreria u8g2. Definiamo una variabile di tipo u8g2_t:

u8g2_t u8g2;

In base al display scelto, dobbiamo utilizzare la relativa funzione di setup. A tale funzione dobbiamo passare come parametri:

  • il puntatore alla variabile u8g2_t definita in precedenza
  • una costante che indica la rotazione del display
  • le due funzioni di HAL di invio dati sul bus e di delay

Le costanti disponibili per la rotazione del display sono:

u8g2-06

mentre le due funzioni implementate da Kolban sono:

  • u8g2_esp32_msg_i2c_cb
  • u8g2_esp32_msg_i2c_and_delay_cb

Per il nostro display utilizziamo la funzione di setup per controller ssd1306 “noname” con prefisso _f che indica full framebuffer:

u8g2_Setup_ssd1306_128x64_noname_f(
  &u8g2, U8G2_R0,
  u8g2_esp32_msg_i2c_cb,
  u8g2_esp32_msg_i2c_and_delay_cb);

framebuffer

full framebuffer indica che tutti i dati da visualizzare sul display sono conservati nella memoria RAM del microcontrollore. Questo rende più veloce la visualizzazione, a discapito di un maggiore consumo di RAM. La libreria u8g2 supporta anche modalità di page buffer per ridurre il consumo di RAM. Un confronto tra le modalità, con pregi e difetti di ognuna, è presente sulla wiki della libreria.

Infine – se il display è I2C – dobbiamo indicare il suo indirizzo alla libreria:

u8x8_SetI2CAddress(&u8g2.u8x8,0x78);

u8g2, utilizzo

Eseguiamo l’inizializzazione del display con:

u8g2_InitDisplay(&u8g2);

Il display è inizialmente in modalità power save, per “accenderlo” dobbiamo disabilitare questa modalità con:

u8g2_SetPowerSave(&u8g2, 0);

Ora possiamo utilizzare i diversi metodi che la libreria offre per visualizzare testo, immagini, forme geometriche. Stiamo utilizzando la modalità full framebuffer, quindi andremo prima a preparare il buffer in memoria e poi lo invieremo al display.

Prepariamo il buffer con il metodo ClearBuffer():

u8g2_ClearBuffer(&u8g2);

Utilizziamo i metodi SetFont()DrawStr() per scrivere del testo con il font scelto nel buffer:

u8g2_SetFont(&u8g2, u8g2_font_timR14_tf);
u8g2_DrawStr(&u8g2, 2,17,"Hello World!");

Infine visualizziamo il contenuto del buffer sul display con:

u8g2_SendBuffer(&u8g2);

u8g2-00c

Demo

Nel seguente video vi illustro brevemente come installare la libreria, come preparare una immagine per essere visualizzata sul display e infine l’esecuzione, sulla mia D-duino-32, del programma di esempio che trovate su Github.

Sono disponibili i sottotitoli in italiano:

ESP32 (26) – UART

$
0
0

UART (Universal Asynchronous Receiver-Transmitter) è una periferica hardware che consente una comunicazione seriale asincrona, con formato dati e velocità configurabili. La periferica UART normalmente funziona a livello logico: i segnali elettrici sono poi generati da un diverso circuito, secondo gli standard propri del bus di comunicazione scelto.

Ad esempio la classica “porta seriale” dei personal computer si basa sullo standard EIA RS-232, standard che definisce a livello fisico come i segnali sono generati sul mezzo di comunicazione. Esistono appositi chip (il più famoso è sicuramente il MAX232 di Maxim Integrated) per convertire i livelli logici di una periferica UART in segnali secondo lo standard EIA RS232:

uart-001

Il chip esp32 offre 3 controller UART. Questi controller sono collegati alla matrice GPIO; in tal modo è possibile assegnare loro uno i diversi pin digitali del chip:

uart-002

Il framework esp-idf contiene un driver (uart.c) per semplificare l’utilizzo dei controller; per utilizzarlo includiamo il relativo file header nel nostro programma:

#include "driver/uart.h"

I nomi dei 3 controller per il driver sono:

uart-003

In questo primo articolo vediamo l’utilizzo base di un controller; eventi ed interrupts saranno argomento di un prossimo articolo.

Iniziamo con il configurare il controller tramite la struct uart_config_t:

uart_config_t uart_config = {
  .baud_rate = 115200,
  .data_bits = UART_DATA_8_BITS,
  .parity = UART_PARITY_DISABLE,
  .stop_bits = UART_STOP_BITS_1,
  .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
  • baud_rate indica la velocità di trasmissione
  • data_bits, e stop_bits indicano il numero di bit per ogni “parola” e il numero di bit di stop
  • parity indica se trasmettere o meno il bit di parità
  • flow_ctrl indica la tipologia di controllo di flusso (hardwaresoftwaredisabilitata)

Vi sono due ulteriori parametri (rx_flow_ctrl_threshuse_ref_tick) utilizzabili per indicare la soglia per il segnale RTS in caso di controllo di flusso hardware e per utilizzare il segnale REF_TICK come clock per il controller UART.

Le costanti da utilizzare per data_bitsstop_bits… sono definite nel file uart.h.

Spesso i parametri da utilizzare per comunicare con un dispositivo in modalità seriale sono espressi in forma “condensata”, ad esempio se trovate indicato 9600,8N1 significa

  • velocità 9600 baud
  • “parola” di 8 bit
  • Nessuna parità
  • 1 stop bit

Configuriamo il controller con il metodo:

uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);

indicando il numero del controller scelto (uart_num) e la struct che contiene la configurazione preparata in precedenza (uart_config).

Terminiamo la configurazione indicando al controller quali pin utilizzare per i diversi segnali:

uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, 
  int rts_io_num, int cts_io_num);

Possiamo utilizzare la costante UART_PIN_NO_CHANGE se quel particolare segnale non è utilizzato o se vogliamo mantenere il pin di default.

Ad esempio per mappare il controller 0 sui pin 4 e 5 senza utilizzare i segnali RTS e CTS:

uart_set_pin(UART_NUM_0, 4, 5, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

Possiamo ora installare il driver con:

uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, 
  int queue_size, QueueHandle_t* uart_queue, int intr_alloc_flags);

Oltre al numero del controller, dobbiamo indicare la dimensione dei due buffer di ricezione e trasmissione. I parametri relativi alla queue li vedremo nel prossimo articolo.

I due buffer devono avere dimensione maggiore rispetto ai buffer hardware (128). Solo per il buffer di trasmissione è possibile indicare dimensione = 0; in tal caso il driver si comporterà in maniera bloccante, ovvero l’esecuzione del task sarà bloccata fino al termine della trasmissione.

Vediamo ora come inviare dati. Per meglio comprendere le differenze tra i comandi disponibili, è necessario comprendere come esistano due buffer: uno hardware, integrato nel controller UART e uno software, implementato nel driver:

uart-004

Il primo comando di invio – da usare quando viene disabilitato il buffer di trasmissione software – è uart_tx_chars():

int uart_tx_chars(uart_port_t uart_num, const char* buffer, uint32_t len);

Tale comando invia len bytes prendendoli da buffer. Non utilizzando un buffer software, è possibile che il comando non riesca a inviare tutti i bytes specificati per riempimento del buffer hardware; il metodo uart_tx_chars restituisce quindi il numero di bytes effettivamente inviati.

Per utilizzare il buffer software invece esiste il comando uart_write_bytes():

int uart_write_bytes(uart_port_t uart_num, const char* src, size_t size);

Questo comando copia size bytes dall’array src nel buffer del driver: sarà poi il driver in maniera autonoma a gestire la comunicazione con il controller e l’effettivo invio dei dati. Anche il comando uart_write_bytes() restituisce il numero di bytes effettivamente scritti nel tx buffer.

In ricezione, possiamo utilizzare il comando uart_read_bytes():

int uart_read_bytes(uart_port_t uart_num, uint8_t* buf, uint32_t length, 
  TickType_t ticks_to_wait);

Tale comando legge un massimo di length bytes dal buffer di ricezione e li memorizza nell’array buf. Il comando rimane in attesa di dati il numero di ticks specificati, quindi ritorna il numero di bytes effettivamente letti.

E’ possibile sapere il numero di bytes attualmente presenti nel buffer di ricezione con:

uart_get_buffered_data_len(uart_port_t uart_num, size_t* size);

UART e stdio

Il framework esp consente di utilizzare uno dei controller UART come periferica per le funzionalità di I/O standard. Gli stream standard Unix (stdin, stdout e stderr) sono infatti associati a operazioni di RX e TX da tale controller; è quindi possibile utilizzare i metodi standard C quali printf()scanf()… per scrivere e leggere da tale controller.

Tramite menuconfig è possibile indicare quale controller utilizzare, i parametri di tale controller o se disabilitare del tutto questa funzionalità:

uart-005

Demo

Nel seguente filmato (sottotitoli in italiano disponibili) mostro il funzionamento del controller UART del chip esp32 collegandolo ad un convertitore USB->seriale. Sia il convertitore che la scheda di sviluppo esp32 sono connessi al mio laptop: in questo modo posso inviare dei dati dal laptop alla porta UART1 del chip, leggerli e inviarli nuovamente al laptop tramite la porta UART0.

ESP32 (27) – GPS

$
0
0

Nell’articolo di oggi vedremo come interfacciare il chip esp32 ad un ricevitore GPS per conoscere posizione attuale, velocità e molti altri dati…

Ricevitore GPS

Sul mercato esistono moltissimi ricevitori GPS… grazie alla diffusione di navigatori, cellulari e multicotteri è ora possibile acquistarne uno per pochi euro. Per questo tutorial ho utilizzato un ricevitore GPS venduto da Banggood e basato sul chip u-blox NEO-M8:

gps-010 gps-011

Praticamente tutti i ricevitori GPS offrono almeno una interfaccia seriale, possiamo quindi collegarli al chip esp32 sfruttando una delle sue periferiche UART come spiegato in un precedente articolo. A volte non è semplice identificare i vari pin e può essere necessario – come nel mio caso – aprire il case plastico che racchiude il chip per leggere la serigrafia del PCB:

gps-001 gps-002

Dalle fotografie sopra si comprende che il mio ricevitore ha la seguente piedinatura:

  • GND (massa) -> cavo nero
  • VCC (alimentazione) -> cavo rosso
  • TXD (trasmissione) -> cavo verde
  • RXD (ricezione) -> cavo giallo

Ho scelto di utilizzare la periferica UART1 del chip esp32 con i pin 4 (TX) e 16 (RX). Ho quindi effettuato i collegamenti, ricordando che il pin TX del ricevitore GPS va collegato al pin RX del chip esp32 e viceversa:

gps-003

NMEA

I ricevitori GPS inviano dati secondo lo standard NMEA 0183 (normalmente chiamato semplicemente NMEA).

Lo standard prevede l’invio di stringhe (sentences = frasi) formate da un massimo di 80 caratteri e terminate da CRLF. Ogni stringa ha il seguente formato:

$PREFIX,data1,data2 ... dataN-1,datoN*CHECKSUM

Il prefisso (5 caratteri) indica il tipo di dispositivo (per i ricevitori GPS è GP) e il tipo di frase (i successivi 3 caratteri). Ogni frase termina con un checksum (XOR) che consente di verificare la correttezza della frase ricevuta.

Lo standard NMEA definisce molte frasi diverse e i singoli produttori possono aggiungere tipi di frasi proprietarie per comunicare ulteriori dati. Un elenco esaustivo di frasi NMEA è disponibile a questo indirizzo. Vediamo un esempio di frase GGA (Global Positioning System Fix Data):

$GPGGA,183619,3877.038,N,07702.110,W,1,08,0.9,545.4,M,46.9,M,,*47

Il ricevitore sta comunicando la posizione attuale (latitudine 38°77.038′ NORD e longitudine 77°02.110′ OVEST), ottenuta alle 18:36:19 grazie a 8 satelliti. La qualità della posizione è GPS Fix (1).

Per poter ottenere i dati dal ricevitore GPS dobbiamo quindi essere in grado di “comprendere” le frasi NMEA. Ho trovato una comoda libreria, minmea, sviluppata in C e in grado di effettuare il parsing delle frasi NMEA più importanti.

Per integrare la libreria in un progetto esp-idf è sufficiente copiare i due files sorgenti (minmea.c e minmea.h) in una cartella all’interno della cartella components e creare un file component.mk come segue:

gps-001

Il flag evidenziato è necessario perché il framework non contiene la funzione timegm() e quindi, come indicato anche nel README della libreria, va indicato al compilatore di sostituire la funzione con mktime().

Programma

Nel mio repository Github trovate il programma di esempio che ho sviluppato per questo tutorial. Vediamo gli elementi principali:

Per poter utilizzare la libreria minmea, includiamo il relativo file header:

#include "minmea.h"

Leggiamo una riga dalla periferica UART1 e passiamola alla libreria per eseguire il parsing:

char *line = read_line(UART_NUM_1);
switch (minmea_sentence_id(line, false)) {
  case MINMEA_SENTENCE_RMC:
  [...]
  case MINMEA_SENTENCE_GGA:
  [...]
}

Infine verifichiamo se i parametri ricevuti sono diversi da quelli già memorizzati e, in tal caso, visualizziamoli sullo schermo:

float new_latitude = minmea_tocoord(&frame.latitude);
if((new_latitude != NAN) && (abs(new_latitude - latitude) > 0.001)) {
  latitude = new_latitude;
  printf("New latitude: %f\n", latitude);
}

Ho aggiunto un valore di soglia (0.001) per evitare di stampare piccoli spostamenti, dovuti a disturbi o oscillazioni sui dati ricevuti.
Ecco il programma in esecuzione:

gps-010

DCC, decoder accessori per led

$
0
0

Dopo aver realizzato uno shield per interfacciare Arduino ad un bus DCC, vediamo oggi come realizzare un semplice decoder accessori per controllare dei led.

dccled-101 dccled-100

Decoder accessori

Lo standard DCC (in particolare il documento S-9.2.1 DCC Extended Packet Formats) definisce diversi tipi di decoders, ovvero di dispositivi che – collegati al bus DCC – eseguono operazioni in base ai comandi ricevuti.

Un decoder accessori è “intended for control of a number of simple functions such as switch machine control or turning on and off lights” (pensato per controllare alcune semplici funzioni come scambi o luci).

dcc2servo2relay

uno dei decoder DCC autocostruiti di Paco

Indirizzamento

Ogni decoder deve rispondere ad uno o più indirizzi. Il documento S-9.2.1 spiega nel dettaglio la struttura di un pacchetto DCC inviato ad un decoder accessori (basic accessory decoder packet format):

dcc-001

9 bit del pacchetto (AAAAAAAAA) identificano l’indirizzo del singolo decoder. I bit più significativi (MSB) dell’indirizzo sono quelli nel secondo bytes, mentre quelli meno significativi (LSB) sono quelli nel primo bytes. Per complicare un po’ le cose, i bit del secondo bytes sono in complemento a uno.

Vediamo un esempio pratico per capire meglio:

dcc-003a

I bit più significativi sono 011. Effettuandone il complemento uno diventano 100.

I bit meno significativi sono 0010011. Concatenando i 9 bit si ottiene 1000010011, il decoder avrà quindi indirizzo 267.

2 bit (PP) indicano l’indirizzo della singola porta del decoder. Per convenzione un decoder accessori ha quindi 4 porte, ognuna formata da una coppia di uscite.

Il bit O indica, per la porta indicata dai bit PP, l’uscita a cui il comando è indirizzato. Infine il bit C indica se l’uscita deve essere attivata (1) o disattivata (0).

dcc-002

Alcuni produttori preferiscono utilizzare un indirizzamento “piatto” (non distinguendo tra indirizzo decoder, numero porta e uscita). Una buona spiegazione di come funzionano le diverse modalità di indirizzamento (MADAPADA…) è presente sulla wiki di Rocrail.

Arduino

Per sviluppare lo sketch del nostro decoder accessori con Arduino, dobbiamo per prima cosa installare la libreria NmraDcc utilizzando il Library Manager:

dcc-acc-001

Includiamola quindi nello sketch, istanziando anche un oggetto di tipo NmraDcc:

#include "NmraDcc.h"
NmraDcc Dcc;

Nel setup() dobbiamo configurare e inizializzare la libreria. Per prima cosa utilizziamo il metodo pin() per indicare alla libreria il pin di Arduino a cui è collegato il segnale DCC (ExtIntPinNum), il relativo interrupt (ExtIntNum). Decidiamo inoltre se abilitare o meno la resistenza di pullup interna:

void pin(uint8_t ExtIntNum, uint8_t ExtIntPinNum, uint8_t EnablePullup);

L’elenco dei pin utilizzabili per ogni board Arduino è disponibile nella wiki, mentre si può utilizzare il metodo digitalPinToInterrupt() per ottenere il numero di interrupt associato ad un determinato pin.

Se state utilizzando lo shield che ho presentato in un precedente articolo con una scheda Arduino Uno, il pin da configurare è il numero 2 con resistenza di pullup attiva, quindi:

#define DCC_PIN 2
[...]
Dcc.pin(digitalPinToInterrupt(DCC_PIN), DCC_PIN, 1);

Passiamo ora alla configurazione della libreria, con il metodo:

void init(uint8_t ManufacturerId, uint8_t VersionId, uint8_t Flags, 
  uint8_t OpsModeAddressBaseCV);

I primi due valori indicano il codice produttore e la versione del decoder e possono essere ottenuti dalla centrale DCC leggendo le relative CV (configuration variables). Per i decoder autocostruiti, il ManufacturedId viene impostato a MAN_ID_DIY.

Il terzo valore consente di indicare, in OR, alcuni flags:

dcc-acc-002

Per un decoder accessori dobbiamo utilizzare il terzo flag (DCC_ACCESSORY_DECODER).

FLAG_MY_ADDRESS_ONLY

Il primo flag indica alla libreria di filtrare i messaggi DCC ricevuti, processando solo quelli inviati all’indirizzo del decoder. Normalmente preferisco non utilizzare questo flag, in modo che il mio sketch “veda” tutti i messaggi DCC e gestendo poi nel programma quali messaggi utilizzare e quali no.

Terminata la configurazione della libreria, perché questa gestisca i messaggi DCC in ingresso dobbiamo chiamare, all’interno del loop(), il metodo process() quanto più frequentemente possibile:

void loop() {
  Dcc.process();
}

Alla ricezione di un messaggio diretto ad un decoder accessori, la libreria può chiamare diversi metodi, in base a quanto definito all’interno dello sketch:

  • notifyDccAccState()
  • notifyDccAccTurnoutBoard()
  • notifyDccAccTurnoutOutput()
  • notifyDccSigState()

I primi 3 metodi sono invocati per messaggi di tipo basic accessory decoder packet, mentre l’ultimo per extended accessory decoder control packet.

Per il decoder che stiamo realizzando ho scelto il metodo notifyDccAccState():

void notifyDccAccState(uint16_t Addr, uint16_t BoardAddr, 
  uint8_t OutputAddr, uint8_t State)

Questo metodo mette a disposizione alcuni parametri. Vediamo il loro significato tenendo presente quanto spiegato ad inizio articolo sull’indirizzamento:

  • BoardAddr è l’indirizzo (9 bit) del decoder
  • OutputAddr rappresenta i 3 bit PP e O, ovvero l’indirizzo della porta e dell’uscita
  • State è il comando (1 = attivazione, 0 = disattivazione)
  • Addr è indirizzo diretto della porta (modalità PADA)

Possiamo separare l’indirizzo della porta (2 bit) da quello della relativa uscita (1 bit) con:

int portAddress = (OutputAddr >> 1) + 1;
int outAddress = OutputAddr & 0x01;

Ho aggiunto 1 all’indirizzo della porta per identificare le porte del decoder con indirizzi da 1 a 4 invece che da 0 a 3.

Il decoder di questo articolo avrà definito come costante nello sketch sia l’indirizzo della scheda, sia quello della porta (in un prossimo articolo vi mostrerò come renderli dinamici):

#define BOARD_ADDRESS   5
#define PORT_ADDRESS    1

Possiamo quindi verificare che il comando sia indirizzato al nostro decoder con:

if((BoardAddr == BOARD_ADDRESS) && (portAddress == PORT_ADDRESS)) {

e modificare lo stato di due pin digitali (a cui ho collegato due led) in base all’indirizzo dell’uscita e al comando:

if(outAddress == 0) {
  digitalWrite(RED_LED_PIN, HIGH);
  digitalWrite(GREEN_LED_PIN, LOW);
   Serial.println("! RED led activated");
} else {
  digitalWrite(RED_LED_PIN, LOW);
  digitalWrite(GREEN_LED_PIN, HIGH);
  Serial.println("! GREEN led activated");      
    }

Il codice sorgente del decoder è disponibile sul mio repository Github; ecco un filmato che ne mostra il funzionamento (sottotitoli in italiano disponibili):

Viewing all 84 articles
Browse latest View live