Über den Beitrag
In diesem Beitrag möchte ich euch Konzepte für einen DS3231-basierten Wecker vorstellen. Über das Echtzeituhr-Modul DS3231 habe ich schon ausführlich hier berichtet, einschließlich der Alarmfunktion. Dem Wecker habe ich nun noch einen eigenen Beitrag gewidmet, da es eine gewisse Herausforderung darstellt, Einstellungen wie die Uhrzeit, die Alarmzeit oder die Schlummerfunktion mit wenigen Knöpfen (Tastern) zu realisieren. Überdies zeige ich, wie ihr den Strombedarf so weit senken könnt, dass Ihr den Wecker mit Batterien betreiben könnt.
„Nebenbei“ lernt ihr in diesem Beitrag das Nokia 5110 Display und die EA DOG Display Serie kennen. Und ihr erfahrt, wie ihr einen Arduino Pro Mini in den Tiefschlaf schickt und wieder aufweckt. Alles in einem Beitrag!
Erwartungsmanagement: Dieser Beitrag ist keine fertige Blaupause für einen Wecker mit Platinen-Layout, 3D-Druckvorlage usw. Ich habe auch darauf verzichtet, den akustischen Alarm zu definieren. Für mich ging es darum, die aus meiner Sicht größten Stolpersteine zu adressieren. Den Rest überlasse ich eurer Kreativität.
- Vorbereitung: die „Basis-Uhr“ (Version1)
- Wecker – Version 2 mit zwei Tastern
- Wecker – Version 3 mit zwei Tastern und einem Schiebeschalter
- Strombedarf der Versionen 1 – 3
- Konzept für den Batteriebetrieb
- Wecker – Version 4 mit Nokia 5110 Display
- Wecker – Version 5 mit DOGS164 Display
- Wecker – Version 6 mit ST7567 basiertem Display
- Übertragung auf andere Boards
- Anhang 1: „Reparatur“ des Nokia 5110 Displays
- Anhang 2: Alternative Versionen des Nokia 5110 Displays
- Anhang 3: Eine einfache Uhr mit einem E-Paper Display
Vorbereitung: die „Basis-Uhr“ (Version 1)
Ich werde auf das DS3231-Modul in diesem Beitrag nicht noch einmal im Detail eingehen. Schaut hier, falls ihr euch informieren möchtet. Trotzdem starten wir zum „Warmwerden“ erst einmal mit der blanken Uhr ohne Einstellfunktionen.
Als Board verwende ich zunächst einen klassischen Arduino Nano. Ein I2C-gesteuertes LCD-Display dient zur Anzeige. Board und Display könnt ihr – solange wir nicht in den Batteriebetrieb gehen – relativ einfach durch eure Lieblingsgeräte ersetzen.
Für die Ansteuerung des DS3231-Moduls verwende ich die Bibliothek RTCLib von Adafruit. Ihr könnt sie über den Bibliotheksmanager der Arduino IDE installieren. Gleiches gilt für die Bibliothek LiquidCrystal_I2C von John Rickman, mit der ich das LCD-Display ansteuere.
Alle Komponenten laufen auf 5 Volt und werden per I2C gesteuert, was die Verkabelung denkbar einfach macht:

Das DS3231 Modul verwende ich ohne Batterie. Batterie und externe Stromversorgung ist keine gute Idee (siehe meinen DS3231-Beitrag).
Hier der zugehörige Sketch:
#include <Wire.h> #include <RTClib.h> // Adafruit library for the DS3231 #include <LiquidCrystal_I2C.h> // library for the LCD display RTC_DS3231 rtc; // create RTC_DS3231 object LiquidCrystal_I2C lcd(0x27,20,2); // create display object (address, columns, rows) void setup () { Serial.begin(115200); lcd.init(); lcd.backlight(); lcd.clear(); if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin } void loop() { if(millis()%1000 == 0){ // update date/time every second printDateTime(); delay(1); } } void printDateTime(){ DateTime now = rtc.now(); char datBuf[] = "DDD, DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm:ss"; // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time lcd.setCursor(0,0); lcd.print(datBuf); // display date lcd.setCursor(0,1); lcd.print(timeBuf); // display time }
Die aktuelle Zeit wird mit rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
eingestellt. Das ist die Systemzeit eures Computers, wenn der Sketch kompiliert wird. Da der Upload einige Zeit in Anspruch nimmt, geht die Uhr leicht nach.
Hier das Ergebnis auf dem Display:
Wecker – Version 2 mit zwei Tastern
Das Bedienkonzept
In der Version 2 machen wir aus unserer Uhr einen Wecker. Die Uhr- und die Weckzeit (im Folgenden: Alarmzeit) werden über zwei Taster eingestellt. Hier die Schaltung:

Die Taster haben Doppelfunktionen:
- Taster 1 – „Item Key“:
- 1 Sekunde drücken führt in den Zeiteinstellmodus.
- Im Zeiteinstellmodus werden die Zeiteinheiten Jahr, Monat, Tag, Stunde und Minute hintereinander eingestellt. Ein kurzes Drücken übernimmt die Einstellung und führt zur nächsten Zeiteinheit („Item“).
- Im Alarmeinstellmodus könnt ihr die Stunde und die Minute einstellen und den Alarm aktivieren.
- Mit der Übernahme der Minuten werden die Sekunden auf null gesetzt.
- Taster 2 – „Value Key“:
- 1 Sekunde drücken führt in den Alarmeineinstellmodus.
- Im Zeit- und Alarmeinstellmodus führt ein kurzes Drücken zur Erhöhung des Einstellwertes um 1 bis zu einem Maximalwert. Ist der Maximalwert überschritten, springt der Einstellwert auf das Minimum.
Zeiteinstellung
Noch einmal im Detail: Wenn ihr den „Item Key“ drückt, dann fordert euch das Display auf, dies eine Sekunde lang zu tun. Wenn die folgende Meldung erscheint, lasst ihr den Taster los:

Dann könnt ihr, wenn nötig, als Erstes das Jahr einstellen. Ein Drücken auf den „Value Key“ erhöht das Jahr um 1. Dauerhaftes Drücken erhöht den Wert in Dauerschleife. Nach 2050 springt der Wert wieder auf 2025. Ein Drücken des „Item Key“ übernimmt den Wert. Dann sind die Monate dran, dann der Tag usw. bis zu den Minuten:

Da bei der Übernahme der Minuten die Sekunden „genullt“ werden, könnt ihr die Zeit so sekundengenau einstellen.
Alarmeinstellung
Das DS3231 ist hinsichtlich der Einstellung von Alarmen sehr flexibel (siehe meinen Beitrag dazu). Ich habe mich dazu entschlossen, lediglich einen täglichen Alarm zu einer bestimmten Uhrzeit zu implementieren. Drückt ihr den „Value Key“ für eine Sekunde, geht ihr in den Alarmeinstellmodus:

Wie im Zeiteinstellmodus inkrementiert ihr den Wert mit dem „Value Key“ und übernimmt ihn mit dem „Item Key“. Das macht ihr bis zu den Minuten:

Danach erscheint die Alarmzeit und der Aktivierungsstatus auf dem Display:

Drückt ihr jetzt den „Value Key“, dann wechselt die Anzeige von „Activate: no“ zu „Activate: yes“. Mit dem „Item Key“ schließt ihr die Einstellung ab. Auf dem Display erscheint wieder die Zeit, aber der aktivierte Alarm gibt sich durch ein „A“ unten rechts zu erkennen:

Alarmereignis
Wenn der Wecker einen Alarm auslöst, blinkt die folgende Anzeige:

Die blinkende Anzeige würde euch natürlich nicht wecken. Hier seid ihr gefragt, das Konzept entsprechend zu erweitern, um euch beispielsweise mit eurem Lieblingslied wecken zu lassen. Dazu könntet ihr ein MP3-Playermodul wie den DFPlayer Mini oder den YX5300 verwenden.
Ich habe den Alarm so programmiert, dass er nach 60 Sekunden von allein ausgeht. Falls ihr nicht zu Hause seid und euer Wecker richtig Lärm macht, werden eure Nachbarn für die Funktion dankbar sein. Die maximale Alarmzeit könnt ihr beliebig verlängern (#define MAX_ALARM_DURATION
).
Ist die maximale Alarmzeit noch nicht überschritten, dann geht ihr mit einem kurzen Druck auf eine der beiden Tasten in den Schlummermodus. Die Schlummerzeit habe ich auf eine Minute voreingestellt. Sie beginnt zu dem Zeitpunkt, zu dem ihr die Taste drückt. Nach Ablauf der Schlummerzeit gibt es einen neuen Alarm.
Der Schlummermodus macht sich auf der Anzeige durch ein „S“ bemerkbar:

Um den Alarm abzuschalten, geht ihr in den Alarmeinstellmodus und wählt dort die Einstellung „Activate: no“.
Der Sketch zum Wecker (Version 2)
Hier der Sketch zur Version 2:
#include <Wire.h> #include <RTClib.h> // library for the DS3231 #include <LiquidCrystal_I2C.h> // library for the LCD display #define SNOOZE_TIME 60 // set snooze time in seconds #define SET_INCREMENT_DELAY 260 // incrementation time for settings #define MAX_ALARM_DURATION 60000 // maximum duration of an alarm in milliseconds volatile bool setItemKeyPressed = false; // flag for item key volatile bool setValueKeyPressed = false; // flag for value key volatile unsigned long lastLow; // needed to handle bouncing bool alarmOn = false; // alarm activated? const int setItemPin = 2; // choose item to set / time setting mode const int setValuePin = 3; // set (increment) value / alarm setting mode typedef enum TIME_UNIT { // needed for struct "timeElement" YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } timeUnit; typedef enum SET_MODE { // setting modes DATE_TIME, ALARM } setMode; struct timeElement{ // for date/time/alarm setting timeUnit tUnit; uint16_t tMin; // minimum value uint16_t tMax; // maximum value char tname[7]; // name to display }; timeElement tElementYear = {YEAR, 2025, 2050, "Year"}; timeElement tElementMonth = {MONTH, 1, 12, "Month"}; timeElement tElementDay = {DAY, 1, 31, "Day"}; timeElement tElementHour = {HOUR, 0, 23, "Hour"}; timeElement tElementMinute = {MINUTE, 0, 59, "Minute"}; RTC_DS3231 rtc; // // create RTC_DS3231 object DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0); LiquidCrystal_I2C lcd(0x27,20,2); // create display object (address, columns, rows) void setup () { Serial.begin(115200); lcd.init(); lcd.backlight(); lcd.clear(); if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.clearAlarm(1); // clear alarm rtc.clearAlarm(2); rtc.disableAlarm(1); // disable alarm function rtc.disableAlarm(2); rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin /* pressing the item or value button will cause a low signal at the corresponding pin and trigger an interrupt */ pinMode(setItemPin, INPUT_PULLUP); pinMode(setValuePin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING); attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING); setItemKeyPressed = false; setValueKeyPressed = false; } void loop() { if(millis()%1000 == 0){ // update date/time every second printDateTime(); delay(1); // avoids multiple updates per second } if(setItemKeyPressed){ // item key = date/time setting key goIntoSetMode(DATE_TIME); } if(setValueKeyPressed){ // value key = alarm key goIntoSetMode(ALARM); } if(rtc.alarmFired(1) && alarmOn){ alarmAction(); // act on alarm } } void goIntoSetMode(setMode settingMode){ lcd.clear(); lcd.setCursor(0,0); lcd.print(F("Press 1 sec")); delay(1000); // press 1 second to go into setting mode if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){ lcd.setCursor(0,0); lcd.print(F("Setting Mode")); lcd.setCursor(0,1); if(settingMode == DATE_TIME){ lcd.print(F("Date/Time")); } else{ lcd.print(F("Alarm")); } /* wait until button is released */ while(digitalRead(setItemPin) == LOW){} while(digitalRead(setValuePin) == LOW){} delay(50); // debouncing if(settingMode == DATE_TIME){ setDateTime(); } else{ setAlarmTime(); } } setItemKeyPressed = false; setValueKeyPressed = false; } void setDateTime(){ // date/time setting procedure DateTime now = rtc.now(); // get current time uint16_t tItem = 0; tItem = setDateTimeElement(&tElementYear, now.year()); adjustRTC(YEAR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMonth, now.month()); adjustRTC(MONTH, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementDay, now.day()); adjustRTC(DAY, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementHour, now.hour()); adjustRTC(HOUR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMinute, now.minute()); adjustRTC(MINUTE, tItem); adjustRTC(SECOND, 0); lcd.clear(); if(alarmOn){ lcd.setCursor(15,1); lcd.print("A"); // display "alarm is on" flag } printDateTime(); } void setAlarmTime(){ // procedure to set an alarm uint16_t value = 0; value = setDateTimeElement(&tElementHour, alarmTime.hour()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0); value = setDateTimeElement(&tElementMinute, alarmTime.minute()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0); lcd.clear(); char alarmTimeBuf[] = "hh:mm"; alarmTime.toString(alarmTimeBuf); lcd.setCursor(0,0); lcd.print("Alarm: "); lcd.print(alarmTimeBuf); lcd.setCursor(0,1); alarmOn = false; lcd.print(F("Activate: no ")); setItemKeyPressed = false; setValueKeyPressed = false; while(!setItemKeyPressed && !setValueKeyPressed){} if(setValueKeyPressed){ setValueKeyPressed = false; while(!setItemKeyPressed){ if(digitalRead(setValuePin) == LOW){ if(alarmOn){ lcd.setCursor(10,1); lcd.print("no "); alarmOn = false; rtc.disableAlarm(1); delay(500); } else{ lcd.setCursor(10,1); lcd.print(F("yes ")); alarmOn = true; rtc.setAlarm1(alarmTime, DS3231_A1_Hour); delay(500); } } } } lcd.clear(); if(alarmOn){ lcd.setCursor(15,1); lcd.print("A"); } printDateTime(); } void alarmAction(){ lcd.clear(); unsigned long alarmStart = millis(); /* alarm was fired, not key pressed, and alarm did not exceed max. duration */ while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){ lcd.setCursor(5,0); lcd.print(F("Alarm!")); delay(500); lcd.clear(); delay(500); if(millis() < alarmStart) { // if millis() overflow occured alarmStart = 0; } } /* wait till buttons are released: */ while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){} rtc.clearAlarm(1); delay(50); // debouncing DateTime now = rtc.now(); if(millis() - alarmStart < MAX_ALARM_DURATION){ alarmTime = now + TimeSpan(SNOOZE_TIME); // set new alarm (snooze) rtc.setAlarm1(alarmTime, DS3231_A1_Hour); lcd.setCursor(15,1); lcd.print("S"); // display snooze flag } else{ alarmOn = false; } setItemKeyPressed = false; setValueKeyPressed = false; } /* This functions sets a time element. Pressing the value key increments the value until the maximum value is reached. Pressing the item key will return. */ uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){ char buf[13]; clearLineLCD(0); lcd.setCursor(0,0); sprintf(buf, "Set %s: ", tE->tname); lcd.print(buf); lcd.setCursor(12,0); sprintf(buf, "%02d", currentSet); lcd.print(buf); setItemKeyPressed = false; while(!setItemKeyPressed){ if(setValueKeyPressed){ while(digitalRead(setValuePin) == LOW){ currentSet++; if(currentSet > tE->tMax){ currentSet = tE->tMin; } lcd.setCursor(12,0); sprintf(buf, "%02d", currentSet); lcd.print(buf); delay(SET_INCREMENT_DELAY); } } } setItemKeyPressed = false; return currentSet; } void printDateTime(){ // display date/time DateTime now = rtc.now(); char datBuf[] = "DDD, DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm:ss"; // // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time lcd.setCursor(0,0); lcd.print(datBuf); // display date lcd.setCursor(0,1); lcd.print(timeBuf); // display time } void adjustRTC(timeUnit tU, uint16_t value){ // adjust year, month, day etc. DateTime now = rtc.now(); switch(tU){ case(YEAR): rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second())); break; case(MONTH): rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second())); break; case(DAY): rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second())); break; case(HOUR): rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second())); break; case(MINUTE): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second())); break; case(SECOND): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value)); break; } delay(10); } void clearLineLCD(int line){ // clears on line on the display char buf[17]; sprintf(buf, "%16s", " "); lcd.setCursor(0, line); lcd.print(buf); } void setItemKeyISR(){ // ISR for pressed item key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setItemKeyPressed = true; } lastLow = millis(); } void setValueKeyISR(){ // ISR for pressed value key if(millis()-lastLow >= 50){ // ignore bouncing when button is released setValueKeyPressed = true; } lastLow = millis(); }
Erklärungen zum Code
Ich möchte den Sketch nicht Zeile für Zeile durchgehen und hoffe, er ist mit den Kommentaren und den folgenden Anmerkungen auch so einigermaßen verständlich.
- Die Taster befinden sich an den Interruptpins 2 und 3. Ein Druck auf die Taster löst einen Interrupt aus. In der ISR stellt die Abfrage
if(millis()-lastLow >= 300)
sicher, dass das Prellen beim Loslassen des Tasters nicht als Tasterdruck gewertet wird. - Wurde ein gültiger Interrupt ausgelöst, dann werden entsprechende Flags gesetzt (
setItemKeyPressed
/setValueKeyPressed
), die dazu führen, dass die EinstellfunktiongoIntoSetMode()
aufgerufen wird (sofern sich der Sketch inloop()
befindet). - von
goIntoSetMode()
geht es dann insetDateTime()
odersetAlarmTime()
. Da die Einstellung der Jahre, Monate, Tage, Stunden und Minuten immer nach demselben Schema abläuft, habe ich dafür die FunktionsetDateTimeElement()
geschrieben. Dieser Funktion wird eine Struktur vom TyptimeElement
übergeben, die spezifiziert, um was für einen Wert es sich handelt, sein Minimum und Maximum und die Bezeichnung auf dem Display. Ferner wird der aktuelle Wert des Zeitelements übergeben und der einzustellende Wert zurückgegeben. - Der einzustellende Wert wird mit der Funktion
adjustRTC()
auf den DS3231 übertragen.
Wecker – Version 3 mit zwei Tastern und einem Schiebeschalter
Bei der Version 2 störte mich die Prozedur zum Abschalten des Alarms. Die Vorstellung, sich morgens verschlafen durch eine Einstellprozedur durchzuarbeiten, behagt mir nicht. Ich habe die Schaltung in der Version 3 deshalb noch um einen Schiebeschalter am Arduino Pin 4 erweitert, der den Alarm ein- oder ausschaltet:

An der Bedienung ändert sich sonst nichts. Im Sketch entfällt der entsprechende Punkt am Ende der Alarmzeiteinstellung. Allerdings muss jetzt in loop()
geprüft werden, ob der Alarm ein- oder abgeschaltet wurde. Dazu wird der Zustand von Pin 4 mit dem alarmOn-Flag verglichen.
#include <Wire.h> #include <RTClib.h> // library for the DS3231 #include <LiquidCrystal_I2C.h> // library for the LCD display #define SNOOZE_TIME 60 // set snooze time #define SET_INCREMENT_DELAY 260 // incrementation time for settings #define MAX_ALARM_DURATION 20000 // maximum duration of an alarm volatile bool setItemKeyPressed = false; // flag for item key volatile bool setValueKeyPressed = false; // flag for value key volatile unsigned long lastLow; // needed to handle bouncing bool alarmOn = false; // alarm activated const int setItemPin = 2; // choose item to set / time setting mode const int setValuePin = 3; // set (increment) value / alarm setting mode const int alarmPin = 4; // Pin for the alarm switch typedef enum TIME_UNIT { // needed for struct "timeElement" YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } timeUnit; typedef enum SET_MODE { // setting modes DATE_TIME, ALARM } setMode; struct timeElement{ // for date/time/alarm setting timeUnit tUnit; uint16_t tMin; // minimum value uint16_t tMax; // maximum value char tname[7]; // name to display }; timeElement tElementYear = {YEAR, 2025, 2050, "Year"}; timeElement tElementMonth = {MONTH, 1, 12, "Month"}; timeElement tElementDay = {DAY, 1, 31, "Day"}; timeElement tElementHour = {HOUR, 0, 23, "Hour"}; timeElement tElementMinute = {MINUTE, 0, 59, "Minute"}; RTC_DS3231 rtc; // create RTC_DS3231 object DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0); LiquidCrystal_I2C lcd(0x27,20,2); // create display object (address, columns, rows) void setup () { Serial.begin(115200); lcd.init(); lcd.backlight(); lcd.clear(); if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.clearAlarm(1); rtc.clearAlarm(2); rtc.disableAlarm(1); rtc.disableAlarm(2); rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin /* pressing the item or value button will cause a low signal at the corresponding pin and trigger an interrupt */ pinMode(setItemPin, INPUT_PULLUP); pinMode(setValuePin, INPUT_PULLUP); pinMode(alarmPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING); attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING); setItemKeyPressed = false; setValueKeyPressed = false; } void loop() { if(millis()%1000 == 0){ // update date/time every second printDateTime(); delay(1); } if(setItemKeyPressed){ // item key = date/time setting key goIntoSetMode(DATE_TIME); } if(setValueKeyPressed){ // value key = alarm key goIntoSetMode(ALARM); } if(rtc.alarmFired(1) && alarmOn){ alarmAction(); // act on alarm } checkAlarmSetting(); // check whether alarm was (de-)activated } void checkAlarmSetting(){ if(!digitalRead(alarmPin) && !alarmOn){ alarmOn = true; lcd.setCursor(15,1); lcd.print("A"); // display alarm flag rtc.setAlarm1(alarmTime, DS3231_A1_Hour); } else if (digitalRead(alarmPin) && alarmOn){ alarmOn = false; lcd.setCursor(15,1); lcd.print(" "); // delete alam flag rtc.disableAlarm(1); } } void goIntoSetMode(setMode settingMode){ alarmOn = false; lcd.clear(); lcd.setCursor(0,0); lcd.print(F("Press 1 sec")); delay(1000); // press 1 second to go into setting mode if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){ lcd.setCursor(0,0); lcd.print(F("Setting Mode")); lcd.setCursor(0,1); if(settingMode == DATE_TIME){ lcd.print(F("Date/Time")); } else{ lcd.print(F("Alarm")); } /* wait until button is released */ while(digitalRead(setItemPin) == LOW){} while(digitalRead(setValuePin) == LOW){} delay(50); // debouncing if(settingMode == DATE_TIME){ setDateTime(); } else{ setAlarmTime(); } } setItemKeyPressed = false; setValueKeyPressed = false; } void setDateTime(){ // date/time setting procedure DateTime now = rtc.now(); // get current time uint16_t tItem = 0; tItem = setDateTimeElement(&tElementYear, now.year()); adjustRTC(YEAR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMonth, now.month()); adjustRTC(MONTH, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementDay, now.day()); adjustRTC(DAY, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementHour, now.hour()); adjustRTC(HOUR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMinute, now.minute()); adjustRTC(MINUTE, tItem); adjustRTC(SECOND, 0); lcd.clear(); if(alarmOn){ lcd.setCursor(15,1); lcd.print("A"); // display "alarm is on" flag } printDateTime(); } void setAlarmTime(){ // procedure to set an alarm uint16_t value = 0; value = setDateTimeElement(&tElementHour, alarmTime.hour()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0); value = setDateTimeElement(&tElementMinute, alarmTime.minute()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0); lcd.clear(); char alarmTimeBuf[] = "hh:mm"; alarmTime.toString(alarmTimeBuf); lcd.setCursor(0,0); lcd.print("Alarm: "); lcd.print(alarmTimeBuf); delay(1000); // Show Alarm Time for one second lcd.clear(); printDateTime(); } void alarmAction(){ lcd.clear(); unsigned long alarmStart = millis(); /* alarm was fired, not key pressed, and alarm did not exceed max. duration */ while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){ lcd.setCursor(5,0); lcd.print(F("Alarm!")); delay(500); lcd.clear(); delay(500); if(millis() < alarmStart) { // if millis() overflow occured alarmStart = 0; } } /* wait till buttons are released: */ while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){} rtc.clearAlarm(1); delay(50); // debouncing DateTime now = rtc.now(); if(millis() - alarmStart < MAX_ALARM_DURATION){ alarmTime = now + TimeSpan(SNOOZE_TIME); // set new alarm (snooze) rtc.setAlarm1(alarmTime, DS3231_A1_Hour); lcd.setCursor(15,1); lcd.print("S"); // display snooze flag } else{ alarmOn = false; } setItemKeyPressed = false; setValueKeyPressed = false; } /* This functions sets a time element. Pressing the value key increments the value until the maximum value is reached. Pressing the item key will return. */ uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){ char buf[13]; clearLineLCD(0); lcd.setCursor(0,0); sprintf(buf, "Set %s: ", tE->tname); lcd.print(buf); lcd.setCursor(12,0); sprintf(buf, "%02d", currentSet); lcd.print(buf); setItemKeyPressed = false; while(!setItemKeyPressed){ if(setValueKeyPressed){ while(digitalRead(setValuePin) == LOW){ currentSet++; if(currentSet > tE->tMax){ currentSet = tE->tMin; } lcd.setCursor(12,0); sprintf(buf, "%02d", currentSet); lcd.print(buf); delay(SET_INCREMENT_DELAY); } } } setItemKeyPressed = false; return currentSet; } void printDateTime(){ // display date/time DateTime now = rtc.now(); char datBuf[] = "DDD, DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm:ss"; // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time lcd.setCursor(0,0); lcd.print(datBuf); // display date lcd.setCursor(0,1); lcd.print(timeBuf); // display time } void adjustRTC(timeUnit tU, uint16_t value){ // adjust year, month, day etc. DateTime now = rtc.now(); switch(tU){ case(YEAR): rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second())); break; case(MONTH): rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second())); break; case(DAY): rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second())); break; case(HOUR): rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second())); break; case(MINUTE): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second())); break; case(SECOND): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value)); break; } delay(10); } void clearLineLCD(int line){ // clears on line on the display char buf[17]; sprintf(buf, "%16s", " "); lcd.setCursor(0, line); lcd.print(buf); } void setItemKeyISR(){ // ISR for pressed item key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setItemKeyPressed = true; } lastLow = millis(); } void setValueKeyISR(){ // ISR for pressed value key if(millis()-lastLow >= 50){ // ignore bouncing when button is released setValueKeyPressed = true; } lastLow = millis(); }
Strombedarf der Versionen 1 – 3
Dieses Weckerkonzept hat einen Strombedarf von ~44 mA. Ca. 24 mA gehen dabei auf das Konto des Displays, der Nano zieht um die 16.5 mA und der DS3231 gute 3.5 mA. Für Stromversorgung per Batterie ist das viel zu viel.
Wenn ihr das DS3231-Modul über eine Knopfzelle mit Strom versorgt und die Leitung zu VCC kappt, fällt das Modul nicht mehr ins Gewicht. Beim Display könntet ihr ca. 20 mA sparen, indem ihr das Backlight ausschaltet. Dazu könntet ihr folgende Zeilen in die Hauptschleife loop()
einfügen:
if(millis()-lastLow < 20000){ lcd.backlight(); } else lcd.noBacklight();
Wenn ihr eine der Einstelltasten drückt, geht das Backlight für 20 Sekunden an. Das könnte euch entgegenkommen, wenn die helle Anzeige nachts stört. Aber zugegeben, ideal ist das nicht. Zumindest am Tage würde man die Anzeige gerne lesen können, ohne irgendwelche Tasten zu drücken. Und die verbleibenden ~20 mA sind immer noch ein Killer für den Batteriebetrieb, es sei denn, ihr wollt alle paar Tage Batterien wechseln.
Konzept für den Batteriebetrieb
Was wir also brauchen, ist eine stromsparende Lösung für das Mikrocontrollerboard und ein sparsames Display.
Mikrocontroller-Board
Für das Board fiel meine Wahl auf den Arduino Pro Mini in der Version 3.3 V / 8 MHz. Der Pro Mini basiert wie der klassische Arduino Nano auf dem ATmega328P. Im Normalbetrieb ist der ATmega328P zwar kein Weltmeister im Stromsparen, aber im Deep-Sleep-Modus lässt sich sein Verbrauch auf unter < 1 µA senken (siehe auch hier meinen Beitrag zu den Sleep Modes).
Zum sekündlichen Aufwecken nutzen wir einen Pin-Change-Interrupt, den wir über einen zweiten Alarm des DS3231 auslösen. Auch die Interrupts, die über die Taster ausgelöst werden, wecken den Pro Mini.
Wie fast alle Boards besitzt auch der Pro Mini eine LED, die bei Versorgung mit Strom dauerhaft leuchtet. Diese solltet ihr entfernen. Das geht per Lötkolben oder einfach mit gezielter Gewalt, z. B. durch vorsichtiges Herunterkratzen mit einem Schraubendreher.
Display
Bei einer Recherche nach energieeffizienten Displays habe ich folgende Alternativen gefunden:
- Das Nokia 5110 Display.
- EA DOG Displays von Display Visions.
- E-Paper-Displays, z. B. von Waveshare.
- ST7567-basiertes Display von Open-Smart.
Im weiteren Verlauf gehe ich auf das Nokia 5110, das DOGS164 Display aus der EA DOGS Reihe und das ST7567 basierte Display ein. Ein Beispiel mit einem E-Paper-Display (MH-ET LIVE) bringe ich im Anhang, allerdings nur als einfache Uhr.
Die E-Paper-Displays sind die Weltmeister im Stromsparen, haben aber gewisse Eigenarten. Sie haben unter anderem einen hohen Arbeitsspeicherbedarf und es gibt für die meisten kein Backlight, was im Dunkeln für einen Wecker nicht gut ist. Oder man baut sich eine Display-Beleuchtung selbst.
DS3231
Das DS3231-Modul verbraucht im Batteriebetrieb (also mit Knopfzelle im Modul) außerordentlich wenig Strom. Allerdings hatte ich Probleme, den Arduino Pro Mini mit dem Alarm-Interrupt des DS3231 im Batteriebetrieb zu wecken. Ich bin deswegen auf Stromversorgung per VCC zurückgegangen. Da dann die Betriebs-LED leuchtet und Strom frisst, habe ich sie einfach entfernt. Damit verbraucht das DS3231 Modul um die 80 µA.
Wecker – Version 4 mit Nokia 5110 Display
Vorab die schlechte Nachricht zum Nokia 5110 Display: Die Qualität der meisten Displays lässt zu wünschen übrig. Ich habe mittlerweile eine ganze Reihe Displays aus verschiedenen Shops bestellt. In Deutschland habe ich in einem namhaften Shop zweimal drei Displays in Deutschland bestellt und beide Male funktionierten zwei von dreien nicht auf Anhieb. Drei der nicht funktionierenden Displays konnte ich aber doch noch zum Laufen bringen. Immerhin! Dazu mehr im Anhang.
Dann habe ich noch ein paar Probekäufe auf AliExpress getätigt. Hier bekam ich drei verschiedene Versionen. Die eine Version war dieselbe, wie ich sie schon in Deutschland erworben habe – nur konnte ich diese gar nicht zum Laufen bringen. Die anderen Versionen hatten andere Probleme. Dazu mehr in Anhang_2.
Die guten Seiten dieses Displays (wenn es denn funktioniert), sind:
- Sehr niedriger Stromverbrauch (ca. 100 – 200 µA ohne Beleuchtung).
- Ihr „Nostalgischer Charme“.
Folgender Aufbau kam zum Einsatz:

Ihr könnt die oberen oder die unteren Anschlüsse des Nokia 5110 Displays Pins nutzen, das spielt keine Rolle. Ich habe sie folgendermaßen verbunden:
- RST (Reset) – Arduino Pin 6
- CE (Chip Enable) – Arduino Pin 7
- DC (Data/Control) – Arduino Pin 8
- DIN (Data In) – Arduino Pin 9
- CLK (Clock) – Arduino Pin 10
- VCC – 3.3 Volt
- LIGHT – Arduino Pin 11
- GND – GND
Ein paar Hinweise
Das Backlight leuchtet (bei dieser Version!), wenn ihr den LIGHT-Pin mit GND verbindet. Oder ihr hängt den LIGHT-Pin an einen I/O-Pin und schaltet das Licht an, indem ihr den I/O-Pin auf OUTPUT/LOW einstellt. Per PWM bzw. analogWrite()
könntet ihr es dimmen.
Das Nokia 5110 Display ist nicht mit 5 Volt kompatibel. Da wir aber einen 3.3 Volt – Arduino Pro Mini verwenden, brauchen wir keinen Levelshifter oder Spannungsteiler. Für die Programmierung des Arduino Pro Mini bietet sich ein USB-zu-TTL Adapter an, der sich auf 3.3 Volt einstellen lässt. Im späteren Betrieb könnt ihr die Schaltung alternativ mit höheren Spannungen über den 3.3-Volt-Spannungsregler des Arduino Pro Mini versorgen. Dazu geht ihr über den RAW Pin. Aber noch einmal: das funktioniert in der Form nur mit der 3.3 Volt Variante des Pro Mini!
Der Sketch zum Wecker – Version 4
Zur Ansteuerung des Nokia 5110 Displays habe ich die Nokia 5110 LCD Library von Dimitris Platis (platisd) verwendet. Da gehe ich nicht im Detail drauf ein. Eine Besonderheit der Bibliothek ist, dass die Cursorposition vertikal in Zeilen (0 bis 5) und horizontal in Pixeln (0 bis 83) angegeben wird.
Dann kommen die hardwarespezifischen Schlaffunktionen für den ATmega328P hinzu. Falls ihr hierzu mehr Details wissen wollt, dann schaut in diesen Beitrag. Ebenso sind die Pin-Change-Interrupt-Funktionen hardwarespezifisch.
Da der Alarm1 die Option bietet, einen sekündlichen Alarm auszulösen (DS3231_A1_PerSecond), nutzen wir diesen zum regelmäßigen Wecken aus dem Schlafmodus. Die Weckfunktion des Weckers verlegen wir auf den Alarm 2.
Ich glaube, das war es dann im Wesentlichen. Der Code ist einfach zu lang, um ihn Zeile für Zeile durchzugehen.
#include <Wire.h> #include <RTClib.h> // library for the DS3231 #include <Nokia_LCD.h> // Nokia 5110 display lib #include "Bold_LCD_Fonts.h" // bold fonts for time display #include <avr/sleep.h> // hardware-specific sleep library for AVR MCUs #define SNOOZE_TIME 60 // set snooze time #define SET_INCREMENT_DELAY 260 // incrementation time for settings #define MAX_ALARM_DURATION 20000 // maximum duration of an alarm volatile bool setItemKeyPressed = false; // flag for item key volatile bool setValueKeyPressed = false; // flag for value key volatile unsigned long lastLow; // needed to handle bouncing bool alarmOn = false; // alarm activated const int setItemPin = 2; // choose item to set / time setting mode const int setValuePin = 3; // set (increment) value / alarm setting mode const int alarmPin = 4; // Pin for the alarm switch const int wakeUpPin = 5; const int backlightPin = 11; typedef enum TIME_UNIT { // needed for struct "timeElement" YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } timeUnit; typedef enum SET_MODE { // setting modes DATE_TIME, ALARM } setMode; char daysOfTheWeek[7][12] = {"Sunday ", "Monday ", "Tuesday ", "Wednesday", "Thursday ", "Friday ", "Saturday "}; struct timeElement{ // for date/time/alarm setting timeUnit tUnit; uint16_t tMin; // minimum value uint16_t tMax; // maximum value char tname[7]; // name to display }; const LcdFont BoldFont{ // Bold Font for Nokia 5110 Display [](char c) { return Bold_LCD_Fonts::kFont_Table[c - 0x20]; }, // method to retrieve the character Bold_LCD_Fonts::kColumns_per_character, // width of each char Bold_LCD_Fonts::hSpace, // horizontal spacing array 1 // size of horizontal spacing array }; timeElement tElementYear = {YEAR, 2025, 2050, "Year"}; timeElement tElementMonth = {MONTH, 1, 12, "Month"}; timeElement tElementDay = {DAY, 1, 31, "Day"}; timeElement tElementHour = {HOUR, 0, 23, "Hour"}; timeElement tElementMinute = {MINUTE, 0, 59, "Minute"}; RTC_DS3231 rtc; // create RTC_DS3231 object DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0); Nokia_LCD lcd(10 /* CLK */, 9 /* DIN */, 8 /* DC */, 7 /* CE */, 6 /* RST */); void setup () { Serial.begin(115200); ADCSRA = 0; // ADC off (would consume current in deep sleep) lcd.begin(); lcd.setContrast(60); lcd.clear(false); setBacklight(false); // Light off if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.clearAlarm(1); rtc.clearAlarm(2); rtc.disableAlarm(1); rtc.disableAlarm(2); rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin rtc.setAlarm1(alarmTime, DS3231_A1_PerSecond); /* pressing the item or value button will cause a low signal at the corresponding pin and trigger an interrupt */ pinMode(setItemPin, INPUT_PULLUP); pinMode(setValuePin, INPUT_PULLUP); pinMode(alarmPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING); attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING); PCICR = (1<<PCIE2); // enable PCINT[23:16] interrupts PCMSK2 = (1<<PCINT21); // D5 = PCINT21 for wake up setItemKeyPressed = false; setValueKeyPressed = false; } void loop() { printDateTime(); if(digitalRead(setItemPin) == LOW){ // item key = date/time setting key goIntoSetMode(DATE_TIME); } if(digitalRead(setValuePin) == LOW){ // value key = alarm key goIntoSetMode(ALARM); } if(rtc.alarmFired(2) && alarmOn){ alarmAction(); // act on alarm } checkAlarmSetting(); // check whether alarm was (de-)activated set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode sleep_mode(); // sleep now! rtc.clearAlarm(1); } void checkAlarmSetting(){ if(!digitalRead(alarmPin) && !alarmOn){ alarmOn = true; lcd.setCursor(77,5); lcd.print("A"); // display alarm flag rtc.setAlarm2(alarmTime, DS3231_A2_Hour); } else if (digitalRead(alarmPin) && alarmOn){ alarmOn = false; lcd.setCursor(77,5); lcd.print(" "); // delete alarm flag rtc.disableAlarm(2); } } void goIntoSetMode(setMode settingMode){ alarmOn = false; lcd.clear(false); lcd.setCursor(0,0); lcd.print("Press 1 sec"); delay(1000); // press 1 second to go into setting mode clearLineLCD(0); if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){ lcd.setCursor(0,0); lcd.print("Setting Mode"); lcd.setCursor(0,2); if(settingMode == DATE_TIME){ lcd.print("Set Date/Time"); } else{ lcd.print("Set Alarm"); } /* wait until button is released */ while(digitalRead(setItemPin) == LOW){} while(digitalRead(setValuePin) == LOW){} delay(50); // debouncing if(settingMode == DATE_TIME){ setDateTime(); } else{ setAlarmTime(); } } rtc.clearAlarm(1); setItemKeyPressed = false; setValueKeyPressed = false; } void setDateTime(){ // date/time setting procedure DateTime now = rtc.now(); // get current time uint16_t tItem = 0; tItem = setDateTimeElement(&tElementYear, now.year()); adjustRTC(YEAR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMonth, now.month()); adjustRTC(MONTH, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementDay, now.day()); adjustRTC(DAY, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementHour, now.hour()); adjustRTC(HOUR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMinute, now.minute()); adjustRTC(MINUTE, tItem); adjustRTC(SECOND, 0); lcd.clear(false); if(alarmOn){ lcd.setCursor(77,5); lcd.print("A"); // display "alarm is on" flag } printDateTime(); } void setAlarmTime(){ // procedure to set an alarm uint16_t value = 0; value = setDateTimeElement(&tElementHour, alarmTime.hour()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0); value = setDateTimeElement(&tElementMinute, alarmTime.minute()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0); lcd.clear(false); char alarmTimeBuf[] = "hh:mm"; alarmTime.toString(alarmTimeBuf); lcd.setCursor(0,0); lcd.print("Alarm: "); lcd.print(alarmTimeBuf); delay(1000); // Show Alarm Time for one second lcd.clear(); printDateTime(); } void alarmAction(){ lcd.clear(); unsigned long alarmStart = millis(); /* alarm was fired, not key pressed, and alarm did not exceed max. duration */ while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){ lcd.setCursor(23,3); lcd.print("Alarm!"); delay(500); lcd.clear(); delay(500); if(millis() < alarmStart) { // if millis() overflow occurred alarmStart = 0; } } /* wait till buttons are released: */ while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){} delay(50); // debouncing DateTime now = rtc.now(); if(millis() - alarmStart < MAX_ALARM_DURATION){ alarmTime = now + TimeSpan(SNOOZE_TIME); // set new alarm (snooze) rtc.setAlarm2(alarmTime, DS3231_A2_Hour); // alarm if hours/minutes match lcd.setCursor(77,5); lcd.print("S"); // display snooze flag } else{ alarmOn = false; } rtc.clearAlarm(2); rtc.clearAlarm(1); setItemKeyPressed = false; setValueKeyPressed = false; } /* This functions sets a time element. Pressing the value key increments the value until the maximum value is reached. Pressing the item key will return. */ uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){ char buf[13]; clearLineLCD(0); lcd.setCursor(0,0); sprintf(buf, "%s:", tE->tname); lcd.print(buf); lcd.setCursor(47,0); sprintf(buf, "%02d", currentSet); lcd.print(buf); setItemKeyPressed = false; while(!setItemKeyPressed){ if(setValueKeyPressed){ while(digitalRead(setValuePin) == LOW){ currentSet++; if(currentSet > tE->tMax){ currentSet = tE->tMin; } lcd.setCursor(47,0); sprintf(buf, "%02d", currentSet); lcd.print(buf); delay(SET_INCREMENT_DELAY); } } } setItemKeyPressed = false; return currentSet; } void printDateTime(){ // display date/time DateTime now = rtc.now(); char datBuf[] = "DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm:ss"; // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time lcd.setCursor(0,0); lcd.print(daysOfTheWeek[now.dayOfTheWeek()]); lcd.setCursor(0,2); lcd.print(datBuf); // display date lcd.setCursor(0,4); lcd.setFont(&BoldFont); lcd.print(timeBuf); // display time lcd.setDefaultFont(); } void adjustRTC(timeUnit tU, uint16_t value){ // adjust year, month, day etc. DateTime now = rtc.now(); switch(tU){ case(YEAR): rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second())); break; case(MONTH): rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second())); break; case(DAY): rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second())); break; case(HOUR): rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second())); break; case(MINUTE): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second())); break; case(SECOND): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value)); break; } delay(10); } void clearLineLCD(int line){ // clears on line on the display char buf[15]; sprintf(buf, "%14s", " "); lcd.setCursor(0, line); lcd.print(buf); } void setBacklight(bool on){ if(on){ pinMode(backlightPin, OUTPUT); } else pinMode(backlightPin, INPUT); } void setItemKeyISR(){ // ISR for pressed item key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setItemKeyPressed = true; } lastLow = millis(); } void setValueKeyISR(){ // ISR for pressed value key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setValueKeyPressed = true; } lastLow = millis(); } ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD
Das Ergebnis
So sieht die Anzeige des Datums und der Uhrzeit auf dem Display aus.
Mit diesem Aufbau (einschließlich der entfernten LEDs) habe ich in der Schlafphase einen Stromverbrauch 0.224 mA gemessen. Für die Wachphase habe ich einen Stromverbrauch von 2.8 mA ermittelt. Die Wachphase hat eine Länge von 166 ms und ist damit durchaus relevant. Der Gesamtstromverbrauch beträgt damit ca. 0.65 mA. Mehr in Richtung der 0.224 mA könnt ihr euch bewegen, indem ihr auf die Sekundenanzeige verzichtet. Dazu kommen wir gleich.
Variationen
Minuten- anstelle Sekundenanzeige
Wenn ihr auf die Sekundenanzeige verzichten möchtet, dann sind nur wenige Änderungen im Sketch erforderlich:
- Ändert Zeile 85 auf:
PCMSK2 = (1<<PCINT21) | (1<<PCINT20);
- Damit wacht der Arduino Pro Mini auch dann auf, wenn ihr den Schiebeschalter an Pin 4 (= PCINT20) betätigt.
- Um den Alarm 1 von Sekunden auf Minuten umzustellen, ersetzt ihr Zeile 75 durch:
rtc.setAlarm1(alarmTime, DS3231_A1_Second);
- Die Anzeige der Stunden und Minuten legt ihr in Zeile 272 fest:
char timeBuf[] = "hh:mm";
- Um die Zeitanzeige zu zentrieren, schreibt ihr in Zeile 279:
lcd.setCursor(23,4);
- Dann fiel mir auf, dass die Datum-/Zeitanzeige nach Einstellung von Uhr- und Weckzeit verschwunden war. Dazu fügt eine zusätzliche Zeile nach Zeile 231 ein:
printDateTime();
Backlight verwenden
Für die Weckerversion 3 habe ich gezeigt, wie ihr das Backlight für eine begrenzte Zeit einschaltet. Das funktioniert so nicht, wenn ihr den Schlafmodus verwendet, da der für millis()
zuständige Timer0 aus ist. Die pragmatische Lösung ist, nur die Wachzeit zu zählen und den Wert für die Backlightphase entsprechend zu reduzieren. Allerdings müsste man dann bei der Minutenanzeige das Licht mindestens eine Minute lang anlassen.
Alternativ nutzt ihr anstelle millis()
die RTC zum Messen der Backlightzeit. Mit now.unixtime()
erhaltet ihr die Zeit seit dem 1.1.1970 in Sekunden und könnt damit bequem rechnen. Oder ihr fügt einen weiteren Schiebeschalter hinzu, der das Backlight aktiviert und die Schlaffunktion abschaltet. Das birgt die Gefahr, dass ihr das Licht vergesst, die Batterie leer zieht und dann verschlaft. Aber auch da könntet ihr wieder eine Obergrenze einbauen. Oder, oder, oder… – es gibt viele Möglichkeiten.
Wecker – Version 5 mit DOGS164 Display
Das DOGS164 gehört zu der Familie der EA DOG Displays. Sie basieren auf einem interessanten, modularen Konzept. Ihr stellt das Display, das Backlightmodul und ggf. noch ein Touch-Panel nach euren Wünschen zusammen. Es gibt die Teile als Text- oder Grafik-Displays. Eine Übersicht findet ihr hier.
Der Buchstabe hinter dem „DOG“ gibt euch einen Hinweis auf die (Schrift-)Größe, also S, M, L oder XL. Bei den Textdisplays geben die nächsten zwei Nummern die Zahl der Zeichen pro Zeile an. Die dritte Zahl verrät die Anzahl der Zeilen.
Dann könnt ihr noch auswählen, wie die Zeichen auf dem Display dargestellt werden sollen. So gibt es beispielsweise ein DOGS164B, DOGS164W und ein DOGS164N:
- B: blauer Hintergrund, transmissiv, d. h. die Schriftfarbe wird durch die Beleuchtung bestimmt (also ähnlich wie das LCD der Wecker-Versionen 1-3).
- W: schwarze Schrift, Hintergrundfarbe wird durch das Backlightmodul bestimmt.
- N: schwarze Schrift, kein Backlight möglich, sieht aus wie „W“ ohne Backlight (Bild unten rechts).
Auch die Backlight Panels gibt es in verschiedenen Ausfertigungen, z. B. in Amber oder Grün/Rot/Weiß (umschaltbar). Schaut also genau hin, was ihr euch bestellt! Die B-Version ist beispielsweise nur mit Backlight wirklich lesbar.
Ich habe mich für das Textdisplay DOGS164W mit Amber-farbigem Backlight entschieden. Ich nehme das Ergebnis mal vorweg:

Ein gewisser Nachteil an den EA DOG Displays ist, dass man sie, wenn sie erst einmal auf dem Backlight-Panel fixiert sind, nicht mehr in Breadboards stecken kann. Die Beinchen sind dann zu kurz. Ich habe zum Probieren Jumperkabel an einer Seite abgeschnitten und angelötet:

Die Backlight-Anschlüsse sollen lt. Datenblatt übrigens „mit wenig Zinn von oben“ verlötet werden. Nun ja, ging trotzdem.
Anschluss an den Arduino Pro Mini
Die verschiedenen EA DOG Displays unterscheiden sich hinsichtlich der Anzahl und Belegung ihrer Pins. Bei einigen Displays habt ihr die Wahlfreiheit zwischen SPI (3-Wire), SPI (4-Wire) und I2C. Das DOGS164 ist per I2C oder SPI (3-Wire) ansprechbar. Da schon das DS3231-Modul per I2C angeschlossen wird, habe ich dasselbe mit dem DOG-Display getan. Das sparte drei Arduino-Pins.

Hier die Schaltung ohne Backlight-Anschluss:

Noch ein paar Hinweise (speziell für das DOGS164 / I2C):
- Alle Anschlüsse vertragen nur 3.3 Volt. Verwendet ihr ein 5V Board, müsst ihr Levelshifter oder Spannungsteiler einsetzen.
- VOUT und CS könnt ihr unverbunden lassen.
- Mit SA0 stellt ihr die I2C-Adresse ein. Die Angabe oben ist die 8-Bit-I2C-Adresse! In der Arduino-Welt wird die 7-Bit-Adresse verwendet. Das letzte Bit ist also zu streichen. Aus 0x7A wird 0x3D und aus 0x78 wird 0x3C.
- Mit IM1 stellt ihr I2C (GND) oder SPI (VDD) ein.
- SID und SOD kommen gemeinsam an SDA.
- SCLK hängt an SCL.
- Das Backlight wird über die Anschlüsse A1/A2 (Anode) und C1/C2 (Kathode) gesteuert.
- Vorwiderstände sind Pflicht! Der Backlightstrom kann über das Limit der Arduino-Pins hinausgehen. Ihr könntet das Backlight über MOSFETs mit dem Arduino steuern.
- Lest das Datenblatt! Es ist leicht verständlich und Deutsch / Englisch verfügbar. Dort stehen auch die empfohlenen Widerstände für das Backlight.
Der Sketch zum Wecker – Version 5
Zur Ansteuerung des DOGS164-Displays habe ich die Bibliothek SSD1803A_I2C von Stefan Staub (sstaub) verwendet. Sie ist sehr einfach zu bedienen. Der Code sollte leicht verständlich sein. Ansonsten bleibt alles gleich.
#include <Wire.h> #include <RTClib.h> // library for the DS3231 #include <SSD1803A_I2C.h> // DOGS164 Lib #include <avr/sleep.h> #define SNOOZE_TIME 60 // set snooze time #define SET_INCREMENT_DELAY 260 // incrementation time for settings #define MAX_ALARM_DURATION 20000 // maximum duration of an alarm volatile bool setItemKeyPressed = false; // flag for item key volatile bool setValueKeyPressed = false; // flag for value key volatile unsigned long lastLow; // needed to handle bouncing bool alarmOn = false; // alarm activated const int setItemPin = 2; // choose item to set / time setting mode const int setValuePin = 3; // set (increment) value / alarm setting mode const int alarmPin = 4; // Pin for the alarm switch const int wakeUpPin = 5; const uint8_t i2caddr = 0x3D; const uint8_t resetPin = 8; typedef enum TIME_UNIT { // needed for struct "timeElement" YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } timeUnit; typedef enum SET_MODE { // setting modes DATE_TIME, ALARM } setMode; char daysOfTheWeek[7][12] = {"Sunday ", "Monday ", "Tuesday ", "Wednesday", "Thursday ", "Friday ", "Saturday "}; struct timeElement{ // for date/time/alarm setting timeUnit tUnit; uint16_t tMin; // minimum value uint16_t tMax; // maximum value char tname[7]; // name to display }; timeElement tElementYear = {YEAR, 2025, 2050, "Year"}; timeElement tElementMonth = {MONTH, 1, 12, "Month"}; timeElement tElementDay = {DAY, 1, 31, "Day"}; timeElement tElementHour = {HOUR, 0, 23, "Hour"}; timeElement tElementMinute = {MINUTE, 0, 59, "Minute"}; RTC_DS3231 rtc; // create RTC_DS3231 object DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0); SSD1803A_I2C lcd(i2caddr); void setup () { Serial.begin(115200); ADCSRA = 0; // ADC off lcd.begin(DOGS164, resetPin); if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.clearAlarm(1); rtc.clearAlarm(2); rtc.disableAlarm(1); rtc.disableAlarm(2); rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin rtc.setAlarm1(alarmTime, DS3231_A1_PerSecond); /* pressing the item or value button will cause a low signal at the corresponding pin and trigger an interrupt */ pinMode(setItemPin, INPUT_PULLUP); pinMode(setValuePin, INPUT_PULLUP); pinMode(alarmPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING); attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING); PCICR = (1<<PCIE2); // enable PCINT[23:16] interrupts PCMSK2 = (1<<PCINT21); // D5 = PCINT21 for wake up setItemKeyPressed = false; setValueKeyPressed = false; } void loop() { printDateTime(); if(digitalRead(setItemPin) == LOW){ // item key = date/time setting key goIntoSetMode(DATE_TIME); } if(digitalRead(setValuePin) == LOW){ // value key = alarm key goIntoSetMode(ALARM); } if(rtc.alarmFired(2) && alarmOn){ alarmAction(); // act on alarm } checkAlarmSetting(); // check whether alarm was (de-)activated set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode sleep_mode(); // sleep now! rtc.clearAlarm(1); } void checkAlarmSetting(){ if(!digitalRead(alarmPin) && !alarmOn){ alarmOn = true; lcd.locate(4,16); lcd.print("A"); // display alarm flag rtc.setAlarm2(alarmTime, DS3231_A2_Hour); } else if (digitalRead(alarmPin) && alarmOn){ alarmOn = false; lcd.locate(4,16); lcd.print(" "); // delete alam flag rtc.disableAlarm(2); } } void goIntoSetMode(setMode settingMode){ alarmOn = false; lcd.cls(); lcd.locate(1,1); lcd.print("Press 1 sec"); delay(1000); // press 1 second to go into setting mode lcd.clr(1); if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){ lcd.locate(1,1); lcd.print("Setting Mode"); lcd.locate(2,1); if(settingMode == DATE_TIME){ lcd.print("Set Date/Time"); } else{ lcd.print("Set Alarm"); } /* wait until button is released */ while(digitalRead(setItemPin) == LOW){} while(digitalRead(setValuePin) == LOW){} delay(50); // debouncing if(settingMode == DATE_TIME){ setDateTime(); } else{ setAlarmTime(); } } rtc.clearAlarm(1); setItemKeyPressed = false; setValueKeyPressed = false; } void setDateTime(){ // date/time setting procedure DateTime now = rtc.now(); // get current time uint16_t tItem = 0; tItem = setDateTimeElement(&tElementYear, now.year()); adjustRTC(YEAR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMonth, now.month()); adjustRTC(MONTH, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementDay, now.day()); adjustRTC(DAY, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementHour, now.hour()); adjustRTC(HOUR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMinute, now.minute()); adjustRTC(MINUTE, tItem); adjustRTC(SECOND, 0); lcd.cls(); if(alarmOn){ lcd.locate(4,16); lcd.print("A"); // display "alarm is on" flag } printDateTime(); } void setAlarmTime(){ // procedure to set an alarm uint16_t value = 0; value = setDateTimeElement(&tElementHour, alarmTime.hour()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0); value = setDateTimeElement(&tElementMinute, alarmTime.minute()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0); lcd.cls(); char alarmTimeBuf[] = "hh:mm"; alarmTime.toString(alarmTimeBuf); lcd.locate(1,1); lcd.print("Alarm: "); lcd.print(alarmTimeBuf); delay(1000); // Show Alarm Time for one second lcd.cls(); printDateTime(); } void alarmAction(){ lcd.cls(); unsigned long alarmStart = millis(); /* alarm was fired, not key pressed, and alarm did not exceed max. duration */ while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){ lcd.locate(2,5); lcd.print("Alarm!"); delay(500); lcd.cls(); delay(500); if(millis() < alarmStart) { // if millis() overflow occured alarmStart = 0; } } /* wait till buttons are released: */ while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){} delay(50); // debouncing DateTime now = rtc.now(); if(millis() - alarmStart < MAX_ALARM_DURATION){ alarmTime = now + TimeSpan(SNOOZE_TIME); // set new alarm (snooze) rtc.setAlarm2(alarmTime, DS3231_A2_Hour); // alarm if hours/minutes match lcd.locate(4,16); lcd.print("S"); // display snooze flag } else{ alarmOn = false; } rtc.clearAlarm(2); rtc.clearAlarm(1); setItemKeyPressed = false; setValueKeyPressed = false; } /* This functions sets a time element. Pressing the value key increments the value until the maximum value is reached. Pressing the item key will return. */ uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){ char buf[13]; lcd.clr(1); lcd.locate(1,1); sprintf(buf, "%s:", tE->tname); lcd.print(buf); lcd.locate(1,9); sprintf(buf, "%02d", currentSet); lcd.print(buf); setItemKeyPressed = false; while(!setItemKeyPressed){ if(setValueKeyPressed){ while(digitalRead(setValuePin) == LOW){ currentSet++; if(currentSet > tE->tMax){ currentSet = tE->tMin; } lcd.locate(1,9); sprintf(buf, "%02d", currentSet); lcd.print(buf); delay(SET_INCREMENT_DELAY); } } } setItemKeyPressed = false; return currentSet; } void printDateTime(){ // display date/time DateTime now = rtc.now(); char datBuf[] = "DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm:ss"; // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time lcd.locate(1,4); lcd.print(daysOfTheWeek[now.dayOfTheWeek()]); lcd.locate(2,4); lcd.print(datBuf); // display date lcd.locate(4,4); lcd.print(timeBuf); // display time } void adjustRTC(timeUnit tU, uint16_t value){ // adjust year, month, day etc. DateTime now = rtc.now(); switch(tU){ case(YEAR): rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second())); break; case(MONTH): rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second())); break; case(DAY): rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second())); break; case(HOUR): rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second())); break; case(MINUTE): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second())); break; case(SECOND): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value)); break; } delay(10); } void setItemKeyISR(){ // ISR for pressed item key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setItemKeyPressed = true; } lastLow = millis(); } void setValueKeyISR(){ // ISR for pressed value key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setValueKeyPressed = true; } lastLow = millis(); } ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD
Das Ergebnis
Ich habe einen Gesamtstrom von ca. 0,85 Milliampere gemessen. Das ist etwas mehr als mit dem Nokia Display, aber immer noch in einem Bereich, der Batteriebetrieb möglich macht. Das Display selbst hat lt. Hersteller einen Strombedarf von 440 µA, nach meiner Messung sind es 560 µA.
Das EA DOG Display scheint wesentlich schneller zu sein als Nokia 5110. Die Wachphase betrug lediglich 8 ms. Insofern lohnt es sich hier auch nicht wirklich, von der Sekundenanzeige zu Minutenanzeige zu wechseln.
Wecker – Version 6 mit ST7567 basiertem Display
Das ST7567-basierte Display von OPEN-SMART habe ich auf AliExpress gefunden. Es steht stellvertretend für andere LCD-Displays, die ohne Backlight auskommen. Ich habe zwei bestellt und beide funktionierten(!). Ich nehme das Ergebnis gleich vorweg:

Ein Fritzing-Schema für die Schaltung spare ich mir hier. Aus den vorherigen Beispielen und dem Sketch sollte es sich erschließen. Die Ansteuerung erfolgte über die U8x8 Bibliothek, die Teil der U8g2 Bibliothek von olikraus ist. Mit ihr erreichte ich einen Strombedarf von ca. 0.8 mA in der Schlafphase.
Um für ein klein wenig Abwechslung zu sorgen, habe ich hier einmal eine Funktion in den Sketch eingebaut, die das Backlight für 10 Sekunden ( = 10x aufwachen) zuschaltet, wenn man einen der Taster drückt:
#include <Wire.h> #include <RTClib.h> // library for the DS3231 #include <U8x8lib.h> #include <SPI.h> #include <avr/sleep.h> // hardware-specific sleep library for AVR MCUs #define SNOOZE_TIME 60 // set snooze time #define SET_INCREMENT_DELAY 260 // incrementation time for settings #define MAX_ALARM_DURATION 20000 // maximum duration of an alarm #define BACKLIGHT_ON_PERIOD 10 // backlight on period in seconds volatile bool setItemKeyPressed = false; // flag for item key volatile bool setValueKeyPressed = false; // flag for value key volatile unsigned long lastLow; // needed to handle bouncing unsigned long wakeUpNo = 0; unsigned long int lastKeyPress = 0; bool alarmOn = false; // alarm activated const int setItemPin = 2; // choose item to set / time setting mode const int setValuePin = 3; // set (increment) value / alarm setting mode const int alarmPin = 4; // Pin for the alarm switch const int wakeUpPin = 5; const int backlightPin = 6; typedef enum TIME_UNIT { // needed for struct "timeElement" YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } timeUnit; typedef enum SET_MODE { // setting modes DATE_TIME, ALARM } setMode; char daysOfTheWeek[7][12] = {"SUNDAY ", "MONDAY ", "TUESDAY ", "WEDNESDAY", "THURSDAY ", "FRIDAY ", "SATURDAY "}; struct timeElement{ // for date/time/alarm setting timeUnit tUnit; uint16_t tMin; // minimum value uint16_t tMax; // maximum value char tname[7]; // name to display }; timeElement tElementYear = {YEAR, 2025, 2050, "YEAR"}; timeElement tElementMonth = {MONTH, 1, 12, "MONTH"}; timeElement tElementDay = {DAY, 1, 31, "DAY"}; timeElement tElementHour = {HOUR, 0, 23, "HOUR"}; timeElement tElementMinute = {MINUTE, 0, 59, "MINUTE"}; RTC_DS3231 rtc; // create RTC_DS3231 object DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0); U8X8_ST7567_JLX12864_4W_HW_SPI u8x8(/* cs=*/ 7, /* dc=*/ 9, /* reset=*/ 8); void setup () { Serial.begin(115200); ADCSRA = 0; // ADC off (would consume current in deep sleep) u8x8.begin(); u8x8.setPowerSave(0); pinMode(backlightPin, OUTPUT); u8x8.setFont(u8x8_font_saikyosansbold8_u); setBacklight(true); // Light on if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.clearAlarm(1); rtc.clearAlarm(2); rtc.disableAlarm(1); rtc.disableAlarm(2); rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin rtc.setAlarm1(alarmTime, DS3231_A1_PerSecond); /* pressing the item or value button will cause a low signal at the corresponding pin and trigger an interrupt */ pinMode(setItemPin, INPUT_PULLUP); pinMode(setValuePin, INPUT_PULLUP); pinMode(alarmPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING); attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING); PCICR = (1<<PCIE2); // enable PCINT[23:16] interrupts PCMSK2 = (1<<PCINT21); // D5 = PCINT21 for wake up setItemKeyPressed = false; setValueKeyPressed = false; } void loop() { printDateTime(); if(digitalRead(setItemPin) == LOW){ // item key = date/time setting key goIntoSetMode(DATE_TIME); lastKeyPress = wakeUpNo; } if(digitalRead(setValuePin) == LOW){ // value key = alarm key goIntoSetMode(ALARM); lastKeyPress = wakeUpNo; } if(rtc.alarmFired(2) && alarmOn){ alarmAction(); // act on alarm } checkAlarmSetting(); // check whether alarm was (de-)activated set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode sleep_mode(); // sleep now! rtc.clearAlarm(1); wakeUpNo++; backlightCheck(); } void checkAlarmSetting(){ if(!digitalRead(alarmPin) && !alarmOn){ alarmOn = true; u8x8.drawString(15,0,"A"); // display alarm flag rtc.setAlarm2(alarmTime, DS3231_A2_Hour); } else if (digitalRead(alarmPin) && alarmOn){ alarmOn = false; u8x8.drawString(15,0," "); // delete alarm flag rtc.disableAlarm(2); } } void goIntoSetMode(setMode settingMode){ alarmOn = false; u8x8.clear(); u8x8.drawString(0,0,"PRESS 1 SEC"); delay(1000); // press 1 second to go into setting mode u8x8.clearLine(0); if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){ u8x8.drawString(0,0,"SETTING MODE"); if(settingMode == DATE_TIME){ u8x8.drawString(0,2,"SET DATE/TIME"); } else{ u8x8.drawString(0,2,"SET ALARM"); } /* wait until button is released */ while(digitalRead(setItemPin) == LOW){} while(digitalRead(setValuePin) == LOW){} delay(50); // debouncing if(settingMode == DATE_TIME){ setDateTime(); } else{ setAlarmTime(); } } rtc.clearAlarm(1); setItemKeyPressed = false; setValueKeyPressed = false; } void setDateTime(){ // date/time setting procedure DateTime now = rtc.now(); // get current time uint16_t tItem = 0; tItem = setDateTimeElement(&tElementYear, now.year()); adjustRTC(YEAR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMonth, now.month()); adjustRTC(MONTH, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementDay, now.day()); adjustRTC(DAY, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementHour, now.hour()); adjustRTC(HOUR, tItem); now = rtc.now(); tItem = setDateTimeElement(&tElementMinute, now.minute()); adjustRTC(MINUTE, tItem); adjustRTC(SECOND, 0); u8x8.clear(); if(alarmOn){ u8x8.drawString(15,0,"A"); // display "alarm is on" flag } printDateTime(); } void setAlarmTime(){ // procedure to set an alarm uint16_t value = 0; value = setDateTimeElement(&tElementHour, alarmTime.hour()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0); value = setDateTimeElement(&tElementMinute, alarmTime.minute()); alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0); u8x8.clear(); char alarmTimeBuf[] = "hh:mm"; alarmTime.toString(alarmTimeBuf); u8x8.drawString(0,0,"ALARM: "); u8x8.drawString(8,0, alarmTimeBuf); delay(1000); // Show Alarm Time for one second u8x8.clear(); printDateTime(); } void alarmAction(){ u8x8.clear(); unsigned long alarmStart = millis(); /* alarm was fired, not key pressed, and alarm did not exceed max. duration */ while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){ u8x8.drawString(4,3,"ALARM!"); delay(500); u8x8.clear(); delay(500); if(millis() < alarmStart) { // if millis() overflow occurred alarmStart = 0; } } /* wait till buttons are released: */ while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){} delay(50); // debouncing DateTime now = rtc.now(); if(millis() - alarmStart < MAX_ALARM_DURATION){ alarmTime = now + TimeSpan(SNOOZE_TIME); // set new alarm (snooze) rtc.setAlarm2(alarmTime, DS3231_A2_Hour); // alarm if hours/minutes match u8x8.drawString(15,0,"S"); // display snooze flag } else{ alarmOn = false; } rtc.clearAlarm(2); rtc.clearAlarm(1); setItemKeyPressed = false; setValueKeyPressed = false; } /* This functions sets a time element. Pressing the value key increments the value until the maximum value is reached. Pressing the item key will return. */ uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){ char buf[13]; u8x8.clearLine(0); u8x8.setCursor(0,0); sprintf(buf, "%s:", tE->tname); u8x8.drawString(0,0,buf); sprintf(buf, "%02d", currentSet); u8x8.drawString(8,0,buf); setItemKeyPressed = false; while(!setItemKeyPressed){ if(setValueKeyPressed){ while(digitalRead(setValuePin) == LOW){ currentSet++; if(currentSet > tE->tMax){ currentSet = tE->tMin; } sprintf(buf, "%02d", currentSet); u8x8.drawString(8,0,buf); delay(SET_INCREMENT_DELAY); } } } setItemKeyPressed = false; return currentSet; } void printDateTime(){ // display date/time DateTime now = rtc.now(); char datBuf[] = "DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm:ss"; // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time u8x8.drawString(0,0,daysOfTheWeek[now.dayOfTheWeek()]); u8x8.drawString(0,2,datBuf); // display date u8x8.setFont(u8x8_font_courB18_2x3_f); u8x8.drawString(0,4,timeBuf); // display time u8x8.setFont(u8x8_font_saikyosansbold8_u); } void adjustRTC(timeUnit tU, uint16_t value){ // adjust year, month, day etc. DateTime now = rtc.now(); switch(tU){ case(YEAR): rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second())); break; case(MONTH): rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second())); break; case(DAY): rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second())); break; case(HOUR): rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second())); break; case(MINUTE): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second())); break; case(SECOND): rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value)); break; } delay(10); } void setBacklight(bool on){ if(on){ digitalWrite(backlightPin, LOW); } else digitalWrite(backlightPin, HIGH); } void backlightCheck(){ if(wakeUpNo - lastKeyPress < BACKLIGHT_ON_PERIOD){ setBacklight(true); } else setBacklight(false); } void setItemKeyISR(){ // ISR for pressed item key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setItemKeyPressed = true; } lastLow = millis(); } void setValueKeyISR(){ // ISR for pressed value key if(millis()-lastLow >= 300){ // ignore bouncing when button is released setValueKeyPressed = true; } lastLow = millis(); } ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD
Übertragung auf andere Mikrocontroller
Wenn ihr das Weckerkonzept im Batteriebetrieb auf andere Mikrocontroller bzw. Boards übertragen wollt, dann müsst ihr euch vor allem erst einmal mit den zur Verfügung stehenden Schlafmodi, deren Stromverbräuche und den Aufweckmethoden beschäftigen.
Nehmen wir mal den ESP32 als Beispiel. Dieser ist an sich ein echter Stromfresser. Im Deep Sleep jedoch verbrauchen einige ESP32 Boards (z.B. Adafruit ESP32 Feather V2 oder FireBeetle) sehr wenig Strom. Als Aufweckmethoden stehen der Timer, externe Interrupts oder Touchpins zu Verfügung. Daraus lässt sich was machen.
Allerdings führt der ESP32 beim Aufwachen aus dem Tiefschlaf einen Neustart aus. Mit anderen Worten: er hat alles vergessen, wenn ihr keine weiteren Maßnahmen ergreift. Uhrzeit und Alarmzeit könnt ihr im DS3231 abfragen. Aber woher soll der ESP32 beispielsweise noch wissen, dass sich der Wecker im Schlummermodus befindet? Das ließe sich beispielsweise in die RTC Memory oder den Flash schreiben (z.B. über die EEPROM-Funktion).
Also, es gibt für alles eine Lösung. Eine Übertragung ist nur nicht unbedingt „mal so eben“ realisiert.
Anhang 1: Nokia 5110 Displays reparieren
Wenn euer Nokia Display nichts anzeigt und ihr etwaige Verkabelungs- oder Codeprobleme ausgeschlossen habt, dann könnte es an Kontaktproblemen des Leitgummis (elastomeric connector) liegen. Das, was so aussieht wie ein Dämpfer oder Abstandshalter, ist ein wichtiges elektrisches Bauteil, das den Kontakt zwischen der Platine und dem Display herstellt.
Um an das Gummi heranzukommen, löst ihr das Display von der Platine, indem ihr die vier Halte-Clips nach außen biegt. Das Gummi befindet sich am oberen Rand. Reinigt das Gummi und die Kontaktstellen mit Alkohol und setzt das Display wieder zusammen. Nach meiner Erfahrung gibt es also eine gewisse Chance, dass es dann funktioniert.

