DCF77-Generator mit ESP8266        

von Günther Zöppel                       
 
                      Elektronik-Labor  Bastelecke  Projekte  Mikrocontoller                        



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();
}
}



Elektronik-Labor  Bastelecke  Projekte  Mikrocontoller