In diesem Tutorial lernst du, wie du eine digitale Uhr auf einem CrowPanel 1.28″ Round Display mit einem GC9A01 TFT unter Verwendung der Adafruit_GC9A01A Bibliothek implementierst. Der Beispielcode für die Uhr zeigt dir außerdem, wie du den CST816D Touchscreen, den Summer, den Vibrationsmotor, die BM8563 Echtzeituhr (RTC), den Deep-Sleep-Modus und die Internetsynchronisation der Zeit nutzt.
Benötigte Teile
Natürlich benötigst du für dieses Projekt das CrowPanel ESP32 1,28-Zoll Runddisplay-Modul. Abgesehen von einem USB-C-Kabel, das normalerweise mit dem Display-Modul geliefert wird, sind keine weiteren Teile erforderlich.

CrowPanel ESP32 1,28″ Runddisplay
Eigenschaften des CrowPanel 1,28″ Runddisplays
Das CrowPanel 1,28″ Modul besteht aus einem runden TFT-Display (GC9A01) mit einer Auflösung von 240×240 Pixeln, einem kapazitiven Touchscreen (CST816D) und einem integrierten ESP32-C3. Das Bild unten zeigt die Vorderseite des Moduls:

Neben dem ESP32 enthält das Modul einen Summer, einen Vibrationsmotor, eine Echtzeituhr (BM8563) mit Backup-Batterie, Unterstützung für eine LiPo-Batterie mit Ladefunktion und mehrere Tasten. Das folgende Bild zeigt die Rückseite des Moduls, auf der die meisten Bauteile zu sehen sind:

Beachte, dass es auch einen kleinen Encoder mit Tastenfunktion (links) gibt, der es im Grunde erlaubt, eine Krone zum Einstellen der Zeit zu verwenden, wie bei einer mechanischen Armbanduhr – vorausgesetzt, du programmierst den Code dafür. Da er vollständig programmierbar ist, kannst du damit aber auch alle möglichen anderen Funktionen oder Benutzerinteraktionen steuern.
Zum Programmieren (und zur Stromversorgung) gibt es einen USB-C-Anschluss sowie Reset- und Boot-Tasten auf der Rückseite. Wenn eine LiPo-Batterie angeschlossen ist, wird sie über den USB-C-Anschluss geladen.
Die folgende Tabelle fasst die Hauptmerkmale des Display-Moduls zusammen:
| Hauptchip | ESP32-C3 |
| Prozessor | 32-Bit RISC-V Single-Core Prozessor, bis zu 160 MHz |
| Speicher | 384 KB ROM, 400 KB SRAM (16 KB für Cache), 8 KB SRAM in RTC |
| Größe | 1,28 Zoll |
| Auflösung | 240*240 |
| Signal-Schnittstelle | SPI |
| Touch-Typ | Kapazitiver Touch |
| Panel-Typ | TFT LCD, IPS Panel |
| Farbtiefe | 262K |
| Helligkeit | 350 cd/m² |
| Blickwinkel | 178° |
| Tasten | Reset-Taste, Boot-Taste, Custom-Taste |
| Schnittstellen | Type-C Schnittstelle, Batterie-Schnittstelle |
| Encoder | Mit Tastenfunktion, ohne Pin (Pin-Größe: 0,8mm*0,8mm) |
| Betriebsspannung | Modul: DC5V, Hauptchip: 3,3V |
| Aktiver Bereich | 32,51*32,51mm |
| Abmessungen | 42*42*9,8mm |
| Nettogewicht | 15g |
Digitale Uhr auf dem CrowPanel 1,28″ Runddisplay
In diesem Abschnitt implementieren wir eine digitale Uhr auf dem CrowPanel 1,28″ Runddisplay. Die Uhr zeigt den Namen des aktuellen Tages, die Uhrzeit und das Datum an. Außerdem kannst du durch Berühren eines Buttons auf dem Bildschirm zwischen 24-Stunden- und 12-Stunden-Format wechseln. Das Bild unten zeigt das Zifferblatt mit seinen Funktionen.

