Как реализовать авторегулируемый диапазон частот в генераторе сигнала на Arduino

Когда собираешь генератор сигнала на Arduino, рано или поздно встаёт задача: не просто выдавать фиксированную частоту, а автоматически перестраивать её в заданном диапазоне. Нужно это и для тестирования фильтров, и для поиска резонансных частот контуров, и для простого частотного сканирования. То есть Arduino сам должен менять частоту по заложенному алгоритму — без участия человека.

Разберёмся, как это сделать практически: от выбора способа генерации до рабочего кода с плавной разверткой частоты.

Что значит «авторегулируемый диапазон частот»

Под этим понимают режим, при котором генератор сам проходит от нижней частоты к верхней (или в обратном направлении) за определённое время. При этом скорость изменения частоты, шаг и форма зависимости могут задаваться програмно.

Типичные сценарии применения:

  • Проверка АЧХ фильтров и усилителей.
  • Поиск резонанса пьезоэлементов и LC-контуров.
  • Калибровка частотно-зависимых цепей.
  • Простой генератор чирп-сигналов для самодельных измерительных устройств.

Как вообще генерируется частота на Arduino

Прежде чем автоматизировать диапазон, нужно понять, как именно формируется сигнал. На Arduino есть несколько путей, и выбор сильно влияет на точность, диапазон и гибкость.

1. Аппаратные таймеры (рекомендую)

Аппаратные таймеры работают независимо от основного цикла loop(). Они переключивают пин по заданному счёту, и частота при этом не зависит от задержек в коде. Можно использовать таймеры в режиме CTC, Fast PWM или Phase Correct PWM.

Плюсы:

  • Стабильная частота без дрожания.
  • Можно получить диапазон от единиц герц до мегагерц (с делителями).
  • Не блокирует выполнение остальной программы.

2. Фазовая коррекция ШИМ (Phase Correct PWM)

Подходит, если нужна высокая симметричность импульсов. Частота при этом ниже, чем в режиме Fast PWM, зато форма сигнала точнее. Для генерации синуса через фильтр это иногда важнее.

3. Программная генерация (tone() и задержки)

Функция tone() удобна для простых задач, но она тоже использует таймер, и при изменении частоты в цикле возможны паузы. А вот delayMicroseconds() в ручном режиме — совсем плохой путь: частота плывёт, развертка неравномерная.

Вывод: для авторегулируемого диапазона я рекомендую именно аппаратные таймеры в режиме CTC или Fast PWM.

Способы автоматического изменения частоты

Есть несколько подходов к тому, как организовать автоматическое изменение частоты во времени.

Линейная развёртка

Частота меняется равномерно от f_start до f_end за заданное время. Простой и предсказуемый вариант. Подходит для большинства измерений АЧХ.

Формула для расчёта текущей частоты в момент времени t:

f(t) = f_start + (f_end - f_start) * (t / T)

где T — общее время развёртки.

Логарифмическая развёртка

Частота меняется медленно на низких частотах и быстрее на высоких (или наоборот). Это полезно, когда нужно более детальное исследование низкочастотного участка.

f(t) = f_start * (f_end / f_start) ^ (t / T)

Ступенчатое переключение

Генератор поочерёдно выставляет фиксированные частоты из массива, задерживаясь на каждой на определённое время. Удобно для дискретного сканирования и для отладки.

Практическая реализация на Arduino

Приведу пример для Arduino Uno (ATmega328P) с использованием Timer1. Будем генерировать меандр на пине 9 (OC1A) и плавно менять частоту в заданном диапазоне.

Настройка таймера в режиме CTC

В режиме CTC таймер считает до значения в регистре OCR1A, затем сбрасывается и переключает пин. Частота определяется формулой:

f = F_CPU / (2 * prescaler * (OCR1A + 1))

Значит, чтобы получить нужную частоту, мы вычисляем:

OCR1A = (F_CPU / (2 * prescaler * f)) - 1

Базовый код с линейной развёрткой

#include <avr/io.h>
#include <util/delay.h>

// Границы диапазона
#define F_START 1000    // 1 кГц
#define F_END   10000   // 10 кГц
#define SWEEP_TIME 5000 // время развёртки в мс

void setupTimer1CTC(uint16_t ocr_value) {
    // Пин 9 (OC1A) как выход
    DDRB |= (1 << PB1);

    // Отключаем Timer1
    TCCR1A = 0;
    TCCR1B = 0;
    TCNT1 = 0;

    // Режим CTC, переключение OC1A
    TCCR1A |= (1 << COM1A0);  // Toggle OC1A on compare match
    TCCR1B |= (1 << WGM12);  // CTC mode
    TCCR1B |= (1 << CS11);   // Prescaler = 8

    OCR1A = ocr_value;
}

uint16_t freqToOCR(float freq) {
    // f = F_CPU / (2 * prescaler * (OCR1A + 1))
    // OCR1A = (F_CPU / (2 * prescaler * f)) - 1
    return (uint16_t)(F_CPU / (2 * 8 * freq) - 1);
}

void linearSweep() {
    uint32_t start_time = 0;
    uint32_t end_time = SWEEP_TIME;

    float current_freq = F_START;
    uint16_t ocr_val = freqToOCR(current_freq);
    setupTimer1CTC(ocr_val);

    uint32_t last_update = 0;
    uint16_t step_delay = 50; // обновление частоты каждые 50 мс

    while (1) {
        uint32_t now = millis();
        if (now - last_update >= step_delay) {
            float elapsed = (float)(now - start_time) / end_time;
            if (elapsed > 1.0) elapsed = 1.0;

            current_freq = F_START + (F_END - F_START) * elapsed;
            ocr_val = freqToOCR(current_freq);

            // Обновляем OCR1A на лету
            OCR1A = ocr_val;
            last_update = now;
        }

        if (now - start_time >= end_time) break;
    }
}

