Skip to Content

Orologio Digitale su Display e-Paper

Orologio Digitale su Display e-Paper

In questo tutorial imparerai come costruire un orologio digitale usando un display e-Paper e un ESP32. Imparerai anche come sincronizzare l’orologio con un server di tempo internet (server SNTP). Infine, vedrai come combinare queste funzioni con la modalità deep-sleep dell’ESP per aumentare la durata della batteria.

Iniziamo con i componenti necessari.

Componenti necessari

Per i componenti, consiglio un vecchio ESP32 lite, ormai deprecato ma ancora reperibile a basso costo. Esiste un modello successore (Amazon) con specifiche migliorate. Tuttavia, qualsiasi altro ESP32 o ESP8266 con memoria sufficiente funzionerà altrettanto bene.

Display e-Paper da 2,9″

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cavo dati USB

Dupont wire set

Set di fili Dupont

Half_breadboard56a

Breadboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Display e-Paper

Il display e-Paper usato in questo progetto è un modulo da 2,9″ con risoluzione 296×128 pixel e un controller integrato con interfaccia SPI.

Front and Back of 2.9" e-Paper display module
Fronte e retro del modulo display e-Paper da 2,9″

Nota che questo modulo ha un piccolo jumper/interruttore che permette di passare da SPI a 4 fili a SPI a 3 fili. Useremo la modalità SPI a 4 fili di default.

Back of display module with SPI-interface and controller
Retro del modulo display con interfaccia SPI e controller

Il modulo funziona a 3,3V o 5V, ha una corrente di sleep molto bassa di 0,01µA e consuma circa 26,4mW durante l’aggiornamento. Per maggiori informazioni sui display e-Paper, dai un’occhiata ai seguenti due tutorial: Interfacing Arduino To An E-ink Display e Weather Station on e-Paper Display.

Collegamento e test del display e-Paper

Per prima cosa colleghiamo e testiamo il funzionamento del display e-Paper. L’immagine seguente mostra il cablaggio completo per alimentazione e SPI.

Connecting e-Paper to ESP32 via SPI
Collegamento del display e-Paper all’ESP32 via SPI

Di seguito una tabella con tutte le connessioni per comodità. Nota che puoi alimentare il display a 3,3V o 5V, ma l’ESP32-lite ha solo un’uscita a 3,3V e le linee dati SPI devono essere a 3,3V!

Display e-PaperESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

Puoi usare una breadboard per collegare tutto. Io ho collegato il display direttamente all’ESP32 con fili Dupont e ho aggiunto anche una batteria LiPo per testare il funzionamento a batteria.

ESP32 wired with e-Paper and LiPo battery
ESP32 collegato a display e-Paper e batteria LiPo

Installare la libreria GxEPD2

Prima di poter disegnare o scrivere sul display e-Paper dobbiamo installare due librerie. La Adafruit_GFX è una libreria grafica che fornisce un set comune di primitive grafiche (testo, punti, linee, cerchi, ecc.). La GxEPD2 fornisce il driver grafico per controllare un display e-Paper via SPI.

Installa le librerie nel modo consueto. Dopo l’installazione dovrebbero comparire nel Library Manager come mostrato.

Adafruit_GFX and GxEPD2 libraries in Library Manager
Librerie Adafruit_GFX e GxEPD2 nel Library Manager

Codice di test

Una volta installata la libreria, esegui il seguente codice di test per assicurarti che tutto funzioni.

#define ENABLE_GxEPD2_GFX 0
#include "GxEPD2_BW.h"

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(20, 20);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

void loop() {}

Il codice stampa il testo “Makerguides” e dopo un po’ di sfarfallio del display (refresh completo) dovresti vederlo sul display:

Test output on e-Paper display
Output di test sul display e-Paper

Se non funziona, c’è qualcosa che non va. Probabilmente il display non è cablato correttamente o è stato scelto il driver sbagliato. La riga critica è questa:

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

La Readme per la libreria GxEPD2 elenca tutti i display supportati e puoi trovare i dettagli nei file header, ad esempio GxEPD2.h e GxEPD2_display_selection_new_style.h. Trova il driver specifico per il tuo display. Potrebbe servire un po’ di tentativi.

Codice per un orologio digitale su e-Paper

Se il codice di test sopra funziona, non dovresti avere problemi nemmeno con il codice seguente. È il codice completo per un orologio digitale che sincronizza automaticamente l’ora con un server internet SNTP e mostra ora e data su un display e-Paper.