Anhang 2: Alternative Versionen des Nokia 5110 Displays
Es gibt (mindestens) noch zwei weitere Versionen des Displays. Hier ein weiteres Rotes, das sich zunächst einmal im Pinout unterscheidet:

Die Pins sind im Prinzip dieselben, aber anders angeordnet. Außerdem leuchtet hier das Backlight, wenn ihr es mit VCC verbindet. Nun zum Problem dieser Version: Bei Einstellung eines geringen Kontrastes (setContrast(30)
) war das Display recht gut erkennbar. Beim Einschalten des Backlights war allerdings alles überstahlt. Bei einem höheren Kontrast (setContrast(60)
) war das Display zwar mit Backlight zufriedenstellend, ohne Backlight jedoch waren alle Pixel so dunkel, dass man nichts erkennen konnte.

Und zu guter Letzt habe ich noch dieses blaue Modul getestet:

Außer dem Backlight ging nichts! Sehr unbefriedigend.
Anhang 3: Eine einfache Uhr mit einem E-Paper Display
Wie schon erwähnt sind E-Paper Displays etwas eigen. Man sollte sie für Anwendungen vorsehen, bei denen der Bildschirminhalt nicht zu häufig wechselt. Außerdem sollte man wegen des hohen Flash- und SRAM-Bedarfs zu größeren Mikrocontrollern greifen. Dann allerdings stechen sie durch hervorragende Darstellung und niedrigen Strombedarf hervor. Hier schon mal das Ergebnis der einfachen Uhr auf einem 1.54-Zoll-Display von MH-ET LIVE:
Hier die Schaltung (mit 3.3 Volt Version des Arduino Pro Mini!):