int main() {
    linearSweep();
    return 0;
}

Если вы используете Arduino IDE, замените main() на привычные setup() и loop(), а millis() и _delay_ms() оставьте как есть — они доступны и в IDE.

Версия для Arduino IDE

#define F_START 1000
#define F_END 10000
#define SWEEP_TIME 5000

unsigned long sweep_start;
uint16_t step_delay = 50;
unsigned long last_update = 0;

void setup() {
    pinMode(9, OUTPUT);
    noInterrupts();

    TCCR1A = 0;
    TCCR1B = 0;
    TCNT1 = 0;

    TCCR1A |= (1 << COM1A0);
    TCCR1B |= (1 << WGM12);
    TCCR1B |= (1 << CS11); // prescaler 8

    interrupts();

    sweep_start = millis();
}

uint16_t freqToOCR(float freq) {
    return (uint16_t)(16000000UL / (2UL * 8UL * freq) - 1);
}

void loop() {
    unsigned long now = millis();
    if (now - last_update >= step_delay) {
        float elapsed = (float)(now - sweep_start) / SWEEP_TIME;
        if (elapsed > 1.0) elapsed = 1.0;

        float freq = F_START + (F_END - F_START) * elapsed;
        OCR1A = freqToOCR(freq);
        last_update = now;
    }

    if (now - sweep_start >= SWEEP_TIME) {
        // Останавливаем таймер
        TCCR1B &= ~(1 << CS11);
        while (1); // пауза
    }
}

Сравнение подходов к авторегулированию частоты

Подход Диапазон частот Стабильность Сложность реализации Подходит для
Timer CTC, prescaler 1–1024 ~1 Гц – 8 МГц (16 МГц MCU) Высокая Средняя Точная развёртка, измерения АЧХ
Timer Fast PWM ~122 Гц – 8 МГц Высокая Средняя ШИМ-генерация с регулировкой
tone() + loop ~31 Гц – ~65 кГц Низкая при перестройке Низкая Простые тесты, без требований к точности
delayMicroseconds() ~1 кГц – ~100 кГц Очень низкая Низкая Не рекомендуется

Что выбрать под свою задачу

Нужна точная АЧХ фильтра от 100 Гц до 20 кГц? — используйте Timer1 в режиме CTC с линейной или логарифмической развёрткой. Подключите на выход простой RC-фильтр, если нужен синусоидальный сигнал.

Нужен простой генератор для динамика? — хватит tone() с ручным перебором частот в цикле. Но для плавной развёртки всё равно лучше таймер.

Генератор для поиска резонанса? — используйте логарифмическую развёртку и фиксируйте амплитуду через АЦП. Так вы сможете построить резонансную кривую в том же скетче.

Нужна высокая частота (выше 1 МГц)? — уменьшайте прескалер до 1, но учитывайте, что нижняя граница диапазона при этом поднимается.

Частые ошибки и как их избежать

  • Обновление OCR без синхронизации. Если обновляете регистр OCR1A произвольно, при совпадении счёта может получиться сбой импульса. Используйте прерывание по совпадению или обновляйте OCR в момент сброса таймера.
  • Слишком большой шаг частоты. При грубых шагах развёртка становится рывчатой. Уменьшайте шаг или увеличивайте время задержки между обновлениями.
  • Использование delay() вместо millis(). delay() блокирует выполнение кода, и вы не сможете одновременно обрабатывать другие задачи (кнопки, дисплей, АЦП).
  • Не учитывается ограничение OCR1A. OCR1A — 16-битный регистр. На высоких частотах значение может стать меньше 1, и таймер не сработает корректно. Проверяйте границы.
  • Нет фильтрации на выходе. Меандр содержит много гармоник. Если нужен чистый синус — добавьте RC или LC фильтр низких частот.

Рекомендации по улучшению

  1. Добавьте управление кнопками. Пусть одна кнопка запускает развёртку, другая — меняет направление, третья — меняет скорость.
  2. Выводите текущую частоту на дисплей. Так вы будете видеть, где находитесь в диапазоне, и сможете остановить развёртку в нужный момент.
  3. Считывайте амплитуду через АЦП. Если на выходе стоит детектор амплитуды, можно в реальном времени строить АЧХ прямо в скетче.
  4. Используйте прерывание по совпадению для обновления OCR. Это даст более стабильную развёртку и исключит сбои формы сигнала.
  5. Для синуса — используйте таблицу значений и ЦАП. Если нужен синусоидальный сигнал, загрузите в массив 256 значений синуса и выводите их через ШИМ с фильтрацией или через внешний ЦАП.

Итог

Авторегулируемый диапазон частот на Arduino — это не магия, а грамотная работа с аппаратными таймерами. Используйте Timer1 или Timer2 в режиме CTC, вычисляйте OCR по формуле, обновляйте его в цикле через millis() — и получите стабильную развёртку без блокировок.

Если задача простая — можно обойтись tone(). Если нужна точность и чистый сигнал — только аппаратный таймер с фильтром на выходе.

Начните с приведённого выше кода, подставьте свои границы частот и время развёртки — и вы получите рабочий генератор, который сам проходит диапазон так, как вам нужно.

Radio-Blog.ru