Binary DCF77 Clock

With this experiment we will implement a simple binary clock for the Blinkenlighty. Of course you can copy it with a standard Arduino and a Blinkenlight Shield as well. In both cases you will need an additional DCF77 module. If you shop for such a module have a close look at the specs. Some modules are specified for 1.5-3.3V while others are specified for 3-12V. The higher voltage modules typically have more powerful outputs as well. Most of the time the 5V modules are slightly more expensive. If you consider the additional parts and work for adapting a 3V module to 5V then the “expensive” module seems to be the better buy.

DCF77 Module

My first attempt was to use the DCF77 library from the Arduino playground. Unfortunately this library did not work to well with my module. After looking at the raw signal and reading the specification of the module twice the issue became clear. My module has 300 Hz bandwidth while most modules have only 20 Hz bandwidth. Higher bandwidth allows for more accurate timing but also implies that the module will pick up more noise. Noise is of course bad for decoding the signal.

There are several means to deal with such a situation. The most common approach is to apply a low pass filter. In order to keep memory consumption low I will apply exponential smoothing. This filter is very easy to implement, consumes very little memory, has a good impulse response and it is also pretty easy to analyze. I tuned the filter constants such that a pulse of 50ms will result in a 50% signal level. I also added some Schmitt-Trigger behaviour at the 50% signal level. Thus the filtered signal will keep pulses shorter than 50ms “low” while pulses longer than 50ms will be kept high for at least 50ms. The result is a reduced bandwidth and a much cleaner signal.

Enough with the theory here comes the code. In order to run it you need to install the DCF77 library as well as the MsTimer2 library.

//
//  www.blinkenlight.net
//
//  Copyright 2012 Udo Klein
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program. If not, see http://www.gnu.org/licenses/

#include <MsTimer2.h>
#include <DCF77.h>
#include <Time.h>

// Connect the clock module to pin D19 / A5
const uint8_t dcf77_sample_pin = 19; // A5
const uint8_t dcf77_analog_sample_pin = 5;

const uint8_t filtered_dcf77_pin = 2;

const uint8_t bit0_pin = 3;
const uint8_t bit15_pin = bit0_pin + 15;

void low_pass_filter() {
    // http://en.wikipedia.org/wiki/Low-pass_filter#Continuous-time_low-pass_filters

    // I will use fixed point arithmetics with 5 decimals
    const uint16_t decimal_offset = 10000;
    static uint32_t smoothed = 0*decimal_offset;

    //const uint32_t input = digitalRead(dcf77_sample_pin) * decimal_offset;
    const uint32_t input = analogRead(dcf77_analog_sample_pin)>200? decimal_offset: 0;

    // compute N such that the smoothed signal will always reach 50% of
    // the input after at most 50 samples (=50ms).
    // N = 1 / (1- 2^-(1/50)) = 72.635907286
    const uint16_t N = 72;
    smoothed = ((N-1) * smoothed + input) / N;

    // introduce some hysteresis
    static uint8_t square_wave_output = 0;

    if ((square_wave_output == 0) == (smoothed >= decimal_offset/2)) {
        // smoothed value more >= 50% away from output
        // ==> switch output
        square_wave_output = 1-square_wave_output;
        // ==> max the smoothed value in order to introduce some
        //     hysteresis, this also ensures that there is no
        //     "infinite memory"
        smoothed = square_wave_output? decimal_offset: 0;
    }

    digitalWrite(filtered_dcf77_pin, square_wave_output);    
}

DCF77 DCF = DCF77(filtered_dcf77_pin, 0);

volatile boolean sync = false;
volatile time_t time = 0;

unsigned long getDCFTime() {        
    const time_t DCFTime = DCF.getTime();

    sync = (DCFTime != 0);
    return DCFTime;
}

