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.

//  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
//  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

#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() {

    // 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(' ');

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

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

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


void setup() {    
    for (uint8_t pin=bit0_pin; pin < bit15_pin; ++pin) {
        pinMode(pin, OUTPUT);
        digitalWrite(pin, HIGH);
    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);



void loop() {
    static time_t prevDisplay = 0;    

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

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

    Serial.print(' ');
    Serial.print(' ');

    const uint8_t minutes = minute();
    const uint8_t hours = hour();

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

void print_2_digits(const uint8_t digits) {
    if (digits < 10) {
    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.


37 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:

  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 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 …


    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).


  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.

  10. Patrick says:

    Hey Udo,

    ich hatte dank deinem Filter ein relativ regelmäßiges Signal.

    Nun habe ich einen LEDMatrix hinzugefügt und das Signal verschwindet komplett.

    Das einzige was ich hinzugefügt habe, ist:


    const int DIN_PIN = 11;
    const int CS_PIN = 10;
    const int CLK_PIN = 13;
    const int matrixCount = 4;

    LedControl display = LedControl(DIN_PIN, CLK_PIN, CS_PIN, matrixCount);

    Hast du eine Idee, woran das liegen könnte?

    • Zu was hast Du Deinen Code hinzugefügt? Das was Du als Deinen Code angibst kann nicht alles sein. Das ist schon syntaktisch nicht korrekt.
      Und was genau meinst Du mit “relativ regelmäßiges Signal”? Solange Du nicht Deinen vollständigen Code zeigst ist es unmöglich zu sagen an was es liegt. Tritt das Problem auf sobald Du die LED Matrix anschliesst oder bereits wenn Du die Software änderst? Was für eine Library verwendest Du für die Matrix?

      Wenn ich raten sollte würde ich tippen, daß die Library mit Timer2 rumspielt und deshalb dann nichts mehr klappt. Könnte aber genausogut was anderes sein. Ohne Details kann ich da nichts sagen.

  11. Didier Rousseau says:

    Why do you use analog mode on AVR based platforms ?
    I’ve a custom build of the UNO using à 16 MHz crystal and i can’t get it to work unless I use the same mode as the one used with SAM based boards (digital mode).

    • The library examples were developed for my Blinkenlighty / Blinkenlight shield. Because of the additional load of the LEDs on the pullup resistors digital read will not work. Hence I need to read analog. If you have a vanilla Arduino digital mode should be preferred. However the UNO does NOT have a 16 MHz crystal for the main controller. The crystal is for the USB to serial bridge. With other words: the UNO sucks at timekeeping. See also the comments on hardware compatibility here: and the analyzing the test output section here: Having said that the exponential filter will work with the UNO.

      • Didier Rousseau says:

        My UNO clone do have a 16MHz crystal and the LEDS are buffered so they don’t load the digital pins.
        In digital mode, it works fine.

  12. Didier Rousseau says:

    I should add that it does not use an ATmega chip as the USB to UART interface, it uses a FT232RL chip.
    So the only crystal is for the ATmega328P.

  13. Juan Solsona says:

    Grüsse Udo

    I took a look to the code and I do not understand this sentence:
    static uint32_t smoothed = 0*decimal_offset;
    To me looks like a strange way to initialize the variable to 0, may you help me to understand?


    • Well, it is an indicator that smoothed is a fixed point decimal scaled by “decimal_offset”. The point is that if you want set this to “1” then you need to channge the value to 1*decimal_offset. For example 0 cm and 0 m are the same. However x cm and x m are only the same if x happens to be zero. That is: I give some hint about the scale by implementing it that way.

  14. says:

    Hello Udo,

    i have a maybe silly question: You mention “you will need an additional DCF77 module”. Does that mean, i add a second module to my arduino? Say, the first module is connected at sample_pin and the second at analog_sample_pin?

    Thx in advane!

  15. igor says:

    One thing to comment: I’m currently using an ELV DCF-module which is an open-collector type. First thing: No signal on output. I had to solder a rsistor from output to VDD to get any signal. Tested with an oscilloscope. Even the internal pullup of the ATmega didnt help. Second: Quality is not existent. Only at night after half an hour to one hour connected i get a time. During daytime there is no time output! I’m living 200km from Frankfurt. No more to say. I go to buy a better one (HKW) – like you mentioned: Bigger antenna is better.

    • 200 km from Frankfurt I would expect that the signal is good enough for my decoder. Can you provide some sample output of the debug helper? Maybe logfiles for 3-4 hours?

      • igor says:

        I am awaiting the new receiver/antenna from HKW, so i can send you the ELV-thing. So you can make your own tests like you did with the other receivers. Its complete with the docu and useless for me. I would throw it…
        Send me via mail a note where to send.

Leave a Reply to Patrick Cancel reply

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

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

Facebook photo

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

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.