DCF77 Generator

This is an Arduino based DCF77 generator. Yes, this tool allows to set the time of any DCF77 clock to whatever you want. This is definitely not the first project to implement this, but I am pretty much sure that it is one of the simplest Arduino based generators.

In case you use an Arduino the setup just requires a piece of wire plus a 1k resistor. See the schematic below for the wiring.

C10_DCF77_Generator_Arduino

If you use a Blinkenlighty I recommend a slightly more complicated setup.

C10_DCF77_Generator_Blinkenlighty

Although the wiring appears to be more complex the actual setup is simpler. This is because the Blinkenlighty has an integrated Blinkenlight Shield. The shield contains the LED + resistor. Thus it is sufficient to wire it like sketched below.

C10_DCF77_Generator_Blinkenlighty_Board

The idea is of course that the wire (red) will be wrapped once around the target clock. Thus the signal will be coupled inductively into the target’s clock receiver antenna. See the next two pictures how this looks in practice.

C10_DCF77_Generator_Compressed_1280

C10_DCF77_Generator_Wiring_Magnified

So the hardware side is very simple. The same is not true for the software side though. The issue is that the Arduino is running at 16 MHz and we need to generate a 77.5 kHz carrier signal. Lets look at the math. 16000000 / 77500 = 206 + 70/155 = 206 + 14/31. So the straightforware approach would be to setup a timer for 206 or 207 ticks. However this yields 16000000 / 206 ~ 77670 or 16000000 / 207 ~ 77295 as the target frequency. That is a frequency error of at least 170 Hz. For low bandwidth receivers this would not really work. Bresenham’s Algorithm to the rescue. This is the same idea I already used for the crystal frequency experiment.

The result is a reasonable good 77.5 kHz carrier. If you wonder why I do not care about crystal accuracy: an crystal deviation of 100 ppm would result in a frequency error of 7.5 Hz. This is no issue for typical DCF77 receivers. Of course it is an issue for my clock library as soon as it finishes tuning and thus increases its time constants. But during testing I usually will disable this piece of the library if necessary.

The next challenge is how to modulate the carrier. First I was experimenting with switching resistors in series with the antenna and bypassing them. This did not work out. But then I got it. The idea is not to control the amplitude of the signal but the power fed into the output. That is pulse width modulation will do the trick.

At first this looks odd but once you start to thing about the fourier transforms of the signal it all becomes clear. A signal with reduced duty cycle will have a lower amplitude at the carrier frequency. As it is a rectangular signal it will have significant harmonics at multiples of the carrier (77.5 kHz). Since typical DCF77 receivers have a bandwidth below 1 kHz (often below 500 Hz or even below 20 Hz) the harmonics are irrelevant for them. Actually most receivers have tuned Antennas with a bandwidth below 1 kHz. The collective effect is that controlling the PWM duty cycle of a 77.5 kHz carrier is from the point of view of a DCF77 receiver equivalent to modulation of the carrier.

So I only need to control the PWM depending on the bit stream that shall be emitted. Computing the bitstream is stright forward. I did not even bother to (re)implement it. I just copied it from the error control logic of my DCF77 library.

For convenience I added some simple parser that will allow me to input whatever time I want. By default it will start to emulate the first of January 2009, 00:52 am. Or 8 minutes before a leap second. You may wonder why. Well – I am always curious how commercial clocks deal with the edge cases of the protocoll. So far ALL that I tested failed to properly deal with leap seconds. I thus infer that my library blows a lot of the commercial implementations out of the water. To be fair I have to admit that most commercial clocks operate on a very tight power budget which is not the case for my library though.

As I wanted to test this as well my generator supports any *theoretically* valid (and some invalid) outputs. This makes the code sligthly larger than one might expect. But in the end the really complicated part was the carrier and PWM setup.

Here is the code.

//
//  www.blinkenlight.net
//
//  Copyright 2014 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/



// Please take care to NOT amplify the output of pin 3.
// Also do not connect pin 3 to a tuned antenna.

// Stay within in legal bounds. You have been warned.

// http://www.mikrocontroller.net/articles/Allgemeinzuteilung
// http://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Frequenzen/Allgemeinzuteilungen/2014_04_InduktiveFunkanwendungen_pdf.pdf?__blob=publicationFile&v=5

// In Germany this implies that you must not exceed
// 42 dBμA/m in a distance of 10m of your transmitter
// If you connect 1k resistor to the output pin then
// the current will stay below 5 mA which will keep
// the output well below regulatory limits.

namespace BCD {
    typedef union {
        struct {
            uint8_t lo:4;
            uint8_t hi:4;
        } digit;

        struct {
            uint8_t b0:1;
            uint8_t b1:1;
            uint8_t b2:1;
            uint8_t b3:1;
            uint8_t b4:1;
            uint8_t b5:1;
            uint8_t b6:1;
            uint8_t b7:1;
        } bit;

        uint8_t val;
    } bcd_t;

    void increment(bcd_t &value);

    bcd_t int_to_bcd(const uint8_t value);
    uint8_t bcd_to_int(const bcd_t value);
}

namespace Debug {
    void bcddigit(uint8_t data);
    void bcddigits(uint8_t data);
}

typedef union {
    struct {
        uint8_t b0:2;
        uint8_t b1:2;
        uint8_t b2:2;
        uint8_t b3:2;
    } signal;
    uint8_t byte;
} tData;

namespace DCF77 {
    typedef enum {
        long_tick  = 3,
        short_tick = 2,
        undefined  = 1,
        sync_mark  = 0
    } tick_t;

    typedef struct {
        BCD::bcd_t year;     // 0..99
        BCD::bcd_t month;    // 1..12
        BCD::bcd_t day;      // 1..31
        BCD::bcd_t weekday;  // Mo = 1, So = 7
        BCD::bcd_t hour;     // 0..23
        BCD::bcd_t minute;   // 0..59
        uint8_t second;      // 0..60
        bool uses_summertime           : 1;  // false -> wintertime, true, summertime
        bool uses_backup_antenna       : 1;  // typically false
        bool timezone_change_scheduled : 1;
        bool leap_second_scheduled     : 1;

        bool undefined_minute_output                    : 1;
        bool undefined_uses_summertime_output           : 1;
        bool undefined_uses_backup_antenna_output       : 1;
        bool undefined_timezone_change_scheduled_output : 1;
    } time_data_t;

    typedef void (*output_handler_t)(const DCF77::time_data_t &decoded_time);

    typedef enum {
        useless  = 0,  // waiting for good enough signal
        dirty    = 1,  // time data available but unreliable
        free     = 2,  // clock was once synced but now may deviate more than 200 ms, must not re-lock if valid phase is detected
        unlocked = 3,  // lock was once synced, inaccuracy below 200 ms, may re-lock if a valid phase is detected
        locked   = 4,  // no valid time data but clock driven by accurate phase
        synced   = 5   // best possible quality, clock is 100% synced
    } clock_state_t;
}