void diagnosis() {
    Serial.println("Waiting for DCF77 time ... ");
    
    int rising_edge = 0;
    int falling_edge = 0;
    int previous_rising_edge;
    bool was_high = false;

    while (timeStatus() == timeNotSet) {
        const uint8_t sensor_value = digitalRead(filtered_dcf77_pin);
        if (sensor_value) {
             if (!was_high) {
                rising_edge = millis();
                was_high = true;
            }
        } else {
            if (was_high) {
                falling_edge = millis();
                Serial.print("Cycle, Pulse: ");

                const int cycle = rising_edge - previous_rising_edge;
                if (cycle < 1000) {
                    Serial.print(' ');
                }
                Serial.print(cycle);                
                Serial.print(',');
                Serial.print(' ');

                const int pulse = falling_edge - rising_edge;
                if (pulse < 100) {
                    Serial.print(' ');
                }                
                Serial.print(pulse);
                previous_rising_edge = rising_edge;
                was_high = false;

                Serial.print(' ');
                Serial.print(pulse < 180? '.': 'X');

                Serial.print(' ');
                Serial.print(cycle <1800? ' ': 'm');

                Serial.println();
             }
        }
    }
    
    Serial.println();
}

void setup() {    
    for (uint8_t pin=bit0_pin; pin < bit15_pin; ++pin) {
        pinMode(pin, OUTPUT);
        digitalWrite(pin, HIGH);
    }
    delay(500);
    for (uint8_t pin=bit0_pin; pin < bit15_pin; ++pin) {
        digitalWrite(pin, LOW);
    }  

    
    pinMode(dcf77_sample_pin, INPUT);
    digitalWrite(dcf77_sample_pin, HIGH);

    pinMode(filtered_dcf77_pin, OUTPUT);

    MsTimer2::set(1, low_pass_filter);
    MsTimer2::start();

    
    DCF.Start();
    setSyncInterval(30);
    setSyncProvider(getDCFTime);

    
    Serial.begin(9600);;
    diagnosis();    
}

void loop() {
    static time_t prevDisplay = 0;    

    if( now() != prevDisplay) {
        prevDisplay = now();
        digitalClockDisplay();
    }
}

void digitalClockDisplay() {
    if (sync) {
        Serial.println("DCF sync good");
        sync = false;
    }

    Serial.print(day());
    Serial.print('.');
    Serial.print(month());
    Serial.print('.');
    Serial.print(year());
    Serial.print(' ');
    Serial.print(' ');


    const uint8_t minutes = minute();
    const uint8_t hours = hour();
    
    print_2_digits(hours);
    Serial.print(':');
    print_2_digits(minutes);
    Serial.print(':');
    print_2_digits(second());

    display_digit(minutes % 10, bit0_pin + 0, 4);
    display_digit(minutes / 10, bit0_pin + 5, 3);
        
    display_digit(hours % 10, bit0_pin +  9, 4);
    display_digit(hours / 10, bit0_pin + 14, 2);
    
    Serial.println();
}

void print_2_digits(const uint8_t digits) {
    if (digits < 10) {
        Serial.print('0');
    }
    Serial.print(digits, DEC);
}

void display_digit(const uint8_t digit, const uint8_t base_pin, const uint8_t bits) {
    for (uint8_t bit=0; bit < bits; ++bit) {
        digitalWrite(base_pin + bit, bitRead(digit, bit));
    }    
}

If you look at the code you there are some things that might need some explanation. First of all have a look at the “const” statements.

const uint8_t sensor_value = digitalRead(filtered_dcf77_pin);

You might wonder how can this be? How can the code repeatedly read new values into a constant? Well, it can not. The point is that const declares a constant in the current scope. If the scope is local this constant will be a different instance of a constant each time the scope is entered. With other words it is better to think about consts as immutable variables. You may wonder why to use them. There are two uses. One is to allow the compiler for better optimizations. The other – more important use – is to protect the code from stupid changes by stupid developers. The archetypical stupid developer is the author two months later ;)

