Когда собираешь генератор сигнала на 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 фильтр низких частот.
Рекомендации по улучшению
- Добавьте управление кнопками. Пусть одна кнопка запускает развёртку, другая — меняет направление, третья — меняет скорость.
- Выводите текущую частоту на дисплей. Так вы будете видеть, где находитесь в диапазоне, и сможете остановить развёртку в нужный момент.
- Считывайте амплитуду через АЦП. Если на выходе стоит детектор амплитуды, можно в реальном времени строить АЧХ прямо в скетче.
- Используйте прерывание по совпадению для обновления OCR. Это даст более стабильную развёртку и исключит сбои формы сигнала.
- Для синуса — используйте таблицу значений и ЦАП. Если нужен синусоидальный сигнал, загрузите в массив 256 значений синуса и выводите их через ШИМ с фильтрацией или через внешний ЦАП.
Итог
Авторегулируемый диапазон частот на Arduino — это не магия, а грамотная работа с аппаратными таймерами. Используйте Timer1 или Timer2 в режиме CTC, вычисляйте OCR по формуле, обновляйте его в цикле через millis() — и получите стабильную развёртку без блокировок.
Если задача простая — можно обойтись tone(). Если нужна точность и чистый сигнал — только аппаратный таймер с фильтром на выходе.
Начните с приведённого выше кода, подставьте свои границы частот и время развёртки — и вы получите рабочий генератор, который сам проходит диапазон так, как вам нужно.
