Second Decoder

Since my previous phase detection experiment allows me to locate the phase to ~1/100 second I can use this to create a very fine tuned detector.

The point is that I do not have the typical uncertainity about the pulse length and what it means. Since the clock is already locked to the phase I can just average in the two 100 ms windows at the start of each second to derive the bits. Any pulses in the trailing 800ms become completely irrelevant.

These two 100 ms windows are the time slots between the dotted lines in the picture below.

60% noise

60% noise

Unfortunately at higher noise levels the decoded bits may have a significant error rate. So the next intermediate goal is to find the start of the second. As I already outlined in the phase detection experiment we need a way to reduce the average amount of data that goes into the equation. Let’s have a look at the DCF77 encoding scheme.

DCF77 Code

DCF77 Code

In the clean signal there is always a “sync mark” at the last second of the minute. In addition there is a 0 at second 0 and a 1 at second 20. Attention: I did NOT say that the sync mark is at second 59. It is at the last second of the minute. This is very often the same but unfortunately not always. The issue is caused by leap seconds. My DCF77 introduction contains more details on this. We will have to deal with this in a later experiment. For now we will ignore it.

Also we have no clue where the minute starts we know that every 60 seconds we should see this pattern repeat somewhere in the signal train. Thus the idea is to have 60 “bins”. In each bin we collect evidence for the desired pattern. The bin with the highest score should be the searched for start of the signal pattern. With other words we now implement a phase detector for the signal train.

If you look at the way I score the different values you will notice that I score 6 points for a detected sync mark but only 1 point for a zero or one in the proper position. This is not by chance. This approximates the relative entropy of these different signals. The sync mark appears roughly 1 out of 60 in the clean signal. The zeros or ones appear roughly 1 out of 2 in the clean signal. Thus the information gain should be {-\log_2 (1/60)} \approx 6 for the sync mark and \approx {-\log_2 (1/2)} = 1 for the other data.

For a pretty clean signal this is a very robust approach. For a noisy signal I needed to add some means to ensure that bins that do not fit the pattern will collect scores as well. Hence I added the degrading scores for mismatches. As you might notice this is not 100% clean. In theory every sync mark that is seen should reduce the score of all other bins. In order to reduce CPU and RAM consumption I did not implement it that way. Otherwise I would have required at least two bytes per bin (to store the fractional part). And I would have required significantly more CPU cycles. Right now this is not an issue but I still need to add the stages for decoding the rest of the signal.

Instead I implemented this pragmatic distribution approach. If you analyze the limits that are introduced by the 1 byte per bin setup you will find that I will collect data of up to 1 hour to determine the seconds phase. So far this provided me with very good results. If you have such a weak signal that this is insufficient you need to extend the bin size to words (uint16_t) and set the upper limits accordingly. Alternatively you should get a significantly better receiver and even more important a better antenna.

So much for the theory – here is the code.

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


// signatures and types --> will go into a separate .h file later on
namespace Hamming {
    typedef struct {
        uint8_t lock_max;
        uint8_t noise_max;
    } lock_quality_t;
}

namespace DCF77 {
    const uint8_t long_tick  = 3;
    const uint8_t short_tick = 2;
    const uint8_t undefined  = 1;
    const uint8_t sync_mark  = 0;
    
    typedef struct {
        uint8_t second;      // 0..60
    } time_data;
}

namespace DCF77_Second_Decoder {
    void setup();
    void process_single_tick_data(const uint8_t tick_data);
    uint8_t get_second();
    void get_quality(Hamming::lock_quality_t &lock_quality);
    
    void debug();
}

namespace DCF77_Clock_Controller {
    void setup();
    void process_single_tick_data(const uint8_t tick_data);
    
    void flush();
    typedef void (*flush_handler)(const DCF77::time_data &decoded_time);
    
    void default_handler(const DCF77::time_data &decoded_time);
    void set_flush_handler(const flush_handler output_handler);
    
    typedef Hamming::lock_quality_t lock_quality_t;
    
    typedef struct {
        struct {
            uint32_t lock_max;
            uint32_t noise_max;
        } phase;
        
        lock_quality_t second;
    } clock_quality_t;
    
    void get_quality(clock_quality_t &clock_quality);
    
    // blocking, will unblock at the start of the second
    void get_current_time(DCF77::time_data &now);
}

namespace DCF77_Demodulator {
    void setup();
    void detector(const uint8_t sampled_data);
    void get_quality(uint32_t &lock_max, uint32_t &noise_max);
    void get_noise_indicator(uint32_t &noise_indicator);
    
    void debug();
}


// library implementation --> will go into a separate .cpp file later on
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; }
    }
}

namespace Hamming {
    template <typename bins_t>
    void advance_tick(bins_t &bins) {
        const uint8_t number_of_bins = sizeof(bins.data) / sizeof(bins.data[0]);
        if (bins.tick < number_of_bins - 1) {
            ++bins.tick;
        } else {
            bins.tick = 0;
        }
    }
    
    template <typename bins_t>
    void compute_max_index(bins_t &bins) {
        const uint8_t number_of_bins = sizeof(bins.data) / sizeof(bins.data[0]);
        
        bins.noise_max = 0;
        bins.max = 0;
        bins.max_index = 255;
        for (uint8_t index = 0; index < number_of_bins; ++index) {
            const uint8_t bin_data = bins.data[index];
            
            if (bin_data >= bins.max) {
                bins.noise_max = bins.max;
                bins.max = bin_data;
                bins.max_index = index;
            } else if (bin_data > bins.noise_max) {
                bins.noise_max = bin_data;
            }
        }
    }
    