The next thing is the way I deal with the DCF77 input. Since my DCF77 board is open collector it can not drive any output directly. Since I want to use the build in LEDs of the Blinkenlighty as a clock display the LEDs are all connected. Thus they will pull down any pull up to far to low to be ever recognized as a high signal. There are two ways to remedy this. The obvious one is to use an external pullup resistor such that an empty output will result in a clean high signal. Unfortunately this needs an external part and I was to lazy to search for a resistor. So here comes my lazy solution. I pull up the input with one of the build in resistors and read it out as an analog pin. Thus I can recognize the much lower voltage levels.

The next trick is how to connect this to the DCF77 library. The DCF77 library has its own idea on how it wants to read its input. Since I want to have a visible indicator of the signal anyway I solve this as follows.

  1. The DCF77 library is initialized.
  2. I change the pinMode of the DCF77 input pin to output.
  3. I write the output of my filter into said output pin.

The point here is that reading a pin never cares if it is an input or an output pin.

Finally you might have already noticed that my exponential low pass filter is never called from the main loop. This is where the MsTimer2 library comes into play. This library will call the filter once per millisecond in a timer interrupt. Once the filter code is processed it will return to the main program. Since the DCF77 library itself is also interrupt driven I can process anything else in the main loop.

As you can see the main loop thus just retrieves the time from the DCF77 library and uses the result to feed the LEDs with a binary clock display.

Now how good is this filter? Frankly I did not have any clue. So I just simulated it a little bit. Suppose we have a short pulse and a long pulse encoding a 0 bit followed by a 1 bit like in the graph below.

Short Pulse Followed by Long Pulse

The transmitted signal will then be amplitude modulated like in the blue graph.

DCF77 Amplitude Modulation

For any number of reasons during transmission and demodulation we may pick up noise. So in my simulation I added 60% of random noise. Notice that this means that 30% of the levels will be inverted. This is already some significant distortion of the signal.

60% noise

A simple exponential filter would smooth this to the signal form below.

60% noise – exponential filtered

However if you look into my code I integrated the trigger into the filter. The red graph below shows the internal filtered “average” and the green graph shows the final output.

60% noise – triggered exponential filter

You might wonder why I combined the trigger and the filter into one filter instead of cascading them. If you look closely at the plots you can spot the answer. First have a look at the reconstructed signal. It is pretty clean. However there is some phase shift. This is typical for any averaging filter. Now if I cascade the filter with a trigger I would need some hysteresis which would have to trigger after the 50% level. Thus I would get even more phase shift.

Another point in favour of the integrated trigger is that it gives a more symmetric behaviour for low/high and high/low transitions.