Mein Beispielsketch unten belegte 29254 Bytes des Flashs, was schon ziemlich knapp ist. Das Ganze funktioniert auf dem Pro Mini auch nur mit „Paged Writing“. D.h. dass der Bildschirminhalt nicht als Ganzes, sondern in Stücken aufgebaut wird. Es geht, aber eben nicht ganz ideal. Der Stromverbrauch lag übrigens bei 0.32 mA in der Schlafphase.
Ich verzichte hier auf weitere Erklärungen, sondern bringe nur den Sketch. Vielleicht mache ich mal einen separaten Beitrag.
Zur Ansteuerung habe ich die Bibliothek GxEPD2 von Jean-Marc Zingg verwendet.
#include <Wire.h> #include <RTClib.h> // library for the DS3231 #include <avr/sleep.h> // hardware-specific sleep library for AVR MCUs #include <GxEPD2_BW.h> #include <GxEPD2_3C.h> #include <GxEPD2_4C.h> #include <GxEPD2_7C.h> #include <Fonts/FreeMonoBold12pt7b.h> #include <Fonts/FreeMonoBold24pt7b.h> #include "GxEPD2_display_selection_new_style.h" #define COMPILE_DURATION 40 // to const int wakeUpPin = 5; char daysOfTheWeek[7][12] = {"Sunday ", "Monday ", "Tuesday ", "Wednesday", "Thursday ", "Friday ", "Saturday "}; RTC_DS3231 rtc; // create RTC_DS3231 object DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0); void setup () { Serial.begin(115200); ADCSRA = 0; // ADC off (would consume current in deep sleep) display.init(115200); // default 10ms reset pulse, e.g. for bare panels with DESPI-C02 //display.init(115200, true, 2, false); // USE THIS for Waveshare boards with "clever" reset circuit, 2ms reset pulse display.setRotation(1); display.setFont(&FreeMonoBold12pt7b); display.setTextColor(GxEPD_BLACK); display.fillScreen(GxEPD_WHITE); if (! rtc.begin()) { Serial.println(F("Couldn't find RTC")); while(1){} } rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation rtc.disable32K(); // stop signal at the 32K pin rtc.clearAlarm(1); rtc.disableAlarm(1); rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin rtc.setAlarm1(alarmTime, DS3231_A1_Second); rtc.adjust(rtc.now() + TimeSpan(COMPILE_DURATION)); /* pressing the item or value button will cause a low signal at the corresponding pin and trigger an interrupt */ PCICR = (1<<PCIE2); // enable PCINT[23:16] interrupts PCMSK2 = (1<<PCINT21); // } void loop() { printDateTime(); set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode sleep_mode(); // sleep now! rtc.clearAlarm(1); } void printDateTime(){ // display date/time DateTime now = rtc.now(); char datBuf[] = "DD.MM.YYYY"; // define the date format char timeBuf[] = "hh:mm"; // define the time format Serial.println(now.toString(datBuf)); // print date Serial.println(now.toString(timeBuf)); // print time display.firstPage(); do { display.setCursor(30, 20); display.print(daysOfTheWeek[now.dayOfTheWeek()]); display.setCursor(30, 60); display.print(datBuf); display.setFont(&FreeMonoBold24pt7b); display.setCursor(30, 150); display.print(timeBuf); display.setFont(&FreeMonoBold12pt7b); } while (display.nextPage()); display.hibernate(); } ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD
#define GxEPD2_DISPLAY_CLASS GxEPD2_BW #define GxEPD2_DRIVER_CLASS GxEPD2_150_BN // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1 #ifndef EPD_CS #define EPD_CS SS #endif #if defined(GxEPD2_DISPLAY_CLASS) && defined(GxEPD2_DRIVER_CLASS) #define GxEPD2_BW_IS_GxEPD2_BW true #define GxEPD2_3C_IS_GxEPD2_3C true #define GxEPD2_4C_IS_GxEPD2_4C true #define GxEPD2_7C_IS_GxEPD2_7C true #define GxEPD2_1248_IS_GxEPD2_1248 true #define GxEPD2_1248c_IS_GxEPD2_1248c true #define IS_GxEPD(c, x) (c##x) #define IS_GxEPD2_BW(x) IS_GxEPD(GxEPD2_BW_IS_, x) #define IS_GxEPD2_3C(x) IS_GxEPD(GxEPD2_3C_IS_, x) #define IS_GxEPD2_4C(x) IS_GxEPD(GxEPD2_4C_IS_, x) #define IS_GxEPD2_7C(x) IS_GxEPD(GxEPD2_7C_IS_, x) #define IS_GxEPD2_1248(x) IS_GxEPD(GxEPD2_1248_IS_, x) #define IS_GxEPD2_1248c(x) IS_GxEPD(GxEPD2_1248c_IS_, x) #if defined(ARDUINO_ARCH_AVR) #define MAX_DISPLAY_BUFFER_SIZE 800 // #endif #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) #endif GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7)); #endif
Hallo Wolfgang,
mal wieder sehr schön ausgearbeitet.
Wenn es ums Stromsparen geht, würde ich auf die Sekunden verzichten und die Synchronisation auch nur minütlich durchführen. (Ist ja keine Stoppuhr ;-))
e.v. ein eInk Display. Wobei ich mir nicht ganz sicher bin, ob bei minütlichem Update viel Strom gespart werden kann. Ganz abgesehen davon, dass bei einem Ausfall des Weckers, die letzte Uhrzeit im Display steht.
Statt der DCF würde ich heute eher einen Zeitserver über das Heimnetz einbinden. Ich spare mir die Hardware und brauche keine Antenne auszurichten.
Liebe Grüße
Siggi
Hi Siggi,
danke für das Feedback. Beim Nokia 5110 Display ist der Stromspareffekt bei Minutenanzeige signifikant, da die Wachphase 166 ms beträgt. Beim EA DOG Display waren es nur 8 ms. Da fällt es nicht wirklich ins Gewicht.
VG, Wolfgang
Ein wirklich sehr schönes Projekt und so ausführlich die Beschreibung bezüglich der Displaytypen.
Aber einen Punkt sollte man berücksichtigen, solange es noch die Sommer- / Winterzeitumstellung gibt, dass man dann mit dieser RTC dann auf dem Holzweg ist. Und ich kann in dem Code auch keine automatisierte Zeitumstelllungsfunktion finden.
Interessant wäre hier noch oder ggf eine Einbindung eines DCF77 Moduls zur regelmäßigen RTC Re-Synchronisierung, ggf auch einen Batterie-Alarm, bei schwindender Leistung der Buffer-Batterie, oder einer zuschaltbaren akustischen Ausgabe ( Buzzer ) oder einer optischen Visualisierung …nur mal weil das Projekt ja „Wecker“ genannt wird, sollte es auch möglich sein eine schlafende Person aus dem Tiefschlaf zu holen 😉
ggf auch eine PWM Duty 50% Ausgabe auf ein FlatPiezzo Modul, auch wenn es ohne Resonator nicht besonders laut ist 😉
Wenn man schon auf ein ESP32 mit WLAN Modul wechselt, warum dann nicht die Einbindung in eine WLAN ( als Backup Synchronisationsroutine ) ermöglichen und NTC nutzen ?
Hallo Wolfgang,
danke für die Anregungen und ja, Sommer-/Winterzeit hätte ich erwähnen können. Allerdings kann ich nicht alles wiederholen, was ich schon in meinem Beitrag über den DS3231 geschrieben habe und auf den ich zu Beginn des Beitrages verweise. Dort habe ich einen Sketch, mit dem der DS3231 per DCF77 nachgestellt wird:
https://wolles-elektronikkiste.de/ds3231-echtzeituhr#set_ds3231_with_dcf77
Auf den akustischen Weckalarm bin ich im Beitrag eingegangen und habe ja geschrieben, dass ich genau das der Kreativität des Lesers überlasse und beispielsweise vorgeschlagen, einen MP3-Player einzusetzen, um sich mit seinem Lieblingslied wecken zu lassen. Ob nun ein MP3-Player oder einfach ein lauter Weckton, eine Anbindung zum Smarthome, die die Jalousien hochfährt oder eine Schaltung, die das Radio anstellt. Die Geschmäcker sind hier wahrscheinlich sehr verschieden, und es sollte nicht schwerfallen, so etwas dann selbst einzubinden.
Und ich wollte eigentlich auch nicht empfehlen, auf den ESP32 umzusteigen – das war nur ein Beispiel um aufzuzeigen, dass die Übertragung auf andere Mikrocontroller nicht ganz einfach ist, da die Schlaffunktionen sehr Hardware-spezifisch sind
Man könnte auch noch eine Platine dazu entwerfen und eine 3D-Vorlage für ein Weckergehäuse. Also komplett fertiges Projekt – das war aber nicht mein Ziel. Es geht hier darum, Wege aufzuzeigen, einen DS3231 mit wenigen Tastern zu steuern und so ein Projekt batteriefähig zu machen.
VG, Wolfgang
Hallo Wolle,
Vielen Dank für Deinen Artikel. Der Vergleich verschiedener Displays ist wirklich nützlich! Erlaube mir eine kleine Korrektur. Du schriebst „Allerdings führt der ESP32 beim Aufwachen aus dem Tiefschlaf einen Neustart aus. Mit anderen Worten: er hat alles vergessen.“ – Das hängt jedoch vom DeepSleep Mode ab…
Zitat: „If some variables in the program are placed into RTC SLOW memory (for example, using RTC_DATA_ATTR attribute), RTC SLOW memory will be kept powered on by default. “
Hier die Doku: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/sleep_modes.html#id1
Das funktioniert beispielsweise bei einem FireBeetle 2 Board genau wie beschrieben. Der gemessene Stromverbrauch im Deepsleep mit RTC SLOW unter Strom beträgt bei mir 18-20 uA.
Hi Gregor,
ja, stimmt, danke für den Hinweis. Wusste ich mal – habe es aber zwischenzeitlich wieder vergessen. Lerne aber auch immer gerne dazu. Was ich hier aber eigentlich zum Ausdruck bringen wollte ist, das eine Übertragung Hardware-spezifischen Codes eine gewisse Herausforderung darstellt.
Viele Grüße, schönes Wochenende, Wolfgang
Meines Wissens nach verfügt der ESP32 über einen User EEPROM, so dass das nicht wirklich das Problem seine sollte kleinere Datenmengen, auch wenn es nur Pointer sind über den Deepsleep hinweg „aufzuheben“ 😉
Danke – aber ich hatte doch eigentlich geschrieben, dass die EEPROM Funktion sich dazu eignet!? Wobei ich absichtlich EEPROM Funktion und nicht EEPROM geschrieben habe, da die EEPROM-Funktion lediglich einen Teil des Flashs wie einen EEPROM nutzt.
VG, Wolfgang