In diesem Tutorial lernst du, wie man eine Digitaluhr mit einem e-Paper-Display und einem ESP32 baut. Du erfährst außerdem, wie man die Uhr mit einem Internet-Zeitserver (SNTP-Server) synchronisiert. Schließlich lernst du, wie man diese Funktionen mit der Deep-Sleep-Fähigkeit des ESP kombiniert, um die Laufzeit bei Batteriebetrieb zu verlängern.
Fangen wir mit den benötigten Teilen an.
Benötigte Teile
Für die Teile empfehle ich hier einen älteren ESP32 lite, der zwar veraltet ist, aber noch günstig erhältlich ist. Es gibt ein Nachfolgemodell (Amazon) mit verbesserten Spezifikationen. Aber jeder andere ESP32 oder ESP8266 mit ausreichend Speicher funktioniert ebenfalls.

2,9″ e-Paper Display

ESP32 lite

USB-Datenkabel

Dupont-Kabelset

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.
E-Paper Display
Das in diesem Projekt verwendete e-Paper-Display ist ein 2,9″ Modul mit 296×128 Pixel Auflösung und einem eingebetteten Controller mit SPI-Schnittstelle.

Beachte, dass dieses Modul einen kleinen Jumper-Pad/Schalter hat, mit dem du zwischen 4-Draht-SPI und 3-Draht-SPI wechseln kannst. Wir verwenden hier den Standard 4-Draht-SPI.

Das Display-Modul läuft mit 3,3V oder 5V, hat einen sehr niedrigen Schlafstrom von 0,01µA und verbraucht beim Aktualisieren nur etwa 26,4mW. Für mehr Informationen zu e-Paper-Displays schau dir die folgenden zwei Tutorials an: Interfacing Arduino To An E-ink Display und Weather Station on e-Paper Display.
Anschluss und Test des e-Paper
Zuerst verbinden und testen wir die Funktion des e-Paper. Das folgende Bild zeigt die komplette Verkabelung für Stromversorgung und SPI.

Unten findest du eine Tabelle mit allen Verbindungen zur Übersicht. Beachte, dass du das Display mit 3,3V oder 5V versorgen kannst, aber der ESP32-lite hat nur einen 3,3V-Ausgang und die SPI-Datenleitungen müssen 3,3V sein!
| e-Paper Display | ESP32 lite |
|---|---|
| CS/SS | 5 |
| SCL/SCK | 18 |
| SDA/DIN/MOSI | 23 |
| BUSY | 15 |
| RES/RST | 2 |
| DC | 0 |
| VCC | 3,3V |
| GND | G |
Du kannst ein Breadboard verwenden, um alles zu verkabeln. Ich habe das Display jedoch direkt mit Dupont-Kabeln an den ESP32 angeschlossen und auch eine LiPo-Batterie hinzugefügt, um den Betrieb der Uhr mit Batterie zu testen.

GxEPD2 Bibliothek installieren
Bevor wir auf dem e-Paper-Display zeichnen oder schreiben können, müssen wir zwei Bibliotheken installieren. Die Adafruit_GFX Bibliothek ist eine Grafikbibliothek, die eine gemeinsame Menge an Grafikprimitiven (Text, Punkte, Linien, Kreise usw.) bereitstellt. Und die GxEPD2 Bibliothek stellt die Grafiktreiber-Software zur Steuerung eines e-Paper-Displays über SPI bereit.
Installiere die Bibliotheken einfach auf die übliche Weise. Nach der Installation sollten sie im Library Manager wie folgt erscheinen.

Testcode
Nachdem du die Bibliothek installiert hast, führe den folgenden Testcode aus, um sicherzustellen, dass alles funktioniert.
#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() {}
Der Textcode gibt den Text „Makerguides“ aus und nach einem kurzen Flackern des Displays (Vollrefresh) solltest du ihn auf deinem Display sehen:

Wenn nicht, stimmt etwas nicht. Wahrscheinlich ist das Display nicht korrekt verkabelt oder der falsche Display-Treiber gewählt. Die kritische Codezeile ist diese:
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));
Die Readme für die GxEPD2 Bibliothek listet alle unterstützten Displays auf, die Details findest du in den Header-Dateien, z.B. GxEPD2.h und GxEPD2_display_selection_new_style.h. Finde den Display-Treiber, der zu deinem Display passt. Das kann etwas Ausprobieren erfordern.
Code für eine Digitaluhr auf e-Paper
Wenn der obige Testcode funktioniert, solltest du auch mit dem folgenden Code keine Probleme haben. Es ist der komplette Code für eine Digitaluhr, die ihre Zeit automatisch mit einem Internet-Zeitserver (SNTP) synchronisiert und Zeit sowie Datum auf einem e-Paper-Display anzeigt.
Zwischen den Display-Updates werden der ESP32 und das Display in den Deep-Sleep-Modus versetzt, um Batterie zu sparen. Schau dir zuerst den kompletten Code an, danach gehen wir ins Detail:
#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() {}
Wenn du den Code auf deinen ESP32 hochlädst, solltest du Zeit und Datum auf deinem e-Paper wie unten gezeigt sehen:

Bibliotheken
Wir beginnen mit der Definition der Konstante ENABLE_GxEPD2_GFX als 0. Wenn auf 1 gesetzt, erlaubt sie der Basisklasse GxEPD2_GFX, Zeiger auf die Display-Instanz als Parameter zu übergeben. Das benötigt aber ~1,2k mehr Code und wir brauchen das nicht, daher ist sie auf 0 gesetzt.
#define ENABLE_GxEPD2_GFX 0
Als nächstes binden wir die GxEPD2_BW.h Header-Datei für das schwarz-weiße (BW) e-Paper-Display ein. Wenn du ein 3-Farben-Display hast, würdest du GxEPD2_3C.h, oder GxEPD2_4C.h für ein 4-Farben-Display und GxEPD2_7C.h für ein 7-Farben-Display einbinden.
#include "GxEPD2_BW.h" #include "Fonts/FreeSans12pt7b.h" #include "Fonts/FreeSansBold24pt7b.h"
Wir binden außerdem zwei Dateien für die Schriftarten ein, die verwendet werden, um Zeit und Datum anzuzeigen. Die Zeit soll in einer größeren, 24pt, fetten Schrift (FreeSansBold24pt7b) und das Datum in einer kleineren, 7pt Schrift (FreeSans12pt7b) dargestellt werden. Eine Übersicht der AdaFruit GFX fonts findest du hier.
Schließlich binden wir die WiFi.h und die esp_snt.h Bibliotheken ein, die wir benötigen, um die Uhr mit einem SNTP-Internet-Zeitserver zu synchronisieren. Mehr dazu später.
#include "WiFi.h" #include "esp_sntp.h"
Konstanten
Die folgenden drei Konstanten musst du anpassen. Zuerst die SSID und PASSWORD für dein WiFi und dann die TIMEZONE, in der du lebst.
const char* SSID = "SSID"; const char* PWD = "PASSWORD"; const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
Die obige Zeitzonenspezifikation „AEST-10AEDT,M10.1.0,M4.1.0/3“ ist für Australien und entspricht der Australian Eastern Standard Time (AEST) mit Sommerzeit-Anpassungen.
Die Teile dieser Zeitzonendefinition sind wie folgt
- AEST: Australian Eastern Standard Time
- -10: UTC-Versatz von 10 Stunden vor der koordinierten Weltzeit (UTC)
- AEDT: Australian Eastern Daylight Time
- M10.1.0: Wechsel zur Sommerzeit am ersten Sonntag im Oktober
- M4.1.0/3: Wechsel zurück zur Standardzeit am ersten Sonntag im April, mit 3 Stunden Unterschied zu UTC.
Für andere Zeitzonendefinitionen schau dir das Posix Timezones Database an. Einfach den dort gefundenen String kopieren und die TIMEZONE Konstante entsprechend anpassen.
Neben Zeit und Datum wollen wir auch den Namen des aktuellen Tages und Monats anzeigen. Die folgenden Konstanten definieren diese Namen. Du kannst die Sprache ändern oder längere Namen wählen. Achte nur darauf, dass sie auf das Display passen.
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"
};
Zum Schluss definieren wir Konstanten für die Breite W und Höhe H des Displays. Das dient hauptsächlich der Bequemlichkeit. Beachte, dass Breite und Höhe vertauscht sind, da wir das Display (setRotation(1)) in der initDisplay Funktion drehen.
// W, H flipped due to setRotation(1) const int W = GxEPD2_290_BS::HEIGHT; const int H = GxEPD2_290_BS::WIDTH;
Variablen und Objekte
Als nächstes definieren wir einige globale Variablen und Objekte.
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));
Die syncing Variable zeigt an, ob die Uhr gerade ihre Zeit mit dem SNTP-Server synchronisiert. Wenn das passiert, zeigt das Display ein ‚*‘ Zeichen nach der Zeit an. Schau dir die drawTime() Funktion an, wo diese Variable verwendet wird. Du brauchst sie nicht zwingend, aber es ist praktisch zum Debuggen, um zu sehen, ob die Zeitsynchronisation funktioniert.
Die wakeups Variable wird bei jedem Aufwachen des ESP32 aus dem Deep-Sleep inkrementiert. Sie wird verwendet, um zu entscheiden, ob ein Vollrefresh des Displays oder eine Zeitsynchronisation durchgeführt wird. Schau dir die setup() Funktion an, wo sie verwendet wird.
Das epd Objekt definiert das Display-Objekt (e–paper display). Du musst dieses Objekt passend zu deinem e-Paper-Display definieren. Siehe die Readme der GxEPD2 Bibliothek und die GxEPD2.h Datei für Beispiele.
initDisplay Funktion
Die initDisplay Funktion initialisiert das Display, stellt die Display-Orientierung auf Querformat, die Textgröße auf 1 und die Textfarbe auf Schwarz ein.
void initDisplay() {
bool initial = wakeups == 0;
epd.init(115200, initial, 50, false);
epd.setRotation(1);
epd.setTextSize(1);
epd.setTextColor(GxEPD_BLACK);
}
Wenn die initial Variable wahr ist, führt das e-Paper-Display einen Vollrefresh durch, wenn der ESP32 aus dem Deep-Sleep aufwacht. Um das zu vermeiden, müssen wir die initial Variable auf falsch setzen. Aber das wollen wir erst nach dem ersten Aufwachen machen, was wakeups == 0 erreicht.
initTime Funktion
Die initTime Funktion setzt einfach die TIMEZONE. Du musst sicherstellen, dass diese Funktion jedes Mal ausgeführt wird, wenn der ESP32 aufwacht, sonst läuft deine Uhr falsch.
void initTime() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
syncTime Funktion
Die syncTime Funktion stellt eine WiFi-Verbindung her und synchronisiert dann die interne Uhr des ESP32 mit dem SNTP-Server „pool.ntp.org“ durch Aufruf von configTzTime().
Du kannst auch andere oder mehrere SNTP-Server angeben. Schau dir das How to synchronize ESP32 clock with SNTP server Tutorial für mehr Informationen an.
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
syncing = true;
}
Wir setzen außerdem die syncing Variable auf wahr, die in drawTime verwendet wird, um anzuzeigen, dass eine Zeitsynchronisation durchgeführt wurde. Beim nächsten Aufwachen (alle 30 Sekunden) wird die syncing Variable wieder auf falsch gesetzt.
drawBackground Funktion
Die drawBackground Funktion führt einen Vollrefresh (setFullWindow) des e-Paper-Displays durch und füllt einfach den Bildschirm mit Weiß. Du könntest auch statischen Text, Grafiken oder andere selten ändernde Informationen hinzufügen.
void drawBackground(const void* pv) {
epd.setFullWindow();
epd.fillScreen(GxEPD_WHITE);
}
Da der Vollrefresh langsam ist und stark flackert, wird er nur gelegentlich aufgerufen – im Wesentlichen, um ein „Einbrennen“ von Nachbildern durch den Teilrefresh zu vermeiden. Das Waveshare Manual enthält mehr Informationen dazu.
drawTime Funktion
Die drawTime Funktion führt einen Teilrefresh (setPartialWindow) durch, der viel schneller als ein Vollrefresh ist und vor allem Inhalte ohne Flackern aktualisiert.
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);
}
Sie holt einfach die lokale Zeit und gibt dann Zeit- und Datumsinformationen mit verschiedenen Schriftarten an bestimmten Cursorpositionen auf dem Bildschirm aus. Wenn du andere oder zusätzliche Zeitinformationen, z.B. Sekunden, ausgeben möchtest, findest du hier alle verfügbaren Werte im tm struct Datentyp:
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
setup Funktion
Die setup Funktion wird jedes Mal ausgeführt, wenn der ESP32 aufwacht. Sie startet mit der Initialisierung des Displays und der Einstellung der Zeitzone wie oben beschrieben.
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();
}
Danach führt sie verschiedene Aktionen aus, abhängig davon, wie oft der ESP32 bereits aufgeweckt wurde, gezählt in der wakeups Variable.
Konkret synchronisiert sie die Zeit bei jedem 50. Aufwachen. Da die Deep-Sleep-Dauer auf 30 Sekunden gesetzt ist, bedeutet das eine Synchronisierung alle 30sec * 50 / 60 sec = 25 Minuten. Du kannst das ändern, aber je öfter du synchronisierst, desto höher ist der Batterieverbrauch. Andererseits wollen wir mindestens einmal pro Stunde synchronisieren, um den Wechsel zwischen Sommer- und Normalzeit nicht zu verpassen.
if (wakeups % 50 == 0)
syncTime();
Alle 2000 Aufwachvorgänge führen wir einen Vollrefresh (drawBackground) durch, um Nachbilder zu entfernen, die durch den Teilrefresh entstehen, der Zeit und Datum zeichnet (drawTime). Auch hier kannst du die Häufigkeit anpassen. So wie es ist, ergibt sich ein Vollrefresh alle 16,6 Stunden (30sec * 2000 / 60 sec / 60 min).
if (wakeups % 2000 == 0)
epd.drawPaged(drawBackground, 0);
Um einen Integer-Überlauf zu vermeiden, erhöhen wir wakeups bis maximal 100000, bevor er zurückgesetzt wird. Denk daran, dass die wakeups-Variable im RTC-Speicher liegt und ihren Wert im Deep-Sleep behält.
wakeups = (wakeups + 1) % 100000;
Zum Schluss rufen wir drawTime auf, um die Zeit- und Datumsanzeige zu aktualisieren, und versetzen dann das e-Paper in den Ruhezustand, um Batterie zu sparen.
Danach versetzen wir den ESP32 für 30 Sekunden in den Deep-Sleep. Das bedeutet, das Display wird alle 30 Sekunden aktualisiert. Du könntest auch langsamer gehen, z.B. 45 Sekunden, oder schneller, z.B. alle 10 Sekunden. Wie zuvor ist es ein Kompromiss zwischen einer sehr reaktiven Zeitanzeige und dem Erhalt der Batterielaufzeit.
Die Aufwach- und Aktualisierungsintervalle sind so gewählt, dass sie den Empfehlungen für e-Paper-Displays im Waveshare Manual entsprechen. Für mehr Informationen schau dir auch das Partial Refresh of e-Paper Display Tutorial an.
Fazit
In diesem Tutorial hast du gelernt, wie man eine Digitaluhr baut, die ihre Zeit mit einem SNTP-Server synchronisiert und immer genaue Zeit und Datum auf einem e-Paper-Display anzeigt. Die Uhr nutzt die Deep-Sleep-Fähigkeit des ESP32, um den Stromverbrauch zu reduzieren. So kannst du die Uhr lange mit einer Batterie betreiben.
Wenn du den vorgeschlagenen ESP32 LOLIN Lite verwendest, ist das Anschließen einer wiederaufladbaren LiPo-Batterie einfach. Ich würde auch empfehlen, Monitor Battery Levels with a MAX1704X und vielleicht ein kleines Batteriesymbol anzuzeigen, um den Ladezustand zu visualisieren.
Eine weitere häufige Erweiterung für Digitaluhren ist die Anzeige der Umgebungstemperatur oder Wetterdaten. Schau dir das Weather Station on e-Paper Display Tutorial für mehr Details an.
Und wenn du lieber eine analoge statt einer digitalen Uhr möchtest, schau dir unser Analog Clock on e-Paper Display Tutorial an, das diesem hier sehr ähnlich ist.
Wenn du Kommentare hast, hinterlasse sie gerne im Kommentarbereich.
Viel Spaß beim Tüfteln ; )

