In diesem Tutorial bauen wir eine analoge Uhr mit einem 4,2″ e-Paper-Display und einem ESP32. Die Uhr synchronisiert ihre Zeit mit einem Internet-Zeitserver (SNTP-Server). Außerdem gehen der ESP32 und das Display zwischen den Aktualisierungen in den Schlafmodus, um die Laufzeit der Uhr im Batteriebetrieb zu verlängern.
Fangen wir mit den benötigten Teilen an.
Benötigte Teile
Ich verwende den ESP32 lite als Mikroprozessor, da er günstig ist und eine Schnittstelle zum Laden von Batterien hat, was es ermöglicht, die Uhr mit einem LiPo-Akku zu betreiben. Jeder andere ESP32 oder ESP8266 mit ausreichend Speicher funktioniert ebenfalls, aber am besten nimmt man einen mit einer Batterie-Ladeschnittstelle.

4,2″ 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 4,2″ Modul mit 400×300 Pixeln Auflösung, 4 Graustufen, einer partiellen Aktualisierungszeit von 0,4 Sekunden und einem eingebetteten Controller mit SPI-Schnittstelle.

Beachte, dass das Modul auf der Rückseite einen kleinen Jumper-Pad/Schalter hat, um von 4-Draht-SPI auf 3-Draht-SPI umzuschalten. Wir verwenden hier den Standard 4-Draht-SPI, daher musst du nichts ändern.

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 allgemein, 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-Displays. Das folgende Bild zeigt die komplette Verkabelung für Stromversorgung und SPI.

Unten 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 einen LiPo-Akku hinzugefügt, um den Betrieb der Uhr mit Batterie zu testen. Das Bild unten zeigt, wie mein Setup aussah.

GxEPD2 Bibliothek installieren
Bevor wir auf dem e-Paper-Display zeichnen oder schreiben können, müssen wir zwei Bibliotheken installieren. Die Adafruit_GFX Grafikbibliothek, die eine gemeinsame Sammlung von Grafikprimitiven (Text, Punkte, Linien, Kreise usw.) bereitstellt. Und die GxEPD2 Bibliothek, die die Grafiktreiber-Software für das e-Paper-Display liefert.
Installiere die Bibliotheken einfach auf die übliche Weise. Nach der Installation sollten sie im Library Manager wie folgt erscheinen.

