В этом материале представлен простой генератор сигналов на основе ATtiny85. Он может генерировать треугольные, пилообразные, квадратные и прямоугольные формы сигналов, последовательность импульсов и шум. Частота может быть отрегулирована с помощью поворотного энкодера от 1 Гц до 5 кГц с шагом 1 Гц, а выбранный сигнал и частота отображаются на OLED дисплее.
Этот проект использует всю производительность ATtiny85, где контроллер генерирует 8-битные выборки с частотой дискретизации 16 кГц, декодирует поворотный энкодер, переключается между сигналами и обновляет OLED дисплей через линию I2C.
Вступление
Генератор использует прямой цифровой синтез или DDS для генерации сигналов. Обычно DDS использует таблицу предварительно вычисленного сигнала, такого как синусоида. Для генерации определенной частоты вы шагаете по таблице, выбирая каждую n-ю выборку. Чем меньше число, используемое для шага, тем больше времени требуется, чтобы обойти один цикл значений и тем ниже частота.
ATtiny85 идеально подходит для DDS, так как у него есть внутренний источник тактового сигнала частотой 64 МГц, который вы можете использовать для управления таймером/счетчиком 1 и быстрого цифро-аналогового преобразования. Вот процедура инициализации таймеров/счетчиков для DDS:
void SetupDDS () { // Включаем PLL на 64 МГц и используем в качестве источника тактового сигнала для таймера/счетчика 1 PLLCSR = 1 << PCKE | 1 << PLLE; // Настройка таймера/счетчика 1 для выхода ШИМ TIMSK = 0; // Прерывания от таймера отключены TCCR1 = 1 << PWM1A | 2 << COM1A0 | 1 << CS10; // Канал A, сброс при совпадении, без предделителя pinMode(1, OUTPUT); // Включить выходной контакт ШИМ // Установка таймера/счетчика 0 для прерывания 20 кГц для вывода выборок. TCCR0A = 3 << WGM00; // Быстрый ШИМ TCCR0B = 1 << WGM02 | 2 << CS00; // Предделитель на 8 TIMSK = 1 << OCIE0A; // Прерывание по переполнению таймера OCR0A = 60; // Делим на 61 }
В первой строке включается генератор PLL на 64 МГц с ФАПЧ и выбирается в качестве источника синхронизации для таймера/счетчика 1.
Затем таймер/счетчик 1 устанавливается в режиме ШИМ, чтобы он действовал как цифро-аналоговый преобразователь, используя значение в OCR1A для изменения коэффициента заполнения и, следовательно, аналогового выхода. Частота прямоугольной волны определяется OCR1C; мы оставляем значение по умолчанию 255, которое делит тактовую частоту на 256, давая меандр частотой 250 кГц.
Таймер/счетчик 0 используется для генерации прерывания вывода выборок. Частота этого прерывания равна системной тактовой частоте 8 МГц, деленной на 8 и делитель 61, что составляет около 16,4 кГц. Прерывание вызывает подпрограмму обработки прерывания ISR (TIMER0_COMPA_vect), которая вычисляет и выводит выборки.
К счастью, делитель 61 позволяет нам получить шаг частоты очень близкий к 1 Гц. Например, вот расчет, показывающий, какую частоту вы получаете для значения Jump, равного 4. Процедура обслуживания прерываний вызывается один раз каждые 8000000/(8*61) Гц, и каждый раз, когда Jump добавляется в 16-разрядный фазовый аккумулятор - Acc. Поэтому старший бит Acc будет меняться с частотой:
8000000/(8*61)/(65536/4) или 10006 Гц
В итоге: изменение значения Jump с шагом 4 даст нам частоту 1 Гц с точностью до 0,1%.
Генерация сигналов
Чтобы избежать необходимости расчета и сохранения таблиц сигналов, этот генератор рассчитывает сигналы на лету, как описано в следующих разделах. Код для каждого сигнала исключает условные операторы, чтобы обеспечить согласованность времени выполнения в каждом цикле.
Форма сигнала "Меандр"
Эта прямоугольная волна имеет коэффициент заполнения 50% и содержит только нечетные гармоники. Вот осциллограмма формы волны:
Для прямоугольной волны мы берем старший байт аккумулятора и сдвигаем его вправо на 7 бит. Поскольку это целое число со знаком, это дает 0, если старший бит равен 0, и 0xFF, если старший бит равен единице:
void Square () { Acc = Acc + Jump; int8_t temp = Acc >> 8; OCR1A = temp >> 7; }
Форма сигнала "Прямоугольник"
Форма сигнала "Прямоугольник" имеет рабочий цикл 25%:
Прямоугольная волна с рабочим циклом D имеет отсутствующую гармонику n всякий раз, когда n * D является целым числом, поэтому в этой волне отсутствуют 4-я, 8-я, 12-я гармоники и т. Д.
Для прямоугольной волны мы берем старший байт аккумулятора И вместе два верхних бита, затем сдвигаем его на 7 бит вправо. Это дает 0xFF, если старшие два бита были 1 и 0 в противном случае:
void Rectangle () { Acc = Acc + Jump; int8_t temp = Acc >> 8; temp = temp & temp << 1; OCR1A = temp >> 7; }
Форма сигнала "Пульс"
Форма сигнала "Пульс" представляет собой прямоугольную волну с соотношением сигнал/пустота 1:16.
Для пульсовой волны мы берем старший байт аккумулятора И вместе четыре старших бита, затем сдвигаем его на 7 бит вправо. Это дает 0xFF, если старшие четыре бита были 1 и 0 в противном случае:
void Pulse () { Acc = Acc + Jump; int8_t temp = Acc >> 8; temp = temp & temp << 1 & temp << 2 & temp << 3; OCR1A = temp >> 7; }
Форма сигнала "Пила"
Пилообразная форма сигнала содержит все гармоники. Она имеет амплитуду от 0 до 255 в каждом цикле, названную так потому, что выглядит как зуб пилы:
Для пилообразной волны мы просто копируем верхний байт аккумулятора на выход:
void Sawtooth () { Acc = Acc + Jump; OCR1A = Acc >> 8; }
Форма сигнала "Треугольник"
Форма сигнала "Треугольник" близка к чисто синусоидальной форме, но с добавлением нечетных гармоник на более низких уровнях, чем прямоугольная волна. Он считает от 0 до 255, а затем снова до 0:
Для генерации треугольной волны мы берем старший байт аккумулятора и инвертируем его, когда старший бит равен единице:
void Triangle () { int8_t temp, mask; Acc = Acc + Jump; temp = Acc >> 8; mask = temp >> 7; temp = temp ^ mask; OCR1A = temp << 1; }
Форма сигнала "Бензопила"
Для забавы я включил изобретенную форму волны, которая представляет собой нечто среднее между пилообразной волной и прямоугольной волной:
Вот как это генерируется:
void Chainsaw () { int8_t temp, mask, top; Acc = Acc + Jump; temp = Acc >> 8; mask = temp >> 7; top = temp & 0x80; temp = (temp ^ mask) | top; OCR1A = temp; }
Форма сигнала "Шум"
Форма сигнала "Шум" имеет равномерное распределение энергии на всех частотах. Это не зависит от настройки контроля частоты.
Здесь используется генератор псевдослучайных чисел для генерации случайных байтов:
void Noise () { int8_t temp = Acc & 1; Acc = Acc >> 1; if (temp == 0) Acc = Acc ^ 0xB400; OCR1A = Acc; }
Схема
Вот схема генератора DSS:
Дисплей я выбрал разрешением 128x32 и интерфейсом I2C. Резистор 33 кОм и конденсатор 0,1 мкФ гарантируют правильную инициализацию дисплея при первом включении питания.
Поворотный энкодер содержит в себе кнопку, которую можно использовать для пошагового переключения сигналов. Клеммы поворотного энкодера были немного широки, чтобы поместиться в макетную плату, поэтому я припаял куски медной проволки небольшой длины к каждой клемме и вставил их в макетную плату.
Резисторы на 4,7 кОм и конденсаторы на 4,7 нФ образуют двухполосный фильтр нижних частот для фильтрации несущей частоты ШИМ. Сглаживание этой цепи составляет 1/2ПRC, поэтому эти значения дают срез 1/(2*3,14*4700*4,7E10-9), что составляет 7,2 кГц. Выход около 1 В, достаточный для управления усилителем или пьезодинамиком, чтобы повысить амплитуду вы можете использовать активный фильтр.
Поскольку выходной сигнал на PB1 переключается между 0 В и +5 В, в сигнале присутствует смещение + 2,5 В постоянного тока. Смещения можно избежать, взяв выходной сигнал относительно виртуального заземления, созданного двумя резисторами 10 кОм.
Программа
Поворотный энкодер
Поворотный энкодер подключен к контактам 3 и 4 и использует прерывание смены контактов для обновления частоты генератора функций.
Прерывание смены контакта устанавливается в функции SetupRotaryEncoder ():
void SetupRotaryEncoder () { pinMode(EncoderA, INPUT_PULLUP); pinMode(EncoderB, INPUT_PULLUP); PCMSK = 1 << EncoderA; // Прерывание по изменению состояния вывода GIMSK = 1 << PCIE; // Разрешаем прерывания GIFR = 1 << PCIF; // Очищаем флаг прерываний }
Поворот энкодера вызывает процедуру ChangeValue() с логическим аргументом для указания направления. Это увеличивает частоту и обновляет дисплей:
void ChangeValue (bool Up) { int step = 1; if (Freq >= 1000) step = 100; else if (Freq >= 100) step = 10; Freq = max(min((Freq + (Up ? step : -step)), MaxFreq), MinFreq); PlotFreq(Freq, 1, 7); Jump = Freq*4; }
Генератор может потенциально изменять частоту с 0,25 Гц с шагом 0,25 Гц во всем частотном диапазоне, но для удобства я выбрал изменение частоты с шагом 1 Гц в диапазоне от 1 Гц до 99 Гц, с шагом 10 Гц в диапазоне от 100 Гц до 999 Гц, и с шагом 100 Гц от 1000 Гц до 5000 Гц, но вы можете изменить это.
Кнопка энкодера используется для переключения типов сигналов. Поскольку на ATtiny85 нет свободных выводов, для подключения кнопки я использовал вывод сброса ATtiny85. Переменная Wave используется для подсчета количества сбросов и, следовательно, выбора следующего типа сигнала. Эта переменная определена в программе как .noinit, поэтому компилятор не сбросит его до нуля при сбросе. Текущая частота, Freq, также определяется как .noinit, поэтому она не инициализируется при сбросе.
I2C OLED дисплей
Частота сигнала и значок, представляющий текущую форму сигнала, отображаются на OLED-дисплее 128x32 пикселей. Такие дисплеи доступны в версиях SPI и I2C. Я выбрал версию I2C, потому что для ее управления требуется всего две линии ввода/вывода, а на ATtiny85 доступно только две линии.
Согласно данным, контроллер SSD1306, используемый дисплеем, поддерживает отправку комбинации команд и/или данных, поэтому теоретически вы можете записать весь дисплей в одной 512-байтовой передаче I2C. Сначала я изо всех сил пытался заставить это работать. Оказывается, библиотека Arduino Wire работает путем буферизации данных, которые вы отправляете при вызове Wire.write () , и фактически передает их только при вызове Wire.endTransmission () . Кроме того, длина буферов составляет всего 32 байта, поэтому максимальная длина передачи составляет 32 байта. Одним из обходных путей является запись данных и команд в виде серии отдельных однобайтовых сообщений, что и делает Adafruit в своей библиотеке SSD1306, но это очень неэффективно.
Мое решение состояло в том, чтобы разбить передачи максимум на 32 байта за раз. Например, процедура PlotChar() отправляет 24 байта для символа двойного размера, так что это можно сделать за одну передачу.
Частота и значок отображаются в виде двойных символов, чтобы сделать дисплей более читабельным. Символы определяются массивом CharMap [] [] . Иконки осциллограмм создаются из двух символов. В эту программу я включил только определения для цифр, символов "Гц" и значков сигналов, но вы можете добавить полный набор символов, если хотите отображать другие символы.
Выбор формы волны
Наиболее очевидный способ позволить вам выбрать форму сигнала - это использовать оператор case или серию операторов if, чтобы выбрать часть кода для соответствующей формы сигнала в зависимости от значения глобальной переменной; например, это будет выглядеть примерно так:
ISR(TIMER0_COMPA_vect) { if (Wave == 0) { // Код для формы сигнала "Треугольник" } else if (Wave == 1) { // Код для формы сигнала "Пила" } else if (Wave == 2) { // Код для формы сигнала "Прямоугольник" } else { // Код для формы сигнала "Шум" } }
Однако процедура обработки прерываний вызывается 16 000 раз в секунду, поэтому каждый цикл времени выполнения является критическим, и этот подход потенциально добавляет четыре сравнения к каждому вызову. Более элегантное решение состоит в том, чтобы определить каждый из сигналов как именованную подпрограмму, а затем использовать таблицу поиска для вызова соответствующей подпрограммы. Так, например, мы определяем пилообразную форму как:
void Sawtooth () { Acc = Acc + Jump; OCR1A = Acc >> 8; }
Мы определяем тип wavefun_t, который является функцией без аргументов и без возвращаемого значения:
typedef void (*wavefun_t)();
а затем определите массив адресов подпрограммы формы волны с помощью:
tconst int nWaves = 4; wavefun_t Waves[nWaves] = {Triangle, Sawtooth, Square, Noise};
Чтобы изменить форму волны, мы выполняем:
Wave = (Wave + 1) % nWaves; Wavefun = Waves[Wave];
Процедура обработки прерывания становится:
ISR(TIMER0_COMPA_vect) { Wavefun(); }
Компиляция программы
Я скомпилировал программу, используя Spence Konde's ATTiny Core. Выберите тип МК ATtiny25/45/85 под заголовком ATtinyCore в меню Board. Затем выберите Timer 1 Clock: CPU , BOD: Disabled, ATtiny85: 8 МГц (внутренний). Выберите Burn Bootloader для правильной установки fuse битов. Затем загрузите программу с помощью ISP (внутрисистемное программирование). Я использовал плату разработчика Sparkfun Tiny AVR.
Калибровка частоты
Точность этого генератора зависит от точности внутреннего 8 МГц тактового сигнала ATtiny85. Чтобы получить максимально точную частоту, вы можете откалибровать внутренний генератор, используя регистр OSCCAL. Самый простой способ - проверить частоту с помощью частотомера. Если у вас нет измерителя частоты, подключите пьезодинамик к выходу, выберите прямоугольную волну с частотой 1 Гц и подсчитайте щелчки с помощью секундомера!
Разместите:
OSCCAL = 128;
в начале setup() и перекомпилируйте программу с различными значениями OSCCAL, изменяя это значение сначала большими шагами, а затем меньшими шагами по мере приближения к правильной частоте.
Использование ATtiny861
В этом проекте возможности устройства в основном ограничены количеством линий ввода/вывода, доступных в ATtiny85. Чтобы расширить функционал, вы можете использовать ATtiny861, который имеет тот же высокоскоростной генератор PLL, что и ATtiny85, но и содержит более 15 линий ввода/вывода. Это позволит вам подключить внешний кристалл для более точного управления частотой и переключатели, чтобы обеспечить более удобный выбор формы волны и диапазона частот.
Обновления
7 марта 2018 года: я добавил форму сигнала "Синус" к доступным.
10 марта 2018 года: я обновил схему и описание, чтобы снизить частоту среза фильтра нижних частот до 7,2 кГц.
23 сентября 2019 года: добавлено объяснение того, как вывод сброса используется для переключения между сигналами.
Файлы к статье "Простой генератор сигналов на ATtiny85" | |
Описание:
Исходный код, макет печатной платы Eagle |
|
Размер файла: 23.04 KB Количество загрузок: 608 | Скачать |