
DCF77-Generator mit ESP8266
von Günther Zöppel

Schon immer störte mich die wechselnde und nicht konstante Feldstärke
des DCF77-Langwellensignals hier im Erzgebirge, dadurch dauerte es
manchmal sehr lange, bis die Uhren sich synchronisierten. In letzter
Zeit kam es, wahrscheinlich bedingt durch starke Bebauung, auch zu
schlechteren Ausbreitungsbedingungen des DCF77-Signals, und ich
überlegte, eine Alternative zu finden, die zeitgenau alle meine Uhren
synchronisieren kann. In diesem Artikel stelle ich eine praktische
Anwendung des ESP8266 vor: die Simulation eines mit der amtlichen Zeit
übereinstimmenden DCF77-Signals.
Mit diesem Projekt kann man ein DCF77-Zeittelegramm erzeugen, das von
Radiouhren empfangen und interpretiert werden kann. Dieses Projekt ist
auch geeignet für Entwickler, die die DCF77-Zeitsynchronisation
simulieren oder testen möchten, ohne auf den echten Sender angewiesen
zu sein.
Die Übertragung der Impulsfolge zur jeweiligen Uhr kann durch Abgreifen
der Impulse am Simulatorausgang in TTL-Form sowie mit 3,3V-Pegel (echt
und negiert) erfolgen und direkt über Kabel in die Uhr eingespeist
werden, es ist aber auch die Anwendung eines Modulators möglich, der
ein 77,5 KHz-HF-Signal erzeugt, welches per Funk drahtlos die
entsprechende Uhr (z.B. Armbanduhr) steuert.
Hardware-Komponenten
- ESP8266 (z. B. NodeMCU oder D1 Mini)
- RTC-Modul (z. B. DS3231) zur genauen Zeitmessung
- 20x2 LCD-Display mit I2C-Schnittstelle
- WLAN-Netzwerk zur Synchronisation mit einem NTP-Server
- Netztrafo und Netzteilelektronik
- Kleinmaterial für den Aufbau der Schaltung
Der ESP8266 verbindet sich mit einem WLAN und synchronisiert die Zeit
über einen NTP-Server. Dies gewährleistet eine hochgenaue Basiszeit.
Der Sketch nutzt die Bibliothek `NTPClient`, um die aktuelle Unix-Zeit
zu beziehen, und überträgt diese an das RTC-Modul. Damit ist man von
der vom Sender Mainflingen ausgestrahlten Impulsfolge unabhängig und
kann das auch noch nutzen, wenn der Sender abgeschaltet würde, was ja
ab und zu diskutiert wird.
Das DCF77-Telegramm besteht aus 59 Bits, die jede Minute übertragen
werden. Die Bits 17 und 18 geben Informationen zur Sommerzeit (MESZ)
und Übergangszeit an. Der Sketch stellt sicher, dass diese Bits korrekt
gesetzt werden, basierend auf der aktuellen Zeit und den gültigen
Sommerzeitregeln. Die Aussendung der Impulsfolge beginnt erst,
wenn im RTC eine gültige Zeit eingetragen ist und eine weitere Minute
mit der 1. Sekunde beginnt, zu sehen am Blinken der grünen Puls-LED im
Sekundentakt. Die Paritätsbits für das Generieren der Pulsfolge werden
jede Minute neu berechnet.
Ein 20x2 LCD zeigt die aktuelle Zeit und das Datum an. Zusätzlich
informiert es über den verwendeten Zeitversatz (MEZ oder MESZ), der
ebenfalls aus der NTP-Synchronisation abgeleitet wird.
Hier sind einige der Schlüsselkonzepte des Sketches:
- Sommerzeit-Berechnung: Die Funktion `isSummerTime()` bestimmt, ob die
aktuelle Zeit in den Sommerzeitbereich fällt. Hierbei wird der letzte
Sonntag im März und im Oktober berechnet, um den Beginn bzw. das Ende
der Sommerzeit festzustellen.
- Generierung des DCF77-Telegramms: Die Funktion `mkdcf77()` erstellt
die 59 Bits des Zeittelegramms. Paritätsbits werden dynamisch
berechnet, um sicherzustellen, dass das Telegramm von Empfängern
korrekt interpretiert wird.
- Timer-Interrupt: Ein Timer des ESP8266 sorgt dafür, dass das
DCF77-Telegramm im richtigen Takt ausgegeben wird. Dies stellt sicher,
dass die Impulsdauer den DCF77-Spezifikationen entspricht (Impulse von
100 bzw. 200ms Länge im Sekundentakt und Pause in der 59. Sekunde zur
Synchronisation auf eine neue Minute)
-Präzision: Die Synchronisation mit einem NTP-Server und die Verwendung
eines RTC-Moduls sorgen für eine hohe Genauigkeit.
-Flexibilität: Der Sketch ist modular aufgebaut und kann leicht
erweitert werden. -Bildung: Dieses Projekt eignet sich hervorragend, um
die Funktionsweise von DCF77 zu verstehen und zu testen. -
kann in jedem Haushalt verwendet werden, wenn der DCF-Empfang schwach
ist oder ganz ausfällt.
Fazit: Der vorgestellte Sketch kombiniert die Vielseitigkeit des
ESP8266 mit der Funktionalität eines RTC-Moduls, um ein vollständiges
DCF77-Zeittelegramm zu simulieren. Mit minimalem Hardwareaufwand kann
dieses Projekt Radiouhren synchronisieren, aber auch als Lehrmittel
dienen. Für potentielle Nachbauer geben die beigefügten Bilder, die
Hinweise im Sketch sowie die Schaltung genügend Anregungen. Der ESP8266
bietet sich aufgrund seiner integrierten WLAN-Zugriffsmöglichkeit
geradezu dafür an, es wäre aber auch ein ESP32 möglich (bei
entsprechender Änderung der angesteuerten GPIo´s im Sketch). Alle meine
DCF-gesteuerten Uhren synchronisieren sich jetzt innerhalb von maximal
3 min nach Einspeisen des Simulationssignals.
G.Zöppel
Juli 2025
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
LiquidCrystal_I2C lcd(0x27, 20, 2); // 20x2 LCD-Display
RTC_DS3231 rtc;
const char* ssid = "Mein_WLAN";
const char* password = "Mein_Passwort";
WiFiUDP ntpUDP;
int timeOffset = 7200; // 3600 = MEZ, 7200 = MESZ
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);
#define DCF77OUT 13
#define DCF77OUT_NEG 12
int count = 0;
long lobit = 0;
long hibit = 0;
time_t unixtime;
void IRAM_ATTR timer0_ISR() {
int curbit;
int sec = count / 10;
if (lobit != 0) {
if (count < 290) {
curbit = (lobit >> sec) & 1;
} else {
curbit = (hibit >> (sec - 29)) & 1;
}
if (sec != 59) {
if (count % 10 == 0) {
digitalWrite(DCF77OUT, HIGH);
digitalWrite(DCF77OUT_NEG, LOW); // Invertiert
} else if (count % 10 == 1 && curbit == 0) {
digitalWrite(DCF77OUT, LOW);
digitalWrite(DCF77OUT_NEG, HIGH); // Invertiert
} else if (count % 10 == 2 && curbit == 1) {
digitalWrite(DCF77OUT, LOW);
digitalWrite(DCF77OUT_NEG, HIGH); // Invertiert
}
}
}
if (count == 590) {
noInterrupts();
mkdcf77();
interrupts();
}
if (count % 10 == 0) ++unixtime;
++count;
if (count == 600) count = 0;
timer0_write(ESP.getCycleCount() + 8000000L);
}
void mkdcf77() {
DateTime now = rtc.now();
int minutes = now.minute() + 2;
if (minutes >= 60) minutes -= 60;
int hours = now.hour();
int day = now.day();
int month = now.month();
int year = now.year() - 2000;
int week = (now.dayOfTheWeek() + 6) % 7 + 1;
lobit = 0;
hibit = 0;
// Setze Bit 17 und 18 basierend auf Sommerzeit und Übergangszeit
bool isMESZ = isSummerTime(now.year(), now.month(), now.day());
bool isTransition = isTransitionTime(now.year(), now.month(), now.day());
if (isMESZ) {
lobit |= 1 << 17; // MESZ aktiv
} else {
lobit &= ~(1 << 17); // MESZ nicht aktiv
}
if (isTransition) {
lobit |= 1 << 18; // Übergangszeit
} else {
lobit &= ~(1 << 18); // Keine Übergangszeit
}
lobit |= 1 << 20; // Start Bit
lobit |= (minutes % 10) << 21;
lobit |= (minutes / 10) << 25;
hibit |= (hours % 10);
hibit |= (hours / 10) << 4;
hibit |= (day % 10) << 7;
hibit |= (day / 10) << 11;
hibit |= week << 13;
hibit |= (month % 10) << 16;
hibit |= (month / 10) << 20;
hibit |= (year % 10) << 21;
hibit |= (year / 10) << 25;
// Paritätsberechnungen
int parity = 0;
for (int i = 21; i <= 27; ++i) parity += (lobit >> i) & 1;
if (parity & 1) lobit |= 1 << 28;
parity = 0;
for (int i = 0; i <= 5; ++i) parity += (hibit >> i) & 1;
if (parity & 1) hibit |= 1 << 6;
parity = 0;
for (int i = 7; i <= 28; ++i) parity += (hibit >> i) & 1;
if (parity & 1) hibit |= 1 << 29;
}
bool isSummerTime(int year, int month, int day) {
if (month < 3 || month > 10) return false;
if (month > 3 && month < 10) return true;
int lastSunday = getLastSunday(year, month);
if (month == 3 && day >= lastSunday) return true;
if (month == 10 && day < lastSunday) return true;
return false;
}
bool isTransitionTime(int year, int month, int day) {
if (month == 3 || month == 10) {
int lastSunday = getLastSunday(year, month);
return day == lastSunday - 1; // Tag vor dem Wechsel
}
return false;
}
int getLastSunday(int year, int month) {
DateTime lastDay(year, month, 31);
int weekday = lastDay.dayOfTheWeek();
return 31 - weekday;
}
void syncTimeWithNTP() {
if (!timeClient.update()) {
Serial.println("NTP Sync failed!");
return;
}
time_t ntpTime = timeClient.getEpochTime();
rtc.adjust(DateTime(ntpTime));
Serial.println("RTC synchronized with NTP server.");
DateTime now = rtc.now();
unixtime = now.unixtime();
count = (unixtime % 60) * 10;
}
void setup() {
Serial.begin(115200);
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
while (1);
}
if (rtc.lostPower()) {
Serial.println("RTC lost power, setting default time!");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
timeClient.begin();
timeClient.setTimeOffset(timeOffset);
syncTimeWithNTP();
lcd.init();
lcd.backlight();
pinMode(DCF77OUT, OUTPUT);
pinMode(DCF77OUT_NEG, OUTPUT);
DateTime now = rtc.now();
unixtime = now.unixtime();
count = (unixtime % 60) * 10;
noInterrupts();
timer0_isr_init();
timer0_attachInterrupt(timer0_ISR);
timer0_write(ESP.getCycleCount() + 8000000L);
interrupts();
}
void loop() {
static unsigned long lastSyncTime = 0;
static unsigned long lastLCDUpdate = 0;
unsigned long currentTime = millis();
if (currentTime - lastSyncTime >= 60000) {
lastSyncTime = currentTime;
syncTimeWithNTP();
}
if (currentTime - lastLCDUpdate >= 1000) {
lastLCDUpdate = currentTime;
noInterrupts();
DateTime now = rtc.now();
lcd.setCursor(0, 0);
lcd.print("Time: ");
if (now.hour() < 10) lcd.print("0");
lcd.print(now.hour());
lcd.print(":");
if (now.minute() < 10) lcd.print("0");
lcd.print(now.minute());
lcd.print(":");
if (now.second() < 10) lcd.print("0");
lcd.print(now.second());
lcd.setCursor(16, 0);
lcd.print(timeOffset == 3600 ? "MEZ " : "MESZ");
lcd.setCursor(0, 1);
lcd.print("Date: ");
if (now.day() < 10) lcd.print("0");
lcd.print(now.day());
lcd.print("/");
if (now.month() < 10) lcd.print("0");
lcd.print(now.month());
lcd.print("/");
lcd.print(now.year());
const char* tage[] = {"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"};
int correctedDay = (now.dayOfTheWeek() + 6) % 7;
lcd.setCursor(18, 1);
lcd.print(tage[correctedDay]);
interrupts();
}
}