    template <typename bins_t>
    void setup(bins_t &bins) {
        const uint8_t number_of_bins = sizeof(bins.data) / sizeof(bins.data[0]);
        
        for (uint8_t index = 0; index < number_of_bins; ++index) {
            bins.data[index] = 0;
        }
        bins.tick = 0;
        
        bins.max = 0;
        bins.max_index = 255;
        bins.noise_max = 0;
    }
    
    template <typename bins_t>
    uint8_t get_time_value(const bins_t &bins) {
        // there is a trade off involved here:
        //    low threshold --> lock will be detected earlier
        //    low threshold --> if lock is not clean output will be garbled
        //    a proper lock will fix the issue
        //    the question is: which start up behaviour do we prefer?
        const uint8_t threshold = 2;
        
        const uint8_t number_of_bins = sizeof(bins.data) / sizeof(bins.data[0]);
        
        if (bins.max-bins.noise_max >= threshold) {
            return (bins.max_index + bins.tick + 1) % number_of_bins;
        } else {
            const uint8_t undefined = 0xff;
            return undefined;
        }
    }
    
    template <typename bins_t>
    void get_quality(const bins_t bins, Hamming::lock_quality_t &lock_quality) {
        lock_quality.lock_max = bins.max;
        lock_quality.noise_max = bins.noise_max;
    }
    
    template <typename bins_t>
    void debug (const bins_t &bins) {
        const uint8_t number_of_bins = sizeof(bins.data) / sizeof(bins.data[0]);
        const bool uses_integrals = sizeof(bins.max) == 4;
        
        Serial.print(get_time_value(bins), HEX);
        Serial.print(F(" Tick: "));
        Serial.print(bins.tick);
        Serial.print(F(" Quality: "));
        Serial.print(bins.max, DEC);
        Serial.print('-');
        Serial.print(bins.noise_max, DEC);
        Serial.print(F(" Max Index: "));
        Serial.print(bins.max_index, DEC);
        Serial.print('>');
        
        for (uint8_t index = 0; index < number_of_bins; ++index) {
            if (index == bins.max_index ||
                (!uses_integrals && index == (bins.max_index+1) % number_of_bins) ||
                (uses_integrals && (index == (bins.max_index+10) % number_of_bins ||
                (index == (bins.max_index+20) % number_of_bins)))) {
                Serial.print('|');
                }
                Serial.print(bins.data[index],HEX);
        }
        Serial.println();
    }
}

namespace DCF77_Second_Decoder {
    using namespace DCF77;
    
    const uint8_t seconds_per_minute = 60;
    // this is a trick threshold
    //    lower it to get a faster second lock
    //    but then risk to garble the successive stages during startup
    //    --> to low and total startup time will increase
    const uint8_t lock_threshold = 12;
    
    typedef struct {
        uint8_t data[seconds_per_minute];
        uint8_t tick;
        
        uint8_t noise_max;
        uint8_t max;
        uint8_t max_index;
    } sync_bins;
    
    sync_bins bins;
    
    void sync_mark_binning(const uint8_t tick_data) {
        // We use a binning approach to find out the proper phase.
        // The goal is to localize the sync_mark. Due to noise
        // there may be wrong marks of course. The idea is to not
        // only look at the statistics of the marks but to exploit
        // additional data properties:
        
        // Bit position  0 after a proper sync is a 0.
        // Bit position 20 after a proper sync is a 1.
        
        // The binning will work as follows:
        
        //   1) A sync mark will score +6 points for the current bin
        //      it will also score -2 points for the previous bin
        //                         -2 points for the following bin
        //                     and -2 points 20 bins later
        //  In total this will ensure that a completely lost signal
        //  will not alter the buffer state (on average)
        
        //   2) A 0 will score +1 point for the previous bin
        //      it also scores -2 point 20 bins back
        //                 and -2 points for the current bin
        
        //   3) A 1 will score +1 point 20 bins back
        //      it will also score -2 point for the previous bin
        //                     and -2 points for the current bin
        
        //   4) An undefined value will score -2 point for the current bin
        //                                    -2 point for the previous bin
        //                                    -2 point 20 bins back
        
        //   5) Scores have an upper limit of 255 and a lower limit of 0.
        
        // Summary: sync mark earns 6 points, a 0 in position 0 and a 1 in position 20 earn 1 bonus point
        //          anything that allows to infer that any of the "connected" positions is not a sync will remove 2 points
        
        // It follows that the score of a sync mark (during good reception)
        // may move up/down the whole scale in slightly below 64 minutes.
        // If the receiver should glitch for whatever reason this implies
        // that the clock will take about 33 minutes to recover the proper
        // phase (during phases of good reception). During bad reception things
        // are more tricky.
        using namespace Arithmetic_Tools;
        
        const uint8_t previous_tick = bins.tick>0? bins.tick-1: seconds_per_minute-1;
        const uint8_t previous_21_tick = bins.tick>20? bins.tick-21: bins.tick + seconds_per_minute-21;
        
        switch (tick_data) {
            case sync_mark:
                bounded_increment<6>(bins.data[bins.tick]);
                
                bounded_decrement<2>(bins.data[previous_tick]);
                bounded_decrement<2>(bins.data[previous_21_tick]);
                
                { const uint8_t next_tick = bins.tick< seconds_per_minute-1? bins.tick+1: 0;
                bounded_decrement<2>(bins.data[next_tick]); }
                break;
                
            case short_tick:
                bounded_increment<1>(bins.data[previous_tick]);
                
                bounded_decrement<2>(bins.data[bins.tick]);
                bounded_decrement<2>(bins.data[previous_21_tick]);
                break;
                
            case long_tick:
                bounded_increment<1>(bins.data[previous_21_tick]);
                
                bounded_decrement<2>(bins.data[bins.tick]);
                bounded_decrement<2>(bins.data[previous_tick]);
                break;
                
            case undefined:
            default:
                bounded_decrement<2>(bins.data[bins.tick]);
                bounded_decrement<2>(bins.data[previous_tick]);
                bounded_decrement<2>(bins.data[previous_21_tick]);
        }
        bins.tick = bins.tick<seconds_per_minute-1? bins.tick+1: 0;
        
        // determine sync lock
        if (bins.max - bins.noise_max <=lock_threshold ||
            get_second() == 3) {
            // after a lock is acquired this happens only once per minute and it is
            // reasonable cheap to process,
            //
            // that is: after we have a "lock" this will be processed whenever
            // the sync mark was detected
            
            Hamming::compute_max_index(bins);
            }
    }
    