namespace DCF77_Encoder {
    // What *** exactly *** is the semantics of the "Encoder"?
    // It only *** encodes *** whatever time is set
    // It does never attempt to verify the data

    void reset(DCF77::time_data_t &now);

    uint8_t weekday(const DCF77::time_data_t &now);  // sunday == 0
    BCD::bcd_t bcd_weekday(const DCF77::time_data_t &now);  // sunday == 7

    DCF77::tick_t get_current_signal(const DCF77::time_data_t &now);

    // This will advance the second. It will consider the control
    // bits while doing so. It will NOT try to properly set the
    // control bits. If this is desired "autoset" must be called in
    // advance.
    void advance_second(DCF77::time_data_t &now);

    // The same but for the minute
    void advance_minute(DCF77::time_data_t &now);

    // This will set the weekday by evaluating the date.
    void autoset_weekday(DCF77::time_data_t &now);

    // This will set the control bits, as a side effect it sets the weekday
    // It will generate the control bits exactly like DCF77 would.
    // Look at the leap second and summer / wintertime transistions
    // to understand the subtle implications.
    void autoset_control_bits(DCF77::time_data_t &now);

    void debug(const DCF77::time_data_t &clock);
    void debug(const DCF77::time_data_t &clock, const uint16_t cycles);

    // Bit      Bezeichnung     Wert    Pegel   Bedeutung
    // 0        M                       0       Minutenanfang (

    // 1..14    n/a                             reserviert

    // 15       R                               Reserveantenne aktiv (0 inaktiv, 1 aktiv)
    // 16       A1                              Ankündigung Zeitzonenwechsel (1 Stunde vor dem Wechsel für 1 Stunde, d.h ab Minute 1)
    // 17       Z1               2              Zeitzonenbit Sommerzeit (MEZ = 0, MESZ = 1); also Zeitzone = UTC + 2*Z1 + Z2
    // 18       Z2               1              Zeitzonenbit Winterzeit (MEZ = 1, MESZ = 0); also Zeitzone = UTC + 2*Z1 + Z2
    // 19       A2                              Ankündigung einer Schaltsekunde (1 Stunde vor der Schaltsekunde für 1 Stunde, d.h. ab Minute 1)

    // 20       S                       1       Startbit für Zeitinformation

    // 21                        1              Minuten  1er
    // 22                        2              Minuten  2er
    // 23                        4              Minuten  4er
    // 24                        8              Minuten  8er
    // 25                       10              Minuten 10er
    // 26                       20              Minuten 20er
    // 27                       40              Minuten 40er
    // 28       P1                              Prüfbit 1 (gerade Parität)

    // 29                        1              Stunden  1er
    // 30                        2              Stunden  2er
    // 31                        4              Stunden  4er
    // 32                        8              Stunden  8er
    // 33                       10              Stunden 10er
    // 34                       20              Stunden 20er
    // 35       P2                              Prüfbit 2 (gerade Parität)

    // 36                        1              Tag  1er
    // 37                        2              Tag  2er
    // 38                        4              Tag  4er
    // 39                        8              Tag  8er
    // 40                       10              Tag 10er
    // 41                       20              Tag 20er

    // 42                        1              Wochentag 1er (Mo = 1, Di = 2, Mi = 3,
    // 43                        2              Wochentag 2er (Do = 4, Fr = 5, Sa = 6,
    // 44                        4              Wochentag 4er (So = 7)

    // 45                        1              Monat  1er
    // 46                        2              Monat  2er
    // 47                        4              Monat  4er
    // 48                        8              Monat  8er
    // 49                       10              Monat 10er

    // 50                        1              Jahr  1er
    // 51                        2              Jahr  2er
    // 52                        4              Jahr  4er
    // 53                        8              Jahr  8er
    // 54                       10              Jahr 10er
    // 55                       20              Jahr 20er
    // 56                       40              Jahr 40er
    // 57                       80              Jahr 80er

    // 58       P3                              Prüftbit 3 (gerade Parität)

    // 59       sync                            Sync Marke, kein Impuls (übliches Minutenende)
    // 59                               0       Schaltsekunde (sehr selten, nur nach Ankündigung)
    // 60       sync                            Sync Marke, kein Impuls (nur nach Schaltsekunde)

    // Falls eine Schaltsekunde eingefügt wird, wird bei Bit 59 eine Sekundenmarke gesendet.
    // Der Syncimpuls erfolgt dann in Sekunde 60 statt 59. Üblicherweise wird eine 0 als Bit 59 gesendet

    // Üblicherweise springt die Uhr beim Wechsel Winterzeit nach Sommerzeit von 1:59:59 auf 3:00:00
    //                               beim Wechsel Sommerzeit nach Winterzeit von 2:59:59 auf 2:00:00

    // Die Zeitinformation wird immer 1 Minute im Vorraus übertragen. D.h. nach der Syncmarke hat
    // man die aktuelle Zeit

    // http://www.dcf77logs.de/SpecialFiles.aspx

    // Schaltsekunden werden in Deutschland von der Physikalisch-Technischen Bundesanstalt festgelegt,
    // die allerdings dazu nur die international vom International Earth Rotation and Reference Systems
    // Service (IERS) festgelegten Schaltsekunden übernimmt. Im Mittel sind Schaltsekunden etwa alle 18
    // Monate nötig und werden vorrangig am 31. Dezember oder 30. Juni, nachrangig am 31. März oder
    // 30. September nach 23:59:59 UTC (also vor 1:00 MEZ bzw. 2:00 MESZ) eingefügt. Seit der Einführung
    // des Systems 1972 wurden ausschließlich die Zeitpunkte im Dezember und Juni benutzt.
}

namespace Debug {
    void bcddigit(uint8_t data) {
        if (data <= 0x09) {
            Serial.print(data, HEX);
        } else {
            Serial.print('?');
        }
    }

    void bcddigits(uint8_t data) {
        bcddigit(data >>  4);
        bcddigit(data & 0xf);
    }
}

namespace BCD {
    void print(const bcd_t value) {
        Serial.print(value.val >> 4 & 0xF, HEX);
        Serial.print(value.val >> 0 & 0xF, HEX);
    }

    void increment(bcd_t &value) {
        if (value.digit.lo < 9) {
            ++value.digit.lo;
        } else {
            value.digit.lo = 0;

            if (value.digit.hi < 9) {
                ++value.digit.hi;
            } else {
                value.digit.hi = 0;
            }
        }
    }

    bcd_t int_to_bcd(const uint8_t value) {
        const uint8_t hi = value / 10;

        bcd_t result;
        result.digit.hi = hi;
        result.digit.lo = value-10*hi;

        return result;
    }

