Crystal Frequency Compensation

As I demonstrated in several experiments on Crystal Deviation the Arduino’s crytal does not run at exactly 16 Mhz. Usually it is of by something around 20-30 ppm (or 320-480 Hz). This is not a lot but it has implications for my library.

In the artical on phase detection I noticed that clock deviation is equivalent to smoothing the filter kernel. This is something that has to be avoided because it decreases filter performance. Since I want maximum noise resilence I thus want to get rid of this frequency deviation.

The other issue arises in case the DCF77 signal is lost. The way the clock is designed makes it easier for the filter stack to relock to the DCF77 signal if the local synthesizer is as close to the real signal as possible. Hence it is highly desirable to have a precise local clock.

Therefore I enhanced the 1 kHz generator. As you can see I added to functions void adjust(const int16_t pp16m) and int16_t read_adjustment().

namespace DCF77_1_Khz_Generator {
    void setup(const DCF77_Clock::input_provider_t input_provider);
    uint8_t zero_provider();
    // positive_value --> increase frequency
    // pp16m = parts per 16 million = 1 Hz @ 16 Mhz
    void adjust(const int16_t pp16m);
    int16_t read_adjustment();
    void isr_handler();
}

The implementation of the adjust(const int16_t pp16m) and read_adjustment() functions are pretty straightforward. The real magic happens in the isr_handler().

namespace DCF77_1_Khz_Generator {
    uint8_t zero_provider() {
        return 0;
    }
    
    static DCF77_Clock::input_provider_t the_input_provider = zero_provider;
    static int16_t adjust_pp16m = 0;
    static int32_t cumulated_phase_deviation = 0;
    
    void adjust(const int16_t pp16m) {
        const uint8_t prev_SREG = SREG;
        cli();
        // positive_value --> increase frequency
        adjust_pp16m = pp16m;
        SREG = prev_SREG;
    }
    
    int16_t read_adjustment() {
        // positive_value --> increase frequency
        const uint8_t prev_SREG = SREG;
        cli();
        const int16_t pp16m = adjust_pp16m;
        SREG = prev_SREG;
        return pp16m;
    }
    
    void init_timer_2() {
        // Timer 2 CTC mode, prescaler 64
        TCCR2B = (1<<WGM22) | (1<<CS22);
        TCCR2A = (1<<WGM21);
        
        // 249 + 1 == 250 == 250 000 / 1000 =  (16 000 000 / 64) / 1000
        OCR2A = 249;
        
        // enable Timer 2 interrupts
        TIMSK2 = (1<<OCIE2A);
    }
    
    void stop_timer_0() {
        // ensure that the standard timer interrupts will not
        // mess with msTimer2
        TIMSK0 = 0;
    }
    
    void setup(const DCF77_Clock::input_provider_t input_provider) {
        init_timer_2();
        stop_timer_0();
        the_input_provider = input_provider;
    }
    
    void isr_handler() {
        cumulated_phase_deviation += adjust_pp16m;
        // 1 / 250 / 64000 = 1 / 16 000 000
        if (cumulated_phase_deviation >= 64000) {
            cumulated_phase_deviation -= 64000;
            // cumulated drift exceeds 1 timer step (4 microseconds)
            // drop one timer step to realign
            OCR2A = 248;
        } else
            if (cumulated_phase_deviation <= -64000) {
                // cumulated drift exceeds 1 timer step (4 microseconds)
                // insert one timer step to realign
                cumulated_phase_deviation += 64000;
                OCR2A = 250;
            } else {
                // 249 + 1 == 250 == 250 000 / 1000 =  (16 000 000 / 64) / 1000
                OCR2A = 249;
            }
            DCF77_Clock_Controller::process_1_kHz_tick_data(the_input_provider());
    }
}

As you can see from the implementation the isr handler will cumulate the phase deviation. According to the frequency which was set by the adjust function. Once the cumulated phase deviation exceeds 4 microseconds it will insert or drop one timer step. Since the timer is prescaled with 64 this has the same effect as if the crystal would be slower / faster depending on the adjustment.

