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 a 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 / 44;  // 15% Amplitude ~ 1/44 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("xYY.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);
}

89 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

  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? .gist table { margin-bottom: 0; }
    //
    //  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.
    
    #define VCC_PIN       0
    
    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_4 {
        // 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 timer4 interrupts during setup
            TIMSK4 = 0;
     
            // enable OC4D pin 6 for output (27    (T0/OC4D/ADC10) PD7    Digital Pin 6 (PWM))
            DDRD |= (1 << 6);
     
            // 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.
     
     
            TCCR4A = 1<< WGM40 | 1<<WGM41 | // Fast PWM
                     1<<COM4D1;             // Clear OC4D on Compare Match
     
            TCCR4D = 1<<CS40 | // no Prescaler
                     1<<WGM41; // Fast PWM
     
            OCR4A = ticks_per_period - 1;  // period length
     
            OCR4D = pwm_full_carrier;  // duty cycle
     
            TIMSK4 = 1<<OCIE4A;  // enable match interrupts
        }
    }
     
    ISR(TIMER4_COMPA_vect) {
        static uint8_t accumulated_fractional_ticks = 0;
     
        accumulated_fractional_ticks += timer_4::nominator;
        if (accumulated_fractional_ticks < timer_4::denominator) {
            OCR4A = timer_4::ticks_per_period - 1;
        } else {
            OCR4A = timer_4::ticks_per_period;
            accumulated_fractional_ticks -= timer_4::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;
    
     
    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 modulate() {
        static uint8_t times_100ms = 0;
     
        if (times_100ms == 0) {
            // start of second
            if (times_100ms != stop_modulation_after_times_100ms) {
                OCR4D = timer_4::pwm_modulated_carrier;
            }
        }
     
        if (times_100ms == stop_modulation_after_times_100ms) {
            OCR4D = timer_4::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);
            // Serial.println(\"test\");
            Parser::show(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();
    }
    
    void setup() {
        timer_0::setup();
        timer_1::setup();
        timer_4::setup();
        
        pinMode(VCC_PIN, OUTPUT);
        digitalWrite(VCC_PIN, HIGH);
        
        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);
    }
    view raw dcf77-micro.ino.md hosted with ❤ by GitHub
    • 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

  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.

  19. tzok says:

    I’ve tried your code on Arduino Duemilanove clone and it does work, but the string format used for clock setting is wrong. It should be “xYY.MM.DD hh:mm:ss w sbtl” and not “x:YY.MM.DD hh:mm.ss w sbtl” as stated. Another problem is – when I update the time via USB/Serial every second it will never synchronize with my DCF77 alarm clock, if set it only once – the alarm clock will synchronize, but it will be 1 minute late (or, to be precise, 58 seconds late).

    • Thanks for pointing this one out. I will fix the parser’s documentation as soon as I am back from vacation. With regard to the setting of the time this tool is not intended for setting the time. It is intended for emulating the DCF77 signal. The point is that DCF77 will always transmit the time for the next minute. Hence if you assume my tool will set the time it will always be 1 minute late. With regard to the 2 seconds I do not know why this happens. As I said this is not for setting time. It is intended for debugging of the receivers. I see two possible options out of this for you: (1) account for your specific setup by setting the time 58 seconds earlier (2) the tool is open source, rewrite it to fit your needs.

  20. tzok says:

    Ok, I didn’t know about the 1 minute offset. I’ve got a DCF77 clock which has no option to set time manually. Usually it synchronizes without problem, but sometimes not. So I’ve decided to use your code to set it running after replacing a battery in it, without needing to wait for a “better weather”. Obviously I’ve added this 1 minute offset and it did what I wanted. Thank you!

  21. mourad BOUATE says:

    Please, can you help me with this:
    i want to create a signal dcf77 (to transmite it after ) using an arduino. In fact, i would have the time/date as input and as result, i would have at the end a signal dcf77 as output .I’d like a code on arduino how to generate binary dcf signal like (01010001110…….) (coding part) for my project.
    my objective is to built my own signal dcf77 with the same protocol . how can i do it??
    thank you.

  22. Well, you could use the code from this page as a starting point. It more or less does what you describe. I do not really get your question.

    • mourad BOUATE says:

      Hi
      i need to build a DCF77 time-code generator

      there are many projects and library codes for DECODING Dcf77 time code but i could not find any library for making it!!!

      any helping hand?

  23. It would be cool this simulator ported to ESP8266 and add NTP server

  24. spiessa says:

    Hi Udo,
    I would like the same as Oleg (a DCF77 transmitter on an ESP8266 sending the NTP signal). My DCF77 clock is in the basement and it isn’t always updated, and sometimes wrongly.
    I looked at your code, but it needs quite some background know-how about the DCF77 signaling to be understandable.

    • Hi Andreas,

      this is GPLed open source software. If you want it, feel free to implement it. If you have any specific questions or need ***some*** assistance in order to contribute I will help you. However I would like to point out that there is a much simpler route. Identify where the signal of your DCF77 clock is already demodulated and feed the synthesized (demodulated) signal directly there. Of course this requires some minor modifcation of your clock but no need to code anything.

      Another simple route would be to feed my synthesizer approach from the ESP module. This should even be in the range of a beginner.

  25. Yanneck & Jacob says:

    Moin Udo,
    Wir sind zwei Azubis zum Elektroniker für Geräte und Systeme im vierten Lehrjahr und sitzen nun vor deinem DCF Generator Code.
    Wir würden gerne die Zeitzeichen ohne Trägerfrequenz ausgeben. Leider ist unser Arduino können noch nicht ganz auf dem Level, um alle Zeilen nachvollziehen zu können.
    Könntest du uns da eventuell Hilfestellung geben, damit wir wie gesagt nur die Zeitzeichen ausgeben?

  26. Andrade says:

    Hello
    I’m an Arduino beginner. When synchronizing my Daewoo watch “0:55” appears with the Arduino UNO connected to the serial / usb port.
    what am I doing wrong? Which command line should I fix?
    Thank you

  27. oliverb123 says:

    Hi Udo.
    Have been meaning to incorporate your DCF generator into a project for some time now.
    Have just completed another DCF77 analyzer using a TFT screen (you may spot your Super Filter in there) http://www.brettoliver.org.uk/DCF77_ESP32_Analyzer/ESP32_DCF77_Analyzer.htm

    I was thinking it would be nice to watch your generated DCF77 signal as it was output to another device on the TFT analyzer screen. Would it be easy to add a couple of DCF77 digital outputs on some spare pins?

    This way I could use 1 for the DCF Analyzer and another if needed to plug directly into a test projects DCF77 input. This would save having to go via the projects aerial like I have to when testing commercial clocks.

    Regards. Brett

  28. oliverb123 says:

    Hi Udo.
    Were those links correct for your DCF77 Generator code?
    I want to output DCF77 code on digital outs as well as the RF on pin D3.
    Brett.

  29. oliverb123 says:

    Hi Udo. Have been looking at the DCF77 Generator code and DCF77_Encoder::get_current_signal(now)
    should give me the outputs I need?
    Values 1 per second as follows.
    long_tick = 3,
    short_tick = 2,
    undefined = 1,
    sync_mark = 0
    I presume the undefined is the Meteo Data?

  30. oliverb123 says:

    Hi Udo. Have been testing the 77KHz output from the DCF77 generator.
    With a 1K resistor from pin 3 to Gnd on the Arduino the DCF77 output is interfering with my DCF77 receiver 2 stories up in the loft.

    I have changed the 1K resistor to a 10K all is now fine.

    I built a quick DCF77 tester with an old aerial and receiver and a 3v battery and the signal now has a range of about 1M. With the 10K in place I can set time on DCF77 clocks by just sitting them near the wire off pin 3.

    Your diagram with an Arduino shows the wire from pin 3 via a 1K to gnd. I presume it is OK to use the Arduino Gnd?

    • GND is supposed to be the Arduino GND. Anything else is undefined relative to Arduino.
      I calculated that a 1k resistor is within legal limits in Germany. This does not imply that the range is limited to < 1m. Obviously this also depends on the strength of DCF77 at your location. Here it did only work if I wraped the wire around the target clock. But of course I am significantly closer to DCF77 than you are. So here the field strength is at least one order of magnitude higher than at your location.

  31. oliverb123 says:

    Hi Udo. I have my DCF generator and DCF analyzer setup and have been using it to get leap second detection working on my analyzer.
    Thanks to your generator I was able to get it working and it now captures the leap second and records it over serial and also on the TFT.

    I note you found no commercial clocks seem to handle leap seconds correctly. I wonder if they count all the 60 leap second warning bits on second 19 and only then add the leap second once they are all present including the extra bit on 60?

    I set your DCF generator back over an hour before the leap second is due and it was not producing the Leap Second warning bit from the hour before the Leap second is due.

    Brett.

  32. Albert says:

    Hi, I have been using the DCF generator as a local sender to use my DCF77-based clock at a place, where the DCF77 signal quality is insufficient. This project was really helpfull, as I could setup the sender in less than 10 minutes, thanks Udo!
    Recently, I setup an ESP32 to have the clock synced via NTP rather than DCF77 (but emulating the demodulated signal, ie. no local sender anymore). Please find the code at https://github.com/wakalixes/NTPtoDCF77 if you are interested.

  33. Salman Kouhi says:

    Hi there,
    We are a language institute located in Oman (Muscat) and have some radio clocks that do not work here. How can we make them work?

  34. IrishCoffee17 says:

    Hi Udo,
    great work, congratulations.
    Running your sketch (from this page) on an Arduino Nano, compiled nicely out of the box.

    Only issue: From my available 8 commercial clocks in the household only one single synchs to the generator, and this is an industrial HOPF from 1975. Yes, I was patient and waited endless minutes for good results. Frequency is 77,500 on the spot (after fine tuning carrier 14/31 to 20/31). I know that this HOPF ignores the parity bits and displays literally everything – garbage included. But – all 3 Parity bits are sent correctly, I checked them very thoroughly with different patterns. Bit 0 sends as 0, Bit 20 as 1, Bit 59 is the sync gap, all fine. It’s also not the carrier range. The Hopf gets it perfectly 3 meters away (with reduced serial resistor on D3), Tried all others from same distance down to a few centimeters. Absolutely no conflict with the original DCF77 signal. No leap second announced in Bit 19. On the oscilloscope the carrier envelope looks ok, and so do the 100/200ms ticks.

    So what else might be important? I’m really lost. Maybe you have a hint for me?

    Alex

    • Does your Nano feature a crystal or a resonator? If it is a resonator then maybe the signal is not spot on at 77.5 kHz and maybe the newer clocks have tighter frequency constraints?

  35. John Haine says:

    Hello Udo, I’ve contacted you previously about your DCF77_xtal program and on your advice actually decided to use the “other” DCF77 Arduino library as I have a very clean DCF77 signal from our GPS time source. This is just the on a serial link, with no modulated RF carrier. However for convenience in development it would be handy to have an emulated DCF77 signal, again without carrier, and your DCF77_generator should be ideal and we don’t need the carrier generation functionality. Is there a simple way to output a signal corresponding to the “envelope” of the modulated signal, I guess as it would be input to the modulator, please?

    As background the GPS time source and the clock controller are in a different building and doing development actually at the clock is rather difficult. So a separate Arduino generating a DCF77 pulse stream will be very handy.

    Thanks, John.

    • In function void modulate() the carrier gets modulated by setting OCR2B. If you add digitalWrite statements in addition to setting OCR2 then you would get a demodulated signal.

      • John Haine says:

        Thanks Udo. If i add a statement after each OCR2 assignment to set a pin high, should I then have another digitalWrite statement before the end of the function to set the pin low?

  36. No, in one branch you set it to high and in the other branch you set it to low.

  37. John Haine says:

    Great, that works. Many thanks again!

  38. raphik says:

    Hallo, Udo.

    Let me ask a rhetorical question. Is 31 cycles / 400 µs equivalent to 77500 Hz?

    In my opinion, it can be both true and false. For this to be true, it is necessary for the period of each and every one of the 31 cycles to remain constant.

    Alternating cycles of different periods should generate a variable frequency wave, not a wave with a constant frequency equivalent to its weighted arithmetic mean. Such a trick can fool a frequency meter, but not a tuner.

    If your code is capable of setting your clocks, it is not because it generates a reasonable good 77.5 kHz carrier. To me, it stands to reason that your clocks are not selective enough to reject the ~77.6 kHz and ~77.2 kHz frequencies generated by your code.

    Sorry for being critical to your work.

    Best regards,

    Raphik.

    • Well, my code does not generate a 77.5 kHz sine carrier. However it generates a 77.5 kHz square wave with some jitter due to the Bresenham algorithm. Since the receivers go for a 77.5 kHz sine carrier this is good enough as the Fourier transform of my generated square wave has of course most of the energy in the 77.5 kHz component. The generated frequency is within the tolerance of the crystal (typicall 100 ppm or better) at 77.5 kHz. Lookup the code in ISR(TIMER2_COMPA_vect) and see how I manipulate OCR2A to achieve this.

      I also suggest that you verify my claim with a frequency analyzer.

      • raphik says:

        Before writing this post I first looked up your code in ISR (TIMER2_COMPA_vect) and understood what it does. This is exactly why it seems to me that your code does not generate a 77.5 kHz square carrier.

        What this function does is storing alternately the values ​​205 and 206 into the register OCR2A (17 times the low value and 14 times high value, repeating the high one at cycles 2, 11 and 22). For this reason, timer_2 will never generate one 77.5 kHz square wave. What Timer_2 really generates are two different frequencies every 206 and 207 clock ticks, which are equivalent to 77669.90 and 77294.68 Hz respectively. Not “some jitter” but “crazy long jumps” from one frequency to another. So such a desired, 77500 Hz frequency is just a mirage.

        This is what I find the function yields every 31 cycles:

        (Sorry, I cannot post a table here)

  39. raphik says:

    Thanks a lot for your answer. One is always glad to learn new things.

    At the present time I do not have enough knowledge to compute a Fourier transform. In fact, until you mentioned it, I didn’t even know such a thing existed.

    Anyway it seems to me that your Arduino code does not generate the same values ​​as the function sign(sin(2*pi*77500*t)).

    Before I comment further, please allow me finish a quick MatLab course I just started in order to learn about computing this Fourier thing.

    I’m back ASAP.

  40. Marco says:

    Dear Udo
    Congratulations for your skills and your code.

    I used the code and modify it to get it working with an NTP server, so that i can generate a DCF signal from Arduino Uno R3 and feed it to the clock in the basement.
    Of course I am less than beginner, so the code may look a bit “dirty”, mainly because I needed time-related events and I just learned that TIMSK0=0 breaks millis ().

    I added an Ethernet shield to connect to the NTP servers (I used 4 of them to avoid being kicked out)

    Now I’m struggling to make it work on an Arduino uno Wifi, since I learned that some of the instructions are ATmega 328-specific, while the Uno Wifi is ATmega 4809 based (i think this one has a different clock too).

    May you please point to some online resources that I can read or watch to make it easier to “translate” the time/registers commands? i.e. some explanations of the equivalent for 4809 of the instructions you use for the timers?

    Thank You

    Marco

    • The resource that I am using are the ATMEL Datasheets. In general I prefer the datasheets + application notes over the “typical online sources” because they will give you the full picture. Having the full picture usually makes stuff a lot easier to understand. Once you get the hang of datasheets they are even easier to grasp than all these explanatory videos and stuff. I suggest to go to ATMEL’s homepage and download the datasheets for ATMEGA 328 as well as 4809.

Leave a reply to John Haine Cancel reply

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