Tra un aggiornamento e l’altro, l’ESP32 e il display vengono messi in modalità deep-sleep per risparmiare batteria. Dai un’occhiata veloce al codice completo, poi ne vedremo i dettagli:

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans12pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"
#include "WiFi.h"
#include "esp_sntp.h"

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const char *DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
const char *MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May",
  "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 
};

// W, H flipped due to setRotation(1)
const int W = GxEPD2_290_BS::HEIGHT;
const int H = GxEPD2_290_BS::WIDTH;

bool syncing = false;
RTC_DATA_ATTR uint16_t wakeups = 0;

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));


void initDisplay() {
  bool initial = wakeups == 0;
  epd.init(115200, initial, 50, false);
  epd.setRotation(1);
  epd.setTextSize(1);
  epd.setTextColor(GxEPD_BLACK);
}

void initTime() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
  syncing = true;
}

void drawBackground(const void* pv) {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_WHITE);
}

void drawTime(const void* pv) {
  static char buff[40];
  struct tm t;
  getLocalTime(&t);

  epd.setPartialWindow(10, 10, W - 20, H - 20);

  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(40, 60);
  sprintf(buff, " %s %2d:%02d%s ",
          DAYSTR[t.tm_wday],
          t.tm_hour, t.tm_min,
          syncing ? "*" : " ");
  epd.print(buff);

  epd.setFont(&FreeSans12pt7b);
  epd.setCursor(55, 100);
  sprintf(buff, " %s  %02d-%02d-%04d ",          
          MONTHSTR[t.tm_mon],
          t.tm_mday, t.tm_mon + 1, t.tm_year + 1900);
  epd.print(buff);
}

void setup() {
  initDisplay();
  initTime();

  if (wakeups % 50 == 0)
    syncTime();
  if (wakeups % 2000 == 0)
    epd.drawPaged(drawBackground, 0);
  wakeups = (wakeups + 1) % 100000;

  epd.drawPaged(drawTime, 0);
  epd.hibernate();

  esp_sleep_enable_timer_wakeup(30 * 1000 * 1000);
  esp_deep_sleep_start();
}

void loop() {}

Se carichi il codice sul tuo ESP32, dovresti vedere ora e data visualizzate sul display e-Paper come mostrato qui sotto:

Time and Date on a 2.9" e-Paper Display
Ora e data su un display e-Paper da 2,9″

Librerie

Iniziamo definendo la costante ENABLE_GxEPD2_GFX a 0. Se impostata a 1, abilita la classe base GxEPD2_GFX a passare puntatori all’istanza del display come parametro. Ma usa circa 1,2k di codice in più e non ci serve, quindi è impostata a 0.

#define ENABLE_GxEPD2_GFX 0

Poi includiamo il file header GxEPD2_BW.h per il display e-Paper bianco e nero (BW). Se hai un display a 3 colori includeresti GxEPD2_3C.h, o GxEPD2_4C.h per un display a 4 colori, e GxEPD2_7C.h per un display a 7 colori.

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans12pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"

Includiamo anche due file per i font usati per mostrare ora e data. Vogliamo mostrare l’ora con un font più grande, 24pt, grassetto (FreeSansBold24pt7b) e la data con un font più piccolo, 7pt (FreeSans12pt7b). Puoi trovare una panoramica dei AdaFruit GFX fonts qui.

Infine includiamo le librerie WiFi.h e esp_snt.h, necessarie per sincronizzare l’orologio con un server SNTP. Ne parleremo più avanti.

#include "WiFi.h"
#include "esp_sntp.h"

Costanti

Le seguenti tre costanti dovrai cambiarle. Prima le SSID e PASSWORD per il WiFi, poi la TIMEZONE della tua zona.

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

La definizione del fuso orario “AEST-10AEDT,M10.1.0,M4.1.0/3” è per l’Australia, corrispondente all’Australian Eastern Standard Time (AEST) con aggiustamenti per l’ora legale.

Le parti di questa definizione di fuso orario sono:

  • AEST: Australian Eastern Standard Time
  • -10: offset UTC di 10 ore avanti rispetto al Tempo Coordinato Universale (UTC)
  • AEDT: Australian Eastern Daylight Time
  • M10.1.0: transizione all’ora legale la prima domenica di ottobre
  • M4.1.0/3: ritorno all’ora standard la prima domenica di aprile, con differenza di 3 ore da UTC.

Per altre definizioni di fuso orario dai un’occhiata al Posix Timezones Database. Basta copiare e incollare la stringa trovata e modificare la costante TIMEZONE di conseguenza.