    void get_quality(Hamming::lock_quality_t &lock_quality) {
        Hamming::get_quality(bins, lock_quality);
    }
    
    uint8_t get_second() {
        if (bins.max - bins.noise_max >= lock_threshold) {
            // at least one sync mark and a 0 and a 1 seen
            // the threshold is tricky:
            //   higher --> takes longer to acquire an initial lock, but higher probability of an accurate lock
            //
            //   lower  --> higher probability that the lock will oscillate at the beginning
            //              and thus spoil the downstream stages
            
            // we have to subtract 2 seconds
            //   1 because the seconds already advanced by 1 tick
            //   1 because the sync mark is not second 0 but second 59
            
            uint8_t second = 2*seconds_per_minute + bins.tick - 2 - bins.max_index;
            while (second >= seconds_per_minute) { second-= seconds_per_minute; }
            
            return second;
        } else {
            return 0xff;
        }
    }
    
    void process_single_tick_data(const uint8_t tick_data) {
        sync_mark_binning(tick_data);
    }
    
    void setup() {
        Hamming::setup(bins);
    }
    
    void debug() {
        static uint8_t prev_tick;
        
        if (prev_tick == bins.tick) {
            return;
        } else {
            prev_tick = bins.tick;
            
            Serial.print(F("second: "));
            Serial.print(get_second(), DEC);
            Serial.print(F(" Sync mark index "));
            Hamming::debug(bins);
            
            Serial.println();
        }
    }
}

namespace DCF77_Clock_Controller {
    flush_handler output_handler = 0;
    
    DCF77::time_data decoded_time;
    volatile bool second_toggle;
    
    void get_current_time(DCF77::time_data &now) {
        for (bool stopper = second_toggle; stopper == second_toggle; ) {
            // wait for second_toggle to toggle
            // that is wait for decoded time to be ready
        }
        now = decoded_time;
    }
    
    void flush() {
        // this is called at the end of each second / before the next second begins
        // it is most interesting to propagate this further at the end of a sync marks
        // it is also interesting to propagate this to reference clocks
        
        decoded_time.second = DCF77_Second_Decoder::get_second();
        second_toggle = !second_toggle;
        
        // decoded_time holds the value of the current second but
        // we are immediately before the start of the next
        // second --> add 1 second to decoded_time
        
        if (decoded_time.second < 60) {
            decoded_time.second = (decoded_time.second + 1) % 60;
        }
        
        if (output_handler) {
            output_handler(decoded_time);
        }
    }
    
    void set_flush_handler(const flush_handler new_output_handler) {
        output_handler = new_output_handler;
    }
    
    void get_quality(clock_quality_t &clock_quality) {
        DCF77_Demodulator::get_quality(clock_quality.phase.lock_max, clock_quality.phase.noise_max);
        DCF77_Second_Decoder::get_quality(clock_quality.second);
    }
    
    void setup() {
        DCF77_Second_Decoder::setup();
    }
    
    void process_single_tick_data(const uint8_t tick_data) {
        DCF77_Second_Decoder::process_single_tick_data(tick_data);
    }
}

namespace DCF77_Demodulator {
    using namespace DCF77;
    
    const uint8_t bin_count = 100;
    
    typedef struct {
        uint16_t data[bin_count];
        uint8_t tick;
        
        uint32_t noise_max;
        uint32_t max;
        uint8_t max_index;
    } phase_bins;
    
    phase_bins bins;
    
    const uint16_t samples_per_second = 1000;
    
    const uint16_t samples_per_bin = samples_per_second / bin_count;
    const uint16_t bins_per_10ms  = bin_count / 100;
    const uint16_t bins_per_50ms  =  5 * bins_per_10ms;
    const uint16_t bins_per_60ms  =  6 * bins_per_10ms;
    const uint16_t bins_per_100ms = 10 * bins_per_10ms;
    const uint16_t bins_per_200ms = 20 * bins_per_10ms;
    const uint16_t bins_per_500ms = 50 * bins_per_10ms;
    
    void setup() {
        Hamming::setup(bins);
        
        DCF77_Clock_Controller::setup();
    }
    
    void decode_220ms(const uint8_t input, const uint8_t bins_to_go) {
        // will be called for each bin during the "interesting" 220ms
        
        static uint8_t count = 0;
        static uint8_t decoded_data = 0;
        
        count += input;
        if (bins_to_go >= bins_per_100ms + bins_per_10ms) {
            if (bins_to_go == bins_per_100ms + bins_per_10ms) {
                decoded_data = count > bins_per_50ms? 2: 0;
                count = 0;
            }
        } else {
            if (bins_to_go == 0) {
                decoded_data += count > bins_per_50ms? 1: 0;
                count = 0;
                // pass control further
                // decoded_data: 3 --> 1
                //               2 --> 0,
                //               1 --> undefined,
                //               0 --> sync_mark
                DCF77_Clock_Controller::process_single_tick_data(decoded_data);
            }
        }
    }
    
