Skip to Content

Digitale Uhr auf CrowPanel 1.28″ Runddisplay

Digitale Uhr auf CrowPanel 1.28″ Runddisplay

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:

Front of CrowPanel 1.28" Round Display
Vorderseite des CrowPanel 1,28″ Runddisplays (source)

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:

Back of CrowPanel 1.28" Round Display
Rückseite des CrowPanel 1,28″ Runddisplays (source)

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:

HauptchipESP32-C3
Prozessor32-Bit RISC-V Single-Core Prozessor, bis zu 160 MHz
Speicher384 KB ROM, 400 KB SRAM (16 KB für Cache), 8 KB SRAM in RTC
Größe1,28 Zoll
Auflösung240*240
Signal-SchnittstelleSPI
Touch-TypKapazitiver Touch
Panel-TypTFT LCD, IPS Panel
Farbtiefe262K
Helligkeit350 cd/m²
Blickwinkel178°
TastenReset-Taste, Boot-Taste, Custom-Taste
SchnittstellenType-C Schnittstelle, Batterie-Schnittstelle
EncoderMit Tastenfunktion, ohne Pin (Pin-Größe: 0,8mm*0,8mm)
BetriebsspannungModul: DC5V, Hauptchip: 3,3V
Aktiver Bereich32,51*32,51mm
Abmessungen42*42*9,8mm
Nettogewicht15g

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.

Clock face with features
Zifferblatt mit 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:

Adafruit_GC9A01A Library installed
Adafruit_GC9A01A Bibliothek installiert

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:

CrowPanel 1.28" Round Display demo code folder
CrowPanel 1,28″ Runddisplay Demo-Code-Ordner

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:

Digital Clock project folder
Projektordner Digitale Uhr

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:

Schematics for TFT and Touch Panel Connector
Schaltplan für TFT- und Touchpanel-Anschluss (source)

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:

Schematics for PI4IOE5V6408 GPIO Expander
Schaltplan für den PI4IOE5V6408 GPIO-Expander (source)

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:

24h and 12h states of clock display
24h- und 12h-Zustände der Uhranzeige

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:

Schaltplan für die Taste (source)

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:

Demo of the Digital Clock on CrowPanel 1.28" Display
Demo der digitalen Uhr auf dem CrowPanel 1,28″ Display

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:

CrowPanel ESP32 1,28-Zoll Runddisplay

Datenblätter