DCF77 – Funkuhr

Über den Beitrag

Nachdem ich in meinem letzten Beitrag über das Echtzeituhrmodul DS3231 berichtet habe, passt nun ein Folgebeitrag über DCF77 Funkuhren thematisch sehr gut.

Auf die folgenden Punkte gehe ich ein:

  • Was ist der DCF77 Zeitzeichensender?
  • Decodierung des DCF77 Signals.
  • DCF77 Empfängermodule
  • Erfassung und Auswertung des DCF77 Signals mit dem Arduino.
  • Den DCF77 Empfänger komfortabel mit der RTCLib nutzen.

Der DCF77 Zeitzeichensender

Der DCF77 ist ein sogenannter Zeitzeichensender, welcher das aktuelle Datum und die Uhrzeit in digitaler Form auf der Langwellenfrequenz 77.5 kHz verbreitet. Die Uhrzeit ist dabei die MEZ (Mitteleuropäische Zeit) beziehungsweise die MESZ (Mitteleuropäische Sommerzeit). 

Der DCF77 Sender steht in Mainflingen in Hessen. Hier könnt ihr ihn auf Google Maps bewundern. Er hat eine Reichweite von ca. 2000 km und steuert einen großen Teil der Funkuhren in Westeuropa. Andere Teile der Welt haben Zeitzeichensender, die auf anderen Frequenzen senden und auch anders kodiert sind. 

DCF77 steht für:

  • D: Deutschland
  • C: Kürzel für Langwelle
  • F: für die Nähe zu Frankfurt
  • 77: Übertragungsfrequenz

Mehr Informationen über den DCF77 Sender und seine Geschichte findet ihr hier.

Wie das DCF77 Signal aufgebaut ist

Die Informationen werden übertragen, indem das Funksignal einmal pro Sekunde für entweder 100 oder 200 Millisekunden auf 25 Prozent abgesenkt wird. Eine Absenkung für 100 Millisekunden bedeutet eine „0“, 200 Millisekunden bedeutet eine „1“. Eine Sequenz beträgt eine Minute. Damit würden 60 Bit übertragen, allerdings wird das letzte Bit weggelassen, um die Sequenzen eindeutig voneinander zu trennen.

Die meisten DCF77 Empfängermodule sind so aufgebaut, dass sie während der Absenkung ein HIGH-Signal liefern, ansonsten ist es LOW. Damit erhaltet ihr Sequenzen, die folgendermaßen aussehen:

Schema des DCF77 Signals am Ausgangspin des Empfängermoduls
Schema des DCF77 Signals am Ausgangspin des Empfängermoduls

Habt ihr ein Modul, dass andersherum arbeitet, müsst ihr umdenken und die Beispielsketche entsprechend ändern.

Decodierung des DCF77 Signals

Bestimmte Bits beziehungsweise bestimmte Abschnitte sind für einen bestimmten Teil der zu übermittelnden Informationen reserviert:

Bedeutung der Bits einer DCF77 Sequenz
Bedeutung der Bits einer DCF77 Sequenz
  • Um die Wetterinformationen lesen zu können, müsstet ihr eine Lizenz erwerben.
  • Die eigentliche Übertragung der Uhrzeit und des Datums beginnt nach Bit 20. Alles davor betrachte ich nicht.
  • Bei zweistelligen Zahlen werden die „Einer“ und die „Zehner“ getrennt übertragen, jede Ziffer für sich aber im Zweiersystem – eine etwas eigentümliche Mischung von Binär- und Dezimalsystem.
  • Die Zeitangaben beziehen sich auf den Zeitpunkt, wenn das nächste Startbit gesendet wird.

Mit den Paritätsbits lassen sich die übertragenen Daten auf Richtigkeit prüfen. Die Überprüfung erfolgt auf gerade Parität. Am besten erkläre ich das an einem Beispiel. Die Minuten werden in den Bits 21 bis 27 übertragen, also 7 Nullen oder Einsen. Alle Einsen werden zusammengezählt. Ist das Ergebnis eine ungerade Zahl, dann ist das Paritätsbit eine 1. Ist das Ergebnis hingegen eine gerade Zahl, ist das Paritätsbit 0. Genauso wird mit den Stunden (Bits 29-34) und dem Datum (Bits 40-57) verfahren. Anders ausgedrückt: Die Parität des Paritätsbits entspricht der Parität der zu prüfenden Bitfolge.

Sind alle Paritäten OK und habt ihr 59 Bits in einer Sequenz erhalten, könnt ihr euch recht sicher sein, dass die Datenübertragung korrekt war.

DCF77 Empfängermodule