    uint16_t wrap(const uint16_t value) {
        // faster modulo function which avoids division
        uint16_t result = value;
        while (result >= bin_count) {
            result-= bin_count;
        }
        return result;
    }
    
    void phase_detection() {
        // We will compute the integrals over 200ms.
        // The integrals is used to find the window of maximum signal strength.
        uint32_t integral = 0;
        
        for (uint16_t bin = 0; bin < bins_per_100ms; ++bin)  {
            integral += ((uint32_t)bins.data[bin])<<1;
        }
        
        for (uint16_t bin = bins_per_100ms; bin < bins_per_200ms; ++bin)  {
            integral += (uint32_t)bins.data[bin];
        }
        
        bins.max = 0;
        bins.max_index = 0;
        for (uint16_t bin = 0; bin < bin_count; ++bin) {
            if (integral > bins.max) {
                bins.max = integral;
                bins.max_index = bin;
            }
            
            integral -= (uint32_t)bins.data[bin]<<1;
            integral += (uint32_t)(bins.data[wrap(bin + bins_per_100ms)] + bins.data[wrap(bin + bins_per_200ms)]);
        }
        
        // max_index indicates the position of the 200ms second signal window.
        // Now how can we estimate the noise level? This is very tricky because
        // averaging has already happened to some extend.
        
        // The issue is that most of the undesired noise happens around the signal,
        // especially after high->low transitions. So as an approximation of the
        // noise I test with a phase shift of 200ms.
        bins.noise_max = 0;
        const uint16_t noise_index = wrap(bins.max_index + bins_per_200ms);
        
        for (uint16_t bin = 0; bin < bins_per_100ms; ++bin)  {
            bins.noise_max += ((uint32_t)bins.data[wrap(noise_index + bin)])<<1;
        }
        
        for (uint16_t bin = bins_per_100ms; bin < bins_per_200ms; ++bin)  {
            bins.noise_max += (uint32_t)bins.data[wrap(noise_index + bin)];
        }
    }
    
    uint8_t phase_binning(const uint8_t input) {
        // how many seconds may be cummulated
        // this controls how slow the filter may be to follow a phase drift
        // N times the clock precision shall be smaller 1
        // clock 30 ppm => N < 300
        const uint16_t N = 300;
        
        Hamming::advance_tick(bins);
        
        if (input) {
            if (bins.data[bins.tick] < N) {
                ++bins.data[bins.tick];
            }
        } else {
            if (bins.data[bins.tick] > 0) {
                --bins.data[bins.tick];
            }
        }
        return bins.tick;
    }
    
    void detector_stage_2(const uint8_t input) {
        const uint8_t current_bin = bins.tick;
        
        const uint8_t threshold = 30;
        
        if (bins.max-bins.noise_max < threshold ||
            wrap(bin_count + current_bin - bins.max_index) == 53) {
            // Phase detection far enough out of phase from anything that
            // might consume runtime otherwise.
            phase_detection();
            }
            
            static uint8_t bins_to_process = 0;
        
        if (bins_to_process == 0) {
            if (wrap((bin_count + current_bin - bins.max_index)) <= bins_per_100ms ||   // current_bin at most 100ms after phase_bin
                wrap((bin_count + bins.max_index - current_bin)) <= bins_per_10ms ) {   // current bin at most 10ms before phase_bin
                // if phase bin varies to much during one period we will always be screwed in may ways...
                
                // last 10ms of current second
                DCF77_Clock_Controller::flush();
            
            // start processing of bins
                bins_to_process = bins_per_200ms + 2*bins_per_10ms;
                }
        }
        
        if (bins_to_process > 0) {
            --bins_to_process;
            
            // this will be called for each bin in the "interesting" 220ms
            // this is also a good place for a "monitoring hook"
            decode_220ms(input, bins_to_process);
        }
    }
    
    void detector(const uint8_t sampled_data) {
        static uint8_t current_sample = 0;
        static uint8_t average = 0;
        
        // detector stage 0: average 10 samples (per bin)
        average += sampled_data;
        
        if (++current_sample >= samples_per_bin) {
            // once all samples for the current bin are captured the bin gets updated
            // that is each 10ms control is passed to stage 1
            const uint8_t input = (average> samples_per_bin/2);
            
            phase_binning(input);
            
            detector_stage_2(input);
            
            average = 0;
            current_sample = 0;
        }
    }
    
    void get_quality(uint32_t &lock_max, uint32_t &noise_max) {
        lock_max = bins.max;
        noise_max = bins.noise_max;
    }
    
    void debug() {
        Serial.print(F("Phase: "));
        Hamming::debug(bins);
    }
}


// main program starts here

const uint8_t dcf77_sample_pin = 19; // A5
const uint8_t dcf77_analog_sample_pin = 5;
const uint8_t dcf77_monitor_pin = 18;

const uint8_t first_output_pin = 5;


void process_one_sample() {
    // The Blinkenlight LEDes will cut off the input signal
    // below a logical high. This is due to the weak
    // output signal of the DCF module.
    // Hence for the Blinkenlighty
    // and the Blinkenlight shield we can not use
    // digitalRead.
    
    // Comment the line below if you are not using this code with a
    // Blinkenlighty or Blinkenlight shield.
    const uint8_t sampled_data = analogRead(dcf77_analog_sample_pin)>200? 1: 0;
    // Uncomment the line below if you are using this code with a standard Arduino
    //const uint8_t sampled_data = digitalRead(dcf77_sample_pin);
    
    digitalWrite(dcf77_monitor_pin, sampled_data);

    DCF77_Demodulator::detector(sampled_data);
}