This way I get fractional adjustments with integer computations only. This is exactly the same idea as used in Bresenham’s algorithm or or in Digital Differential Analyzers.

Now that I have this nice frequency adjustment the question is how to use it. Actually this is very simple. Just add the following line add the end of the setup code.

DCF77_1_Khz_Generator::adjust(<frequency adjust>);

Of course you have to substitute the proper value (in Hz) for your Arduino. This now leaves the question how to determine the proper adjustment constant for a given Arduino. Fortunately I already solved this issue before with the DCF77 scope experiment.

Here I repeat the important steps. The log below was captured with a Blinkenlighty with a DCF77 module. As you can see the signal is very clean. You can also see that after ~6500 seconds the phase of the DCF77 signal drifted about 200 ms relative to the local clock. Since DCF77 is 100% accurate by definition and the signal comes in 200 ms later this implies that the Blinkenlighty is 200 ms to fast every 6500 seconds. Or with other words the local clock deviates by 200 ms / 6500 s ~ 31 ppm. Of course we want the deviation in Hz. This is 200 ms / 6500 s * 16 Mhz ~ 492 Hz. With other words we need to have frequency_adjust = -492.

 340, +---------+---------3XXXXXXXXXXXXXXXXXXX6---------+---------+---------+---------+---------+---------
 341, +---------+---------3XXXXXXXXXX2--------+---------+---------+---------+---------+---------+---------
 342, +---------+---------4XXXXXXXXXXXXXXXXXXXX3--------+---------+---------+---------+---------+---------
 343, +---------+---------3XXXXXXXXXX1--------+---------+---------+---------+---------+---------+---------
 344, +---------+---------2XXXXXXXXXXXXXXXXXXXX2--------+---------+---------+---------+---------+---------
 345, +---------+---------5XXXXXXXXXXXXXXXXXXXX2--------+---------+---------+---------+---------+---------
 346, +---------+---------+XXXXXXXXXX2--------+---------+---------+---------+---------+---------+---------
 347, +---------+---------3XXXXXXXXXX---------+---------+---------+---------+---------+---------+---------
 348, +---------+---------+XXXXXXXXXX2--------+---------+---------+---------+---------+---------+---------
 349, +---------+---------3XXXXXXXXXX8--------+---------+---------+---------+---------+---------+---------
...
6500, +---------+---------+---------+---------4XXXXXXXXXX3--------+---------+---------+---------+---------
6501, +---------+---------+---------+---------4XXXXXXXXXX2--------+---------+---------+---------+---------
6502, +---------+---------+---------+---------4XXXXXXXXXXXXXXXXXXXX---------+---------+---------+---------
6503, +---------+---------+---------+---------2XXXXXXXXXXXXXXXXXXXX---------+---------+---------+---------
6504, +---------+---------+---------+---------3XXXXXXXXXXXXXXXXXXX8---------+---------+---------+---------
6505, +---------+---------+---------+---------2XXXXXXXXXXXXXXXXXXXX2--------+---------+---------+---------
6506, +---------+---------+---------+---------3XXXXXXXXXX1--------+---------+---------+---------+---------
6507, +---------+---------+---------+---------3XXXXXXXXXXXXXXXXXXX9---------+---------+---------+---------
6508, +---------+---------+---------+---------2XXXXXXXXXX2--------+---------+---------+---------+---------
6509, +---------+---------+---------+---------2XXXXXXXXXXXXXXXXXXX9---------+---------+---------+---------
6510, +---------+---------+---------+---------4XXXXXXXXXXXXXXXXXXX9---------+---------+---------+---------
6511, +---------+---------+---------+---------+XXXXXXXXXXXXXXXXXXXX---------+---------+---------+---------
6512, +---------+---------+---------+---------1XXXXXXXXX9---------+---------+---------+---------+---------

If you notice that the signal log seems to wander from right to left then the signals are coming in faster than they should. Since DCF77 is stable by definition this would imply that the local clock is to slow. Then you can compute the frequency adjustment accordingly but adjust by a positive value (in order to make the local clock faster).

Leave a comment

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