Ein Modul mit Antenne erhaltet ihr für 5 bis 15 Euro in Online-Shops. Für das hier abgebildete Modell habe ich 11 Euro bezahlt und bin mit seiner Empfangsqualität sehr zufrieden:

Ein DCF77 Empfängermodul mit Antenne
Ein DCF77 Empfängermodul mit Antenne

Dieses Modell hier kostete nur die Hälfte und ist deutlich kleiner:

Ein alternatives DCF77 Modul
Ein alternatives DCF77 Modul

Es gab dazu keine Dokumentation (nicht mal bezüglich der Versorgungsspannung!), außerdem habe damit unter gleichen Bedingungen deutlich mehr Fehlmessungen als mit dem anderen Modul erhalten. Es gibt hier also deutliche Qualitätsunterschiede.

Fehlmessungen sind aber nicht nur eine Frage der Modulqualität, sondern auch die Ausrichtung der Antenne, das Wetter, der Standort (Geologie) und weitere Faktoren können den Empfang beeinflussen. Ein paar Fehlmessungen sind aber auch nicht schlimm, solange ihr sie identifiziert und euer Code nicht darauf angewiesen ist jede Minute einen validen Datensatz zu erhalten. Wenn ihr zu viele Fehlmessungen feststellt, dann versucht erst einmal die Antenne anders zu positionieren.

Wenn ihr euch ein Modul besorgen wollt, dann schaut am besten erst mal, ob es ein Datenblatt oder zumindest grundlegende technische Daten dazu gibt. Einige Vertreter können anscheinend nur bis zu 3.3 V, andere bis 5 V vertragen. Für den Stromverbrauch habe ich übereinstimmend Angaben von < 100 Mikroampere gefunden.

Das DCF77 Signal empfangen und auswerten

Schaltung

Wie schon erwähnt, variieren die Module in ihrer Ausführung. Für das oben abgebildete Modul habe ich die folgende Schaltung verwendet: 

Schaltung für die DCF77 Beispielsketche
Schaltung für die DCF77 Beispielsketche
  • Die LED ist an sich nicht essenziell, ich nutze sie für einige Beispielsketche.
  • Mit dem Enable Pin lässt sich das Modul an- und ausschalten (das hat nicht jedes Modul).
  • Der Datenausgang muss für die meisten meiner Sketche an einem Interrupt Pin hängen.

Ein einfacher Funktionstest

Wenn ihr einfach prüfen wollt, ob euer Modul prinzipiell Daten empfängt, dann könnt ihr diesen Sketch verwenden:

byte interruptPin=2;
byte ledPin=7;

void setup(){
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), DCF77_ISR, CHANGE);
}

void loop(){
  
}

void DCF77_ISR(){
  if(digitalRead(interruptPin)){
    digitalWrite(ledPin, HIGH);
  }
  else{
    digitalWrite(ledPin, LOW);
  }
}

 

Alternativ ginge es natürlich auch ohne Interrupt, als zum Beispiel so:

int dataPin = 2;
int ledPin = 7;

void setup() {
  pinMode(dataPin, INPUT);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  if(digitalRead(dataPin)){
    digitalWrite(ledPin, HIGH);
  }
  else{
    digitalWrite(ledPin, LOW);
  }
}

 

Die Interrupt Methode hat den Vorteil, dass die Hauptschleife leer ist, ihr also irgendetwas anderes nebenbei machen könnt. Ich werde deswegen auf der Interrupt Methode aufbauen.

Wenn der Sketch läuft, solltet ihr sehen, dass die LED im Sekundentakt blinkt. Wenn ihr genau hinschaut, werdet ihr erkennen, dass die LED mal kürzer und mal länger leuchtet. Einmal pro Minute beträgt die Pause fast zwei Sekunden. Empfangsstörungen machen sich meistens durch schnelles und unregelmäßiges Blinken bemerkbar.  

Sequenzen erfassen

Mit dem nächsten Sketch messen wir die Signallängen und erfassen ganze Sequenzen. Ein paar Anmerkungen zur Signallänge:

  • Nach dem Auslösen des Interrupts wird geprüft, ob der Interrupt Pin HIGH oder LOW ist. Das verrät, ob es bei der abgeschlossenen Phase um das 100/200 Millisekunden-Signal oder die Pause handelt.
  • Die Signallänge wird über die millis() Funktion ermittelt.
  • Alle HIGH-Signale, die kürzer als 150 Millisekunden sind, werden als „0“ interpretiert, die längeren als „1“.
  • Eine LOW-Phase von größer 1500 Millisekunden wird als Minutenmarke interpretiert.