ISR(TIMER2_COMPA_vect) {
    process_one_sample();
}

void initTimer2() {
    // Timer 2 CTC mode, prescaler 64
    TCCR2B = (0<<WGM22) | (1<<CS22);
    TCCR2A = (1<<WGM21) | (0<<WGM20);

    // 249 + 1 == 250 == 250 000 / 1000 =  (16 000 000 / 64) / 1000
    OCR2A = 249;

    // enable Timer 2 interrupts
    TIMSK2 = (1<<OCIE2A);
}

void stopTimer0() {
    // ensure that the standard timer interrupts will not
    // mess with msTimer2
    TIMSK0 = 0;
}

void output_handler(const DCF77::time_data &decoded_time) {
    uint8_t lo;
    uint8_t hi;
    uint8_t out;

    if (decoded_time.second < 60) {
        // bcd conversion
        hi = decoded_time.second / 10;
        lo = decoded_time.second - 10 * hi;
        out = (hi<<4) + lo;

        uint8_t pin = first_output_pin;
        for (uint8_t bits=0; bits<8; ++bits) {
            digitalWrite(pin++, out & 0x1);
            out >>= 1;
        }
    }
}

void setup() {
    Serial.begin(115200);
    Serial.println();

    pinMode(dcf77_monitor_pin, OUTPUT);

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

    for (uint8_t pin=first_output_pin; pin<first_output_pin+8; ++pin) {
        pinMode(pin, OUTPUT);
        digitalWrite(pin, HIGH);
    }

    stopTimer0();
    initTimer2();
    DCF77_Demodulator::setup();
    DCF77_Clock_Controller::set_flush_handler(&output_handler);
    
//    Serial.println(F("acquiring lock - please wait"));
}

void loop() {
/*
    DCF77::time_data now;
    
    DCF77_Clock_Controller::get_current_time(now);
    if (now.second <= 60) {
        Serial.println();
        Serial.print(F("Decoded time: "));
        Serial.println(now.second);
    }
*/

    //Serial.println();
    //DCF77_Demodulator::debug();
    DCF77_Second_Decoder::debug();
}

Before we go through the implementation details lets have a look at the overall structure of the code.

Basic Building Blocks

The basic architecture is still simple. Every millisecond timer 2 triggers its interupt handler. The handler will sample the DCF77 clock module output and feeds the result into the detector stage. This is a slight extension of the phase lock code. I added “stage 2” in order to decode the data bits. The output of this detector is then fed to the clock controller which passes the data on to the second decoder. At first glance the controller looks somewhat unnecessary. However in the future there will be more decoders (for the minutes, the hours, the months and so on). The task of the controller will be to coordinate these decoders. I introduce it already because the overall structure is still simple enough. Hopefully this makes the whole project easier to understand.

The implementation utilizes some techniques that are not to common in the Arduino community. First I make heavy use of namespaces. This is a simple means to structure your program. Especially it allows to have different variables and functions with virtually the same name. Actually the namespaces are part of the names thus the names only look the same. This is a way to structure programs into different parts that can be used even if you want to avoid libraries or object oriented techniques. Right now I do not want to use libraries such that everything will reside in one flat file (and can be copied and pasted into the IDE). I also do not want to use object oriented techniques because they provide no significant benefits in this specific context.

The other thing that I use are so called templates. Basically these allow to define special functions where you can pass parameters at compile time. These parameters may be numbers or even types. Thus you can have dedicated type specific functions. This allows me to reduce the amount of code. You may want to play around with my code to figure out how they work. I will make more use of them in the future experiments.

A final word about the Hamming and DCF77 namespaces. Right now these look somewhat strange. They are to small. However they will grow as I add more decoders. Then their use will become clear. For the time being just assume that these are places for some “utility” functions and reusable type definitions.

So how does this code perform in reality? Lets have a small look at the debug output while my tumbler is running. The tumbler generates enough undesirable noise such that none of my commercial DCF77 clocks can decode the signal.

As you can see it takes a some minutes until one of the bins is standing out. From then on the second decoder has locked to the phase. The neat part is that the underlying phase lock keeps the signal locked on the second level. So there is no need (but for the leap seconds) to control the phase. Just detect it and that’s it 🙂

second: 255 Sync mark index FF Tick: 1 Quality: 1-0 Max Index: 39>000000000000000000000000000000000000000|1|00000000000000000000

second: 255 Sync mark index FF Tick: 2 Quality: 1-1 Max Index: 40>0000000000000000000000000000000000000001|1|0000000000000000000

second: 255 Sync mark index FF Tick: 3 Quality: 1-1 Max Index: 41>00000000000000000000000000000000000000011|1|000000000000000000

second: 255 Sync mark index FF Tick: 4 Quality: 1-1 Max Index: 41>00100000000000000000000000000000000000011|1|000000000000000000

second: 255 Sync mark index A Tick: 5 Quality: 6-1 Max Index: 4>0010|6|0000000000000000000000000000000000111000000000000000000

second: 255 Sync mark index B Tick: 6 Quality: 4-1 Max Index: 4>0010|4|0000000000000000000000000000000000111000000000000000000

second: 255 Sync mark index C Tick: 7 Quality: 4-1 Max Index: 4>0010|4|0000000000000000000000000000000000111000100000000000000

second: 255 Sync mark index D Tick: 8 Quality: 4-1 Max Index: 4>0010|4|0100000000000000000000000000000000111000100000000000000

