GC9A01 Runde Uhr mit DS3231 bauen: Analoge Zeiger & digitales Ziffernblatt
Ein rundes Display, das aussieht wie ein echtes Uhren-Zifferblatt, kombiniert mit einer Echtzeituhr, die auch nach dem Ausstecken die Zeit auf die Sekunde genau hält – das ergibt eines der schönsten Anfänger-Projekte überhaupt. Das GC9A01 ist ein 1,28-Zoll-Runddisplay mit 240×240 Pixeln, das DS3231 ist die genaueste günstige RTC, die du bekommst. In diesem Tutorial verbindest du beide, setzt einmalig die Uhrzeit, zeichnest ein Zifferblatt, lässt analoge Zeiger laufen und baust am Ende eine kombinierte Anzeige aus Analoguhr und digitaler Ziffernanzeige. Fertiger Code, saubere SPI-Verkabelung und alle Fallstricke inklusive.
Warum gerade diese Kombination?
Runde Displays wie das GC9A01 haben einen enormen Charme: Weil sie kreisrund sind, wirkt eine Analoguhr darauf nicht wie ein Notbehelf, sondern wie das echte Vorbild. Kein eckiger Rahmen, kein abgeschnittener Kreis – das Ziffernblatt füllt die gesamte Fläche.
Aber ein Display allein kennt keine Zeit. Der Mikrocontroller würde nach jedem Neustart bei null anfangen. Genau hier kommt die DS3231 Echtzeituhr ins Spiel. Sie hat einen eigenen, temperaturkompensierten Quarz und eine Knopfzellen-Batterie. Dadurch tickt sie autark weiter, selbst wenn dein Arduino längst stromlos ist. Ihre Abweichung liegt bei nur rund zwei Minuten pro Jahr – eine gewöhnliche RTC schafft das bei Weitem nicht.
ℹ️ Zwei Busse, ein Projekt: Das GC9A01 spricht über SPI (schnell, viele Leitungen – gut für Grafik), das DS3231 über I2C (nur zwei Leitungen – gut für Sensoren und Uhren). Die beiden stören sich nicht, weil sie getrennte Pins nutzen. Du bedienst also in diesem Projekt beide wichtigen Bus-Systeme gleichzeitig – ein toller Lerneffekt nebenbei.
Das brauchst du
💡 ESP32 oder Arduino UNO? Für flüssige Grafik empfehlen wir den ESP32: Er ist deutlich schneller und hat genug RAM für einen Bildpuffer (Sprite), was flackerfreie Zeiger ermöglicht. Der UNO funktioniert auch, ist aber langsamer und hat wenig RAM – dort zeichnest du sparsamer. Beide Wege zeigen wir dir.
1Verkabelung: SPI fürs Display, I2C für die Uhr
Das GC9A01 wird über SPI angeschlossen. Die Pin-Bezeichnungen auf dem Modul sind etwas eigen: SCL ist die SPI-Clock, SDA ist der Dateneingang (MOSI). Hier die Verdrahtung für den ESP32:
| GC9A01 (Display) | ESP32 | Bedeutung |
|---|---|---|
| VCC | 3V3 | Versorgung |
| GND | GND | Masse |
| SCL | GPIO 18 | SPI-Clock (SCK) |
| SDA | GPIO 23 | SPI-Daten (MOSI) |
| DC | GPIO 2 | Data/Command |
| CS | GPIO 15 | Chip Select |
| RST | GPIO 4 | Reset |
| BLK | 3V3 | Hintergrundbeleuchtung |
Das DS3231 hängt am I2C-Bus – nur zwei Datenleitungen:
| DS3231 (Uhr) | ESP32 | Arduino UNO |
|---|---|---|
| VCC | 3V3 | 5V |
| GND | GND | GND |
| SDA | GPIO 21 | A4 |
| SCL | GPIO 22 | A5 |
⚠️ GC9A01 will 3,3-V-Logik: Das Display ist für 3,3 V ausgelegt. Am ESP32 passt das perfekt. Am 5-V-Arduino UNO solltest du die Signalleitungen (SCL, SDA, DC, CS, RST) idealerweise über einen Pegelwandler oder zumindest Vorwiderstände führen – viele Module vertragen 5-V-Signale zwar, sauberer und langlebiger ist aber 3,3-V-Pegel.
2Zuerst die Uhr: DS3231 auslesen und Zeit setzen
Bevor wir zeichnen, sorgen wir dafür, dass die Uhr die richtige Zeit kennt. Dafür nutzen wir die beliebte RTClib von Adafruit (über den Bibliotheksverwalter installieren). Der folgende Sketch setzt die Uhr einmalig auf die Compile-Zeit deines PCs und gibt danach die Zeit im seriellen Monitor aus.
#include <Wire.h>
#include "RTClib.h"
RTC_DS3231 rtc;
void setup() {
Serial.begin(115200);
if (!rtc.begin()) {
Serial.println("DS3231 nicht gefunden – Verkabelung pruefen!");
while (1) delay(100);
}
// Setzt die Uhr auf den Zeitpunkt des Kompilierens.
// NUR EINMAL aktiv lassen, danach auskommentieren!
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
void loop() {
DateTime jetzt = rtc.now();
Serial.printf("%02d:%02d:%02d\n",
jetzt.hour(), jetzt.minute(), jetzt.second());
delay(1000);
}
⚠️ Die adjust()-Zeile danach auskommentieren! Solange rtc.adjust(...) im Code steht, wird die Uhr bei jedem Neustart wieder auf die (dann veraltete) Compile-Zeit zurückgesetzt. Setze die Zeit einmal, kommentiere die Zeile aus und lade den Sketch erneut hoch. Ab jetzt läuft die DS3231 dank ihrer Batterie autark weiter – auch nach dem Ausstecken. Mehr Details, etwa zum Wecker und Temperatursensor der DS3231, im Artikel DS3231 RTC Echtzeituhr mit Arduino.
3Das Display zum Leben erwecken
Fürs Zeichnen nutzen wir die TFT_eSPI-Bibliothek – sie ist schnell und unterstützt das GC9A01 direkt. Nach der Installation musst du einmalig die Datei User_Setup.h (im Bibliotheksordner) an dein Display anpassen. Die entscheidenden Zeilen:
// In User_Setup.h der TFT_eSPI-Bibliothek:
#define GC9A01_DRIVER
#define TFT_WIDTH 240
#define TFT_HEIGHT 240
// Pins (ESP32) – an deine Verkabelung anpassen:
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 4
Danach testest du das Display mit einem Minimal-Sketch, der es einfärbt und einen Text ausgibt:
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
void setup() {
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE);
tft.setTextDatum(MC_DATUM); // Text zentrieren
tft.drawString("MAKEROO", 120, 120, 4);
}
void loop() {}
Erscheint "MAKEROO" mittig auf schwarzem Grund, sind Verkabelung und Setup korrekt. Die Koordinate 120, 120 ist der Mittelpunkt des 240×240-Displays – den brauchen wir gleich für die Uhr. Wie du auf dem GC9A01 auch Zeiger, Bögen und Farbverläufe zeichnest, vertieft der Artikel GC9A01 rundes TFT-Display ansteuern.
4Das Ziffernblatt zeichnen
Ein Zifferblatt besteht aus einem Kreis und zwölf Stundenmarkierungen. Die Positionen der Markierungen berechnen wir mit Sinus und Kosinus: Der Vollkreis hat 360 Grad, geteilt durch 12 Stunden ergibt 30 Grad Abstand. Diese Funktion zeichnet das statische Ziffernblatt einmal in den Hintergrund:
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
const int CX = 120; // Mittelpunkt X
const int CY = 120; // Mittelpunkt Y
const int R = 118; // Radius
void zeichneZifferblatt() {
tft.fillScreen(TFT_BLACK);
tft.fillCircle(CX, CY, R, TFT_BLACK);
tft.drawCircle(CX, CY, R, TFT_DARKGREY);
// 12 Stundenstriche
for (int i = 0; i < 12; i++) {
float winkel = i * 30 * DEG_TO_RAD; // 30 Grad je Stunde
int x1 = CX + sin(winkel) * (R - 4);
int y1 = CY - cos(winkel) * (R - 4);
int x2 = CX + sin(winkel) * (R - 18);
int y2 = CY - cos(winkel) * (R - 18);
tft.drawLine(x1, y1, x2, y2, TFT_WHITE);
}
// dickerer Punkt in der Mitte
tft.fillCircle(CX, CY, 4, TFT_WHITE);
}
void setup() {
tft.init();
tft.setRotation(0);
zeichneZifferblatt();
}
void loop() {}
ℹ️ Warum sin() für x und -cos() für y? Auf dem Display zeigt die Y-Achse nach unten. Damit die 12 oben und die 3 rechts landet (wie bei einer echten Uhr), nutzen wir x = sin(winkel) und y = -cos(winkel). So läuft der Winkel 0° nach oben und im Uhrzeigersinn weiter – genau richtig für Uhrzeiger.
5Die Zeiger laufen lassen
Jetzt kombinieren wir alles: Wir lesen die Zeit aus der DS3231 und zeichnen daraus die drei Zeiger. Der Trick gegen Flackern: Wir merken uns die alte Zeigerposition und übermalen den alten Zeiger mit Schwarz, bevor wir den neuen zeichnen – das Ziffernblatt selbst bleibt stehen.
#include <TFT_eSPI.h>
#include <Wire.h>
#include "RTClib.h"
TFT_eSPI tft = TFT_eSPI();
RTC_DS3231 rtc;
const int CX = 120, CY = 120, R = 118;
int altS = -1, altM = -1, altH = -1;
void zeichneZifferblatt() {
tft.fillScreen(TFT_BLACK);
tft.drawCircle(CX, CY, R, TFT_DARKGREY);
for (int i = 0; i < 12; i++) {
float w = i * 30 * DEG_TO_RAD;
tft.drawLine(CX + sin(w)*(R-4), CY - cos(w)*(R-4),
CX + sin(w)*(R-18), CY - cos(w)*(R-18), TFT_WHITE);
}
}
// zeichnet einen Zeiger in gegebener Farbe
void zeiger(float winkelGrad, int laenge, uint16_t farbe) {
float w = winkelGrad * DEG_TO_RAD;
int x = CX + sin(w) * laenge;
int y = CY - cos(w) * laenge;
tft.drawLine(CX, CY, x, y, farbe);
}
void setup() {
tft.init();
tft.setRotation(0);
Wire.begin();
rtc.begin();
zeichneZifferblatt();
}
void loop() {
DateTime t = rtc.now();
int s = t.second();
int m = t.minute();
int h = t.hour() % 12;
// nur neu zeichnen, wenn sich die Sekunde geaendert hat
if (s != altS) {
// alte Zeiger loeschen (in Schwarz uebermalen)
if (altS >= 0) {
zeiger(altS * 6, 100, TFT_BLACK);
zeiger(altM * 6, 80, TFT_BLACK);
zeiger(altH * 30 + altM * 0.5, 55, TFT_BLACK);
zeichneZifferblattStriche(); // Striche ggf. auffrischen
}
// neue Zeiger zeichnen
zeiger(h * 30 + m * 0.5, 55, TFT_WHITE); // Stunde
zeiger(m * 6, 80, TFT_CYAN); // Minute
zeiger(s * 6, 100, TFT_RED); // Sekunde
tft.fillCircle(CX, CY, 4, TFT_WHITE);
altS = s; altM = m; altH = h;
}
}
void zeichneZifferblattStriche() {
for (int i = 0; i < 12; i++) {
float w = i * 30 * DEG_TO_RAD;
tft.drawLine(CX + sin(w)*(R-4), CY - cos(w)*(R-4),
CX + sin(w)*(R-18), CY - cos(w)*(R-18), TFT_WHITE);
}
}
Die Winkelberechnung ist der Kern: Die Sekunde (0–59) mal 6 ergibt 0–354 Grad (60×6=360). Die Stunde bekommt zusätzlich m * 0.5, damit der Stundenzeiger sich – wie bei einer echten Uhr – im Lauf der Stunde langsam weiterbewegt und nicht ruckartig springt.
💡 Flackerfrei mit Sprites (ESP32): Das Übermalen funktioniert, hinterlässt aber manchmal kleine Spuren. Die Profi-Lösung auf dem ESP32 ist ein Sprite: Du zeichnest das komplette Bild zuerst in einen RAM-Puffer und schiebst es dann in einem Rutsch aufs Display. Das ergibt absolut flimmerfreie, butterweiche Zeiger. Auf dem UNO fehlt dafür der RAM – dort bleibst du beim Übermalen.
6Analog trifft digital: die kombinierte Anzeige
Viele moderne Smartwatches zeigen analoge Zeiger und eine digitale Uhrzeit. Das geht auf dem GC9A01 wunderbar: Wir setzen die Digitalzeit ins untere Drittel des Zifferblatts. Diese Zeilen ergänzen den loop() aus Schritt 5:
// digitale Zeit unter der Mitte anzeigen
void zeigeDigital(DateTime t) {
char buf[9];
sprintf(buf, "%02d:%02d:%02d", t.hour(), t.minute(), t.second());
// Bereich erst schwarz fuellen, dann Text
tft.fillRect(60, 150, 120, 24, TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString(buf, 120, 162, 4);
}
Ruf zeigeDigital(t) im Sekundentakt auf, und schon hast du unter den laufenden Zeigern eine gestochen scharfe Digitalanzeige. Willst du zusätzlich das Datum, nutzt du t.day(), t.month() und t.year() und schreibst sie in eine zweite Zeile. Die DS3231 liefert dir all das zuverlässig mit.
🎨 Ideen zum Weiterbauen
-
Temperatur anzeigen: Die DS3231 hat einen eingebauten Temperatursensor –
rtc.getTemperature()gibt ihn dir gratis - Farbwechsel nach Tageszeit: tags helles, nachts gedämpftes Zifferblatt
- Wecker mit Signal: Die DS3231 kann Alarme auslösen, die du auf einem Pin abfragst
- Sekundenzeiger als Farbverlauf über die Minute animieren
Perfekt für: Tischuhr, Wetterstation mit Uhr, Retro-Wecker
Troubleshooting
⚠️ Display bleibt weiß oder schwarz: Fast immer ein Pin- oder Setup-Fehler. Prüfe, ob User_Setup.h wirklich GC9A01_DRIVER aktiviert hat und die Pin-Nummern zu deiner Verkabelung passen. Ein weißes Bild bedeutet oft: RST oder DC falsch angeschlossen.
⚠️ DS3231 nicht gefunden: Lass einen I2C-Scanner laufen – das DS3231 meldet sich meist unter Adresse 0x68. Erscheint nichts, sind SDA/SCL vertauscht oder VCC fehlt. Wie du den Bus systematisch prüfst, steht im Artikel I2C-Bus mit Arduino verstehen.
⚠️ Zeit springt nach jedem Neustart zurück: Die rtc.adjust()-Zeile steht noch aktiv im Code. Auskommentieren und neu hochladen. Bleibt das Problem auch dann, ist die Knopfzelle der DS3231 leer oder falsch herum eingesetzt.
⚠️ Zeiger hinterlassen Spuren: Das Übermalen trifft den alten Zeiger nicht exakt. Nutze auf dem ESP32 einen Sprite oder zeichne nach dem Löschen die Ziffernblatt-Striche neu auf (wie im Beispiel).
🚀 Alles für deine runde Uhr
Diese Teile brauchst du für das Projekt:
- GC9A01 1,28-Zoll-Rundes-TFT-Display – das Zifferblatt
- DS3231 Echtzeituhr-Modul – präzise Zeit, auch ohne Strom
- ESP32 Board (USB-C) – für flüssige Grafik
- Arduino UNO R3 – die klassische Alternative
- Breadboard 400 Kontakte – zum Aufbau
Fazit
Die Kombination aus rundem GC9A01-Display und der präzisen DS3231-Echtzeituhr ist ein Paradebeispiel dafür, wie zwei einfache Module zusammen etwas richtig Schönes ergeben. Du hast gelernt, wie du beide Bus-Systeme parallel bedienst, die Zeit dauerhaft in der RTC speicherst, ein Ziffernblatt mit Sinus und Kosinus zeichnest und die Zeiger flackerarm laufen lässt.
Die wichtigsten Erkenntnisse: Das GC9A01 spricht SPI, das DS3231 spricht I2C – sie kommen sich nicht in die Quere. Die rtc.adjust()-Zeile gehört nach dem einmaligen Setzen der Zeit auskommentiert. Und die ganze Zeigerbewegung ist am Ende nur ein bisschen Winkelrechnung. Von hier aus ist der Weg zur eigenen Smartwatch-Optik, zur Wetterstation mit Uhr oder zum stilvollen Retro-Wecker nur noch ein paar Zeilen Code entfernt. Viel Spaß beim Bauen!