Testcode
Sobald du die Bibliothek installiert hast, führe den folgenden Testcode aus, um sicherzustellen, dass alles funktioniert.
#include "GxEPD2_BW.h"
//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(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(90, 190);
epd.print("Makerguides");
epd.display();
epd.hibernate();
}
void loop() {}
Der Textcode gibt den Text „Makerguides“ aus und nach einem kurzen Flackern des Displays (Vollaktualisierung) solltest du ihn wie unten gezeigt 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, in der wir den 4,2″ Display-Treiber angeben:
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Die Readme für die GxEPD2 Bibliothek listet alle unterstützten Displays auf, und du findest die Details in den Header-Dateien, z.B. GxEPD2.h. Finde den Display-Treiber, der zu deinem Display passt. Das kann etwas Ausprobieren erfordern.
Code für eine analoge Uhr auf e-Paper
Der folgende Code ist der komplette Code für eine analoge Uhr, die ihre Zeit automatisch mit einem Internet-Zeitserver (SNTP) synchronisiert und Zeit sowie Datum auf einem 4,2″ e-Paper-Display anzeigt. Zwischen den Aktualisierungen sind Display und ESP32 im Schlafmodus, um Batterie zu sparen. Schau dir den Code zuerst kurz an, dann gehen wir ins Detail.
#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "WiFi.h"
#include "esp_sntp.h"
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* DAYSTR[] = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};
// W, H flipped due to setRotation(1)
const int H = GxEPD2_420_GDEY042T81::WIDTH;
const int W = GxEPD2_420_GDEY042T81::HEIGHT;
const int CW = W / 2;
const int CH = H / 2;
const int R = min(W, H) / 2;
const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
RTC_DATA_ATTR uint16_t wakeups = 0;
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
void initDisplay() {
bool initial = wakeups == 0;
epd.init(115200, initial, 50, false);
epd.setRotation(1);
epd.setTextSize(1);
epd.setTextColor(BLACK);
}
void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
void syncTime() {
if (wakeups % 50 == 0) {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
}
void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
epd.setCursor(x - w / 2, y + h / 2);
epd.print(text);
}
void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff[64];
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}
void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
alpha = alpha * TWO_PI / 360;
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}
void drawClockFace() {
int cx, cy;
epd.setFont(&FreeSansBold9pt7b);
epd.drawCircle(CW, CH, R - 2, BLACK);
epd.fillCircle(CW, CH, 8, BLACK);
for (int h = 1; h <= 12; h++) {
float alpha = 360.0 * h / 12;
polar2cart(CW, CH, R - 20, alpha, cx, cy);
printfAt(cx, cy, "%d", h);
polar2cart(CW, CH, R - 40, alpha, cx, cy);
epd.fillCircle(cx, cy, 3, BLACK);
}
for (int m = 1; m <= 12 * 5; m++) {
float alpha = 360.0 * m / (12 * 5);
polar2cart(CW, CH, R - 40, alpha, cx, cy);
epd.fillCircle(cx, cy, 2, BLACK);
}
}
void drawTriangle(float alpha, int width, int len) {
int x0, y0, x1, y1, x2, y2;
polar2cart(CW, CH, len, alpha, x2, y2);
polar2cart(CW, CH, width, alpha - 90, x1, y1);
polar2cart(CW, CH, width, alpha + 90, x0, y0);
epd.drawTriangle(x0, y0, x1, y1, x2, y2, BLACK);
}
void drawClockHands() {
struct tm t;
getLocalTime(&t);
float alphaM = 360 * (t.tm_min / 60.0);
float alphaH = 30 * (t.tm_hour % 12);
drawTriangle(alphaM, 8, R - 50);
drawTriangle(alphaH, 8, R - 65);
}
void drawDateDay() {
struct tm t;
getLocalTime(&t);
epd.setFont(&FreeSans9pt7b);
printfAt(CW, CH+R/3, "%02d-%02d-%02d",
t.tm_mday, t.tm_mon + 1, t.tm_year -100);
printfAt(CW, CH-R/3, "%s", DAYSTR[t.tm_wday]);
}
void drawClock(const void* pv) {
if (wakeups % 120 == 0) {
epd.setFullWindow();
} else {
epd.setPartialWindow(0, 0, W, H);
}
epd.fillScreen(WHITE);
drawClockFace();
drawClockHands();
drawDateDay();
}
void setup() {
initDisplay();
setTimezone();
syncTime();
epd.drawPaged(drawClock, 0);
epd.hibernate();
wakeups = (wakeups + 1) % 1000;
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 die folgende Uhr sehen:

Sie zeigt das Zifferblatt, die zwei Zeiger und den aktuellen Wochentag und das Datum.
Bibliotheken
Wir beginnen mit dem Einbinden der GxEPD2_BW.hHeader-Datei für das schwarz-weiße (BW) e-Paper-Display. Wenn du ein 3-Farben-Display hast, würdest du GxEPD2_3C.heinbinden, oder GxEPD2_4C.hfür ein 4-Farben-Display und GxEPD2_7C.hfür ein 7-Farben-Display.
#include "GxEPD2_BW.h" #include "Fonts/FreeSans9pt7b.h" #include "Fonts/FreeSansBold9pt7b.h"
Wir binden außerdem zwei Dateien für die Schriftarten ein, die für die Anzeige der Uhrzeit und des Datums verwendet werden. Die Uhrzeitbeschriftungen sind in einer fetten Schrift (FreeSansBold9pt7b) und das Datum in einer dünneren Schrift (FreeSans9pt7b) – siehe das Uhrbild oben. Eine Übersicht der AdaFruit GFX fonts findest du hier.
Zuletzt binden wir die WiFi.h und die esp_snt.hBibliotheken ein, die wir benötigen, um die Uhrzeit über Wi-Fi mit einem SNTP-Internet-Zeitserver zu synchronisieren.
#include "WiFi.h" #include "esp_sntp.h"
Konstanten
Als nächstes definieren wir einige Konstanten. Am wichtigsten ist, dass du den SSID und PASSWORD für dein WiFi sowie die TIMEZONE, in der du lebst, definierst.
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 Normalzeit am ersten Sonntag im April, mit 3 Stunden Unterschied zur UTC.
Für andere Zeitzonendefinitionen schau dir die Posix Timezones Database an. Einfach den dort gefundenen String kopieren und die TIMEZONE Konstante entsprechend anpassen.
Zusätzlich zum Datum wollen wir auch den Namen des aktuellen Wochentags anzeigen, z.B. Donnerstag. Die DAYSTR Konstante definiert die Wochentagsnamen. Du kannst die Sprache ändern oder kürzere Namen wählen, achte aber darauf, die Reihenfolge beizubehalten.
const char* DAYSTR[] = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};
Als nächstes 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.
const int H = GxEPD2_420_GDEY042T81::WIDTH; const int W = GxEPD2_420_GDEY042T81::HEIGHT;
Schließlich definieren wir Konstanten für die Mittelposition (CW, CH) auf dem Display, den maximalen Radius R eines Kreises auf dem Display und kürzere Namen für die Farben Schwarz und Weiß.
const int CW = W / 2; const int CH = H / 2; const int R = min(W, H) / 2; const uint16_t WHITE = GxEPD_WHITE; const uint16_t BLACK = GxEPD_BLACK;
Variablen und Objekte
Nach den Konstanten definieren wir eine globale Variable zum Zählen der Aufwachvorgänge und das Display-Objekt.
RTC_DATA_ATTR uint16_t wakeups = 0; GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Die wakeups Variable wird bei jedem Aufwachen des ESP32 aus dem Tiefschlaf inkrementiert. Sie wird in RTC memory gespeichert, damit ihr Wert auch im Tiefschlaf erhalten bleibt. Wir nutzen sie, um zu entscheiden, wann das Display vollständig oder nur teilweise aktualisiert wird und wie oft die Zeit synchronisiert wird.
Das epd Objekt repräsentiert das Display (e–paper display). Diese Objektdefinition muss zum Typ deines e-Paper-Displays passen. Hier haben wir ein 4,2 Zoll (= _420) e-Display. Siehe die Readme der GxEPD2 Bibliothek und die GxEPD2.h Datei für unterstützte Displays.
initDisplay Funktion
Die initDisplay Funktion initialisiert das Display, stellt die Ausrichtung 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(BLACK);
}
epd.init()führt normalerweise eine vollständige Aktualisierung des Displays aus. Wir wollen aber nur nach einem Reset des ESP32 eine Vollaktualisierung und beim Aufwachen aus dem Tiefschlaf eine partielle Aktualisierung. Dafür setzen wir den initial Parameter der epd.init() Funktion auf true beim wakeups == 0, also nach einem Reset, und sonst auf initial false für eine partielle Aktualisierung.
setTimezone Funktion
Die setTimezone Funktion setzt die TIMEZONE. Wir müssen sie jedes Mal in der setup-Funktion aufrufen, da nach Tiefschlaf oder Reset die Zeitzoneninformationen verloren gehen.
void setTimezone() {
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. Sieh dir das How to synchronize ESP32 clock with SNTP server Tutorial für mehr Informationen an.
void syncTime() {
if (wakeups % 50 == 0) {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
}
Wir wollen die Zeit aber nur gelegentlich synchronisieren. Da der ESP32 für 30 Sekunden schlafen geht (siehe loop Funktion), sorgt der Ausdruck wakeups % 50 == 0 dafür, dass die Zeit nur alle 25 Minuten synchronisiert wird (30 Sek * 50 / 60 Sek).
Du kannst das ändern, aber beachte, dass häufigere Synchronisationen mehr Batterie verbrauchen (Wi-Fi ist stromhungrig). Weniger häufige Synchronisationen bedeuten hingegen, dass die Uhr langsamer auf Sommerzeitumstellungen reagiert.
printAt Funktionen
Die printAt Funktionen zeigen zentrierten Text an den angegebenen Bildschirmkoordinaten an.
void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
epd.setCursor(x - w / 2, y + h / 2);
epd.print(text);
}
void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff[64];
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}
Während printAt() nur einfachen Text ausgibt, funktioniert die printfAt() Funktion wie printf und erlaubt dir, ein Formatierungszeichen anzugeben, z.B. printfAt(x, y, "%02d-%02d-%02d", m, h, y);
polar2cart Funktion
Die polar2cart Funktion wandelt polar coordinates, die durch einen Radius r und einen Winkel alpha gegeben sind, in cartesian coordinates cx, cy um.
void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
alpha = alpha * TWO_PI / 360;
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}
Diese Funktion ist sehr nützlich, da Zeitangaben wie 8:15 Uhr im Grunde Polarkoordinaten auf der Uhr sind und wir sie in das kartesische Koordinatensystem des Displays umrechnen müssen.
drawClockFace Funktion
Die drawClockFace() Funktion zeichnet, wie der Name sagt, das Zifferblatt der Uhr. Dazu gehören der Rahmen, die Minutenstriche und die Stundenbeschriftungen und -striche. Wie du siehst, nutzt sie die polar2cart() Funktion, die wir vorher besprochen haben, gut aus.
void drawClockFace() {
int cx, cy;
epd.setFont(&FreeSansBold9pt7b);
// Frame
epd.drawCircle(CW, CH, R - 2, BLACK);
epd.fillCircle(CW, CH, 8, BLACK);
// Hour ticks and labels
for (int h = 1; h <= 12; h++) {
float alpha = 360.0 * h / 12;
polar2cart(CW, CH, R - 20, alpha, cx, cy);
printfAt(cx, cy, "%d", h);
polar2cart(CW, CH, R - 40, alpha, cx, cy);
epd.fillCircle(cx, cy, 3, BLACK);
}
// Minute ticks
for (int m = 1; m <= 12 * 5; m++) {
float alpha = 360.0 * m / (12 * 5);
polar2cart(CW, CH, R - 40, alpha, cx, cy);
epd.fillCircle(cx, cy, 2, BLACK);
}
}
drawTriangle Funktion
Die drawTriangle() Funktion zeichnet ein Dreieck, aber in Polarkoordinaten. Die Basis des Dreiecks liegt im Zentrum der Uhr und das Dreieck ist um den Winkel alpha gedreht. width ist die Breite der Basis und len die Länge des Dreiecks. Diese Funktion wird verwendet, um die Zeiger der Uhr zu zeichnen.
void drawTriangle(float alpha, int width, int len) {
int x0, y0, x1, y1, x2, y2;
polar2cart(CW, CH, len, alpha, x2, y2);
polar2cart(CW, CH, width, alpha - 90, x1, y1);
polar2cart(CW, CH, width, alpha + 90, x0, y0);
epd.drawTriangle(x0, y0, x1, y1, x2, y2, BLACK);
}
Du kannst von epd.drawTriangle() auf epd.fillTriangle() wechseln, wenn du lieber gefüllte Zeiger möchtest. Die sind leichter zu sehen, verdecken aber die Anzeige von aktuellem Tag und Datum.
drawClockHands Funktion
Das eigentliche Zeichnen der Zeiger übernimmt die drawClockHands() Funktion. Sie holt die aktuelle Ortszeit, wandelt Minuten und Stunden in Winkel um und nutzt dann drawTriangle(), um die Zeiger zu zeichnen.
void drawClockHands() {
struct tm t;
getLocalTime(&t);
float alphaM = 360 * (t.tm_min / 60.0);
float alphaH = 30 * (t.tm_hour % 12);
drawTriangle(alphaM, 8, R - 50);
drawTriangle(alphaH, 8, R - 65);
}
drawDateDay Funktion
Zusätzlich zur Uhrzeit durch die Zeiger wollte ich auch das aktuelle Datum und den Wochentag anzeigen. Die drawDateDay() Funktion holt die aktuelle Ortszeit und das Datum und gibt dann den Wochentagsnamen, z.B. Donnerstag, oben auf dem Zifferblatt aus und das Datum, z.B. 05-09-24, unten. Du kannst das Format leicht an dein Land anpassen.
void drawDateDay() {
struct tm t;
getLocalTime(&t);
epd.setFont(&FreeSans9pt7b);
printfAt(CW, CH+R/3, "%02d-%02d-%02d",
t.tm_mday, t.tm_mon + 1, t.tm_year -100);
printfAt(CW, CH-R/3, "%s", DAYSTR[t.tm_wday]);
}
drawClock Funktion
Die drawClock() Funktion fasst alles zusammen. Sie löscht den Bildschirm und zeichnet dann das Zifferblatt, die Zeiger und das Datum.
void drawClock(const void* pv) {
if (wakeups % 120 == 0) {
epd.setFullWindow();
} else {
epd.setPartialWindow(0, 0, W, H);
}
epd.fillScreen(WHITE);
drawClockFace();
drawClockHands();
drawDateDay();
}
Je nach wakeups Zähler wird entweder eine vollständige oder eine partielle Aktualisierung des Displays durchgeführt. Eine vollständige Aktualisierung ist langsam (2-4 Sekunden) und verursacht starkes Flackern. Eine partielle Aktualisierung ist viel schneller (<0,5 Sek.) und ohne Flackern.
Allerdings hinterlässt eine partielle Aktualisierung ein schwaches Nachbild des alten Inhalts. Wenn du genau hinschaust, siehst du auf dem Bild unten die Nachbilder des Minutenzeigers, der sich von 5 auf 10 bewegt hat.