second: 255 Sync mark index E Tick: 9 Quality: 4-1 Max Index: 4>0010|4|0110000000000000000000000000000000111000100000000000000

second: 255 Sync mark index F Tick: 10 Quality: 4-1 Max Index: 4>0010|4|0111000000000000000000000000000000111000100000000000000

second: 255 Sync mark index 10 Tick: 11 Quality: 4-1 Max Index: 4>0010|4|0111100000000000000000000000000000111000100000000000000

second: 255 Sync mark index 11 Tick: 12 Quality: 4-1 Max Index: 4>0010|4|0111100000000000000000000000000000111000100001000000000

second: 255 Sync mark index 12 Tick: 13 Quality: 4-1 Max Index: 4>0010|4|0111101000000000000000000000000000111000100001000000000

second: 255 Sync mark index 13 Tick: 14 Quality: 4-1 Max Index: 4>0010|4|0111101000000000000000000000000000111000100001010000000

second: 255 Sync mark index 14 Tick: 15 Quality: 4-1 Max Index: 4>0010|4|0111101010000000000000000000000000111000100001010000000

second: 255 Sync mark index 15 Tick: 16 Quality: 4-1 Max Index: 4>0010|4|0111101011000000000000000000000000111000100001010000000

second: 255 Sync mark index 16 Tick: 17 Quality: 4-1 Max Index: 4>0010|4|0111101011000000000000000000000000111000100001010010000

second: 255 Sync mark index 17 Tick: 18 Quality: 4-1 Max Index: 4>0010|4|0111101011000000000000000000000000111000100001010011000

second: 255 Sync mark index 18 Tick: 19 Quality: 4-1 Max Index: 4>0010|4|0111101011001000000000000000000000111000100001010011000

second: 255 Sync mark index 19 Tick: 20 Quality: 4-1 Max Index: 4>0010|4|0111101011001000000000000000000000111000100001010011010

second: 255 Sync mark index 1A Tick: 21 Quality: 4-1 Max Index: 4>0010|4|0111101011001000000000000000000000111000100001010011011

second: 255 Sync mark index 1B Tick: 22 Quality: 4-1 Max Index: 4>0010|4|0111101011001001000000000000000000111000100001010011011

second: 255 Sync mark index 1C Tick: 23 Quality: 4-1 Max Index: 4>0010|4|0111101011001001100000000000000000111000100001010011011

second: 255 Sync mark index 1D Tick: 24 Quality: 4-2 Max Index: 4>0020|4|0111101011001001100000000000000000111000100001010011011

second: 255 Sync mark index 1E Tick: 25 Quality: 4-2 Max Index: 4>0020|4|0111101011001001101000000000000000111000100001010011011

second: 255 Sync mark index FF Tick: 26 Quality: 2-2 Max Index: 4>0020|2|0111101011001001101100000000000000111000100001010011011

second: 255 Sync mark index FF Tick: 27 Quality: 2-2 Max Index: 4>0020|2|1111101011001001101100000000000000111000100001010011011

second: 255 Sync mark index FF Tick: 28 Quality: 2-2 Max Index: 4>0020|2|1011101011001001101101000000000000111000100001010011011

second: 255 Sync mark index FF Tick: 29 Quality: 2-2 Max Index: 7>0020210|2|1101011001001101101000000000000111000100001010011011

second: 255 Sync mark index FF Tick: 30 Quality: 2-2 Max Index: 7>0020210|2|0101011001001101101010000000000111000100001010011011

second: 255 Sync mark index FF Tick: 31 Quality: 2-2 Max Index: 7>0020210|2|0001011001001101101011000000000111000100001010011011

second: 255 Sync mark index FF Tick: 32 Quality: 2-2 Max Index: 7>0020210|2|0011011001001101101011000000000111000100001010011011

second: 255 Sync mark index FF Tick: 33 Quality: 2-2 Max Index: 7>0020210|2|0010011001001101101011010000000111000100001010011011

second: 255 Sync mark index FF Tick: 34 Quality: 2-2 Max Index: 7>0020210|2|0010011001001101101011011000000111000100001010011011

second: 255 Sync mark index FF Tick: 35 Quality: 2-2 Max Index: 7>0020210|2|0010001001001101101011011100000111000100001010011011

second: 255 Sync mark index FF Tick: 36 Quality: 2-2 Max Index: 14>00202102001000|2|001001101101011011100000111000100001010011011

second: 255 Sync mark index E Tick: 37 Quality: 6-2 Max Index: 36>002021020010002001001101101011011100|6|00111000100001010011011

second: 255 Sync mark index F Tick: 38 Quality: 7-2 Max Index: 36>002021020010002001001101101011011100|7|00111000100001010011011

second: 255 Sync mark index 10 Tick: 39 Quality: 7-2 Max Index: 36>002021020010002002001101101011011100|7|00111000100001010011011

second: 255 Sync mark index 11 Tick: 40 Quality: 7-2 Max Index: 36>002021020010002002001101101011011100|7|01011000100001010011011

second: 255 Sync mark index 12 Tick: 41 Quality: 7-2 Max Index: 36>002021020010002002011101101011011100|7|01001000100001010011011

second: 255 Sync mark index 13 Tick: 42 Quality: 7-2 Max Index: 36>002021020010002002010101101011011100|7|01010000100001010011011

second: 255 Sync mark index 14 Tick: 43 Quality: 7-2 Max Index: 36>002021020010002002010201101011011100|7|01010000100001010011011

second: 255 Sync mark index 15 Tick: 44 Quality: 7-2 Max Index: 36>002021020010002002010211101011011100|7|01010000100001010011011