    uint8_t bcd_to_int(const bcd_t value) {
        return value.digit.lo + 10*value.digit.hi;
    }
}

namespace Arithmetic_Tools {
    template <uint8_t N> inline void bounded_increment(uint8_t &value) __attribute__((always_inline));
    template <uint8_t N>
    void bounded_increment(uint8_t &value) {
        if (value >= 255 - N) { value = 255; } else { value += N; }
    }

    template <uint8_t N> inline void bounded_decrement(uint8_t &value) __attribute__((always_inline));
    template <uint8_t N>
    void bounded_decrement(uint8_t &value) {
        if (value <= N) { value = 0; } else { value -= N; }
    }

    inline void bounded_add(uint8_t &value, const uint8_t amount) __attribute__((always_inline));
    void bounded_add(uint8_t &value, const uint8_t amount) {
        if (value >= 255-amount) { value = 255; } else { value += amount; }
    }

    inline void bounded_sub(uint8_t &value, const uint8_t amount) __attribute__((always_inline));
    void bounded_sub(uint8_t &value, const uint8_t amount) {
        if (value <= amount) { value = 0; } else { value -= amount; }
    }

    inline uint8_t bit_count(const uint8_t value) __attribute__((always_inline));
    uint8_t bit_count(const uint8_t value) {
        const uint8_t tmp1 = (value & 0b01010101) + ((value>>1) & 0b01010101);
        const uint8_t tmp2 = (tmp1  & 0b00110011) + ((tmp1>>2) & 0b00110011);
        return (tmp2 & 0x0f) + (tmp2>>4);
    }

    inline uint8_t parity(const uint8_t value) __attribute__((always_inline));
    uint8_t parity(const uint8_t value) {
        uint8_t tmp = value;

        tmp = (tmp & 0xf) ^ (tmp >> 4);
        tmp = (tmp & 0x3) ^ (tmp >> 2);
        tmp = (tmp & 0x1) ^ (tmp >> 1);

        return tmp;
    }

    void minimize(uint8_t &minimum, const uint8_t value) {
        if (value < minimum) {
            minimum = value;
        }
    }

    void maximize(uint8_t &maximum, const uint8_t value) {
        if (value > maximum) {
            maximum = value;
        }
    }

    uint8_t set_bit(const uint8_t data, const uint8_t number, const uint8_t value) {
        return value? data|(1<<number): data&~(1<<number);
    }
}

namespace DCF77_Encoder {
    using namespace DCF77;

    inline uint8_t days_per_month(const DCF77::time_data_t &now) __attribute__((always_inline));
    uint8_t days_per_month(const DCF77::time_data_t &now) {
        switch (now.month.val) {
            case 0x02:
                // valid till 31.12.2399
                // notice year mod 4 == year & 0x03
                return 28 + ((now.year.val != 0) && ((bcd_to_int(now.year) & 0x03) == 0)? 1: 0);
            case 0x01: case 0x03: case 0x05: case 0x07: case 0x08: case 0x10: case 0x12: return 31;
            case 0x04: case 0x06: case 0x09: case 0x11:                                  return 30;
            default: return 0;
        }
    }

    void reset(DCF77::time_data_t &now) {
        now.second      = 0;
        now.minute.val  = 0x00;
        now.hour.val    = 0x00;
        now.day.val     = 0x01;
        now.month.val   = 0x01;
        now.year.val    = 0x00;
        now.weekday.val = 0x01;
        now.uses_summertime           = false;
        now.uses_backup_antenna       = false;
        now.timezone_change_scheduled = false;
        now.leap_second_scheduled     = false;

        now.undefined_minute_output                    = false;
        now.undefined_uses_summertime_output           = false;
        now.undefined_uses_backup_antenna_output       = false;
        now.undefined_timezone_change_scheduled_output = false;
    }

    uint8_t weekday(const DCF77::time_data_t &now) {  // attention: sunday will be ==0 instead of 7
        using namespace BCD;

        if (now.day.val <= 0x31 && now.month.val <= 0x12 && now.year.val <= 0x99) {
            // This will compute the weekday for each year in 2001-2099.
            // If you really plan to use my code beyond 2099 take care of this
            // on your own. My assumption is that it is even unclear if DCF77
            // will still exist then.

            // http://de.wikipedia.org/wiki/Gau%C3%9Fsche_Wochentagsformel
            const uint8_t  d = bcd_to_int(now.day);
            const uint16_t m = now.month.val <= 0x02? now.month.val + 10:
                                                      bcd_to_int(now.month) - 2;
            const uint8_t  y = bcd_to_int(now.year) - (now.month.val <= 0x02);
            // m must be of type uint16_t otherwise this will compute crap
            uint8_t day_mod_7 = d + (26*m - 2)/10 + y + y/4;
            // We exploit 8 mod 7 = 1
            while (day_mod_7 >= 7) {
                day_mod_7 -= 7;
                day_mod_7 = (day_mod_7 >> 3) + (day_mod_7 & 7);
            }

            return day_mod_7;  // attention: sunday will be == 0 instead of 7
        } else {
            return 0xff;
        }
    }

    BCD::bcd_t bcd_weekday(const DCF77::time_data_t &now) {
        BCD::bcd_t today;

        today.val = weekday(now);
        if (today.val == 0) {
            today.val = 7;
        }

        return today;
    }

    void autoset_weekday(DCF77::time_data_t &now) {
        now.weekday = bcd_weekday(now);
    }

    void autoset_timezone(DCF77::time_data_t &now) {
        // timezone change may only happen at the last sunday of march / october
        // the last sunday is always somewhere in [25-31]

        // Wintertime --> Summertime happens at 01:00 UTC == 02:00 CET == 03:00 CEST,
        // Summertime --> Wintertime happens at 01:00 UTC == 02:00 CET == 03:00 CEST

        if (now.month.val < 0x03) {
            // January or February
            now.uses_summertime = false;
        } else
        if (now.month.val == 0x03) {
            // March
            if (now.day.val < 0x25) {
                // Last Sunday of March must be 0x25-0x31
                // Thus still to early for summertime
                now.uses_summertime = false;
            } else
            if (uint8_t wd = weekday(now)) {
                // wd != 0 --> not a Sunday
                if (now.day.val - wd < 0x25) {
                    // early March --> wintertime
                    now.uses_summertime = false;
                } else {
                    // late march summertime
                    now.uses_summertime = true;
                }
            } else {
                // last sunday of march
                // decision depends on the current hour
                now.uses_summertime = (now.hour.val > 2);
            }
        } else
        if (now.month.val < 0x10) {
            // April - September
            now.uses_summertime = true;
        } else
        if (now.month.val == 0x10) {
            // October
            if (now.day.val < 0x25) {
                // early October
                now.uses_summertime = true;
            } else
            if (uint8_t wd = weekday(now)) {
                // wd != 0 --> not a Sunday
                if (now.day.val - wd < 0x25) {
                    // early October --> summertime
                    now.uses_summertime = true;
                } else {
                    // late October --> wintertime
                    now.uses_summertime = false;
                }
            } else {  // last sunday of october
                if (now.hour.val == 2) {
                    // can not derive the flag from time data
                    // this is the only time the flag is derived
                    // from the flag vector

                } else {
                    // decision depends on the current hour
                    now.uses_summertime = (now.hour.val < 2);
                }
            }
        } else {
            // November and December
            now.uses_summertime = false;
        }
    }

