Синхронизация внутренней частоты с внешней

В этом вопросе: Процедура изменения частоты Nano или аналогичная, @EdgarBonet дал очень хороший метод генерации таймингов, которые не разделяют системные часы равномерно. Он также сказал, что для этого потребуется настроить точное значение путем измерения результирующей частоты. Я хотел бы посмотреть, можно ли сделать эту настройку автоматической.

Предположим, у меня была точная внешняя ссылка, которая могла прерываться на частоте, кратной моей частоте, скажем, 60 Гц. Есть ли способ использовать эту точность, чтобы со временем привести мою сгенерированную частоту в приблизительное соответствие с ней?

Другими словами, для частоты 2400 Гц и эталонной частоты 60 Гц я бы отрегулировал период генератора так, чтобы в каждом периоде 60 Гц было ровно 80 циклов сигнала частотой 2400 Гц, даже если в периоды этих 80 циклов могут быть изменения.

По сути, это грубый множитель с фазовой автоподстройкой. Я знаю, что это не может быть очень точным, но даже близко было бы очень ценно.

, 👍-1


1 ответ


Лучший ответ:

1

Вы можете использовать функцию захвата входного сигнала таймера 1 для отметки времени по краям опорного входного сигнала. Таким образом, вы можете вычислить период ввода относительно тактовой частоты процессора, сравнить его с ожидаемым номинальным периодом и использовать это для настройки периода прерывания.

Объединение этой идеи с ответом на предыдущий вопрос дает:

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) {
    // Обновите таймер для следующего прерывания.
    static uint32_t accumulator;
    accumulator += interrupt_period;
    OCR1A = accumulator >> 16;

    // Периодическая задача находится здесь...
}

void setup() {
    // Настройка таймера 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();
    }
}

Обратите внимание, что округление входного периода до целого числа циклов приводит к ошибке округления в 1,25 промилле, что вполне соответствует вашим требованиям в 10 промилле. При необходимости вы можете сделать это лучше, используя вычисления с фиксированной или плавающей точкой. Обратите также внимание, что таймер переключается несколько раз за цикл входного сигнала, с которым справляются некоторые арифметические трюки, поэтому приведение к uint16_t и int16_t в выражении input_period.

Строго говоря, это не цикл фазовой блокировки. Это больше похоже на петлю блокировки частоты. Настоящий PLL, вероятно, был бы немного более вовлечен. Однако, если вы можете допустить, чтобы ваша результирующая частота составляла 10 ppm , этот подход, вероятно, будет достаточно хорошим.

,

Это здорово. Я бы никогда не разобрался в тонкостях int vs uint vs float здесь. Мне придется изучить это подробнее, чтобы полностью понять это. Вы явно освоили это устройство., @Jim Mack

Мне непонятно одно. Читая документы Atmel, кажется, что флаг ICF1 в регистре TIFR1 устанавливается при возникновении прерывания, и в какой-то момент вы проверяете " if (bit_is_set(TIFR1, ICF1))", так что это имеет смысл. Но затем вы " TIFR1 |= _BV(ICF1)", чтобы очистить прерывание, что, по-видимому, противоположно очистке бита. Что я упускаю?, @Jim Mack

@JimMack: Re “ _ флаг ICF1 [...] устанавливается при возникновении прерывания_”: Нет, он устанавливается при возникновении события захвата. Мы могли бы включить прерывание события захвата, но, учитывая, что это не критично по времени (60 Гц медленно), я предпочитаю просто опросить флаг в " цикле ()". Re “_TIFR1 |= _BV(ICF1) [ ... ], похоже, противоположно очистке бита”: Согласно таблице данных: “ICF1 может быть очищен путем записи логического в его битовое местоположение”. Именно так работает большинство (все?) флагов прерываний, что действительно сбивает с толку!, @Edgar Bonet