Dans ce tutoriel, vous apprendrez à construire une horloge numérique utilisant un écran e-Paper et un ESP32. Vous découvrirez également comment synchroniser l’horloge avec un fournisseur de temps internet (serveur SNTP). Enfin, vous apprendrez à combiner ces fonctions avec la capacité de deep-sleep de l’ESP pour augmenter l’autonomie sur batterie.
Commençons par les pièces nécessaires.
Pièces requises
Pour les pièces, je recommande ici un ancien ESP32 lite, qui est obsolète mais que vous pouvez encore trouver à bas prix. Il existe un modèle successeur (Amazon) avec des spécifications améliorées. Mais tout autre ESP32 ou ESP8266 avec suffisamment de mémoire fonctionnera également.

Écran e-Paper 2,9″

ESP32 lite

Câble USB de données

Jeu de fils Dupont

Plaque d’essai (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.
Écran e-Paper
L’écran e-Paper utilisé dans ce projet est un module de 2,9″ avec une résolution de 296×128 pixels et un contrôleur intégré avec interface SPI.

Notez que ce module possède un petit cavalier/interrupteur qui permet de passer du SPI 4 fils au SPI 3 fils. Nous utiliserons ici le SPI 4 fils par défaut.

Le module d’écran fonctionne en 3,3V ou 5V, a un courant de veille très faible de 0,01µA et consomme environ 26,4mW lors du rafraîchissement. Pour plus d’informations sur les écrans e-Paper, consultez les deux tutoriels suivants : Interfacing Arduino To An E-ink Display et Weather Station on e-Paper Display.
Connexion et test de l’e-Paper
Commençons par connecter et tester le fonctionnement de l’e-Paper. L’image suivante montre le câblage complet pour l’alimentation et le SPI.

Voici un tableau récapitulatif de toutes les connexions pour plus de commodité. Notez que vous pouvez alimenter l’écran en 3,3V ou 5V, mais l’ESP32-lite ne dispose que d’une sortie 3,3V et les lignes de données SPI doivent être en 3,3V !
| Écran e-Paper | ESP32 lite |
|---|---|
| CS/SS | 5 |
| SCL/SCK | 18 |
| SDA/DIN/MOSI | 23 |
| BUSY | 15 |
| RES/RST | 2 |
| DC | 0 |
| VCC | 3.3V |
| GND | G |
Vous pouvez utiliser une breadboard pour tout câbler. Mais j’ai en fait connecté l’écran directement à l’ESP32 avec des fils Dupont et ajouté une batterie LiPo pour tester le fonctionnement sur batterie.

Installer la bibliothèque GxEPD2
Avant de pouvoir dessiner ou écrire sur l’écran e-Paper, nous devons installer deux bibliothèques. La Adafruit_GFX est une bibliothèque graphique qui fournit un ensemble commun de primitives graphiques (texte, points, lignes, cercles, etc.). Et la GxEPD2 fournit le pilote graphique pour contrôler un écran e-Paper via SPI.
Installez simplement les bibliothèques de la manière habituelle. Après installation, elles devraient apparaître dans le Library Manager comme suit.

Code de test
Une fois la bibliothèque installée, lancez le code de test suivant pour vérifier que tout fonctionne.
#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() {}
Le code affiche le texte « Makerguides » et après un clignotement de l’écran (rafraîchissement complet), vous devriez le voir sur votre écran :

Sinon, quelque chose ne va pas. Le plus probable est que l’écran n’est pas correctement câblé ou que le mauvais pilote d’écran est sélectionné. La ligne critique du code est la suivante :
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));
La Readme pour la bibliothèque GxEPD2 liste tous les écrans supportés et vous pouvez trouver les détails dans les fichiers header, par exemple GxEPD2.h et GxEPD2_display_selection_new_style.h. Trouvez le pilote spécifique à votre écran. Cela peut demander quelques essais.
Code pour une horloge numérique sur e-Paper
Si le code de test ci-dessus fonctionne, vous ne devriez pas avoir de problème avec le code ci-dessous non plus. C’est le code complet pour une horloge numérique qui synchronise automatiquement son heure avec un serveur de temps internet (SNTP) et affiche l’heure et la date sur un écran e-Paper.
Entre les mises à jour de l’affichage, l’ESP32 et l’écran sont mis en mode deep-sleep pour économiser la batterie. Jetez d’abord un coup d’œil au code complet, puis nous en détaillerons les parties :
#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() {}
Si vous téléversez le code sur votre ESP32, vous devriez voir l’heure et la date affichées sur votre écran e-Paper comme ci-dessous :