    void autoset_timezone_change_scheduled(DCF77::time_data_t &now) {
        // summer/wintertime change will always happen
        // at clearly defined hours
        // http://www.gesetze-im-internet.de/sozv/__2.html

        // in doubt have a look here: http://www.dcf77logs.de/
        if (now.day.val < 0x25 || weekday(now) != 0) {
            // timezone change may only happen at the last sunday of march / october
            // the last sunday is always somewhere in [25-31]

            // notice that undefined (==0xff) day/weekday data will not cause any action
            now.timezone_change_scheduled = false;
        } else {
            if (now.month.val == 0x03) {
                if (now.uses_summertime) {
                    now.timezone_change_scheduled = (now.hour.val == 0x03 && now.minute.val == 0x00); // wintertime to summertime, preparing first minute of summertime
                } else {
                    now.timezone_change_scheduled = (now.hour.val == 0x01 && now.minute.val != 0x00); // wintertime to summertime
                }
            } else if (now.month.val == 0x10) {
                if (now.uses_summertime) {
                    now.timezone_change_scheduled = (now.hour.val == 0x02 && now.minute.val != 0x00); // summertime to wintertime
                } else {
                    now.timezone_change_scheduled = (now.hour.val == 0x02 && now.minute.val == 0x00); // summertime to wintertime, preparing first minute of wintertime
                }
            } else if (now.month.val <= 0x12) {
                now.timezone_change_scheduled = false;
            }
        }
    }

    void verify_leap_second_scheduled(DCF77::time_data_t &now) {
        // If day or month are unknown we default to "no leap second" because this is alway a very good guess.
        // If we do not know for sure we are either acquiring a lock right now --> we will easily recover from a wrong guess
        // or we have very noisy data --> the leap second bit is probably noisy as well --> we should assume the most likely case

        now.leap_second_scheduled &= (now.day.val == 0x01);

        // leap seconds will always happen at 00:00 UTC == 01:00 CET == 02:00 CEST
        if (now.month.val == 0x01) {
            now.leap_second_scheduled &= ((now.hour.val == 0x00 && now.minute.val != 0x00) ||
                                          (now.hour.val == 0x01 && now.minute.val == 0x00));
        } else if (now.month.val == 0x07 || now.month.val == 0x04 || now.month.val == 0x10) {
            now.leap_second_scheduled &= ((now.hour.val == 0x01 && now.minute.val != 0x00) ||
                                          (now.hour.val == 0x02 && now.minute.val == 0x00));
        } else {
            now.leap_second_scheduled = false;
        }
    }

    void autoset_control_bits(DCF77::time_data_t &now) {
        autoset_weekday(now);
        autoset_timezone(now);
        autoset_timezone_change_scheduled(now);
        // we can not compute leap seconds, we can only verify if they might happen
        verify_leap_second_scheduled(now);
    }

    void advance_second(DCF77::time_data_t &now) {
        // in case some value is out of range it will not be advanced
        // this is on purpose
        if (now.second < 59) {
            ++now.second;
            if (now.second == 15) {
                autoset_control_bits(now);
            }

        } else if (now.leap_second_scheduled && now.second == 59 && now.minute.val == 0x00) {
            now.second = 60;
            now.leap_second_scheduled = false;
        } else if (now.second == 59 || now.second == 60) {
            now.second = 0;
            advance_minute(now);
        }
    }

    void advance_minute(DCF77::time_data_t &now) {
        if (now.minute.val < 0x59) {
            increment(now.minute);
        } else if (now.minute.val == 0x59) {
            now.minute.val = 0x00;
            // in doubt have a look here: http://www.dcf77logs.de/
            if (now.timezone_change_scheduled && !now.uses_summertime && now.hour.val == 0x01) {
                // Wintertime --> Summertime happens at 01:00 UTC == 02:00 CET == 03:00 CEST,
                // the clock must be advanced from 01:59 CET to 03:00 CEST
                increment(now.hour);
                increment(now.hour);
                now.uses_summertime = true;
            }  else if (now.timezone_change_scheduled && now.uses_summertime && now.hour.val == 0x02) {
                // Summertime --> Wintertime happens at 01:00 UTC == 02:00 CET == 03:00,
                // the clock must be advanced from 02:59 CEST to 02:00 CET
                now.uses_summertime = false;
            } else {
                if (now.hour.val < 0x23) {
                    increment(now.hour);
                } else if (now.hour.val == 0x23) {
                    now.hour.val = 0x00;

                    if (now.weekday.val < 0x07) {
                        increment(now.weekday);
                    } else if (now.weekday.val == 0x07) {
                        now.weekday.val = 0x01;
                    }

                    if (bcd_to_int(now.day) < days_per_month(now)) {
                        increment(now.day);
                    } else if (bcd_to_int(now.day) == days_per_month(now)) {
                        now.day.val = 0x01;

                        if (now.month.val < 0x12) {
                            increment(now.month);
                        } else if (now.month.val == 0x12) {
                            now.month.val = 0x01;

                            if (now.year.val < 0x99) {
                                increment(now.year);
                            } else if (now.year.val == 0x99) {
                                now.year.val = 0x00;
                            }
                        }
                    }
                }
            }
        }
    }