21 Responses to Binary DCF77 Clock

  1. User “jurs” of the Arduino Forum pointed out that it is not 100% clear how to connect the DCF77 modul to the Arduino – until you read and understand the whole article. So here is the spoiler for all who do not want to read through the whole article: the output of the DCF77 module must be connected to the analog 5 pin (same as digital 19).
    Thanks jurs for pointing this out.

    You can find his detailed analysis (in German) here:http://arduino.cc/forum/index.php?topic=135153.0

  2. HuB says:

    Very nice article, Its good to know that there is something called exponential smoothing in filtering.

  3. Stan says:

    Many thanks for an excellent project and write-up! I have got this DCF77 module http://www.pvelectronics.co.uk/rftime/SYM-RFT-XX.pdf as I could not find Conrad or other module. I though the receiver is broken so I almost gave up. Until I found your article.

    TCON output from module connected to Arduino Analog 5 pin, it syncs up beautifully after a little while:

    Cycle, Pulse: 994, 102 .
    Cycle, Pulse: 1016, 85 .
    Cycle, Pulse: 986, 203 X
    Cycle, Pulse: 2001, 105 . m

    DCF sync good
    8.12.2012 14:54:00
    8.12.2012 14:54:01
    8.12.2012 14:54:02

    Note: Make sure to use USB cable in full length (1.5meters or longer) to place Arduino and DCF77 module setup further away from computer.

  4. Augusto says:

    your work is very impressive! but I do not find the schematic. Were coud i find it?

  5. There is no schematic as the DCF77 modules are trivial to connect. You just connect GND with GND, Supply with 3.3V or 5V depending on the module and data with the pin stated in the code:

    // Connect the clock module to pin D19 / A5
    const uint8_t dcf77_sample_pin = 19; // A5

    The only issue is that some modules have inverted output. In this case you would need to substitute

    const uint32_t input = analogRead …

    with

    const uint32_t input = !(analogRead …)

    notice the “!”

  6. Andreas says:

    Hallo Udo,
    ich habe das normale DCF77 beispiel der Lib auch zum laufen bkeommen, würde aber dennoch mal Deinen Code testen. leider ist mir die Aussage im Code unverständlich:
    // Connect the clock module to pin D19 / A5
    const uint8_t dcf77_sample_pin = 19; // A5
    const uint8_t dcf77_analog_sample_pin = 5;
    WO soll ich denn nun den DCF-Empfänger anschliessen?
    An A5? Und was ist D19? Ich habe nur einen Arduino Nano, da gibt es nur max. D13…

    Über eine kurze Info würde ich michfreuen.
    Gruß, Andreas

    • Ein Standardardunio hat 20 Pins: 20 digitale Pins wovon 6 auch als analoge Pins angesprochen werden können. Die digitalen sind d0-d19 und die analogen a0-a5. Die Nummerierung fängt jeweils bei 0 an. D.h. 6 der Pins sind sowohl analog als auch digital. Das sind D13 (A0), D14 (A1), …, D19 (A5). Mit digital… werden die mit den Nummern für Digitalpins angsprochen und mit analogRead mit den Nummern für Analogpins. D.h. Du schliesst an D19 an (der mit A5 beschriftet ist).

      • Andreas says:

        Ah, vielen Dank für die Erklärung. Wusste ich nicht, aber nun ist mir die Zählweise klar.
        Funktioniert im übrigen – mit dem Conrad-Modul! Zeitsync nach etwa 3 Minuten. Ich habe nur umgeändert zur Anzeige auf LCD und UTC-Zeit.
        Jetzt muss ich nur noch dien Wochentag zur Anzeige bringen, und die lokale Zeit.

      • Remco says:

        Suggestion: perhaps add the ‘real’ Atmel pin designations: PC5/ADC5 in the source code?

      • Why would this be an improvement? The code is bound to Arduino right now. If you replace the Arduino Libraries with direct port access you most probably know very well which pin assignments you are using. What is the point of your suggestion?

      • Remco says:

        My point is that sometimes pin/port designators seem ambiguous from a hardware point of view..E.g. ‘pin13′ of an ATMega328 = PB1, as in source codes it is referred to as
        ‘D13′, which is ‘pin17′ of the chip.

      • Remco says:

        Forgot to mention: my suggestion only refers to a comment, thus not the real code.

  7. While writing my own DCF77 decoder [1] on a Raspberry Pi, I noticed that the signal can get out-of-phase caused by spurious positive or negative spikes. Because the Pi is not real-time and I’m also not running a real-time OS on it, second lengths tend to vary a bit (approximately 93.5 samples at 100 Hz and 870 samples at 1000 Hz). If bad enough, these spikes and the shorter second lengths can result in e.g. wrong bit values or false minute endings.
    Then I somehow stumbled upon your blog ;) Looking a bit at some raw data I recorded earlier, your exponential filter with Schmitt trigger described at this page seems to be quite adequate when living in the Netherlands.
    Would it be OK if I integrate this into my program, which is currently (and preferably) BSD licensed, instead of using my ad-hoc algorithm described in readpin.c ? (The bin approach would not be useful in my case because of the varying second lengths).

    [1] https://github.com/rene0/dcf77pi/

  8. Jens Grabner says:

    I have filtered the DCF-Signal in Hardware Low-Pass 20 Hz.

  9. Jens Grabner says:

    I use 2V6 for the DCF77 receiver. I think, 3V3 is also posible.
    http://www.grabner-online.org/projekte/dcf77/Low_Pass_20Hz_DCF77.png

    • Jens Grabner says:

      Presence of noise? … Signals, shorter then 70ms, can’t go out from the trigger and filter.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s