Bibliothèques
Nous commençons par définir la constante ENABLE_GxEPD2_GFX à 0. Si elle est à 1, elle permet à la classe de base GxEPD2_GFX de passer des pointeurs vers l’instance d’affichage en paramètre. Mais cela ajoute environ 1,2k de code et ce n’est pas nécessaire, donc elle est mise à 0.
#define ENABLE_GxEPD2_GFX 0
Ensuite, nous incluons le fichier header GxEPD2_BW.h pour l’écran e-Paper noir et blanc (BW). Si vous avez un écran 3 couleurs, vous incluriez GxEPD2_3C.h, ou GxEPD2_4C.h pour un écran 4 couleurs, et GxEPD2_7C.h pour un écran 7 couleurs, à la place.
#include "GxEPD2_BW.h" #include "Fonts/FreeSans12pt7b.h" #include "Fonts/FreeSansBold24pt7b.h"
Nous incluons aussi deux fichiers pour les polices utilisées pour afficher l’heure et la date. Nous voulons afficher l’heure en plus grand, en gras 24pt (FreeSansBold24pt7b) et la date en plus petit, 7pt (FreeSans12pt7b). Vous pouvez trouver un aperçu des AdaFruit GFX fonts ici.
Enfin, nous incluons les bibliothèques WiFi.h et esp_snt.h, nécessaires pour synchroniser l’horloge avec un serveur de temps SNTP. Plus d’infos plus tard.
#include "WiFi.h" #include "esp_sntp.h"
Constantes
Les trois constantes suivantes doivent être modifiées. D’abord le SSID et PASSWORD pour votre WiFi, puis le TIMEZONE correspondant à votre fuseau horaire.
const char* SSID = "SSID"; const char* PWD = "PASSWORD"; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
La spécification de fuseau horaire « AEST-10AEDT,M10.1.0,M4.1.0/3 » est pour l’Australie, correspondant à l’Australian Eastern Standard Time (AEST) avec ajustements pour l’heure d’été.
Les parties de cette définition de fuseau horaire sont les suivantes
- AEST : Australian Eastern Standard Time
- -10 : décalage UTC de 10 heures en avance sur le Temps Universel Coordonné (UTC)
- AEDT : Australian Eastern Daylight Time
- M10.1.0 : passage à l’heure d’été le premier dimanche d’octobre
- M4.1.0/3 : retour à l’heure standard le premier dimanche d’avril, avec un décalage de 3 heures par rapport à l’UTC.
Pour d’autres définitions de fuseaux horaires, consultez le Posix Timezones Database. Copiez-collez simplement la chaîne trouvée et modifiez la constante TIMEZONE en conséquence.
En plus de l’heure et la date, nous voulons aussi afficher le nom du jour et du mois en cours. Les constantes suivantes définissent ces noms. Vous pouvez changer la langue ou choisir des noms plus longs. Assurez-vous simplement qu’ils tiennent sur l’écran.
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"
};
Enfin, nous définissons les constantes pour la largeur W et la hauteur H de l’écran. C’est surtout pour plus de commodité. Notez que largeur et hauteur sont inversées, car nous faisons pivoter l’écran (setRotation(1)) dans la fonction initDisplay.
// W, H flipped due to setRotation(1) const int W = GxEPD2_290_BS::HEIGHT; const int H = GxEPD2_290_BS::WIDTH;
Variables et objets
Ensuite, nous définissons quelques variables et objets globaux.
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 variable syncing indique si l’horloge est en train de synchroniser son heure avec le serveur SNTP. Si c’est le cas, l’écran affichera un caractère ‘*’ après l’heure. Regardez la fonction drawTime() où cette variable est utilisée. Ce n’est pas indispensable, mais c’est utile pour déboguer et vérifier que la synchronisation fonctionne.
La variable wakeups est incrémentée à chaque réveil de l’ESP32 après un deep-sleep. Elle sert à décider s’il faut faire un rafraîchissement complet de l’écran ou juste synchroniser l’heure. Regardez la fonction setup() où elle est utilisée.
L’objet epd définit l’objet d’affichage (e–paper display). Vous devez définir cet objet pour correspondre à votre écran e-Paper. Consultez le Readme de la bibliothèque GxEPD2 et le fichier GxEPD2.h pour des exemples.
Fonction initDisplay
La fonction initDisplay initialise l’écran, définit l’orientation en paysage, la taille du texte à 1 et la couleur du texte en noir.
void initDisplay() {
bool initial = wakeups == 0;
epd.init(115200, initial, 50, false);
epd.setRotation(1);
epd.setTextSize(1);
epd.setTextColor(GxEPD_BLACK);
}
Si la variable initial est vraie, l’écran e-Paper effectuera un rafraîchissement complet au réveil de l’ESP32 après deep-sleep. Pour éviter cela, nous devons mettre la variable initial à false. Mais nous ne voulons le faire qu’après le premier réveil, ce que réalise wakeups == 0.
Fonction initTime
La fonction initTime se contente de définir le TIMEZONE. Vous devez vous assurer que cette fonction s’exécute à chaque réveil de l’ESP32, sinon votre horloge sera décalée.
void initTime() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
Fonction syncTime
La fonction syncTime crée une connexion WiFi puis synchronise l’horloge interne de l’ESP32 avec le serveur SNTP « pool.ntp.org » en appelant configTzTime().
Vous pouvez spécifier d’autres serveurs SNTP, voire plusieurs. Consultez le tutoriel How to synchronize ESP32 clock with SNTP server pour plus d’informations.
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
syncing = true;
}
Nous mettons aussi la variable syncing à true, utilisée dans drawTime pour indiquer qu’une synchronisation a eu lieu. Au prochain réveil (toutes les 30 secondes), la variable syncing est remise à false.
Fonction drawBackground
La fonction drawBackground effectue un rafraîchissement complet (setFullWindow) de l’écran e-Paper et remplit simplement l’écran en blanc. Vous pouvez aussi ajouter du texte statique, des graphiques ou d’autres informations qui changent rarement.
void drawBackground(const void* pv) {
epd.setFullWindow();
epd.fillScreen(GxEPD_WHITE);
}
Comme le rafraîchissement complet est lent et provoque beaucoup de clignotements, il est appelé seulement occasionnellement – essentiellement pour éviter les images fantômes dues au rafraîchissement partiel. Le Waveshare Manual donne plus d’informations à ce sujet.
Fonction drawTime
La fonction drawTime effectue un rafraîchissement partiel (setPartialWindow), beaucoup plus rapide qu’un rafraîchissement complet et surtout qui met à jour le contenu sans clignotement.
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);
}
Elle récupère simplement l’heure locale puis affiche l’heure et la date avec différentes polices à des positions spécifiques sur l’écran. Si vous voulez afficher d’autres informations temporelles, par exemple les secondes, voici toutes les valeurs disponibles dans le type de données 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
Fonction setup
La fonction setup s’exécute à chaque réveil de l’ESP32. Elle commence par initialiser l’écran et définir le fuseau horaire comme décrit plus haut.
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();
}
Ensuite, elle effectue différentes actions selon le nombre de réveils comptés dans la variable wakeups.
Plus précisément, elle synchronise l’heure tous les 50 réveils. Comme la durée du deep-sleep est de 30 secondes, cela signifie une synchronisation toutes les 30sec * 50 / 60 sec = 25 minutes. Vous pouvez modifier cela, mais plus vous synchronisez souvent, plus la consommation batterie est élevée. Par contre, il faut synchroniser au moins une fois par heure pour ne pas rater le passage entre heure d’été et heure standard.
if (wakeups % 50 == 0)
syncTime();
Tous les 2000 réveils, un rafraîchissement complet (drawBackground) est effectué pour éliminer les images fantômes laissées par le rafraîchissement partiel qui affiche l’heure et la date (drawTime). Là encore, vous pouvez ajuster la fréquence. Avec ces réglages, 30sec * 2000 / 60 sec / 60 mins donne un rafraîchissement complet toutes les 16,6 heures.
if (wakeups % 2000 == 0)
epd.drawPaged(drawBackground, 0);
Pour éviter un dépassement d’entier, la variable wakeups est remise à zéro après avoir atteint 100000. Rappelez-vous que cette variable est stockée en mémoire RTC et conserve sa valeur en deep-sleep.
wakeups = (wakeups + 1) % 100000;
Enfin, nous appelons drawTime pour rafraîchir l’affichage de l’heure et de la date, puis mettons l’e-Paper en veille pour économiser la batterie.
Ensuite, nous mettons l’ESP32 en deep sleep pendant 30 secondes. Cela signifie que l’écran est mis à jour toutes les 30 secondes. Vous pouvez ralentir un peu, par exemple 45 secondes, ou accélérer, toutes les 10 secondes par exemple. Comme toujours, c’est un compromis entre réactivité de l’affichage et économie d’énergie.
Les intervalles de réveil et de rafraîchissement sont choisis pour suivre les recommandations pour les écrans e-Paper dans le Waveshare Manual. Pour plus d’informations, consultez aussi le tutoriel Partial Refresh of e-Paper Display.
Conclusions
Dans ce tutoriel, vous avez appris à construire une horloge numérique qui synchronise son heure avec un serveur SNTP et affiche toujours l’heure et la date exactes sur un écran e-Paper. L’horloge utilise la fonction deep-sleep de l’ESP32 pour réduire la consommation d’énergie. Cela permet de faire fonctionner cette horloge longtemps sur batterie.
Si vous utilisez l’ESP32 LOLIN Lite suggéré, connecter une batterie LiPo rechargeable est simple. Je recommande aussi de Monitor Battery Levels with a MAX1704X et peut-être d’afficher un petit symbole de batterie pour visualiser le niveau de charge.
Une autre extension courante pour les horloges numériques est d’afficher aussi la température ambiante ou les données météo. Consultez le tutoriel Weather Station on e-Paper Display pour plus de détails.
Et si vous préférez une horloge analogique plutôt que numérique, regardez notre tutoriel Analog Clock on e-Paper Display, très similaire à celui-ci.
Si vous avez des commentaires, n’hésitez pas à les laisser dans la section commentaires.
Bon bricolage ; )