Oltre a ora e data, vogliamo mostrare anche il nome del giorno e del mese corrente. Le seguenti costanti definiscono questi nomi. Puoi cambiare lingua o scegliere nomi più lunghi, assicurati solo che entrino nel display.

const char *DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
const char *MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May",
  "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 
};

Infine definiamo le costanti per la larghezza W e l’altezza H del display. È per comodità. Nota che larghezza e altezza sono invertite, perché ruotiamo il display (setRotation(1)) nella funzione initDisplay.

// W, H flipped due to setRotation(1)
const int W = GxEPD2_290_BS::HEIGHT;
const int H = GxEPD2_290_BS::WIDTH;

Variabili e oggetti

Definiamo alcune variabili globali e oggetti.

bool syncing = false;
RTC_DATA_ATTR uint16_t wakeups = 0;

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

La variabile syncing indica se l’orologio sta sincronizzando l’ora con il server SNTP. Se sì, il display mostra un ‘*’ dopo l’ora. Guarda la funzione drawTime() dove questa variabile è usata. Non è indispensabile, ma utile per il debug per vedere se la sincronizzazione funziona.

La variabile wakeups viene incrementata ogni volta che l’ESP32 si risveglia dal deep-sleep. Serve a decidere se fare un refresh completo del display o solo sincronizzare l’ora. Guarda la funzione setup() dove è usata.

L’oggetto epd definisce il display (epaper display). Devi definire questo oggetto in base al display e-Paper che hai. Vedi il Readme della libreria GxEPD2 e il file GxEPD2.h per esempi.

Funzione initDisplay

La funzione initDisplay inizializza il display, imposta l’orientamento in landscape, la dimensione del testo a 1 e il colore del testo a nero.

void initDisplay() {
  bool initial = wakeups == 0;
  epd.init(115200, initial, 50, false);
  epd.setRotation(1);
  epd.setTextSize(1);
  epd.setTextColor(GxEPD_BLACK);
}

Se la variabile initial è vera, il display e-Paper esegue un refresh completo al risveglio dal deep-sleep. Per evitarlo, dobbiamo impostare la variabile initial a false. Ma vogliamo farlo solo dopo il primo risveglio, cosa che fa wakeups == 0.

Funzione initTime

La funzione initTime imposta semplicemente il TIMEZONE. Devi assicurarti che questa funzione venga eseguita ogni volta che l’ESP32 si risveglia, altrimenti l’orologio sarà sfasato.

void initTime() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

Funzione syncTime

La funzione syncTime crea una connessione WiFi e sincronizza l’orologio interno dell’ESP32 con il server SNTP “pool.ntp.org” chiamando configTzTime().

Puoi specificare altri o più server SNTP a cui connetterti. Dai un’occhiata al tutorial How to synchronize ESP32 clock with SNTP server per maggiori informazioni.

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
  syncing = true;
}

Impostiamo anche la variabile syncing a true, usata in drawTime per mostrare che la sincronizzazione è avvenuta. Al prossimo risveglio (ogni 30 secondi), la variabile syncing viene riportata a false.

Funzione drawBackground

La funzione drawBackground esegue un refresh completo (setFullWindow) del display e-Paper e riempie semplicemente lo schermo di bianco. Puoi anche aggiungere testo statico, grafica o altre informazioni che cambiano raramente.

void drawBackground(const void* pv) {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_WHITE);
}

Poiché il refresh completo è lento e provoca molto sfarfallio, viene chiamato solo occasionalmente, essenzialmente per evitare il “burn in” delle immagini residue del refresh parziale. Il Waveshare Manual fornisce maggiori dettagli.

Funzione drawTime

La funzione drawTime esegue un refresh parziale (setPartialWindow), molto più veloce del refresh completo e, cosa importante, aggiorna il contenuto senza sfarfallio.

void drawTime(const void* pv) {
  static char buff[40];
  struct tm t;
  getLocalTime(&t);

  epd.setPartialWindow(10, 10, W - 20, H - 20);

  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(40, 60);
  sprintf(buff, " %s %2d:%02d ",
          DAYSTR[t.tm_wday],
          t.tm_hour, t.tm_min,
          syncing ? "*" : " ");
  epd.print(buff);

  epd.setFont(&FreeSans12pt7b);
  epd.setCursor(55, 100);
  sprintf(buff, " %s  %02d-%02d-%04d ",          
          MONTHSTR[t.tm_mon],
          t.tm_mday, t.tm_mon + 1, t.tm_year + 1900);
  epd.print(buff);
}