    DCF77::tick_t get_current_signal(const DCF77::time_data_t &now) {
        using namespace Arithmetic_Tools;

        if (now.second >= 1 && now.second <= 14) {
            // weather data or other stuff we can not compute
            return undefined;
        }

        bool result;
        switch (now.second) {
            case 0:  // start of minute
                return short_tick;

            case 15:
                if (now.undefined_uses_backup_antenna_output) { return undefined; }
                result = now.uses_backup_antenna; break;

            case 16:
                if (now.undefined_timezone_change_scheduled_output) { return undefined; }
                result = now.timezone_change_scheduled; break;

            case 17:
                if (now.undefined_uses_summertime_output) {return undefined; }
                result = now.uses_summertime; break;

            case 18:
                if (now.undefined_uses_summertime_output) {return undefined; }
                result = !now.uses_summertime; break;

            case 19:
                result = now.leap_second_scheduled; break;

            case 20:  // start of time information
                return long_tick;

            case 21:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.lo & 0x1; break;
            case 22:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.lo & 0x2; break;
            case 23:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.lo & 0x4; break;
            case 24:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.lo & 0x8; break;

            case 25:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.hi & 0x1; break;
            case 26:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.hi & 0x2; break;
            case 27:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = now.minute.digit.hi & 0x4; break;

            case 28:
                if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
                result = parity(now.minute.val); break;


            case 29:
                if (now.hour.val > 0x23) { return undefined; }
                result = now.hour.digit.lo & 0x1; break;
            case 30:
                if (now.hour.val > 0x23) { return undefined; }
                result = now.hour.digit.lo & 0x2; break;
            case 31:
                if (now.hour.val > 0x23) { return undefined; }
                result = now.hour.digit.lo & 0x4; break;
            case 32:
                if (now.hour.val > 0x23) { return undefined; }
                result = now.hour.digit.lo & 0x8; break;

            case 33:
                if (now.hour.val > 0x23) { return undefined; }
                result = now.hour.digit.hi & 0x1; break;
            case 34:
                if (now.hour.val > 0x23) { return undefined; }
                result = now.hour.digit.hi & 0x2; break;

            case 35:
                if (now.hour.val > 0x23) { return undefined; }
                result = parity(now.hour.val); break;

            case 36:
                if (now.day.val > 0x31) { return undefined; }
                result = now.day.digit.lo & 0x1; break;
            case 37:
                if (now.day.val > 0x31) { return undefined; }
                result = now.day.digit.lo & 0x2; break;
            case 38:
                if (now.day.val > 0x31) { return undefined; }
                result = now.day.digit.lo & 0x4; break;
            case 39:
                if (now.day.val > 0x31) { return undefined; }
                result = now.day.digit.lo & 0x8; break;

            case 40:
                if (now.day.val > 0x31) { return undefined; }
                result = now.day.digit.hi & 0x1; break;
            case 41:
                if (now.day.val > 0x31) { return undefined; }
                result = now.day.digit.hi & 0x2; break;

            case 42:
                if (now.weekday.val > 0x7) { return undefined; }
                result = now.weekday.val & 0x1; break;
            case 43:
                if (now.weekday.val > 0x7) { return undefined; }
                result = now.weekday.val & 0x2; break;
            case 44:
                if (now.weekday.val > 0x7) { return undefined; }
                result = now.weekday.val & 0x4; break;

            case 45:
                if (now.month.val > 0x12) { return undefined; }
                result = now.month.digit.lo & 0x1; break;
            case 46:
                if (now.month.val > 0x12) { return undefined; }
                result = now.month.digit.lo & 0x2; break;
            case 47:
                if (now.month.val > 0x12) { return undefined; }
                result = now.month.digit.lo & 0x4; break;
            case 48:
                if (now.month.val > 0x12) { return undefined; }
                result = now.month.digit.lo & 0x8; break;

            case 49:
                if (now.month.val > 0x12) { return undefined; }
                result = now.month.digit.hi & 0x1; break;

            case 50:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.lo & 0x1; break;
            case 51:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.lo & 0x2; break;
            case 52:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.lo & 0x4; break;
            case 53:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.lo & 0x8; break;

            case 54:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.hi & 0x1; break;
            case 55:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.hi & 0x2; break;
            case 56:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.hi & 0x4; break;
            case 57:
                if (now.year.val > 0x99) { return undefined; }
                result = now.year.digit.hi & 0x8; break;

            case 58:
                if (now.weekday.val > 0x07 ||
                    now.day.val     > 0x31 ||
                    now.month.val   > 0x12 ||
                    now.year.val    > 0x99) { return undefined; }

                result = parity(now.day.digit.lo)   ^
                         parity(now.day.digit.hi)   ^
                         parity(now.month.digit.lo) ^
                         parity(now.month.digit.hi) ^
                         parity(now.weekday.val)    ^
                         parity(now.year.digit.lo)  ^
                         parity(now.year.digit.hi); break;

            case 59:
                // special handling for leap seconds
                if (now.leap_second_scheduled && now.minute.val == 0) { result = 0; break; }
                // standard case: fall through to "sync_mark"
            case 60:
                return sync_mark;

            default:
                return undefined;
        }

        return result? long_tick: short_tick;
    }

    void debug(const DCF77::time_data_t &clock) {
        using namespace Debug;

        Serial.print(F("  "));
        bcddigits(clock.year.val);
        Serial.print('.');
        bcddigits(clock.month.val);
        Serial.print('.');
        bcddigits(clock.day.val);
        Serial.print('(');
        bcddigit(clock.weekday.val);
        Serial.print(',');
        bcddigit(weekday(clock));
        Serial.print(')');
        bcddigits(clock.hour.val);
        Serial.print(':');
        bcddigits(clock.minute.val);
        Serial.print(':');
        if (clock.second < 10) {
            Serial.print('0');
        }
        Serial.print(clock.second, DEC);
        if (clock.uses_summertime) {
            Serial.print(F(" MESZ "));
        } else {
            Serial.print(F(" MEZ "));
        }
        if (clock.leap_second_scheduled) {
            Serial.print(F("leap second scheduled"));
        }
        if (clock.timezone_change_scheduled) {
            Serial.print(F("time zone change scheduled"));
        }
    }
}

namespace timer_2 {
    // 16000000 / 77500 = 206 + 70/155 = 206 + 14/31
    const uint8_t ticks_per_period = 206;
    // fractional ticks
    const uint8_t nominator = 14;
    const uint8_t denominator = 31;

    const uint8_t pwm_full_carrier      = ticks_per_period / 2;  // ~50  %   duty cycle
    const uint8_t pwm_modulated_carrier = ticks_per_period / 32;  // 1/4 Amplitude = 1/16 Power

    void setup() {
         // disable timer2 interrupts during setup
        TIMSK2 = 0;

        // enable OC2B pin for output (digital pin 3)
        DDRD |= 1<<3;

        // The fast Pulse Width Modulation or fast PWM mode (WGM22:0 = 3 or 7) provides a high fre-
        // quency PWM waveform generation option. The fast PWM differs from the other PWM option by
        // its single-slope operation. The counter counts from BOTTOM to TOP then restarts from BOT-
        // TOM. TOP is defined as 0xFF when WGM2:0 = 3, and OCR2A when MGM2:0 = 7. In non-
        // inverting Compare Output mode, the Output Compare (OC2x) is cleared on the compare match
        // between TCNT2 and OCR2x, and set at BOTTOM. In inverting Compare Output mode, the out-
        // put is set on compare match and cleared at BOTTOM.


        TCCR2A = 1<< WGM20 | 1<<WGM21 | // Fast PWM
                 1<<COM2B1;             // Clear OC2B on Compare Match

        TCCR2B = 1<<CS20 | // no Prescaler
                 1<<WGM22;  // Fast PWM

        OCR2A = ticks_per_period - 1;  // period length

        OCR2B = pwm_full_carrier;  // duty cycle

        TIMSK2 = 1<<OCIE2A;  // enable match interrupts
    }
}