Zur Sequenz:

  • Die 59 Bits einer Sequenz passen in eine acht Byte große unsigned long long Variable, die ich currentBuf genannt habe.
  • Der Zähler für die Position in currentBuf ist bufCounter.
  • currentBuf |= ((unsigned long long)1<<bufCounter); fügt eine „1“ in die Sequenz ein. Ohne die Umwandlung der „1“ in unsigned long long funktioniert es nicht. Diese Erkenntnis hat mich viel Zeit gekostet.
  • Die Serial.print() Funktion ist nicht mit unsigned long long Variablen verträglich. Über Bitoperationen teile ich die currentBuf Variable in zwei unsigned long Stücke um sie ausgeben zu können.
  • Die Ausgabe der Sequenz erfolgt, wenn die Minutenmarke erreicht ist.

Die  Serial.print() Aufrufe nehmen bei 9600 BAUD in Summe eine Zeit im Millisekundenbereich in Anspruch (pro Sekunde). Ich habe deswegen 115200 BAUD gewählt.

Eigentlich predige ich immer Interrupt Service Routinen (hier: DCF77_ISR()) möglichst kurzzuhalten, da das sonst Problemen führen kann. Das gilt insbesondere, wenn ihr weitere Interrupts verwenden wollt. Hier verstoße ich recht drastisch gegen meine Regeln (siehe auch hier). Es dient aber dem Zweck die Uhrzeiterfassung im Hintergrund vorzunehmen. Sonst müsstet Ihr entweder minutenlang warten bis der Prozess abgeschlossen ist oder in der Hauptschleife immer wieder den Zustand des Daten Pins abfragen. Das wiederum kann aber leicht mit dem Rest des Codes kollidieren, insbesondere wenn weitere delays ins Spiel kommen.

byte interruptPin=2;
volatile unsigned long lastInt = 0;
volatile unsigned long long currentBuf = 0;
volatile byte bufCounter;

void setup(){
  Serial.begin(115200);
  Serial.println(" HIGH  /  LOW");
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), DCF77_ISR, CHANGE);
}

void loop(){
  
}

void DCF77_ISR(){
  unsigned int dur = 0;
  dur = millis() - lastInt; 
  
  if(digitalRead(interruptPin)){
    Serial.println(dur);
    if(dur>1500){
      unsigned long highBuf = (currentBuf>>16) & 0x7FFFFFF;
      unsigned long lowBuf = (currentBuf & 0xFFFFFFFF);
      bufCounter = 0;
      Serial.print("Signal, upper 4 bytes: "); 
      Serial.println(highBuf, BIN);
      Serial.print("Signal, lower 4 bytes: "); 
      Serial.println(lowBuf, BIN);
    }
  }
  else{
    Serial.print(bufCounter);
    Serial.print(". ");
    Serial.print(dur);
    Serial.print("  /  ");
    if(dur>150){
      currentBuf |= ((unsigned long long)1<<bufCounter);
    }
    bufCounter++;
  }
  lastInt = millis();
}

 

Ausgabe von dcf77_get_sequence.ino

Hier seht ihr einen Ausschnitt der Ausgabe:

DCF77 Beispielsketch: Ausgabe von dcf77_get_sequence.ino
Ausgabe von dcf77_get_sequence.ino

Die HIGH und LOW Phasen differieren ein wenig von den Ideallängen, aber Nullen und Einsen sind deutlich unterscheidbar.

Die erste aufgenommene Sequenz wird nur in 2 von 60 Fällen vollständig sein, nämlich wenn ihr in den letzten zwei Sekunden einer laufenden Sequenz einsteigt.

Die erfassten Sequenzen auswerten

Nun werten wir die empfangenen Sequenzen aus. Dazu übergibt die ISR die Sequenz currentBuf der Funktion evaluateSequence(). Diese zerhackt die Sequenz zunächst in die relevanten Abschnitte (Minute, Stunde, Wochentag, usw.).

Für die Paritätsprüfung nehme ich die Funktion parity_even_bit(). Um sie verwenden zu können, müsst ihr utils/parity.h einbinden. Diese Datei gehört zur Arduino bzw. AVR Grundausstattung, ihr müsst sie also nicht installieren. parity_even_bit() erwartet einen byte Wert als Argument. Die Funktion liefert „0“ (false) für die Parität „0“ und „1“ für die Parität „1“. Der Ausschnitt für das Datum ist größer als ein Byte, also ermittele ich die Paritäten des Kalendertages, Wochentages, Monats und Jahres einzeln. Ist die Summe der Paritäten eine gerade Zahl, dann ist die Gesamtparität „0“. Ist sie ungerade, dann ist die Parität „1“.

rawByteToInt() zerlegt den Rohwert für Minute, Stunde, Tag, usw. in den Einer- und Zehnerteil und liefert die Summe als Integer zurück. Diese Werte werden dann ausgegeben.

Die blinkende LED in der Hauptschleife soll nur zeigen, dass ihr dort (fast) unbeeinflusst irgendwelche anderen Dinge tun könnt.

#include <util/parity.h>
byte interruptPin=2;
byte ledPin=7;
volatile unsigned long lastInt = 0;
volatile unsigned long long currentBuf = 0;
volatile byte bufCounter;

void setup(){
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), DCF77_ISR, CHANGE);
}

void loop(){
  digitalWrite(ledPin, HIGH); // to illustrate the loop can do something else
  delay(750);
  digitalWrite(ledPin, LOW);
  delay(750);
  
}

void DCF77_ISR(){
  unsigned int dur = 0;
  dur = millis() - lastInt; 
  
  if(digitalRead(interruptPin)){
    Serial.println(dur);
    if(dur>1500){
      unsigned long highBuf = (currentBuf>>16) & 0x7FFFFFF;
      unsigned long lowBuf = (currentBuf & 0xFFFFFFFF);
      bufCounter = 0;
      Serial.print("Signal, upper 4 bytes: "); 
      Serial.println(highBuf, BIN);
      Serial.print("Signal, lower 4 bytes: "); 
      Serial.println(lowBuf, BIN);
      evaluateSequence();
      currentBuf = 0;
    }
  }
  else{
    Serial.print(bufCounter);
    Serial.print(". ");
    Serial.print(dur);
    Serial.print("  /  ");
    if(dur>150){
      currentBuf |= ((unsigned long long)1<<bufCounter);
    }
    bufCounter++;
  }
  lastInt = millis();
}

void evaluateSequence(){
  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){
    Serial.println("Minute parity not OK"); 
  }
  if((parity_even_bit(dcf77Hour)) != parityBitHour){
    Serial.println("Hour parity not OK");
  }
  if(((parity_even_bit(dcf77DayOfMonth) + parity_even_bit(dcf77DayOfWeek) 
  + parity_even_bit(dcf77Month) + parity_even_bit(dcf77Year))%2) != parityBitDate)
  {
    Serial.println("Date parity not OK");
  }
  
  Serial.println("Current date and Time:");
  Serial.print("Year: "); Serial.println(rawByteToInt(dcf77Year));
  Serial.print("Month: "); Serial.println(rawByteToInt(dcf77Month));
  Serial.print("Day of Month: "); Serial.println(rawByteToInt(dcf77DayOfMonth));
  Serial.print("Day of the Week: "); Serial.println(rawByteToInt(dcf77DayOfWeek));
  Serial.print("Hours: "); Serial.println(rawByteToInt(dcf77Hour));
  Serial.print("Minutes: "); Serial.println(rawByteToInt(dcf77Minute));
}

unsigned int rawByteToInt(byte raw){
  return ((raw>>4)*10 + (raw &0x0F));
}

 

Ausgabe von dcf77_sequence_evaluation.ino

DCF77 Beispielsketch: Ausgabe von dcf77_sequence_evaluation.ino
Ausgabe von dcf77_sequence_evaluation.ino

Es ist also Sonntag, der 14. Februar, 2021, 12:59. Es funktioniert, allerdings lässt die Formatierung noch zu wünschen übrig.

Wochen- und Monatsnamen ließen sich zum Beispiel so einfügen:

char dayName[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

Serial.print(dayName[dcf77DayOfWeek]);

Ich gehe aber gleich einen anderen, deutlich bequemeren  Weg.

Den DCF77 mit der RTCLib nutzen

Die RTCLib hatte ich im letzten Beitrag als Bibliothek für das DS3231 Echtzeituhr Modul vorgestellt. Über diese Aufgabe hinaus ermöglicht die RTCLib aber auch (unter anderem):

  • vielfältige Formatierungsoptionen für Datum und Uhrzeit
  • Rechnen mit Datum/Uhrzeit und Zeitspannen
  • Softwaresimulation einer Echtzeituhr (SoftRTC)

Das hatte ich alles in meinem letzten Beitrag vorgestellt. Wenn ihr den nächsten Sketch nicht verstehen solltet, dann geht am besten noch mal dorthin zurück.

Ihr könnt die RTCLib über die Arduino IDE oder hier direkt von GitHub herunterladen.

Wir nutzen SoftRTC und stellen diese „Softwareuhr“ regelmäßig mit dem DCF77 Empfänger. In der Zeit dazwischen läuft die Uhr basierend auf millis().

Genauso gut könnt ihr den DCF77 Empfänger dazu nehmen, ein DS3231 Modul regelmäßig zu stellen.

DCF77 und SoftRTC – der Sketch

Ich habe den im letzten Sketch verwendeten Code von allen verzichtbaren Serial.print() Ausgaben befreit. Dann habe ich ihn mit dem Beispielsketch softrtc.ino der RTCLib vereinigt und etwas angepasst.

Die Uhr wird zunächst auf den 1.1.2000, 00:00 Uhr gestellt. Man könnte auch die Systemzeit des Computers übernehmen, aber ich wollte ein deutlich falsches Startdatum, damit deutlicher wird, wenn die Uhr durch das DCF77 Modul gestellt wird.

Die Uhr wird nur gestellt, wenn ein gültiger Datensatz vorliegt, d.h. wenn 59 Bits übermittelt wurden und die drei Paritätsbits stimmen.

#include <util/parity.h>
#include "RTClib.h"
byte interruptPin=2;
volatile unsigned long lastInt = 0;
volatile unsigned long long currentBuf = 0;
volatile byte bufCounter;

RTC_Millis rtc;

void setup(){
  rtc.adjust(DateTime(2000, 1, 1, 0, 0, 0));
  Serial.begin(115200);
  pinMode(interruptPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptPin), DCF77_ISR, CHANGE);
}

void loop(){
  static DateTime now = rtc.now();
  now = rtc.now();
  char buf1[] = "Today is DDD, MMM DD YYYY";
  Serial.println(now.toString(buf1));
  char buf2[] = "Current time is hh:mm:ss";
  Serial.println(now.toString(buf2));
  Serial.println();
  
  delay(3000);
}

void DCF77_ISR(){
  unsigned int dur = 0;
  dur = millis() - lastInt; 
  
  if(digitalRead(interruptPin)){
    if(dur>1500){
      if(bufCounter==59){
        evaluateSequence();
      }
      bufCounter = 0;
      currentBuf = 0;
    }
  }
  else{
    if(dur>150){
      currentBuf |= ((unsigned long long)1<<bufCounter);
    }
    bufCounter++;
  }
  lastInt = millis();
}

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

 

Ausgabe von dcf77_softRTC.ino

Und sieht dann das Ergebnis aus:

Beispielsketch DCF77: Ausgabe von dcf77_softRTC.ino
Ausgabe von dcf77_softRTC.ino

Idealerweise wird die Softwareuhr – so wie hier – nach ein bis zwei Minuten auf die DCF77 Zeit gestellt. Bei schlechtem Empfang kann es unter Umständen länger dauern.

Wenn ihr keine Interrupts wollt….

Wenn euch das Arbeiten mit den Interrupts stört, habe ich hier eine Version ohne Interrupts. Das Stellen der Uhrzeit ist zeitgesteuert. Da ich keine Lust hatte, lange zu warten, wird die Uhr alle zwei Minuten gestellt. Das macht natürlich praktisch keinen Sinn. Ihr könnt das aber unkompliziert auf z.B. alle paar Stunden abändern.

Der Nachteil an dieser Methode ist, dass ihr während der Einstellprozedur nichts anderes machen könnt.

Der Sketch ist noch recht kommunikativ. Ihr könnt ihn noch deutlich kürzen, wenn ihr etwas damit machen wollt.

#include "RTClib.h"
#include <util/parity.h>
#define DCF77_PIN_HIGH (PIND & (1<<PD7))
int dataPin = 7;

RTC_Millis rtc;

void setup () {
  bool validTime = false;
  Serial.begin(115200);

#ifndef ESP8266
  while (!Serial); // wait for serial port to connect. Needed for native USB
#endif

  pinMode(dataPin, INPUT); 
  
  DateTime currentTime = DateTime(2000, 1, 1, 0, 0, 0); // Initial Date
  
  while(!validTime){
    if(getDcf77Time(currentTime)){
      Serial.println("Date and Time updated!");
      validTime = true;
    }
    else{
      Serial.println("Sorry, something went wrong!");
    }
  }
  rtc.adjust(currentTime);
}

bool getDcf77Time(DateTime &dcf77Time){
  unsigned long long dcf77Sequence = 0;
  bool successfulUpdate = true;
  if(receiveSequence(dcf77Sequence)){
    Serial.println("Sequence OK - received 59 bits");
    printSequence(dcf77Sequence);
  }
  else{
    Serial.println("Sequence NOT OK - wrong nuber of bits");
    successfulUpdate = false;
  }
    
  if(evaluateSequence(dcf77Sequence, dcf77Time)){
    Serial.println("Valid Sequence!");   
  }
  else{
    Serial.println("Invalid Sequence!");
    successfulUpdate = false;
  }
  return successfulUpdate;
}

bool evaluateSequence(unsigned long long &buf, DateTime &dcf77Time){
  bool parityOK = true;
  byte dcf77Year = (buf>>50) & 0xFF;    // year = bit 50-57
  byte dcf77Month = (buf>>45) & 0x1F;       // month = bit 45-49
  byte dcf77DayOfWeek = (buf>>42) & 0x07;   // day of the week = bit 42-44
  byte dcf77DayOfMonth = (buf>>36) & 0x3F;  // day of the month = bit 36-41
  byte dcf77Hour = (buf>>29) & 0x3F;       // hour = bit 29-34
  byte dcf77Minute = (buf>>21) & 0x7F;     // minute = 21-27 
  bool parityBitMinute = (buf>>28) & 1;
  bool parityBitHour = (buf>>35) & 1;
  bool parityBitDate = (buf>>58) & 1;

  if((parity_even_bit(dcf77Minute)) != parityBitMinute){
    parityOK = false;
    Serial.println("Minutes not OK"); 
  }
  if((parity_even_bit(dcf77Hour)) != parityBitHour){
    parityOK = false; 
    Serial.println("Hours not OK");
  }
  if(((parity_even_bit(dcf77DayOfMonth) + parity_even_bit(dcf77DayOfWeek) 
  + parity_even_bit(dcf77Month) + parity_even_bit(dcf77Year))%2) != parityBitDate)
  {
    parityOK = false;
    Serial.println("Date not OK");
  }

  if(parityOK==false){
    return parityOK;
  }
  
  dcf77Time = DateTime(rawByteToInt(dcf77Year) + 2000, rawByteToInt(dcf77Month), 
              rawByteToInt(dcf77DayOfMonth), rawByteToInt(dcf77Hour), rawByteToInt(dcf77Minute), 0);

  return parityOK;
}

unsigned int rawByteToInt(byte raw){
  return ((raw>>4)*10 + (raw &0x0F));
}

bool receiveSequence(unsigned long long &buf){
  unsigned int counter = 0;
  unsigned int lowCounter = 0;
  unsigned int highCounter = 0; 
    
  Serial.println("Waiting for sequence start...");
  waitForSequenceStart(); 
  Serial.println("HIGH / LOW");
  
  while(lowCounter < 150){
    lowCounter = 0;
    highCounter = 0;
    
    while(DCF77_PIN_HIGH){
      delay(10);
      highCounter++;
    }
    if(highCounter >= 15){
      buf |= ((unsigned long long)1<<counter);
    }
         
    while(!DCF77_PIN_HIGH){
      delay(10);
      lowCounter++;
    }
      
    Serial.print(counter);
    Serial.print(".  ");
    Serial.print(highCounter);
    Serial.print("  /  ");
    Serial.println(lowCounter);
    
    counter++; 
  }
  if(counter==59){
    return true;    
  }
  else{
    return false;
  }
}

void waitForSequenceStart(){
  unsigned int lowCounter2 = 0;
  
  while(lowCounter2<150){
    lowCounter2 = 0;
    while(DCF77_PIN_HIGH){}
    while(!DCF77_PIN_HIGH){
      delay(10);
      lowCounter2++;
    }
  }  
}

void printSequence(unsigned long long &buf){
  unsigned long highBuf = (buf>>16) & 0x7FFFFFF;
  unsigned long lowBuf = (buf & 0xFFFFFFFF);
  Serial.print("Sequence, upper 4 bytes: "); 
  Serial.println(highBuf, BIN);
  Serial.print("Sequence, lower 4 bytes: "); 
  Serial.println(lowBuf, BIN);  
}

void loop () {
  DateTime now = rtc.now();
  TimeSpan updatePeriod = TimeSpan(0,0,2,0);
  static DateTime nextUpdate = now + updatePeriod;
  
  char buf1[] = "Today is DDD, MMM DD YYYY";
  Serial.println(now.toString(buf1));
  char buf2[] = "Current time is hh:mm:ss";
  Serial.println(now.toString(buf2));
  Serial.println();

  if(nextUpdate < now){
    if(getDcf77Time(now)){
      rtc.adjust(now);
      nextUpdate = now + updatePeriod;
    }
  }
  
  delay(3000);
}

 

Ausgabe von dcf77_softRTC_without_interrupt.ino

So sieht der Sketch in Aktion aus:

Ausgabe von dcf77_softRTC_without_interrupt.ino: erste Zeiteinstellung
Ausgabe von dcf77_softRTC_without_interrupt.ino: erste Zeiteinstellung

In der oberen Ausgabe seht ihr, dass die Uhr um 17:25 Uhr gestellt wird. Wie im Sketch festgelegt, erfolgt die nächste Einstellung zwei Minuten später:

Ausgabe von dcf77_softRTC_without_interrupt.ino: zweite Zeiteinstellung

Danksagung

Die Grundlage des Beitragsbildes, die Uhr, stammt von OpenClipart-Vectors. Das Funksymbol habe ich Clker-Free-Vector-Images zu verdanken. Beides – wie gewohnt – von Pixabay.

16 thoughts on “DCF77 – Funkuhr

  1. Hallo,

    endlich habe ich eine Seite gefunden, mit deren Hilfe ich mein DCF77 Modul in Betrieb nehmen konnte.
    Vielen Dank für die sehr übersichtlichen Erklärungen und Beispielsketche.
    Ich habe schon Dutzende Seiten durch, aber nun ist es mir erstmal gelungen, das aktuelle Datum und die Zeit zu bekommen.
    Ok, zugegeben, im Zimmer, mit mehreren PCs und Monitoren ist es schwierig und nicht zuletzt hängt es ja auch von der Qualität der Antenne und des Empfängers ab,
    Sobald meine Monitore laufen, ist der Empfang quasi nicht mehr möglich, aber ich hatte ohnehin nur vor, meine Uhr 1x in der Nacht zu synchronisieren, was, wie ich denke, völlig ausreichend ist.

    Ich bin mal gespannt, wie der fertige Aufbau nachher laufen wird.

    Viele Grüße und schon mal ein schönes Weihnachtsfest,

    Andreas.

    1. Hallo Andreas,

      danke für’s Feedback. Hätte ich nicht gedacht, dass PCs und Monitore den Empfang so stören. Auf jeden Fall ein wertvoller Hinweis. Die auch ein schönes Weihnachtsfest!

      VG, Wolfgang

  2. Hallo Wolfgang,

    erst einmal ein Lob auf diese gelungene Website. Ich konnte mir schon einige wichtige Anregungen holen. Mein DCF77-Modul weigert sich bislang aber, mir eine korrekte Zeitangabe zu machen. Ich verwende ein Modul wie das auf Deinem ersten Bild mit zwei P-Anschlüssen, verbunden habe ich nur den ersten an Pin 2:
    Das erste Testprogramm lässt bereits nach wenigen Sekunden die LED blinken. Doch schon bei der sequence-evaluation erhalte ich nur Listen von Wertepaaren, die bis 255 hochgezählt werden und dann wieder bei 0 beginnen. Der zweite Wert bewegt sich in einer Größenordnung <250.
    Verwende ich die letzten beiden Sketche wird beim vorletzten nur die Zeit seit dem 01.01.2000 hochgezählt, beim letzten erscheint nur der Hinweis, dass auf den Beginn der Sequenz gewartet wird.
    Das Modul steht im ersten Stock vor einem Fenster, die Antenne ist senkrecht aufgestellt, was die Schnelligkeit des Empfangs deutlich erhöht hat.
    Was kann ich noch tun, um ein gültiges Zeitsignal zu erhalten?

    Danke vorab,
    Thomas

    1. Hallo Thomas, tut mir leid, dass es nicht funktioniert. Auf die Ferne ist es immer schwer Fehler zu finden. Immerhin blinkt es schon mal, dass ist ja ein Anfang. Wenn die Wertepaare bis 255 hochzählen, zeigt das, dass das lange Minutensignal nach Bit 58 nicht gefunden wird. Da der Zähler als Byte definiert ist, ist das dann nur folgerichtig. Kommen die Wertepaare denn im Sekundentakt? Wenn ja, ist das das nächste gute Zeichen. Und wie sehen die Wertepaare aus. Wenn ich das richtig verstanden habe liegt der zweite Wert immer irgendwo kleiner 250? Wie sehen denn die ersten Werte aus? Hast du vielleicht eines der Module, die ein umgedrehtes Signal abgeben, also kurzes Low und langes High? Das bekommt man ja leicht heraus. Wenn das der Fall ist, müsstest du die High / Low Logik in den Sketchen umdrehen oder das Signal invertieren. Letztere habe ich hier beschrieben:
      https://wolles-elektronikkiste.de/ltc6995-long-timer-low-frequency-oscillator#Power-On-Reset
      Dem Link folgen und ein wenig herunterscrollen.
      VG, Wolfgang

  3. Sehr schöner Beitrag. Ich möchte nur eine Sache noch hinzugeben:
    Der Ausgang der DCF77 Module ist wohl in der Regel ein OpenCollector Ausgang. Das heißt man muss für einen uC den internen PULLUP aktivieren oder einen externen PULLUP Widerstand nutzen. Möglichst keinen zu „steifen/niederohmigen“ PULLUP, da der Ausgangsstrom des Signals sonst nicht reicht ihn auf Masse zu ziehen.
    Ich kann mein DCF Modul (von ELV) problemlos am Arduino UNO auslesen, aber nur, wenn ich den internen PULLUP einschalte. Eine Verstärkerschaltung für das Signal (und diese Ansicht haben glaube ich viele) ist in der Regel nicht nötig, da der CMOS Input eines uC viele MOhm hat und die Signalquelle nicht stark belastet.

  4. Hallo Wolfgang,
    wirklich gute Rutinen für DCF77!!
    Ich habe deinen Code mit RTC_DS3231 statt RTC_MILLIS versucht, das brachte das Programm zu HUNG beim Übertrag der DCF Zeit zur I2C_RTC in der ISR.
    Die Lösung war das setzen der RTC aus der ISR in die Main LOOP zu verlegen.
    Die dazu nötigen Anpassungen waren: Variablen aus der ISR als volatile im Header zu definieren und anstatt des rtc.adjust(DateTime… in der ISR, einen Flag zu setzen. Das rtc.adjust(DateTime… in die Main verlegen … Flag reset und fertig.
    Hardware: LGT8F328P-LQFP32 MiniEVB + DCF77 Pollin
    PS: rtc.begin() nicht vergessen…

  5. Hallo Wolfgang,

    super Beitrag!!!
    Kannst Du bitte Sagen, wo Du Dein Modul gekauft hast. Ich hatte mit zwei Modulen (ähnlich deinem zweiten Modul) große Empfangsprobleme.
    Als Hilfe hatte ich einen Artikel in der HEISE MAKE.
    Ich habe es dann gelassen und einen ESP_32 per ntp synchronisiert.

    Gruß Harald!

    1. Hallo Harald,

      das Teil ist von Amazon:

      https://www.amazon.de/Unbekannt-Empfangsmodul-Funkzeit-Funkuhr-Frankfurt/dp/B07RRXRY36/ref=mp_s_a_1_5?dchild=1&keywords=dcf77&qid=1613815244&sprefix=dcf&sr=8-5

      Sporadisch habe ich damit auch mal Empfangsstörungen, aber im großen und ganzen bin ich zufrieden. Hängt nicht nur vom Modul ab. Manchmal nützt es schon, die Antenne anders auszurichten und sie von Störfaktoren wie elektrischen Geräten und Kabeln fernzuhalten.

      VG , Wolfgang

  6. Sehr toller Bericht und sehr interssante Infurmationen zur Funktion der Funkuhr Übertragung
    Weiter so Wolfgang

  7. Hallo Wolfgang,

    toller Beitrag!
    Frage wie wäre es wenn du mit dem Interupt nur ein Flag setzt, und dann mit dem Flag die ISR als funktion in der loop() ausführst?
    Würde es dann damit dann auch funktionieren?

    viele Grüße
    Frank

    1. Ja, das geht auch. Man darf dann nur nicht zu viel anderes in der loop() machen, was die Auswertung zu sehr verzögert. Solange man sicherstellen kann, dass man den Flag nach sagen wir mal nach spätestens einer hundertstel Sekunde behandelt, würde das gehen. Normalerweise bin ich auch eher ein Freund einer solchen Vorgehensweise.

  8. Ich habe das mal zu einer Zeit, als es noch keine Arduino und kein C für die Megas gab in Assemler
    programmiert.
    Ein anderes , auch sehr effizientes Prinzip:
    Über einen Timer -Interrupt werden alle z.b. 5 ms Zeitscheiben-Unterprogramme aufgerufen.
    Eines zählt die Aufrufe bis zum Wechsel am Eingang, speichert jew. ‚1‘ oder ‚0‘ , bei Pause >100 ms wird dann ausgewertet.
    Zur Datensicherung bei der Übertragung werden zwei aufeinander folgende Zeiten verglichen, die dürfen sich dann nur um 1 Minute unterscheiden. Der Einfachheit halber dürfen sich die Werte für Tag, WTag, Jahr etc gar nicht unterscheiden.
    Das Zeitscheiben – Verfahren lässt eine grosse Zahl gleichzeitig ablaufender Prozesse zu.
    In den Üblichen Steuerungen mit Mega168 ff laufen Scheiben mit 0,125 , 1, 5, 20, 100 ms und etwa 100 unabhängige Prozesse, viele davon natürlich nur in der 20 oder 100 ms Schleife.
    Empfehle sehr, das mal zu probieren. Die Module von Conrad damals haben sehr gut funktioniert,
    5 V- Betrieb und Transistor-Ausgang.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.