// Berlin Uhr v2.00.01 (Begonnen am 13.03.2025)
// Anleitung: https://www.instructables.com/Berlin-Clock-Arduino-Nano-DS1307-Real-Time-Clock-7/
// DCF-77: https://wolles-elektronikkiste.de/dcf77-funkuhr
// Rot = 0
// Orange = 35
// Gelb = 50
// Grün = 130
// Blau = 240
// LCD.display() / LCD.noDisplay() // Display ein. ausschalten
// SAT Antenne wird nur angezeigt, wenn Synchonistion erfolgreich war.
// Synchronisierung nachts um 3:00 Uhr. Wenns nich klappt, alle 10 Minuten, 1h lang.
// Bei Fehlschlag, Antenne ausblenden und Sekunden LED auf rot wechseln.
#include <Wire.h> // I²C einbinden
#include <TimeLib.h> // Zeit- und Datumsfunktionen einbinden
#include <RTClib.h> // RTC 3231 einbinden
#include <LiquidCrystal_I2C.h> // I2 LCD Display einbinden
#include <Adafruit_NeoPixel.h> // LED Stripes (WS2812B) ansteuern
#include <EEPROM.h> // EEprrom einbinden
#define prgVersion "2.06" // Programmversion
#define prgDate "30.05.25" // Programm Erstellungsdatum
#define author " Gucky"
#define lcdAddress 0x27 // I²C Adreresse der RTC
#define rtcAddress 0x30 // I²C Adresse des LCD Displays
#define lcdCols 16 // 16 Zeichen pro LCD Zeile
#define numLed 24
#define lcdRows 2
#define pinInterrupt 2
#define pinStripe 3
#define pinMotion 4
#define pinBeeper 5 // Piepser Pin
#define pinEnter 8
#define pinValDown 9
#define pinValUp 10
#define pinMenuDown 11
#define pinMenuUp 12
#define pinLED 13
#define pinLDR A0
#define eeUseLDR 0x01
#define eeUseMotion 0x02
#define eeUseBeeper 0x03
#define eeSyncHour 0x11;
#define eeSyncMin 0x12;
byte rot = 0;
byte glb = 40;
byte ora = 25;
byte grn = 130;
byte bla = 240;
byte sat = 255;
byte val = 255;
byte light;
// Einstellungen & Menü
bool useMotion = false;
bool useLDR = false;
bool useBeeper = false;
byte tempValue;
byte timeHour;
byte timeMin;
byte timeSec;
byte timeDay;
byte timeMonth;
byte timeYear;
byte timeWeekDay;
byte timeUpdated = 0;
// Variablen für DCF-77
volatile unsigned long lastInt = 0;
volatile unsigned long long currentBuf = 0;
volatile unsigned long long nextBuf = 0;
//volatile bool newMinute = false;
//volatile bool timeValid = false;
volatile byte bufCounter;
byte signal = 0;
bool rtcUpdated = false; // Wird true, wenn Zeit synchronisiert wurde.
// Variablen für Tasten
bool keyMenuDown = HIGH;
bool keyMenuUp = HIGH;
bool keyValDown = HIGH;
bool keyValUp = HIGH;
bool keyEnter = HIGH;
bool keyMenuDownOld = HIGH;
bool keyMenuUpOld = HIGH;
bool keyValDownOld = HIGH;
bool keyValUpOld = HIGH;
bool keyEnterOld = HIGH;
unsigned long menuTimeout;
byte menuPage = 0;
unsigned long keyPressTime;
bool editMode = false;
// Wchentagsnamen
char const PROGMEM day01[] = "So";
char const PROGMEM day02[] = "Mo";
char const PROGMEM day03[] = "Di";
char const PROGMEM day04[] = "Mi";
char const PROGMEM day05[] = "Do";
char const PROGMEM day06[] = "Fr";
char const PROGMEM day07[] = "Sa";
char* const PROGMEM days[] = { day01, day02, day03, day04, day05, day06, day07 };
char dayName[2];
char const PROGMEM month01[] = "Jan";
char const PROGMEM month02[] = "Feb";
char const PROGMEM month03[] = "Mär";
char const PROGMEM month04[] = "Apr";
char const PROGMEM month05[] = "Mai";
char const PROGMEM month06[] = "Jun";
char const PROGMEM month07[] = "Jul";
char const PROGMEM month08[] = "Aug";
char const PROGMEM month09[] = "Sep";
char const PROGMEM month10[] = "Okt";
char const PROGMEM month11[] = "Nov";
char const PROGMEM month12[] = "Dez";
char* const PROGMEM months[] = { month01, month02, month03, month04, month05, month06, month07, month08, month09, month10, month11, month12 };
char monthName[3];
const byte haken[] = { B00000, B00000, B00001, B00011, B10110, B11100, B01000, B00000 };
const byte signal1[] = { B00000, B00000, B00000, B00000, B00000, B00000, B11000, B11000 };
const byte signal2[] = { B00000, B00000, B00000, B00000, B00011, B00011, B11011, B11011 };
const byte signal3[] = { B00000, B00000, B11000, B11000, B11000, B11000, B11000, B11000 };
const byte signal4[] = { B00011, B00011, B11011, B11011, B11011, B11011, B11011, B11011 };
const byte c[] = { B01110, B10001, B10111, B11001, B11001, B10111, B10001, B01110 };
const byte akku[] = { B01110, B11111, B10001, B10001, B11111, B11111, B11111, B11111 };
const byte antenna[] = { B10001, B01001, B00110, B00110, B11001, B00011, B00111, B00111 };
LiquidCrystal_I2C LCD(lcdAddress, lcdCols, lcdRows);
Adafruit_NeoPixel STRIPE(numLed, pinStripe, NEO_GRB + NEO_KHZ800);
RTC_DS3231 RTC;
void setup()
{
pinMode(pinInterrupt, INPUT);
pinMode(pinMenuUp , INPUT_PULLUP);
pinMode(pinMenuDown , INPUT_PULLUP);
pinMode(pinValUp , INPUT_PULLUP);
pinMode(pinValDown , INPUT_PULLUP);
pinMode(pinMotion , INPUT_PULLUP);
pinMode(pinEnter , INPUT_PULLUP);
pinMode(pinLED , OUTPUT);
Serial.begin(115200);
LCD.init();
LCD.createChar(0, haken);
LCD.createChar(1, signal1);
LCD.createChar(2, signal2);
LCD.createChar(3, signal3);
LCD.createChar(4, signal4);
LCD.createChar(5, c);
LCD.createChar(6, antenna);
LCD.createChar(7, akku);
if(!RTC.begin())
{
while(1)
{
digitalWrite(pinLED, HIGH);
delay(50);
digitalWrite(pinLED, HIGH);
delay(950);
}
}
//RTC.adjust(DateTime(2000, 1, 1, 0, 0, 0));
STRIPE.begin();
LCD.clear();
LCD.backlight();
digitalWrite(pinLED, HIGH);
LCD.print(F(" DCF-77 "));
LCD.setCursor(0, 1);
LCD.print(F(" BERLIN CLOCK "));
tone(pinBeeper, 2000, 30);
delay(2000);
for(byte x = 0; x <= numLed; x++)
{
STRIPE.setPixelColor(x - 1, STRIPE.ColorHSV( 0, 0, 0));
STRIPE.setPixelColor( x, STRIPE.ColorHSV( 130, 255, 255));
STRIPE.show();
delay(50);
}
LCD.clear();
LCD.print(F(" Version "));
LCD.print(prgVersion);
LCD.setCursor(0, 1);
LCD.write(5);
LCD.print(prgDate);
LCD.print(author);
delay(2000);
LCD.clear();
digitalWrite(pinLED, LOW);
attachInterrupt(digitalPinToInterrupt(pinInterrupt), DCF77_Interrupt, CHANGE);
}
/*
void checkAdj()
{
if (digitalRead(changeTime) == HIGH)
{
if (digitalRead(setHourPos) == HIGH)
{
unsigned long v = RTC.get();
v = v + 3600;
RTC.set(v);
}
if (digitalRead(setMinPos) == HIGH)
{
unsigned long v = RTC.get();
v = v + 60;
RTC.set(v);
}
if (digitalRead(setHourNeg) == HIGH)
{
unsigned long v = RTC.get();
v = v - 3600;
RTC.set(v);
}
if (digitalRead(setMinNeg) == HIGH)
{
unsigned long v = RTC.get();
v = v - 60;
RTC.set(v);
}
i = (tm.Minute);
k = (tm.Hour);
obtainNo();
obtainRunning();
compileRegs();
sendToRegs();
delay(300);
}
}
*/
void loop()
{
unsigned long t = millis();
DateTime now = RTC.now(); // RTC auslesen
if(timeUpdated != now.second()) // Nur jede Sekunde die Anzeigen aktualisueren
{
timeHour = now.hour(); // Zeitvariablen für die Verwendung in Subroutinen speichern
timeMin = now.minute();
timeSec = now.second();
timeDay = now.day();
timeMonth = now.month();
timeYear = now.year() - 2000;
timeWeekDay = now.dayOfTheWeek();
if(useBeeper && !timeSec)
{
tone(pinBeeper, 2000, 50);
delay(100);
tone(pinBeeper, 2000, 50);
}
if(digitalRead(pinMotion) == LOW && millis() > 15000 && useMotion) // Wenn keine Bewegung, Timeout abgelaufen und LCD ein eschaltet sind, ...
{
LCD.clear(); // ... Display ausschaslten.
LCD.noBacklight();
}
else
{
if(!menuPage)
updateDisplay(); // LCD Display aktualisieren
}
updateStripe(); // LED Stripe aktualisieren
timeUpdated = now.second(); // Neue Sekunde speichern
}
checkKeys(); // Tasten abfragen
//LCD.setCursor(13,1);
//LCD.print(millis() - t);
}
// LCD aktualisieren
void updateDisplay()
{
LCD.backlight(); // Hintergrundbeleuchtung einschalten.
char day; // Für Wochentagsanzeige.
LCD.setCursor(4, 0); // Cursor in Zeile 1, Position 5.
LCD.print(timeHour < 10? "0":""); // Wenn Stunde kleiner als 10 ist, eine 0 voarnstellen, sonst nichts.
//if(timeHour < 10)
// LCD.print(F("0"));
LCD.print(timeHour); // Stunde ausgeben
LCD.print(F(":")); // Gefolgt von :
LCD.print(timeMin < 10? "0":""); // Wenn Minute kleiner als 10 ist, eine 0 voarnstellen, sonst nichts.
//if(timeMin < 10)
// LCD.print(F("0"));
LCD.print(timeMin); // Minuten ausgeben.
LCD.print(timeSec % 2 == 0? ":":" "); // Wenn Minute geradzahlig ist, : ausgeben sonst ein Leerzeichen
//if(timeSec % 2 == 0)
// LCD.print(F(":"));
//else
// LCD.print(F(" "));
// if(timeSec < 10)
// LCD.print(F("0"));
LCD.print(timeSec < 10? "0":""); // Wenn Sekunde kleiner als 10 ist, eine 0 voarnstellen, sonst nichts.
LCD.print(timeSec); // Sekunde ausgeben.
LCD.setCursor(1, 1); // Cursor in Zeile 2,Position 2.
strcpy_P(dayName, (char*)pgm_read_word(&(days[timeWeekDay]))); // Wochentag (Name) ermitteln und...
LCD.print(dayName); // ausgeben.
LCD.print(F(", ")); // Komma anhängen.
LCD.print(timeDay < 10? "0":""); // Wenn TagSekunde kleiner als 10 ist, eine 0 voarnstellen, sonst nichts.
//if(timeDay < 10)
// LCD.print(F("0"));
LCD.print(timeDay); // Tag ausgeben.
LCD.print(F(". ")); // Gefilgt von einem .
strcpy_P(monthName, (char*)pgm_read_word(&(months[timeMonth - 1]))); // Wochentag (Name) ermitteln.
LCD.print(monthName); // Wochentag ausgeben.
//if(timeMonth < 10)
// LCD.print(F("0"));
//LCD.print(timeMonth);
LCD.print(F(" ")); // Gefolgt von einem Leerzeichen.
LCD.print(timeYear); // Jahreszehl ausgeben (2000 werden in loop von now.year() abgezogen).
// Empfangsstäre anzeigen.
LCD.setCursor(0, 0);
LCD.print(F(" "));
signal = random(0, 4);
if(signal > 0) // Empfangsanzeige oben links und rechts.
{
LCD.setCursor(0, 0); // Cursor nach oben Links setzen (Empfangsstärke).
LCD.write(signal < 2? 1:2); // Wenn signal 1 oder 2 ist, nur das 1.Symbol entsprechend anzeigen
if(signal > 2) // Wenn signal größer als 2 ist (nur dann, um 2 Symbol auszublenden/anzuzeigen), ...
LCD.write(signal > 3? 4:3); // ... und 3 oder 4 ist, 2. Symbol entsprechend anzeigen.
}
LCD.setCursor(15, 0); // Cursor nach oben rechts setzen.
LCD.write(signal < 1? 32:6); // Wenn signal 0 ist, Leerzeichen anzeigen, sonst das Antennensymbol.
}
void updateStripe()
{
int light = 0; // light auf 0 setzen.
for(byte x = 0; x <= 4; x++) // Schleife 5mal signalDurationchlaufen.
{
light += analogRead(pinLDR); // Lichtstärke messen und Werte addieren./
delay(5); // kurz warten.
}
byte v = useLDR == true? map(light / 4, 0, 1023, 255, 100):255; // Daraus einen Wert zwichen 50 und 255 mitteln, der die Leuchtstärke der LEDs widerspiegelt.
// Aber nur dann LEDs steuern, wenn useLDR true ist, sonst fix val benutzen.
// Sekunden
STRIPE.setPixelColor( 0, STRIPE.ColorHSV( editMode == false? glb * 256:bla * 256, sat, timeSec % 2? 0:v));
// 1 - 4 Minuten
//for(byte x = 20; x <= 23; x ++)
// STRIPE.setPixelColor( x, STRIPE.ColorHSV( glb * 256, sat, timeSec % 2? 0:val));
STRIPE.setPixelColor( 20, STRIPE.ColorHSV( ora * 256, sat, timeMin % 5 >= 1? v:0));
STRIPE.setPixelColor( 21, STRIPE.ColorHSV( ora * 256, sat, timeMin % 5 >= 2? v:0));
STRIPE.setPixelColor( 22, STRIPE.ColorHSV( ora * 256, sat, timeMin % 5 >= 3? v:0));
STRIPE.setPixelColor( 23, STRIPE.ColorHSV( ora * 256, sat, timeMin % 5 >= 4? v:0));
// 5´er Minuten
//for(byte x = 9; x <= 19; x ++)
// STRIPE.setPixelColor( x, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 5? val:0));
STRIPE.setPixelColor( 9, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 5? v:0));
STRIPE.setPixelColor( 10, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 10? v:0));
STRIPE.setPixelColor( 11, STRIPE.ColorHSV( rot * 256, sat, timeMin >= 15? v:0));
STRIPE.setPixelColor( 12, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 20? v:0));
STRIPE.setPixelColor( 13, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 25? v:0));
STRIPE.setPixelColor( 14, STRIPE.ColorHSV( rot * 256, sat, timeMin >= 30? v:0));
STRIPE.setPixelColor( 15, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 35? v:0));
STRIPE.setPixelColor( 16, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 40? v:0));
STRIPE.setPixelColor( 17, STRIPE.ColorHSV( rot * 256, sat, timeMin >= 45? v:0));
STRIPE.setPixelColor( 18, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 50? v:0));
STRIPE.setPixelColor( 19, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 55? v:0));
// 1 - 4 Stunden
STRIPE.setPixelColor( 5, STRIPE.ColorHSV( rot * 256, sat, timeHour % 5 >= 1? v:0));
STRIPE.setPixelColor( 6, STRIPE.ColorHSV( rot * 256, sat, timeHour % 5 >= 2? v:0));
STRIPE.setPixelColor( 7, STRIPE.ColorHSV( rot * 256, sat, timeHour % 5 >= 3? v:0));
STRIPE.setPixelColor( 8, STRIPE.ColorHSV( rot * 256, sat, timeHour % 5 >= 4? v:0));
//for(byte x = 1; x <= 8; x ++)
// STRIPE.setPixelColor( x, STRIPE.ColorHSV( ora * 256, sat, timeMin >= 5? val:0));
// 5´er Stunden
STRIPE.setPixelColor( 1, STRIPE.ColorHSV( rot * 256, sat, timeHour >= 5? v:0));
STRIPE.setPixelColor( 2, STRIPE.ColorHSV( rot * 256, sat, timeHour >= 10? v:0));
STRIPE.setPixelColor( 3, STRIPE.ColorHSV( rot * 256, sat, timeHour >= 15? v:0));
STRIPE.setPixelColor( 4, STRIPE.ColorHSV( rot * 256, sat, timeHour >= 20? v:0));
//LCD.setCursor (0,0);
//LCD.print(" ");
//LCD.setCursor (0,0);
//LCD.print(timeSec % 5);
//LCD.setCursor(0,1);
//LCD.print(" ");
//LCD.setCursor(0,1);
//LCD.print(_sec);
//LCD.print("-");
//LCD.print(_sec % 60 == 0? 0:1);
STRIPE.show();
}
void checkKeys()
{
// menuTimeout
// showMenu
if(millis() >= menuTimeout + 5000 + (editMode * 55000) && menuPage && !editMode)
{
menuPage = 0;
editMode = false;
LCD.clear();
LCD.noBlink();
updateDisplay();
}
keyMenuUp = digitalRead(pinMenuUp);
keyMenuDown = digitalRead(pinMenuDown);
keyValUp = digitalRead(pinValUp);
keyValDown = digitalRead(pinValDown);
keyEnter = digitalRead(pinEnter);
const byte menuPageMax = 7;
if(keyMenuUp != keyMenuUpOld)
{
keyMenuUpOld = keyMenuUp;
delay(5);
digitalWrite(pinLED, !keyMenuUp);
if(keyMenuUp == LOW)
{
if(menuPage >= menuPageMax)
menuPage = 1;
else
menuPage++;
editMode = false;
showMenu();
}
menuTimeout = millis();
}
if(keyMenuDown != keyMenuDownOld)
{
keyMenuDownOld = keyMenuDown;
delay(5);
digitalWrite(pinLED, !keyMenuDown);
if(keyMenuDown == LOW)
{
if(menuPage <= 1)
menuPage = menuPageMax;
else
menuPage--;
editMode = false;
showMenu();
}
menuTimeout = millis();
}
if(keyEnter != keyEnterOld)
{
keyEnterOld = keyEnter;
delay(5);
digitalWrite(pinLED, !keyEnter);
if(keyEnter == LOW)
{
if(menuPage > 1)
editMode = !editMode;
if(editMode)
LCD.blink();
else
{
LCD.setCursor(0, 1);
LCD.print(F(" "));
}
showMenu();
}
menuTimeout = millis();
}
}
void showMenu()
{
// LCD.setCursor(13, 0);
// LCD.print(menuPage);
// delay(500);
LCD.clear();
switch(menuPage)
{
case 1:
LCD.print(F(" Version "));
LCD.print(prgVersion);
LCD.print(F(" "));
LCD.write(126);
LCD.setCursor(0, 1);
LCD.write(5);
LCD.print(prgDate);
LCD.print(author);
break;
case 2:
LCD.write(127);
LCD.print(F(" DCF-77 Sync. "));
LCD.write(126);
if(editMode)
{
tempValue = timeHour;
LCD.setCursor(0, 1);
LCD.print(F("Sync.Zeit: : "));
LCD.setCursor(11, 1);
LCD.print(tempValue < 10? "0":"");
LCD.print(tempValue);
LCD.setCursor(14, 1);
LCD.print(timeMin < 10? "0":"");
LCD.print(timeMin);
LCD.setCursor(15,1);
}
else
{
timeHour = tempValue;
}
break;
case 3:
LCD.write(127);
LCD.print(F(" LED Helligk. "));
LCD.write(126);
if(editMode)
{
LCD.setCursor(0, 1);
LCD.print(F("Modus: "));
LCD.write(useLDR == true? 0:32);
}
break;
case 4:
LCD.write(127);
LCD.print(F(" LCD Beleucht "));
LCD.write(126);
break;
case 5:
LCD.write(127);
LCD.print(F(" IR Sensor "));
LCD.write(126);
break;
case 6:
LCD.write(127);
LCD.print(F(" LDR Empfindl."));
LCD.write(126);
break;
case 7:
LCD.write(127);
LCD.print(F(" Piepser "));
break;
}
if(useBeeper)
tone(pinBeeper, 1000, 20);
}
void DCF77_Interrupt()
{
unsigned int signalDuration = 0;
signalDuration = millis() - lastInt;
digitalWrite(pinLED, digitalRead(pinInterrupt));
if(digitalRead(pinInterrupt))
{
if(signalDuration > 1500) // Wenn Signaldauer > 1500 mS, dann neue Sekunde.
{
if(bufCounter == 59)
{
//timeValid = true;
if(signal < 4)
signal++;
currentBuf = nextBuf;
}
else
{
if(signal > 0)
signal--;
//timeValid = false;
}
nextBuf = 0;
//newMinute = true;
bufCounter = 0;
}
}
else
{
if(signalDuration > 150)
nextBuf |= ((unsigned long long)1 << bufCounter);
bufCounter++;
}
lastInt = millis();
}
//void evaluateSequence() // Evtl. überflüssig?
//{
// byte dcf77Year = (currentBuf >> 50) & 0xFF; // year = bit 50-57
// byte dcf77Month = (currentBuf >> 45) & 0x1F; // month = bit 45-49
// byte dcf77DayOfWeek = (currentBuf >> 42) & 0x07; // day of the week = bit 42-44
// byte dcf77DayOfMonth = (currentBuf >> 36) & 0x3F; // day of the month = bit 36-41
// byte dcf77Hour = (currentBuf >> 29) & 0x3F; // hour = bit 29-34
// byte dcf77Minute = (currentBuf >> 21) & 0x7F; // minute = 21-27
// bool parityBitMinute = (currentBuf >> 28) & 1;
// bool parityBitHour = (currentBuf >> 35) & 1;
// bool parityBitDate = (currentBuf >> 58) & 1;
// if((parity_even_bit(dcf77Minute)) == parityBitMinute)
// {
// if((parity_even_bit(dcf77Hour)) == parityBitHour)
// {
// if(((parity_even_bit(dcf77DayOfMonth) + parity_even_bit(dcf77DayOfWeek) + parity_even_bit(dcf77Month) + parity_even_bit(dcf77Year))%2) == parityBitDate)
// RTC.adjust(DateTime(rawByteToInt(dcf77Year) + 2000, rawByteToInt(dcf77Month), rawByteToInt(dcf77DayOfMonth), rawByteToInt(dcf77Hour), rawByteToInt(dcf77Minute), 0));
// }
// }
//}
//unsigned int rawByteToInt(byte raw)
//{
// return ((raw >> 4) * 10 + (raw & 0x0F));
//}
//bool parity_even_bit(byte val)
//{
// val ^= val >> 4;
// val ^= val >> 2;
// val ^= val >> 1;
// val &= 0x01;
// return val;
//}
V-
V+
M-
M+
G
O
R
R
R
R
R
R
R
R
R
R
R
Sekunde
Stunde x 5
Stunde 1-4
Minute x 5
Minute 1-4
DCF-77
Signal
O
O
O
O
O
O
O
O
O
O
O
Nur ein LDR, kein Modul.
E
Motion
Piezo
Piepser