second: 255 Sync mark index 16 Tick: 45 Quality: 7-2 Max Index: 36>002021020010002002010212101011011100|7|01010000100001010011011

second: 255 Sync mark index 17 Tick: 46 Quality: 7-2 Max Index: 36>002021020010002002010212001011011100|7|01010001000001010011011

second: 255 Sync mark index 18 Tick: 47 Quality: 7-2 Max Index: 36>002021020010002002010212011011011100|7|01010001000001010011011

second: 255 Sync mark index 19 Tick: 48 Quality: 7-2 Max Index: 36>002021020010002002010212010011011100|7|01010001010001010011011

second: 255 Sync mark index 1A Tick: 49 Quality: 7-2 Max Index: 36>002021020010002002010212010011011100|7|01010001011001010011011

second: 255 Sync mark index 1B Tick: 50 Quality: 7-2 Max Index: 36>002021020010002002010212010001011100|7|01010001011101010011011

second: 255 Sync mark index 1C Tick: 51 Quality: 7-2 Max Index: 36>002021020010002002010212010002011100|7|01010001011100010011011

second: 255 Sync mark index 1D Tick: 52 Quality: 7-2 Max Index: 36>002021020010002002010212010002011100|7|01010001011101010011011

second: 255 Sync mark index 1E Tick: 53 Quality: 7-2 Max Index: 36>002021020010002002010212010002001100|7|01010001011101100011011

second: 255 Sync mark index 1F Tick: 54 Quality: 7-2 Max Index: 36>002021020010002002010212010002000100|7|01010001011101110011011

second: 255 Sync mark index 20 Tick: 55 Quality: 7-2 Max Index: 36>002021020010002002010212010002000000|7|01010001011101111011011

second: 255 Sync mark index 21 Tick: 56 Quality: 7-2 Max Index: 36>002021020010002002010212010002000010|7|01010001011101111001011

second: 255 Sync mark index 22 Tick: 57 Quality: 7-2 Max Index: 36>002021020010002002010212010002000010|7|01010001011101111010011

second: 255 Sync mark index 23 Tick: 58 Quality: 8-2 Max Index: 36>002021020010002002010212010002000010|8|01010001011101111010011

second: 255 Sync mark index 24 Tick: 59 Quality: 8-2 Max Index: 36>002021020010002002010212010002000010|8|01010001011101111010101

second: 255 Sync mark index 25 Tick: 0 Quality: 8-2 Max Index: 36>002021020010002002010212010002000010|8|00010001011101111010110

second: 255 Sync mark index 26 Tick: 1 Quality: 8-2 Max Index: 36>002021020010002002010212010002000010|8|00010001011101111010111

second: 255 Sync mark index 27 Tick: 2 Quality: 8-2 Max Index: 36>102021020010002002010212010002000010|8|00000001011101111010111

second: 255 Sync mark index 28 Tick: 3 Quality: 8-2 Max Index: 36>110021020010002002010212010002000010|8|00000001011101111010111

second: 255 Sync mark index 29 Tick: 4 Quality: 8-2 Max Index: 36>110021020010002002010212010002000010|8|00000101011101111010111

second: 255 Sync mark index 2A Tick: 5 Quality: 8-2 Max Index: 36>110101020010002002010212010002000010|8|00000101011101111010111

second: 255 Sync mark index 2B Tick: 6 Quality: 8-2 Max Index: 36>110100020010002002010212010002000010|8|00000102011101111010111

second: 255 Sync mark index 2C Tick: 7 Quality: 8-2 Max Index: 36>110100020010002002010212010002000010|8|00000102111101111010111

second: 255 Sync mark index 2D Tick: 8 Quality: 8-2 Max Index: 36>110100100010002002010212010002000010|8|00000102101101111010111

second: 255 Sync mark index 2E Tick: 9 Quality: 8-2 Max Index: 36>110100110010002002010212010002000010|8|00000102100101111010111

second: 255 Sync mark index 2F Tick: 10 Quality: 8-2 Max Index: 36>110100111010002002010212010002000010|8|00000102100001111010111

second: 255 Sync mark index 30 Tick: 11 Quality: 8-2 Max Index: 36>110100111100002002010212010002000010|8|00000102100001111010111

second: 255 Sync mark index 31 Tick: 12 Quality: 8-2 Max Index: 36>110100111100002002010212010002000010|8|00000102100002111010111

second: 255 Sync mark index 32 Tick: 13 Quality: 8-2 Max Index: 36>110100111101002002010212010002000010|8|00000102100002011010111

second: 255 Sync mark index 33 Tick: 14 Quality: 8-2 Max Index: 36>110100111101002002010212010002000010|8|00000102100002021010111

second: 255 Sync mark index 34 Tick: 15 Quality: 8-2 Max Index: 36>110100111101010002010212010002000010|8|00000102100002020010111

second: 255 Sync mark index 35 Tick: 16 Quality: 8-2 Max Index: 36>110100111101011002010212010002000010|8|00000102100002020010111

second: 255 Sync mark index 36 Tick: 17 Quality: 8-2 Max Index: 36>110100111101011002010212010002000010|8|00000102100002020020111

second: 255 Sync mark index 37 Tick: 18 Quality: 8-2 Max Index: 36>110100111101011000010212010002000010|8|00000102100002020021111

second: 255 Sync mark index 38 Tick: 19 Quality: 8-2 Max Index: 36>110100111101011001010212010002000010|8|00000102100002020021011

second: 255 Sync mark index 39 Tick: 20 Quality: 8-2 Max Index: 36>110100111101011001000212010002000010|8|00000102100002020021021

second: 255 Sync mark index 3A Tick: 21 Quality: 8-2 Max Index: 36>110100111101011001000212010002000010|8|00000102100002020021022