Um die Nachbilder zu entfernen, wollen wir gelegentlich eine vollständige Aktualisierung durchführen. In der drawClock() Funktion machen wir das, wenn der wakeup Zähler 120 erreicht: wakeups % 120 == 0. Da die Tiefschlafzeit auf 30 Sekunden gesetzt ist, bedeutet das eine vollständige Aktualisierung alle 30 Sek * 120 / 60 Sek = 60 Minuten.
setup und loop Funktion
Zum Schluss haben wir die üblichen setup und loop Funktionen. Die loop-Funktion ist leer, da der ESP32 am Ende der setup Funktion in den Tiefschlaf geht und die loop Funktion daher nie ausgeführt wird.
void setup() {
initDisplay();
setTimezone();
syncTime();
epd.drawPaged(drawClock, 0);
epd.hibernate();
wakeups = (wakeups + 1) % 1000;
esp_sleep_enable_timer_wakeup(30 * 1000 * 1000);
esp_deep_sleep_start();
}
void loop() {
}
Die setup-Funktion startet mit der Initialisierung des Displays, setzt die Zeitzone und synchronisiert (vielleicht) die Zeit (abhängig vom Aufwachzähler).
Dann rufen wir drawPaged auf, um die drawClock Funktionen auszuführen und danach das Display in den Energiesparmodus zu versetzen. Wenn du mehr Details brauchst, schau dir das Partial Refresh of e-Paper Display Tutorial an, in dem wir die drawPaged Funktion ausführlicher erklären.
Bevor wir den ESP32 für 30 Sekunden (30 * 1000 * 1000 Mikrosekunden) in den Tiefschlaf schicken, erhöhen wir den wakeup Zähler und berechnen das Modulo 1000, um einen Überlauf zu vermeiden.
Und das war’s! Mit diesem Code hast du eine schöne analoge Uhr, die immer genau ist und im Batteriebetrieb laufen kann.
Fazit
In diesem Tutorial hast du gelernt, wie man eine analoge Uhr auf einem 4,2″ e-Paper-Display baut, die ihre Zeit mit einem SNTP-Server synchronisiert und immer genaue Zeit und Datum anzeigt. Die Uhr nutzt die Tiefschlaf-Fähigkeit des ESP32, um den Stromverbrauch zu senken und die Laufzeit mit Batterie zu erhöhen.
Wenn du den vorgeschlagenen ESP32 LOLIN Lite verwendest, ist das Anschließen eines wiederaufladbaren LiPo-Akkus einfach und du könntest einen MAX1704X verwenden, um den Ladezustand zu überwachen.
Zum Schluss, wenn du lieber eine Digitaluhr statt einer Analoguhr möchtest, schau dir unser Digital Clock on e-Paper Display Tutorial an.
Viel Spaß beim Tüfteln ; )


jotbes
Tuesday 2nd of December 2025
mmm, hätte ich gesehen das die Minuten als Schatten nachlaufen hätte ich das "Ding" nicht gekauft und gebaut. Einfach schrecklich, Die Zahlen sehen sehr geraspelt aus. SCHADE, wieder mal ein Teil für die Tonne.
Stefan Maetschke
Tuesday 9th of December 2025
Ja, das ist der Nachteil bei den e-Paper displays.