You could use the input capture feature of Timer 1 to timestamp the
edges of the reference input signal. This way you can compute the period
of the input relative to the CPU clock, compare to the expected nominal
period, and use this to adjust the interrupt period.
Combining this idea with the answer to the previous question gives:
const float input_frequency = 60;
const float interrupt_frequency = 2400;
const uint32_t nominal_input_period = round(F_CPU / input_frequency);
const uint32_t nominal_interrupt_period =
round(F_CPU / interrupt_frequency) * 65536;
volatile uint32_t interrupt_period = nominal_interrupt_period;
uint16_t last_input_timestamp;
ISR(TIMER1_COMPA_vect) {
// Update the timer for the next interrupt.
static uint32_t accumulator;
accumulator += interrupt_period;
OCR1A = accumulator >> 16;
// The periodic task goes here...
}
void setup() {
// Configure Timer 1.
TCCR1A = 0; // normal mode
TCCR1B = _BV(CS10); // clock @ F_CPU
TIMSK1 = _BV(OCIE1A); // enable COMPA interrupt
// Capture the first input edge.
TIFR1 |= _BV(ICF1); // clear interrupt flag
loop_until_bit_is_set(TIFR1, ICF1);
TIFR1 |= _BV(ICF1); // clear interrupt flag again
last_input_timestamp = ICR1;
}
void loop() {
// Detect edges of the input signal.
if (bit_is_set(TIFR1, ICF1)) {
TIFR1 |= _BV(ICF1); // clear interrupt flag
// Update the interrupt period.
uint16_t input_timestamp = ICR1;
uint32_t input_period =
(int16_t) (input_timestamp - last_input_timestamp
- (uint16_t) nominal_input_period)
+ nominal_input_period;
last_input_timestamp = input_timestamp;
uint32_t new_interrupt_period = round(nominal_interrupt_period
* ((float) input_period / nominal_input_period));
noInterrupts();
interrupt_period = new_interrupt_period;
interrupts();
}
}
Note that rounding the input period to an integer number of cycles comes
with a rounding error of 1.25 ppm, well within your 10 ppm
requirement. You can do better if needed, by using fixed point or
floating point computations. Note also that the Timer rolls over
multiple times per cycle of the input signal, which is dealt with by
some arithmetic tricks, hence the casts to uint16_t
and int16_t
in
the expression of input_period
.
Strictly speaking, this is not a phase lock loop. It is more like a
frequency lock loop. A real PLL would probably be a bit more involved.
However, if you can tolerate your resulting frequency to be 10 ppm
off, this approach is likely to be good enough.