Zusätzlich kann die Uhr durch Drücken der physischen Taste rechts (Rückseite) des Moduls in den Deep-Sleep-Modus versetzt werden. Ein zweiter Druck weckt die Uhr wieder auf. Das ist wichtig, wenn du die Uhr mit Batteriebetrieb verwenden möchtest.
Wir geben außerdem jede Stunde einen Ton aus und bieten haptisches Feedback, wenn der Touch-Button gedrückt wird, indem der Vibrationsmotor aktiviert wird.
Schließlich synchronisiert die Uhr die interne Echtzeituhr (RTC) automatisch mit einem Internet-Zeitserver (SNTP), sofern WLAN verfügbar ist.
Damit haben wir die meisten technischen Funktionen des CrowPanel Displays erkundet – abgesehen von Bluetooth und dem Encoder.
Code für die digitale Uhr auf dem GC9A01 Display
Das Display des CrowPanel 1,28″ ist ein GC9A01 Display mit einem CST816D Touchscreen. Wir verwenden die Adafruit_GC9A01A Bibliothek zum Zeichnen auf dem Display, da sie viel einfacher ist als die LVGL Bibliothek, die für das demo code verwendet wird, das mit dem CrowPanel Display geliefert wird.
Wenn du jedoch eine komplexere Benutzeroberfläche mit Dropdown-Menüs und anderen Funktionen implementieren möchtest, ist die LVGL Bibliothek der richtige Weg.
Unten findest du den Code für die digitale Uhr. Es ist ein größeres Code-Stück, daher empfehle ich, es erst einmal grob zu überfliegen. Die Details besprechen wir anschließend.
#include "WiFi.h"
#include "Adafruit_GFX.h"
#include "Adafruit_GC9A01A.h"
#include "I2C_BM8563.h"
#include "CST816D.h"
// I2C
#define SDA 4
#define SCL 5
#define PI4IO_I2C_ADDR 0x43
// TFT display
#define TFT_CS 10
#define TFT_DC 2
#define TFT_MOSI 7
#define TFT_SCLK 6
#define TFT_RST 4
#define TFT_BKL 2
#define DW 240
#define DH 240
// Touch panel
#define TP_INT 0
#define TP_RST 3
#define BUTTON 1
#define BUZZER 3
#define MOTOR 0
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
bool is_24h = true;
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* DAYSTR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
const char* MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
// touch button
const int bx = DW / 2;
const int by = DH - 30;
const int br = 10;
Adafruit_GC9A01A tft(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK);
I2C_BM8563 rtc(I2C_BM8563_DEFAULT_ADDRESS, Wire);
CST816D touch(SDA, SCL, TP_RST, TP_INT);
void init_ioex() {
Wire.begin(SDA, SCL);
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x01); // test register
Wire.endTransmission();
Wire.requestFrom(PI4IO_I2C_ADDR, 1);
uint8_t rxdata = Wire.read();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x03);
Wire.write((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4));
Wire.endTransmission();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x07);
Wire.write(~((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4)));
Wire.endTransmission();
}
void write_ioex(uint8_t pin_number, bool value) {
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // test register
Wire.endTransmission();
Wire.requestFrom(PI4IO_I2C_ADDR, 1);
uint8_t rxdata = Wire.read();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // Output register
if (!value)
Wire.write((~(1 << pin_number)) & rxdata);
else
Wire.write((1 << pin_number) | rxdata);
Wire.endTransmission();
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // test register
Wire.endTransmission();
Wire.requestFrom(PI4IO_I2C_ADDR, 1);
rxdata = Wire.read();
}
void set_rtc_time() {
rtc.begin();
struct tm t;
getLocalTime(&t);
I2C_BM8563_TimeTypeDef ts;
ts.hours = t.tm_hour;
ts.minutes = t.tm_min;
ts.seconds = t.tm_sec;
rtc.setTime(&ts);
I2C_BM8563_DateTypeDef ds;
ds.weekDay = t.tm_wday;
ds.month = t.tm_mon + 1;
ds.date = t.tm_mday;
ds.year = t.tm_year + 1900;
rtc.setDate(&ds);
}
void sync_time() {
WiFi.begin(SSID, PWD);
for (int i = 0; (i < 5) && (WiFi.status() != WL_CONNECTED); i++)
delay(100);
if (WiFi.status() == WL_CONNECTED) {
configTzTime(TIMEZONE, "pool.ntp.org");
set_rtc_time();
}
}
void display_time() {
static char buf[16];
static I2C_BM8563_DateTypeDef ds;
static I2C_BM8563_TimeTypeDef ts;
rtc.getDate(&ds);
rtc.getTime(&ts);
tft.setTextColor(GC9A01A_LIGHTGREY, GC9A01A_BLACK);
tft.setCursor(80, 60);
tft.setTextSize(4);
tft.print(DAYSTR[ds.weekDay]);
if (is_24h) {
sprintf(buf, "%02d:%02d:%02d", ts.hours, ts.minutes, ts.seconds);
} else {
int display_hours = ts.hours % 12;
display_hours = display_hours ? display_hours : 12;
const char* period = ts.hours >= 12 ? "pm" : "am";
sprintf(buf, "%02d:%02d %s", display_hours, ts.minutes, period);
}
tft.setTextColor(is_24h ? GC9A01A_WHITE : GC9A01A_YELLOW, GC9A01A_BLACK);
tft.setCursor(45, 110);
tft.setTextSize(3);
tft.print(buf);
sprintf(buf, "%2d %s %04d", ds.date, MONTHSTR[ds.month], ds.year);
tft.setTextColor(GC9A01A_WHITE, GC9A01A_BLACK);
tft.setCursor(45, 150);
tft.setTextSize(2);
tft.print(buf);
}
void sound_hour() {
static I2C_BM8563_TimeTypeDef ts;
rtc.getTime(&ts);
if (ts.minutes == 0 && ts.seconds == 0) {
tone(BUZZER, 1000);
delay(50);
tone(BUZZER, 0);
delay(500);
}
}
void check_touch() {
uint8_t gesture;
uint16_t x, y;
bool touched = touch.getTouch(&x, &y, &gesture);
if (touched) {
if (abs(x - bx) < 2*br && abs(y - by) < 2*br) {
write_ioex(MOTOR, true);
delay(50);
write_ioex(MOTOR, false);
is_24h = !is_24h;
display_touch();
delay(500);
}
}
}
void display_touch() {
if (is_24h) {
tft.fillCircle(bx, by, br, GC9A01A_BLACK);
tft.drawCircle(bx, by, br, GC9A01A_DARKGREY);
} else {
tft.fillCircle(bx, by, br, GC9A01A_DARKGREY);
}
}
void check_button() {
if (digitalRead(BUTTON) == LOW) {
tft.fillScreen(GC9A01A_BLACK);
write_ioex(TFT_BKL, false); // Switch off TFT backlight
delay(300);
esp_deep_sleep_start();
}
}
void init_deep_sleep() {
pinMode(BUTTON, INPUT);
esp_deep_sleep_enable_gpio_wakeup(1ULL << BUTTON, ESP_GPIO_WAKEUP_GPIO_LOW);
}
void init_buzzer() {
pinMode(BUZZER, OUTPUT);
digitalWrite(BUZZER, LOW);
}
void init_tft() {
tft.begin();
tft.setRotation(0);
tft.fillScreen(GC9A01A_BLACK);
display_touch();
}
void set_ioex_pins() {
write_ioex(TFT_RST, true); // TFT Reset
write_ioex(TP_RST, true); // Touch panel reset
write_ioex(TFT_BKL, true); // TFT backlight
}
void setup() {
Serial.begin(115200);
init_ioex();
set_ioex_pins();
touch.begin();
init_buzzer();
init_deep_sleep();
sync_time();
init_tft();
}
void loop() {
static unsigned long st = millis();
if (millis() - st > 300) {
display_time();
sound_hour();
st = millis();
}
check_touch();
check_button();
yield();
}
Lass uns den Code in seine Komponenten aufteilen, um ihn besser zu verstehen.
Bibliotheken
Der Code beginnt mit dem Einbinden der notwendigen Bibliotheken für WiFi, Grafik, Displaysteuerung, Echtzeituhr (RTC) und Touchpanel-Funktionalität.
#include "WiFi.h" #include "Adafruit_GFX.h" #include "Adafruit_GC9A01A.h" #include "I2C_BM8563.h" #include "CST816D.h"
Du musst die Adafruit_GC9A01A Bibliothek installieren. Öffne einfach den Library Manager, suche nach „Adafruit_GC9A01A“ und klicke auf „INSTALL“. Das Bild unten zeigt, wie das nach der Installation aussehen sollte:

Außerdem benötigst du die Dateien für die Echtzeituhr I2C_BM8563 und den Touchscreen CST816D. Lade die Arduino Demo Code herunter, entpacke sie und gehe in den Ordner ESP32_1.28_Arduino_Demo. Dort sollten folgende Unterordner enthalten sein:

In den Unterordnern LvglWidgets und RTC findest du die benötigten Dateien. Kopiere sie in den Projektordner für deinen digitalen Uhr-Code. Der Projektordner sollte so aussehen:

Wie du siehst, habe ich die Arduino-Sketch-Datei „CrowPanel-Digital-Clock-1-28-Inch.ino“ genannt, und da der Ordnername mit dem Sketch übereinstimmen muss, heißt er „CrowPanel-Digital-Clock-1-28-Inch“. Du kannst ihn aber auch anders benennen, achte nur darauf, dass Sketch- und Ordnername übereinstimmen.
Um es dir einfacher zu machen, habe ich mein Projekt auch gezippt und du kannst es über den folgenden Link herunterladen:
Konstanten
Als nächstes definieren wir Konstanten für die I2C-Kommunikation, TFT-Display-Pins und Touchpanel-Pins.
#define SDA 4 #define SCL 5 #define PI4IO_I2C_ADDR 0x43 #define TFT_CS 10 #define TFT_DC 2 #define TFT_MOSI 7 #define TFT_SCLK 6 #define TFT_RST 4 #define TFT_BKL 2 #define DW 240 #define DH 240 // Touch panel #define TP_INT 0 #define TP_RST 3 #define BUTTON 1 #define BUZZER 3 #define MOTOR 0
Diese Pin-Definitionen findest du im Beispielcode und in der Schematics of the CrowPanel 1.28″ Display. Besonders interessant ist der Abschnitt, der den TFT- und Touchpanel-Anschluss zeigt:

Wichtig ist, dass das CrowPanel-Modul einen GPIO-Expander (PI4IOE5V6408) enthält, da das TFT-Display die meisten Pins des ESP32 belegt. Das bedeutet, dass einige Pins GPIO-Pins des ESP32 sind, z.B. (BUZZER), und andere über den GPIO-Expander gesteuert werden. Konkret sind das LCD_RESET (TFT_RST=4), TP_RESET (TP_RST=3), die Hintergrundbeleuchtung LED_P2 (TFT_BKL=2) und der Vibrationsmotor MOTOR_P0 (MOTOR=0). Sieh dir den Schaltplan für den GPIO-Expander unten an:

Im Code wirst du später sehen, dass ESP32-Pins und GPIO-Expander-Pins unterschiedlich gesteuert werden müssen.
Globale Variablen
Wir deklarieren mehrere globale Variablen, darunter WiFi-Zugangsdaten, Zeitzoneneinstellungen und Arrays für Tages- und Monatsnamen. Außerdem definieren wir eine boolesche Variable, um zwischen 12-Stunden- und 24-Stunden-Format umzuschalten.
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
bool is_24h = true;
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* DAYSTR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
const char* MONTHSTR[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
Beachte, dass du die WiFi-Zugangsdaten (SSID, PWD) durch die Daten deines eigenen Netzwerks ersetzen musst.
Wahrscheinlich möchtest du auch die TIMEZONE ändern. Ich verwende AEST-10AEDT,M10.1.0,M4.1.0/3, was Melbourne, Australien entspricht. Dieser String gibt die Standardzeitverschiebung und die Regeln für die Sommerzeit an.
Die Bestandteile dieser Zeitzonendefinition sind:
- 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 zur UTC.
Für andere Zeitzonendefinitionen schau dir die Posix Timezones Database an. Kopiere einfach den dortigen String und passe die TIMEZONE Konstante entsprechend an.
Die übrigen Konstanten sind nur Strings für die Namen der Tage und Monate. Du könntest sie durch Strings in einer anderen Sprache ersetzen, musst aber die Längen ungefähr beibehalten.
Initialisierungsfunktionen
Der Code enthält mehrere Funktionen zur Initialisierung verschiedener Komponenten. Die init_ioex() Funktion richtet die I2C-Kommunikation ein und konfiguriert den I/O-Expander.
void init_ioex() {
Wire.begin(SDA, SCL);
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x01); // test register
...
}
Dieser Code stammt aus der Demo-Datei LvglWidgets.ino. Ich habe den Namen leicht geändert und einige Print-Anweisungen entfernt, ansonsten ist es derselbe Code.
Gleiches gilt für die zugehörige write_ioex() Funktion unten, mit der du auf die GPIO-Pins des IO-Expanders schreiben kannst:
void write_ioex(uint8_t pin_number, bool value) {
Wire.beginTransmission(PI4IO_I2C_ADDR);
Wire.write(0x05); // test register
...
}
Die init_tft() Funktion setzt die Standardwerte für das Zeichnen auf dem TFT-Display und zeichnet auch den weißen Kreis, der den Touch-Button darstellt, indem sie display_touch() aufruft:
void init_tft() {
tft.begin();
tft.setRotation(0);
tft.fillScreen(GC9A01A_BLACK);
display_touch();
}
Die init_deep_sleep() richtet die Taste ein, die den ESP32 aus dem Deep-Sleep-Modus weckt:
void init_deep_sleep() {
pinMode(BUTTON, INPUT);
esp_deep_sleep_enable_gpio_wakeup(1ULL << BUTTON, ESP_GPIO_WAKEUP_GPIO_LOW);
}
Und schließlich richtet die init_buzzer() Funktion den Summer-Pin ein.
void init_buzzer() {
pinMode(BUZZER, OUTPUT);
digitalWrite(BUZZER, LOW);
}
RTC und Zeitsynchronisation
Die set_rtc_time() Funktion initialisiert die RTC und setzt die aktuelle Zeit und das Datum basierend auf der lokalen Zeit, die über die WiFi-Verbindung abgerufen wird.
void set_rtc_time() {
rtc.begin();
struct tm t;
getLocalTime(&t);
// Set RTC time and date...
}
Diese lokale Zeit wird über einen Internet-Zeitserver (SNTP) mit der sync_time() Funktion synchronisiert:
void sync_time() {
WiFi.begin(SSID, PWD);
for (int i = 0; (i < 5) && (WiFi.status() != WL_CONNECTED); i++)
delay(100);
if (WiFi.status() == WL_CONNECTED) {
configTzTime(TIMEZONE, "pool.ntp.org");
set_rtc_time();
}
}
Beachte, dass die Funktion 5 Mal versucht, sich mit dem WiFi zu verbinden, und dann aufgibt. Das ist Absicht! Wenn du die Uhr außerhalb deines WLANs benutzt, kann sie sich beim Neustart nicht verbinden und synchronisieren. Das ist in Ordnung, da die RTC die Zeit weiterführt.
Da wir nur beim Neustart des ESP32 synchronisieren und die RTC keine Sommerzeit kennt, wird die Uhr den Wechsel zwischen Sommer- und Normalzeit verpassen. Es gibt Möglichkeiten, das zu umgehen. Schau dir das Real-Time-Clock DS3231 with ESP32 Tutorial an.
Anzeige der Zeit
Die display_time() Funktion liest die aktuelle Zeit und das Datum von der RTC aus und zeigt sie an den passenden Stellen auf dem TFT-Bildschirm an. Je nach Status der is_24h-Variable wird die Zeit im 12- oder 24-Stunden-Format formatiert.
void display_time() {
static char buf[16];
static I2C_BM8563_DateTypeDef ds;
static I2C_BM8563_TimeTypeDef ts;
...
if (is_24h) {
sprintf(buf, "%02d:%02d:%02d", ts.hours, ts.minutes, ts.seconds);
} else {
int display_hours = ts.hours % 12;
display_hours = display_hours ? display_hours : 12;
const char* period = ts.hours >= 12 ? "pm" : "am";
sprintf(buf, "%02d:%02d %s", display_hours, ts.minutes, period);
}
...
}
Beachte, dass du nicht einfach eine andere Schriftart für Zeit und Datum verwenden kannst. Die Adafruit_GC9A01A Bibliothek erlaubt es, beim Textdruck eine Hintergrundfarbe zu setzen, die die vorherige Zeit- und Datumsanzeige löscht.
tft.setTextColor(GC9A01A_WHITE, GC9A01A_BLACK); ... tft.print(buf);
Das funktioniert aber nur mit der Standardschriftart, nicht mit proportionalen Fonts. Mehr Infos findest du im Adafruit GFX Graphics Library Documentation. Eine Alternative wäre die Verwendung eines Canvas, das ist aber komplexer und die Aktualisierung des TFT-Displays könnte langsam sein.
Touch-Button
Die check_touch() Funktion erkennt Berührungen auf dem Touchpanel. Wenn die Berührung nahe am definierten Button-Bereich (dem weißen Kreis) liegt, wird der Vibrationsmotor kurz aktiviert, um haptisches Feedback zu geben.
void check_touch() {
uint8_t gesture;
uint16_t x, y;
bool touched = touch.getTouch(&x, &y, &gesture);
if (touched) {
if (abs(x - bx) < 2*br && abs(y - by) < 2*br) {
write_ioex(MOTOR, true);
delay(50);
write_ioex(MOTOR, false);
is_24h = !is_24h;
display_touch();
delay(500);
}
}
}
Anschließend wird das Zeitformat umgeschaltet und die Anzeige aktualisiert. Das folgende Bild zeigt die beiden Zustände der Anzeige:

Der kleine weiße Kreis unten auf dem Bildschirm stellt den Touch-Button dar und wird von der display_touch() Funktion gezeichnet. Je nach Zustand wird er als gefüllter oder leerer Kreis dargestellt:
void display_touch() {
if (is_24h) {
tft.fillCircle(bx, by, br, GC9A01A_BLACK);
tft.drawCircle(bx, by, br, GC9A01A_DARKGREY);
} else {
tft.fillCircle(bx, by, br, GC9A01A_DARKGREY);
}
}
Deep-Sleep-Taste
Die check_button() Funktion prüft, ob die physische Taste an der Seite des Moduls gedrückt wird. Wenn ja, versetzt sie das Gerät in den Deep-Sleep-Modus. Wichtig ist dabei, dass auch die Hintergrundbeleuchtung des TFT-Displays ausgeschaltet wird, um Batterie zu sparen.
void check_button() {
if (digitalRead(BUTTON) == LOW) {
tft.fillScreen(GC9A01A_BLACK);
write_ioex(TFT_BKL, false); // Switch off TFT backlight
delay(300);
esp_deep_sleep_start();
}
}
Ein zweiter Tastendruck weckt den ESP32 wieder auf. Beachte, dass diese Taste einen internen Pullup-Widerstand hat und beim Drücken auf LOW geht. Siehe den Schaltplan unten:

Setup-Funktionen
Die setup() Funktion initialisiert die serielle Kommunikation, den I/O-Expander, das Touchpanel, den Summer und das TFT-Display und synchronisiert die Zeit.
void setup() {
Serial.begin(115200);
init_ioex();
...
}
Das bedeutet, die Uhr wird nur beim Neustart mit der Internetzeit synchronisiert. Das spart Batterie, da du das WLAN nach dem Neustart abschalten kannst. Du könntest aber auch eine häufigere Synchronisation einbauen, z.B. stündlich, um den Wechsel zur Sommerzeit zu erfassen.
Loop-Funktion
Die loop() Funktion aktualisiert kontinuierlich alle 300 Millisekunden die Anzeige von Zeit und Datum und prüft Touch-Events sowie den Tastenzustand. Du könntest statt der yield() Funktion auch eine kurze Verzögerung von 50 ms einbauen.
void loop() {
static unsigned long st = millis();
if (millis() - st > 300) {
display_time();
sound_hour();
st = millis();
}
check_touch();
check_button();
yield();
}
Demo der digitalen Uhr
Der folgende kurze Videoclip zeigt die Uhr in Aktion. Du siehst die laufende Zeit, den Wechsel zwischen 24h- und 12h-Format beim Drücken des Touch-Buttons und den Deep-Sleep-Modus:

Und da hast du es! Wir haben die meisten Funktionen des CrowPanel 1,28″ Displays erkundet und du solltest nun einen guten Ausgangspunkt haben, um deine eigene Uhr mit ihren einzigartigen Features zu implementieren.
Fazit
In diesem Tutorial hast du gelernt, wie man eine digitale Uhr auf einem CrowPanel 1.28″ Display Module implementiert. Wir haben viele Funktionen des Display-Moduls genutzt, darunter den Touchscreen, den Summer, den Vibrationsmotor und die RTC. Außerdem haben wir gelernt, wie man die RTC mit einem Internet-Zeitserver synchronisiert und die Uhr in den Deep-Sleep-Modus versetzt.
Wenn du mehr über Zeitsynchronisation lernen möchtest, schau dir das How to synchronize ESP32 clock with SNTP server Tutorial an, und das Real-Time-Clock DS3231 with ESP32 Tutorial hilft dir, Unterstützung für Sommerzeit zu implementieren.
Und wenn du eine analoge Uhr umsetzen möchtest, findest du im Analog Clock on e-Paper Display nützliche Informationen, auch wenn es eher für ein E-Paper- als für ein TFT-Display gedacht ist.
Ansonsten gibt es viele Funktionen, die du deiner Uhr hinzufügen könntest. Eine offensichtliche wäre die Anzeige von Wetterinformationen. Schau dir das Simple ESP32 Internet Weather Station Tutorial als Beispiel an. Dort gibt es auch Infos, wie man connect the CrowPanel Module to Home Assistant.
Wenn du Fragen hast, hinterlasse sie gerne im Kommentarbereich.
Viel Spaß und fröhliches Tüfteln ; )
Links
Hier einige Links, die ich beim Schreiben dieses Tutorials nützlich fand:

