DCF77 – Radio Controlled Clock

About this post

After I reported in my last post about the real-time clock module DS3231, a follow-up article about DCF77 radio clocks fits very well thematically.

I will deal with the following points:

  • What is the DCF77 time signal?
  • Decoding of the DCF77 signal.
  • DCF77 Receiver Modules
  • Detection and evaluation of the DCF77 signal with the Arduino.
  • Using the DCF77 receiver comfortably with the RTCLib.

The DCF77 time signal

DCF77 is a time signal used to transmit the current date and time digitally on the long wave frequency 77.5 kHz. The time zone is CET (Central European Time) or the CEST (Central European Summer Time). 

The DCF77 transmitter is located in Mainflingen near Frankfurt in Germany. Here you can admire it on Google Maps. It has a range of about 2000 kilometers and controls a large part of the radio clocks in Western Europe. Other parts of the world have time signals that use different frequencies and are also encoded differently. 

DCF77 means:

  • D: Germany
  • C: Abbreviation for long wave
  • F: Proximity to Frankfurt
  • 77: Transmission frequency

More information about the DCF77 signal and its history can be found here.

How the DCF77 signal is structured

The information is transmitted by reducing the amplitude of the carrier to 25 percent once per second for either 100 or 200 milliseconds. A reduction for 100 milliseconds corresponds to a “0”, 200 milliseconds corresponds to a “1”. A full sequence is one minute. This would transfer 60 bits, but the last bit is omitted to clearly separate the sequences.

Most DCF77 receiver modules are designed to deliver a HIGH signal while the time signal is reduced, otherwise it is LOW. This will give you sequences that look like the following:

Diagram of the DCF77 signal at the output pin of the receiver module
Diagram of the DCF77 signal at the output pin of the receiver module

If you have a module that works the other way around, you need to rethink and modify the example sketches accordingly.

Decoding the DCF77 signal

Certain bits or sections are reserved for a specific part of the information to be transmitted:

Meaning of bits of a DCF77 sequence
Meaning of bits of a DCF77 sequence
  • To read the weather information, you would need to purchase a license.
  • The actual transmission of the time and date begins after bit 20. I don’t look at anything before that.
  • For two-digit numbers, the “ones” and the “tens” are transmitted separately, but each digit is transmitted as a binary number – a somewhat strange mixture of binary and decimal systems.
  • The times coded in a sequence refer to the next start bit.

The parity bits can be used to check the transmitted data for correctness. The parity of the parity bit plus the parity of the corresponding data is always even. This can best be explained with an example. The minutes are coded in the bits 21 to 27, i.e. 7 zeros or ones. All ones are added. If the result is an odd number, the parity bit is an odd 1. If, on the other hand, the result is an even number, the parity bit is 0. The same applies to the hours (bits 29-34) and the date (bits 40-57). In other words, the parity of the parity bit equals the parity of the bit sequence to be checked.

If all parities are OK and you have received 59 bits in a sequence, you can be quite sure that the data transfer was correct.

DCF77 Receiver Modules

You can get a module with antenna for 5 to 15 euros in online shops. I paid 11 euros for the model shown here, and I am satisfied with the reception quality:

A DCF77 receiver module with antenna
A DCF77 receiver module with antenna

This model here cost only half and is much smaller:

An alternative DCF77 module
An alternative DCF77 module

There was no documentation (not even for the supply voltage!), moreover, significantly more invalid sequences were obtained under the same conditions as with the other module. So there are clear differences in quality here.

However, invalid sequences are not only a question of module quality, but also the orientation of the antenna, the weather, the location (geology) and other factors can influence the reception. But occasional invalid sequences are not dramatic as long as you identify them and your code does not need to get a valid record every minute. If you experience too many incorrect sequences, try to position the antenna differently.

If you want to buy a module, it is best to see if there is a data sheet or at least basic technical data for it. Some modules can only tolerate up to 3.3 V, others up to 5 V. For the power consumption I have found consistent data of < 100 microamperes.

Receiving and evaluating the DCF77 signal

Wiring

As already mentioned, the modules vary in their design. For the module shown above I used the following wiring: 

Wiring for the DCF77 Example Sketches
Wiring for the DCF77 Example Sketches
  • The LED is not essential, I use it for some example sketches.
  • The Enable Pin can be used to turn the module on and off (not every module has this feature).
  • For most of my sketches, the signal pin must be connected to an interrupt pin.

A simple function test

If you just want to check if your module receives data at all, you can use this sketch:

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

 

Alternatively, it would work without interrupt, for example like this:

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

 

The interrupt method has the advantage that the main loop is empty, so you can do anything else in parallel. I will therefore build on the interrupt method.

When the sketch is running, the LED should flash every second. If you look closely, you will notice that the LED lights up shorter times and sometimes longer. Once per minute, the pause is almost two seconds. Interference with reception is usually noticeable by rapid and irregular flashing.  

How to capture sequences

With the next sketch, we measure the signal lengths and capture entire sequences. A few comments on the signal length:

  • After the interrupt is triggered, it is checked whether the interrupt pin is HIGH or LOW. This indicates whether the completed phase is the 100/200 millisecond signal or the pause.
  • The signal length is determined by the millis() function.
  • All HIGH signals shorter than 150 milliseconds are interpreted as “0”, the longer ones as “1”.
  • A LOW phase greater than 1500 milliseconds indicates the minute tag.

