-1

In this question: Procedure for changing frequency of Nano or similar, @EdgarBonet gave a very nice method of generating timings that don't divide the system clock evenly. He also said that it would require tuning of the exact value by measuring the resulting frequency. I'd like to see if that tuning could be made automatic.

Suppose I had a precise external reference that could interrupt at a a sub-multiple of my frequency, say 60Hz. Is there a way to use that precision to, over time, bring my generated frequency into rough agreement with it?

In other words, for a frequency of 2400 Hz, and a reference of 60 Hz, I would adjust the period of the generator so that there are exactly 80 cycles of the 2400 Hz signal in each 60Hz period, even though there might be variations within the periods of those 80 cycles.

In effect this is a crude phase-locked loop multiplier. I know it can't be highly precise, but even close would be very valuable.

1 Answer 1

1

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.

3
  • This is great. I never would have worked out the subtleties of int vs uint vs float here. I'll have to study this more to completely get it. You've clearly mastered this device.
    – Jim Mack
    Nov 13, 2021 at 17:20
  • One thing is unclear to me. Reading the Atmel docs, it seems that the ICF1 flag in the TIFR1 register is set when an interrupt occurs, and at one point you check if (bit_is_set(TIFR1, ICF1)), so that makes sense. But then you TIFR1 |= _BV(ICF1) to clear the interrupt, which seems to be the opposite of clearing the bit. What am I missing?
    – Jim Mack
    Nov 14, 2021 at 1:03
  • 1
    @JimMack: Re “the ICF1 flag [...] is set when an interrupt occurs”: No, it is set when a capture event occurs. We could enable the capture event interrupt, but given that this is not time critical (60 Hz is slow), I prefer to just poll the flag in loop(). Re “TIFR1 |= _BV(ICF1) [...] seems to be the opposite of clearing the bit”: As per the datasheet: “ICF1 can be cleared by writing a logic one to its bit location.” This is how most (all?) interrupt flags work, which is indeed confusing! Nov 14, 2021 at 10:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.