ISR(TIMER2_COMPA_vect) {
    static uint8_t accumulated_fractional_ticks = 0;

    accumulated_fractional_ticks += timer_2::nominator;
    if (accumulated_fractional_ticks < timer_2::denominator) {
        OCR2A = timer_2::ticks_per_period - 1;
    } else {
        OCR2A = timer_2::ticks_per_period;
        accumulated_fractional_ticks -= timer_2::denominator;
    }
}

namespace timer_0 {
    void setup() {
        // disable timer 0 interrupts
        TIMSK0 = 0;
    }
}

namespace timer_1 {
    void setup() {
        TIMSK1 = 0;  // disable timer1 interrupts

        TCCR1A = 0;  // do not toggle or change timer IO pins

        // generate interrupts at 10 Hz
        OCR1A = 6250 - 1;  // (16000000 / 256 / 10) - 1

        TCCR1B = (1<<WGM12) | // CTC using OCR1A
                 (1<<CS12);    // set prescaler to 256

        TIMSK1 = (1 << OCIE1A);  // enable OCR1A match interrupt
    }
}

uint8_t stop_modulation_after_times_100ms = 5;
DCF77::time_data_t now;

void modulate() {
    static uint8_t times_100ms = 0;

    if (times_100ms == 0) {
        // start of second
        if (times_100ms != stop_modulation_after_times_100ms) {
            OCR2B = timer_2::pwm_modulated_carrier;
        }
    }

    if (times_100ms == stop_modulation_after_times_100ms) {
        OCR2B = timer_2::pwm_full_carrier;
    }

    if (times_100ms < 9) {
        ++times_100ms;
    } else {
        // after 900ms)
        times_100ms = 0;


        DCF77::time_data_t l_now = now;
        DCF77_Encoder::advance_second(now);
        const DCF77::tick_t output_tick = DCF77_Encoder::get_current_signal(now);
        stop_modulation_after_times_100ms = output_tick == DCF77::long_tick ? 2:
                                            output_tick == DCF77::short_tick? 1:
                                            output_tick == DCF77::undefined?  1:
                                                                              0;
    }
}

ISR(TIMER1_COMPA_vect) {
    sei();
    // ensure that timer 2 can interrupt timer 1
    modulate();
}

namespace Parser {
    void show(const DCF77::time_data_t &now) {
          cli();
          const DCF77::time_data_t l_now = now;
          sei();

          Debug::bcddigits(l_now.year.val);
          Serial.print('.');
          Debug::bcddigits(l_now.month.val);
          Serial.print('.');
          Debug::bcddigits(l_now.day.val);
          Serial.print(' ');

          Debug::bcddigits(l_now.hour.val);
          Serial.print(':');
          Debug::bcddigits(l_now.minute.val);
          Serial.print(':');
          Serial.print(l_now.second / 10);
          Serial.print(l_now.second % 10);
          Serial.print(' ');

          Serial.print(l_now.weekday.val);
          Serial.print(' ');
          Serial.print(l_now.uses_summertime);
          Serial.print(l_now.uses_backup_antenna);
          Serial.print(l_now.timezone_change_scheduled);
          Serial.println(l_now.leap_second_scheduled);

    }

    char blocking_read() {
        while (!Serial.available()) {}
        const char c = Serial.read();
        /*
        Serial.print('(');
        Serial.print(c);
        Serial.print(')');
        */
        return c;
    }

    uint8_t get_flag(bool &valid) {
        if (valid) {
            const char c = blocking_read();
            if (c == '0' || c == '1') {
                return c - '0';
            } else {
                valid = false;
            }
        }
        return 0;
    }

    uint8_t get_digit(bool &valid) {
        if (valid) {
            const char c = blocking_read();
            if (c >= '0' && c <= '9') {
                return c - '0';
            } else {
                valid = false;
            }
        }
        return 0;
    }

    void assert_char(bool &valid, const char c) {
        valid = valid && blocking_read() == c;
    }


    bool parse_simple(DCF77::time_data_t &now) {
        bool valid = true;

        now.year.digit.hi = get_digit(valid);
        now.year.digit.lo = get_digit(valid);
        assert_char(valid, '.');
        now.month.digit.hi = get_digit(valid);
        now.month.digit.lo = get_digit(valid);
        assert_char(valid, '.');
        now.day.digit.hi = get_digit(valid);
        now.day.digit.lo = get_digit(valid);
        assert_char(valid, ' ');

        now.hour.digit.hi = get_digit(valid);
        now.hour.digit.lo = get_digit(valid);
        assert_char(valid, ':');
        now.minute.digit.hi = get_digit(valid);
        now.minute.digit.lo = get_digit(valid);
        assert_char(valid, ':');
        now.second  = get_digit(valid) * 10;
        now.second += get_digit(valid);

        return valid;
    }

    bool parse_extended(DCF77::time_data_t &now) {
        bool valid = parse_simple(now);

        assert_char(valid, ' ');
        now.weekday.val = get_digit(valid);
        assert_char(valid, ' ');
        now.uses_summertime = get_flag(valid);
        now.uses_backup_antenna = get_flag(valid);;
        now.timezone_change_scheduled = get_flag(valid);
        now.leap_second_scheduled = get_flag(valid);

        return valid;
    }

    void parse(DCF77::time_data_t &now) {
        const char mode = blocking_read();

        bool valid = false;
        DCF77::time_data_t new_time;

        switch (mode) {
            case 's':
                valid = parse_simple(new_time);
                if (valid) {
                    DCF77_Encoder::autoset_timezone(new_time);
                    DCF77_Encoder::autoset_weekday(new_time);
                }
                break;
            case 'x':
                valid = parse_extended(new_time);
                break;
        }

        if (valid) {
            cli();
            now = new_time;
            sei();
        } else {
            Serial.println();
            Serial.println();
            Serial.println(F("To set target time use one of the following formats"));
            Serial.println();
            Serial.println(F("simple mode:"));
            Serial.println(F("sYY.MM.DD hh:mm.ss"));
            Serial.println();
            Serial.println(F("extended mode:"));
            Serial.println(F("x:YY.MM.DD hh:mm.ss w sbtl"));
            Serial.println();
        }
        Serial.println(F("current time setup (YY.MM.DD hh:mm.ss w sbtl)"));
        Serial.println(F("w = weekday, s = summertime, b = backup antenna, t = timzone change scheduled, l = leap second scheduled"));
        show(now);
    }
}