second: 255 Sync mark index 3B Tick: 22 Quality: 8-2 Max Index: 36>010100111101011001001012010002000010|8|00000102100002020021022

second: 255 Sync mark index 0 Tick: 23 Quality: 8-2 Max Index: 36>000100111101011001001102010002000010|8|00000102100002020021022

second: 255 Sync mark index 1 Tick: 24 Quality: 8-2 Max Index: 36>001100111101011001001100010002000010|8|00000102100002020021022

second: 255 Sync mark index 2 Tick: 25 Quality: 8-2 Max Index: 36>001000111101011001001101010002000010|8|00000102100002020021022

second: 255 Sync mark index 3 Tick: 26 Quality: 8-2 Max Index: 36>001000111101011001001101100002000010|8|00000102100002020021022

second: 255 Sync mark index 4 Tick: 27 Quality: 8-2 Max Index: 36>001001111101011001001101100002000010|8|00000102100002020021022

second: 255 Sync mark index 5 Tick: 28 Quality: 8-2 Max Index: 36>001001011101011001001101101002000010|8|00000102100002020021022

second: 255 Sync mark index 6 Tick: 29 Quality: 8-2 Max Index: 36>001001021101011001001101101002000010|8|00000102100002020021022

second: 255 Sync mark index 7 Tick: 30 Quality: 8-2 Max Index: 36>001001020101011001001101101010000010|8|00000102100002020021022

second: 255 Sync mark index 8 Tick: 31 Quality: 8-2 Max Index: 36>001001020201011001001101101010000010|8|00000102100002020021022

second: 255 Sync mark index 9 Tick: 32 Quality: 8-2 Max Index: 36>001001020211011001001101101010000010|8|00000102100002020021022

second: 255 Sync mark index A Tick: 33 Quality: 8-2 Max Index: 36>001001020210011001001101101010010010|8|00000102100002020021022

second: 255 Sync mark index B Tick: 34 Quality: 8-2 Max Index: 36>001001020210011001001101101010011010|8|00000102100002020021022

second: 255 Sync mark index C Tick: 35 Quality: 8-2 Max Index: 36>001001020210001001001101101010011100|8|00000102100002020021022

second: 255 Sync mark index D Tick: 36 Quality: 8-2 Max Index: 36>001001020210002001001101101010011100|8|00000102100002020021022

second: 59 Sync mark index E Tick: 37 Quality: 14-2 Max Index: 36>001001020210002001001101101010011100|E|00000102100002020021022

second: 0 Sync mark index F Tick: 38 Quality: 15-2 Max Index: 36>001001020210002001001101101010011100|F|00000102100002020021022

second: 1 Sync mark index 10 Tick: 39 Quality: 15-2 Max Index: 36>001001020210002002001101101010011100|F|00000102100002020021022

second: 2 Sync mark index 11 Tick: 40 Quality: 15-2 Max Index: 36>001001020210002002101101101010011100|F|00000102100002020021022

second: 3 Sync mark index 12 Tick: 41 Quality: 15-2 Max Index: 36>001001020210002002101101101010011100|F|00100102100002020021022

second: 4 Sync mark index 13 Tick: 42 Quality: 15-2 Max Index: 36>001001020210002002100101101010011100|F|00110102100002020021022

second: 5 Sync mark index 14 Tick: 43 Quality: 15-2 Max Index: 36>001001020210002002100001101010011100|F|00111002100002020021022

second: 6 Sync mark index 15 Tick: 44 Quality: 15-2 Max Index: 36>001001020210002002100011101010011100|F|00111002100002020021022

second: 7 Sync mark index 16 Tick: 45 Quality: 15-2 Max Index: 36>001001020210002002100010101010011100|F|00111010100002020021022

second: 8 Sync mark index 17 Tick: 46 Quality: 15-2 Max Index: 36>001001020210002002100010201010011100|F|00111010000002020021022

second: 9 Sync mark index 18 Tick: 47 Quality: 15-2 Max Index: 36>001001020210002002100010211010011100|F|00111010000002020021022

second: 10 Sync mark index 19 Tick: 48 Quality: 15-2 Max Index: 36>001001020210002002100010210010011100|F|00111010010002020021022

second: 11 Sync mark index 1A Tick: 49 Quality: 15-2 Max Index: 36>001001020210002002100010210010011100|F|00111010011002020021022

second: 12 Sync mark index 1B Tick: 50 Quality: 15-2 Max Index: 36>001001020210002002100010210020011100|F|00111010011002020021022

second: 13 Sync mark index 1C Tick: 51 Quality: 15-2 Max Index: 36>001001020210002002100010210020011100|F|00111010011010020021022

second: 14 Sync mark index 1D Tick: 52 Quality: 15-2 Max Index: 36>001001020210002002100010210020011100|F|00111010011011020021022

second: 15 Sync mark index 1E Tick: 53 Quality: 15-2 Max Index: 36>001001020210002002100010210020001100|F|00111010011011100021022

I used this to implement a binary (BCD coded) second display. Have a look at the video and notice how the signal fluctuates due to the tumbler. Still the clock works like a charm. At 1:21 I switch on two fluorescent lights that were positioned 20 cm away from the antenna. As you can see the receiver completely loses the signal. Still the clock keeps on running. It was running for ~30 minutes before and it was completely phase locked (“Max Index” close to the upper limit). Thus it would take more than 10 minutes to get it out of phase.

2 Responses to Second Decoder

  1. Mary says:

    Hey, on occasion I see a 500 server message when I browse your website. I thought you may wish to know, best wishes

Leave a comment

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