Another few comments on the sequence:

  • The 59 bits of a sequence fit into an eight-byte unsigned long long variable that I called currentBuf.
  • The counter for the position in currentBuf is bufCounter.
  • currentBuf |= ((unsigned long long)1<<bufCounter); inserts a “1” into the sequence. Without the conversion of the “1” to unsigned long long, it will not work. This finding took me a lot of time.
  • The Serial.print() function is not compatible with unsigned long long variables. Via bit operations I split currentBuf variable into two unsigned long pieces to be able to output them.
  • The sequence is output when the minute tag is detected.

In total, the Serial.print() calls take a time in the millisecond range (per second) at 9600 BAUD. That’s why I chose 115200 BAUD.

Actually, I always preach to keep interrupt service routines (here: DCF77_ISR()) as short as possible, otherwise this can lead to problems. This is especially true if you want to use additional interrupts. Here I violate my rules quite drastically (see also here). However, it serves the purpose of recording the date and time in the background. Otherwise, you would either have to wait minutes for the process to complete or check the state of the data pin again and again in the main loop. But that, in turn, can easily collide with the rest of the code, especially if more delays come into play.

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

 

Output of dcf77_get_sequence.ino

Here you can see a snapshot of the output:

DCF77 Example sketch: Output of dcf77_get_sequence.ino
Output of dcf77_get_sequence.ino

The HIGH and LOW phases differ a little from the ideal lengths, but zeros and ones are clearly distinguishable.

The first recorded sequence will only be a complete one in 2 out of 60 cases, namely when you in the last two seconds of a running sequence.

Evaluating the captured sequences

Now let’s evaluate the received sequences. To do this, the ISR passes the currentBuf sequence to the evaluateSequence() function. This first splits the sequence into the relevant sections (minute, hour, day of the week, etc.).

For the parity check, I use the function parity_even_bit(). To use it, you need to include utils/parity.h. This file is part of the Arduino or AVR standard package, so you don’t have to install it. parity_even_bit() expects a byte value as an argument. The function returns “0” (false) for parity “0” and “1” for parity “1”. The date piece is greater than one byte, so I’ll check the parities of the calendar day, day of the week, month, and year individually. If the sum of the parities is an even number, then the total parity is 0. If it is odd, parity is “1”.

rawByteToInt() splits the raw value of minute, hour, day, etc. into the digits and returns the sum as an integer. These values are then output.

The blinking LED in the main loop is just to show that you can do other things there (almost) unaffected.

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

 

Output of dcf77_sequence_evaluation.ino

DCF77 Example sketch: Output of dcf77_sequence_evaluation.ino
Output of dcf77_sequence_evaluation.ino

So it’s Sunday, February 14, 2021, 12:59. It works, but the formatting still leaves opportunities for improvement.

For example, the names for the months or days of the week could be inserted as:

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

Serial.print(dayName[dcf77DayOfWeek]);

But I’m going down a different, much more comfortable   path.

Using the DCF77 with the RTCLib

I introduced the RTCLib in the last post as a library for the DS3231 real-time clock module. In addition to this task, the RTCLib also offers (among other things):

  • A wide range of formatting options for date and time.
  • Calculation operations with date/time and time periods.
  • Software simulation of a real-time clock (SoftRTC).

I had presented all this in my last post. If you don’t understand the next sketch, it’s best to go back there.

You can download the RTCLib via the Arduino IDE or directly from GitHub here.

We use SoftRTC and set this “software watch” regularly with the DCF77 receiver. In between, the clock runs based on millis().

Of course, you can use the DCF77 receive to set a DS3231 module as well.

DCF77 and SoftRTC – the sketch

I freed the code used in the last sketch from all non-essential Serial.print() calls. Then I combined it with the example sketch softrtc.ino of the RTCLib and adapted it somewhat.

The clock is initially set to 1/1/2000, 00:00. You could also take over the system time of the computer, but I wanted a clearly incorrect start date, so that it becomes more obvious when the clock is set by the DCF77 module.

The clock is only set if there is a valid record, i.e. if 59 bits have been submitted and the three parity bits are correct.

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

 

Output of dcf77_softRTC.ino

The result looks like:

Example sketch DCF77: Output of dcf77_softRTC.ino
Output of dcf77_softRTC.ino

Ideally, the software clock is set to the DCF77 time after one to two minutes, as here. In case of poor reception, it may take longer.

If you don’t want interrupts….

If you’re bothered by working with the interrupts, I have a version here without interrupts. Setting the clock is time-controlled. Since I had no desire to wait a long time, the clock is set every two minutes. Of course, this makes virtually no sense. However, you can easily change this to every few hours, for example.

The disadvantage of this method is that you can’t do anything else during the setting procedure.

The sketch is still quite communicative. You can cut it significantly if you want to use it or build on it.

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

 

Output of dcf77_softRTC_without_interrupt.ino

Here’s what the sketch looks like in action:

Output of dcf77_softRTC_without_interrupt.ino: first time setting
Output of dcf77_softRTC_without_interrupt.ino: first time setting

In the upper screenshot you can see that the clock is set at 5:25 p.m. As specified in the sketch, the next setting is two minutes later:

Output of dcf77_softRTC_without_interrupt.ino: second time setting

Acknowledgement

The basis of the post image, the watch, comes from OpenClipart-Vectors. I owe the radio symbol to Clker-Free-Vector-Images. Both are – as usual – from Pixabay.

Leave a Reply

Your email address will not be published. Required fields are marked *