void setup() {
    timer_0::setup();
    timer_1::setup();
    timer_2::setup();

    Serial.begin(115200);

    // compare to http://www.dcf77logs.de/ViewLog.aspx?mode=special&file=06%20-%20Schaltsekunde.log
    // set time to a leap second - because I always wanted to "see" one
    // - without staying up at late at night:)
    now.year.val = 0x09;
    now.month.val = 0x01;
    now.day.val = 0x01;
    now.weekday.val = 4;
    now.hour.val = 0x0;
    now.minute.val = 0x52;
    now.second = 0;
    now.uses_summertime = 0;
    now.uses_backup_antenna = 0;
    now.timezone_change_scheduled = 0;
    now.leap_second_scheduled = 1;

    Serial.println(F("running"));
    Serial.println(F("output on pin D3"));
}


void loop() {
   Parser::parse(now);
}

43 Responses to DCF77 Generator

  1. ALberto says:

    I’m getting errors

    • Would you please be a little bit more specific? This is way to little information to do even the faintest analysis. What are you doing? How are you doing it? What errors do occur while you doing it? What would you have expected? In particular: which version of my code do you use? From this web page or from github? Which Version of Arduino do you use? And most important: what is the exact error text?

  2. ALberto says:

    Thank you very much for your prompt response.

    QUESTION: What would you have expected?

    ANSWER: Quite simple. My German radio controlled wrist-watch (RUHLA GARDE) can not update time and I’m trying to have a tool to keep using it ( I love that watch & hate CASIO).

    QUESTION: What are you doing? How are you doing it?

    ANSWER: I upoaded the library DCF77 and also loaded into the arduino board all the samples included, I had no errors compiling and the board sends data as spected ( as I have no antena no real data indeed but I asume all is okey). As my goal is to reset my watch I copied the text of dcf77 generator from you blog to a blank scketh.
    I have compilation error and I can go forward.

    QUESTION: In particular: which version of my code do you use? From this web page or from github?
    ANSWER: I downloaded, last night, the libraries from github https://github.com/udoklein/dcf77

    QUESTION:Which Version of Arduino do you use?
    ANSWER: Software 1.6.4 Hardware arduino UNO

    QUESTION:What errors do occur while you doing it?
    Error compiling.

    QUESTION: And most important: what is the exact error text?
    ANSWER: this is what I see

    C:\APS\Arduino\hardware\tools\avr/bin/avr-g++ -c -g -Os -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -MMD -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10604 -DARDUINO_AVR_UNO -DARDUINO_ARCH_AVR -IC:\APS\Arduino\hardware\arduino\avr\cores\arduino -IC:\APS\Arduino\hardware\arduino\avr\variants\standard C:\Users\Alberto\AppData\Local\Temp\build1716201695887879411.tmp\dcf77_generator.cpp -o C:\Users\Alberto\AppData\Local\Temp\build1716201695887879411.tmp\dcf77_generator.cpp.o
    dcf77_generator.ino: In function ‘void DCF77_Encoder::advance_minute(DCF77::time_data_t&)’:
    dcf77_generator.ino:1127:1: internal compiler error: Segmentation fault libbacktrace could not find executable to open Please submit a full bug report, with preprocessed source if appropriate.
    See for instructions. Error compiling.

  3. ALberto says:

    Thanks a lot.

  4. ALberto says:

    C O N G R A T U L A T I O N S
    You were right, I used an older version & YOUR software was load to Arduino ONE,
    You were right AONE sucks,
    It is OK for me.
    Again THANKS A LOT

  5. Alberto says:

    Yes, I live in Madrid. I bought Arduino last monday for my kids. On friday i googled arduino + dcf77 that pointed to your blog. I ordered last night an antena to keep digging on this. By the way which software versión are you using to compile te code?

  6. ALberto says:

    halo
    I am not a programer so I beg your pardon for my following comments to your code.

    I found that sentences like this one Serial.print(F(” “)) generates alerts and like this Serial.print(” “), compilation was OK. I changed all matches within the code.

    I also modified
    from Serial.println(F(“sYY.MM.DD hh:mm.ss”)); to Serial.println(F(“s:YY.MM.DD hh:mm:ss”));
    and works better.

  7. Thanks for the effort but I am sorry this was a waste of time. Using stringgs (like you propose) instead of progmem strings (like I did) increases the memory footprint unnecessarily. If you want to help and provide some real value go to the forum, discuss with the members and figure out how to get rid of the warnings while still keeping progmem strings. Once you find out this one publish it to the world. This will benefit quite some people and in the course of doing so you might learn a lot of interestnig stuff.

  8. viamar says:

    Hi,
    I cannot compile the sketch due an error.
    Arduino IDE 1.6.0 -> 1.6.4 I get
    ———————————
    DCF77_Gen.ino: In function ‘void DCF77_Encoder::advance_minute(DCF77::time_data_t&)’:
    DCF77_Gen.ino:1129:1: internal compiler error: Segmentation fault
    libbacktrace could not find executable to open
    Please submit a full bug report,
    with preprocessed source if appropriate.
    See for instructions.
    Error compiling.
    ————————————–
    (the code is correct as don’t has in any place …time_data_t&…)

    Arduino IDE 1.6.6 development

    ————————————
    DCF77_Gen:257: error: ‘namespaceArithmetic_Tools’ does not name a type
    if (value.digit.hi < 9) {
    ^
    DCF77_Gen:268: error: 'namespaceParser' does not name a type
    bcd_t result;
    ^
    exit status 1
    'namespaceArithmetic_Tools' does not name a type

    ——————————————-

    Can you help me please?

    Thanks, Vincenzo, Rome

    • This is an issue with YOUR compiler. You need to get a version with a correct compiler. First thing would be to determine the compiler version and to contact the maintainers of the compiler. avr-gcc -v should give you the version.

  9. viamar says:

    Thanks for the prompt answer. The gcc I use is the one embedded in the arduino distribution. When I try various arduino distro I use the gcc distributed with that version (the 1.6 distribution uses gcc 4.8.1). In any case I have resolved using the release 1.5 of arduino (gcc 4.4.1), with this version the compilation of your program works flawlessy.
    Thanks and compliments for your work!
    Vincenzo

  10. hryamzik says:

    Looks like this code sends time just on serial reads, so here’s the very basic processing app that sends time every second: gist.github.com/hryamzik/c73c8026860732075897. However I still can’t get my clock set up, looks like I’ll have to tune frequency.

    • No, it gets the time through the serial interface. Then it will start to transmit. If you have an Uno (Arduino with resonator) the issue might be that your frequency is not accurate enough.

      • hryamzik says:

        Wow, thanks for immediate reply! How can I get time printed on each transmission? I fact I use arduino pro mini, it could have the same issues with uno. Wha’t the correct way to tune transmission frequency?

      • See “Hardware incompatibilities” on this page: https://blog.blinkenlight.net/experiments/dcf77/dcf77-library/.

        If you want to print, add suitable print statements. However I can not recommend to print at all because this might mess with the interrupt timing. Why would you want to print at all?

      • hryamzik says:

        If time drifts it’s easy to track with console. You don’t seem to like the idea of tuning the resonator based boards so I’ll have to dig into your code myself. Anyway, thanks a lot for this post, it’s really a good starting point and I’ve discovered that pro mini and pro micro boards use ceramic resonators, they could cause issues with v-usb library as well!

  11. hryamzik says:

    @Udo
    I’ve checked real frequency of my arduino and it looks pretty accurate (16013136,1093217). I’ve even tuned “nominator” and “denominator” values a bit. Then I’ve tried to enable signal search on my clock with arduino being off and on and the behaviour is different, when I try to transmit time clock shows different (higher) value of the signal strength.

    So is there any chance of a protocol mismatch?

    According to this video (https://www.youtube.com/watch?v=PxZE15SxwLI) there’s some validation process, there’re some more details here (http://endorphino.de/projects/electronics/timemanipulator/index_en.html). So this version of generator sends 6 (!) code words and it looks different from your code if I didn’t miss something. Do you have any thoughts about that?

    P.S.: Unfortunately code is MCPU specific and doesn’t run on arduino pro micro that comes with quartz crystal. Of course I did order crystals for my pro mini but it could take up to 9 months to deliver…

    • 16013136 is 821 ppm away from the target frequency. This is not “pretty accurate”. It translates to a frequency error of 64 Hz @ 77.5 kHz. Depending on the receiver this is more than enough of the mark to make it fail. Get an Arduino (clone) with a crystal.

      • hryamzik says:

        Indeed, so I’ve set nominator to 12034 and denominator to 19375. Not sure if it works good with that large numbers. Frequency drag seem to be unstable (affected by temperature?) so I test it before launch at once it ended up with nominator=19 and denominator=31. Clock behaviour is always the same, they show high signal strength and do not set up.
        And I did order a crystal based arduino as well but it will take time to arrive.

      • As I said you have a resonator. Resonators have poor stability. Temperature is one of the issues but it is completely pointless to try to stabilize a resonator. As I said: get a crystal.

      • hryamzik says:

        By the way, how did you get 64KHz?
        16013136 ÷ ( ( 206×31 + 14 ) ÷ 31 ) = 77563,6275

      • Correct. I rounded 63,6275 to 64. I wrote Hz, YOU wrote kHz.

  12. hryamzik says:

    Let me ask you one more question before I’ll hibernate till the new hardware delivery. =)
    As I have a pro micro board with a crystal I tried to adopt code for it. 32u4 has no timer 2 but it has timer 3 instead. I tried to rewrite code to use timer 3 but led connected to pin 3 doesn’t blink at all. Is there anything special about timer 2 at this point?

    • My standard answer: have a look into the datasheet. Besides that the line ” DDRD |= 1<<3;" looks somewhat fishy to me.

      • hryamzik says:

        Indeed, thanks! I’ve finally moved to timer 4 and OC4D (updated gist). It still doesn’t work but I might have misconfigured with fast PWM mode, will try to get more understanding of these settings.

  13. joop says:

    Hi,
    i’m quite new in the Arduino-stuff’but i would like to use the DCF77-generator so i’m able to test me own dcf77-software. Especially to be able to set the status-bits. However when i try to compile the skect i get this errors:

    Arduino: 1.6.6 (Windows 7), Board:”Arduino/Genuino Uno”
    DCF77_generator:257: error: ‘namespaceArithmetic_Tools’ does not name a type
    if (value.digit.hi < 9) {
    DCF77_generator:268: error: 'namespaceParser' does not name a type
    bcd_t result;
    exit status 1
    'namespaceArithmetic_Tools' does not name a type

    I don't know what's wrong or how to solve this… I use version 1.6.6

    best regards,

    Joop

  14. joop says:

    Hi Udo,
    thanks for the fast response!
    I use the library i found at github: https://github.com/udoklein/dcf77

    regards,
    Joop

  15. joop says:

    Hi Udo,
    and later i found ‘dcf77-3.0.0.zip’ and installed this zip-file into my Arduino-suite.
    But i get the same error-warnings…

    regards,
    Joop

    • Hi Joop, sorry for asking about the library version. Usually the answer is to get the newest version. However today I looked into the code of the generator and it has no dependencies to the library. Since it compiles for some people (including me) this implies that you have a compiler issue. You need a different version of Arduino either older or newer. Maybe version 1.5 as recommended by hryamzik might help. The first question is now: which compiler and OS version are you using?
      Regards, Udo

      • joop says:

        Hi Hryamzik and Udo,

        i installed an old version (1.5) and YES; it compiled!
        Thanks a lot!
        Best regards,
        Joop

  16. Pires says:

    Perfect at the first try!
    Only a minor problem: I had to program the time with +1 min offset to set it correctly (tried in 2 different clocks).
    Example: programmed time 20:05:00, arduino reset at 20:04:00, synchro at 20:07:00 with the correct time.
    Regards,
    Pires

    • Hi Pires, this is actually a feature. It follows from the way the DCF77 protocoll works. You set the time you want to transmit and the generator will emit it. The protocoll always describes the time for the NEXT minute. That is if you transmit e.g. 20:00 then you have to start transmission of 20:00 at 19:59. This is how PTB documented their signal and both the generator as well as my decoder library comply to this.

  17. hryamzik says:

    Did anybody succeeded with pro mini? I’ve finally received my crystal-based version and it didn’t set my clock. =(

  18. orimage says:

    Hi Udo!

    First of all I would like to emphasize that I am complete Arduino newbie.

    I got a beautiful big Mobatime clock which use IRIG-B, AFNOR or DCF-FSK code to set the time.

    On my Arduino Nano paired with U-blox GPS module I tried to program the IRIG-B code, but gave up because it seems that is too heavy for Arduino Nano which simply becomes too slow.

    Now I began to think about DCF-FSK and so I came to this blog.

    So, my question is, can I use at all your DCF generator to start my clock? The clock doesn’t have any antenna but cable plugs ZEITCODE A | B.

    Please help!

    Best regards,

    Orest

    • How should I know without a datasheet, manual or schematic? Unless you provide more details I can not answer this. Why don’t you just try it out? There are not to many options for generating the signal, right?

      • orimage says:

        Thank you for your fast response!
        Manufacturer says: “DCF – Frequency Shift Keying (DCF-FSK) is a proprietary protocol to transmit DCF frames with FSK-modulation. Instead of the reduction of the amplitude by normal DCF transmission, the frequency of the carrier signal is decreased from 1.25kHz to 1kHz during the second mark. The content of the submitted frames is exactly the same as DCF.”
        Do I have to order an arduino with crystal, or can I just order a RTC to work with your programs?

        Regards, Orest

      • So you already have the details. It says “proprietary”. With other words it will not work with my generator as this uses the standard protocol. You could however use my generator to drive a frequency modulation circuit as the content generated by my generator is standard compatible.

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