Prende semplicemente l’ora locale e stampa ora e data con font diversi in posizioni specifiche sullo schermo. Se vuoi stampare altre informazioni temporali, ad esempio i secondi, ecco tutti i valori disponibili nel tipo di dato tm struct:

  Member    Type  Meaning                   Range
  tm_sec    int   seconds after the minute  0-61*
  tm_min    int   minutes after the hour    0-59
  tm_hour   int   hours since midnight      0-23
  tm_mday   int   day of the month          1-31
  tm_mon    int   months since January      0-11
  tm_year   int   years since 1900
  tm_wday   int   days since Sunday         0-6
  tm_yday   int   days since January 1      0-365
  tm_isdst  int   Daylight Saving Time flag

Funzione setup

La funzione setup viene eseguita ogni volta che l’ESP32 si risveglia. Inizia inizializzando il display e impostando il fuso orario come descritto sopra.

void setup() {
  initDisplay();
  initTime();

  if (wakeups % 50 == 0)
    syncTime();
  if (wakeups % 2000 == 0)
    epd.drawPaged(drawBackground, 0);
  wakeups = (wakeups + 1) % 100000;

  epd.drawPaged(drawTime, 0);
  epd.hibernate();

  esp_sleep_enable_timer_wakeup(30 * 1000 * 1000);
  esp_deep_sleep_start();
}

Dopodiché esegue azioni diverse a seconda di quante volte l’ESP32 è stato risvegliato, conteggiate nella variabile wakeups.

In particolare, sincronizza l’ora ogni 50 risvegli. Poiché la durata del deep-sleep è impostata a 30 secondi, significa che sincronizziamo ogni 30sec * 50 / 60 sec = 25 minuti. Puoi cambiare questo valore, ma più spesso sincronizzi, più consumi batteria. D’altra parte, vogliamo sincronizzare almeno una volta all’ora per non perdere il cambio tra ora legale e ora solare.

if (wakeups % 50 == 0)
    syncTime();

Ogni 2000 risvegli eseguiamo un refresh completo (drawBackground) per rimuovere le immagini residue lasciate dal refresh parziale che disegna ora e data (drawTime). Anche questo valore può essere modificato. Così com’è, 30sec * 2000 / 60 sec / 60 min porta a un refresh completo ogni 16,6 ore.

  if (wakeups % 2000 == 0)
    epd.drawPaged(drawBackground, 0);

Per evitare un overflow intero incrementiamo wakeups fino a un massimo di 100000 prima di resettarlo. Ricorda che la variabile wakeups è memorizzata nella memoria RTC e mantiene il valore durante il deep-sleep.

wakeups = (wakeups + 1) % 100000;

Infine chiamiamo drawTime per aggiornare ora e data e poi mettiamo in ibernazione il display e-Paper per risparmiare batteria.

Dopodiché mettiamo l’ESP32 in deep sleep per 30 secondi. Questo significa che il display si aggiorna ogni 30 secondi. Puoi scegliere intervalli più lunghi, ad esempio 45 secondi, o più brevi, ad esempio ogni 10 secondi. Come sempre, è un compromesso tra reattività e risparmio energetico.

Gli intervalli di risveglio e aggiornamento sono scelti seguendo le raccomandazioni per i display e-Paper nel Waveshare Manual. Per maggiori informazioni, dai anche un’occhiata al tutorial Partial Refresh of e-Paper Display.

Conclusioni

In questo tutorial hai imparato a costruire un orologio digitale che sincronizza l’ora con un server SNTP e mostra sempre ora e data accurate su un display e-Paper. L’orologio sfrutta la modalità deep-sleep dell’ESP32 per ridurre il consumo energetico, permettendoti di farlo funzionare a lungo con una batteria.

Se usi l’ESP32 LOLIN Lite suggerito, collegare una batteria LiPo ricaricabile è semplice. Ti consiglio anche di Monitor Battery Levels with a MAX1704X e magari mostrare un piccolo simbolo batteria per visualizzare il livello di carica.

Un’altra estensione comune per orologi digitali è mostrare anche la temperatura ambiente o dati meteo. Dai un’occhiata al tutorial Weather Station on e-Paper Display per maggiori dettagli.

E se vuoi un orologio analogico invece che digitale, guarda il nostro tutorial Analog Clock on e-Paper Display, molto simile a questo.

Se hai commenti, sentiti libero di lasciarli nella sezione commenti.

Buon divertimento con il fai-da-te ; )