Модуль генератора сигналов GEN01
Часть 3 – настройка и тестирование модуля
Собираем тестовый стенд

Итак, печатная плата изготовлена, элементы на ней смонтированы, остатки флюса отмыты и готовый модуль генератора сигналов лежит перед нами на столе. Пора переходить к его включению и настройке. Для этого нужно собрать схему тестового стенда, показанную ниже. Её основой является микроконтроллер Arduino Nano, подключенный к ПК через разъем USB. Это позволяет загружать в микроконтроллер соответствующие прошивки, запустив на ПК программу Arduino IDE, а кроме того, через тот же USB кабель снабжать контроллер напряжением питания +5V от компьютера. Стремясь максимально упростить схему и программное обеспечение тестового стенда, мы не станем подключать к микроконтроллеру никаких дисплейных модулей, а всю отладочную информацию будем выводить на ПК через Serial Monitor.
Плата KY-040 c установленным на ней энкодером PEC11 (или аналогичная) нужна для управления частотой генератора, а кнопка (любая тактовая, без фиксации) – для переключения диапазонов. Конденсаторы C1 и C2 устраняют дребезг контактов энкодера.
На плате энкодера KY-040 обычно имеются посадочные места для трех постоянных резисторов, необходимых для подтяжки цепей CLK, DT и SW к шине +5V. При поставке от изготовителя часто бывает так, что резисторы в цепях CLK и DT на плате установлены, а в цепи SW – почему-то нет. В этом случае у вас есть два выхода:
- запаять SMD резистор номиналом 10 кОм на соответствующее посадочное место на плате энкодера
- или резистор не запаивать, а в программе для микроконтроллера, в функции setup(), при инициализации пина D4, к которому у нас подключена цепь SW, использовать вызов
pinMode(encSW, INPUT_PULLUP)
где encSW – это пин, который мы инициализируем (в данном случае D4), а константа INPUT_PULLUP означает, что в микроконтроллере будет задействован внутренний резистор для подтяжки пина к шине +5V.
Потребуется также двухполярный источник питания с выходными напряжениями +12V и -12V, а на выход модуля генератора подключим осциллограф и любые другие измерительные приборы – милливольтметр, частотомер, измеритель нелинейных искажений, если таковые найдутся в вашей домашней лаборатории. Низкоомные нагрузки типа громкоговорителя или наушников подключать на выход генератора не стоит – микросхема XR-2206 не имеет защиты от перегрузок на выходе, и подключив такую нагрузку, вы, скорее всего, просто спалите микросхему. Если все-таки вы захотите послушать, что у вас получилось – подайте сигнал с выхода модуля на линейный вход какого-нибудь усилителя, эти входы обычно имеют сопротивление не менее нескольких десятков или сотен килоом и не нанесут вреда микросхеме.
Управляем частотой сигнала
Прежде чем приступать к рассмотрению исходного текста программы, загружаемой в микроконтроллер тестового стенда, необходимо подумать о том, как именно мы собираемся управлять частотой генератора. На схеме стенда видно, что мы подключили к микроконтроллеру кнопку для переключения диапазонов и энкодер для установки точного значения частоты, но как именно должна изменяться частота при вращении энкодера, пока не вполне ясно.
Можно сразу сказать, что если каждому единичному импульсу (“клику”) на выходе энкодера будет соответствовать равное приращение частоты (в Гц), то ничего хорошего не получится. Для самых низких частот нам надо иметь возможность устанавливать частоту хотя бы с точностью до 1-го Гц – скажем, 10 Гц и 11 Гц – это разные частоты, отличающиеся на целых 10%. Но тот же шаг в 1 Гц в верхней части звукового диапазона становится избыточно мелким – чтобы изменить частоту с 10 кГц до 11 кГц, то-есть на те же 10%, нам придется сделать 1000 “кликов”, а на весь диапазон частот генератора – около 26000 “кликов”. Учитывая, что энкодеры, подобные PEC11, обычно выдают 20-24 импульса на один оборот, нам довольно долго придется крутить его ручку...
Ясно, что зависимость частоты генератора от угла поворота энкодера не должна быть линейной, проще говоря, один “клик” должен менять частоту не на фиксированное число Гц, а на определенный процент от текущего значения частоты. Но и тут не все хорошо получается. Если выбрать в качестве шага частоты величину, например, 1% от текущей, то для того чтобы перекрыть весь звуковой диапазон нам все же потребуется около 1000 “кликов”. Это уже лучше чем при линейной зависимости, но тоже многовато будет.
Выход из этой ситуации один – надо делать два разных шага установки частоты – грубый и точный. Сначала в режиме грубой настройки с помощью крупных шагов устанавливаем частоту, максимально близкую к требуемой, а затем уточняем ее, двигаясь мелкими шагами в режиме точной настройки. При этом и частоту установим достаточно точно и “кликов” энкодера сделаем не так много.
В качестве грубых шагов удобно взять широко применяемый в звукотехнике и утвержденный различными отечественными и международными стандартами третьоктавный ряд частот, который выглядит следующим образом:
. . . 16, 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 2000, 25000 . . .
В незапамятные времена ученые мужи, занимавшиеся исследованием спектров звуковых сигналов и строившие для этого различные полосовые фильтры, решили как-то стандартизировать ряд частот, на которых производились измерения, с тем чтобы результаты можно было сравнивать между собой. Они взяли за основу некоторую среднюю частоту диапазона, например, 1000 Гц и построили от нее интервал равный одной октаве. Октава (и в музыке, и в звукотехнике) это такой интервал частот, где верхняя частота отличается от нижней ровно в 2 раза. У них получилась октава 1000 Гц – 2000 Гц. Далее они разделили ее на три равные части, добавив в нее еще две частоты – 1250 Гц и 1600 Гц, в результате чего октава оказалась поделенной на 3 равных интервала: 1000…1250 Гц, 1250…1600 Гц и 1600…2000 Гц. Далее все эти значения распространили по оси частот влево – делением их на 2, и вправо – умножением на 2, откуда и получился этот замечательно красивый ряд частот, называемый третьоктавным. У ученых мужей были определенные соображения по поводу того, почему октаву надо делить именно на три интервала. Эти соображения связаны с особенностями человеческого восприятия звуков разных частот, но мы здесь на них останавливаться не будем.
Таким образом, в режиме грубой настройки один “клик” энкодера будет переключать частоту на следующее значение по третьоктавному ряду вверх или вниз в зависимости от направления вращения. В режиме точной настройки частота по каждому “клику” будет меняться приблизительно на 1% от ее текущего значения, но при желании, величину этого шага можно будет изменить, внеся соответствующие изменения в программу.
Переключаться из режима грубой настройки в режим точной настройки и обратно мы будем по нажатию кнопки энкодера – как известно, энкодеры обычно имеют встроенную кнопку, срабатывающую по нажатию на вал в осевом направлении. Выходом кнопки является цепь, обозначенная на плате энкодера “SW”, на которой в нормальном состоянии присутствует уровень логической 1, а при нажатии на вал появляется логический 0.
Пишем программу для тестового стенда
Исходный текст программы (или, как принято говорить в среде Arduino, скетч) приведен ниже. Как и все другие файлы, которые вам могут понадобиться, его можно скачать по ссылке в конце статьи.
/******************************************************* Name: Generator Module Test Version: 2.0 January 2023 Written by: Alexander Butovsky ******************************************************** Hardware: - Arduino NANO - Generator module vers.2.0 with 12-bit DAC MCP4725 and two frequency ranges - Range pushbutton connected to Nano input pin 5 - Module Range input connected to Nano output pin 6 - MCP4725 on I2C bus (A4 - SDA, A5 - SCL) - Rotary encoder connection: CLK - pin 2, DT - pin 3, SW - pin 4 (toggles coarse or fine tuning mode) ******************************************************** Version history 1.0 September 2022 1st version with rotary encoder 2.0 January 2023 Handling of 2 frequency ranges ********************************************************/ #include <Adafruit_MCP4725.h> // I2C addresses for MCP4725: /******************************************* | IC name | IC marking | ADDR=0 | ADDR=1 | |------------------------------------------| | MCP4725A0 | AJxx | 0x60 | 0x61 | | MCP4725A1 | APxx | 0x62 | 0x63 | | MCP4725A2 | AQxx | 0x64 | 0x65 | | MCP4725A3 | ARxx | 0x65 | 0x67 | *******************************************/ #define DACaddr 0x61 // Pins for encoder: #define encA 2 //Encoder CLK output #define encB 3 //Encoder DT output #define encSW 4 //Encoder switch // Pins for range control: #define rangeButtonPin 5 #define rangeOut 6 /**************************************************** * Standard one-third octave frequencies(in Hz) * ***************************************************** * Range=0 --> 16, 20, 25, 32, 40, 50, 63, 80, 100, * * 125, 160, 200, 250, 315, 400, 500 * * Range=1 --> 630, 800, 1000, 1250, 1600, 2000, * * 2500, 3150, 4000, 5000, 6300, 8000, * * 10000, 12500, 16000, 20000, 25000 * ***************************************************** * fCode values corresponding to frequencies above: * * freqCode array elements [0]...[15] - Range 0 * * freqCode array elements [16]...[32] - Range 1 * ****************************************************/ uint16_t freqCode[33] = { 81, 102, 128, 162, 207, 260, 329, 421, 525, 659, 845, 1057, 1322, 1666, 2117, 2647, 86, 112, 141, 176, 225, 281, 352, 445, 564, 704, 888, 1128, 1415, 1772, 2247, 2814, 3505 }; //----- Global variables ----- enum display_flag { nothing, range_changed, stepMode_changed, frequency_changed }; display_flag df = nothing; //Keeps information what exactly //was changed in each loop uint16_t fCode; //Current frequency code in DAC register. int ind = 0; //Current index in freqCode array (0...32). uint8_t range = 0; //Range: "0" - 16-600 Hz, "1" - 0.6-25 kHz uint8_t stepMode = 0; //Step mode: "0" - 1/3 octave (Coarse) // "1" - 1% steps (Fine) uint8_t currentStateA; //Encoder CLK output value now uint8_t lastStateA; //Encoder CLK output value before // DAC object Adafruit_MCP4725 dac; void waitButtonRelease(int buttonPin) { do { delay(5); } while (digitalRead(buttonPin) == LOW); } /************************************************ * Handle frequency change in Coarse tuning mode * * dir = true --> 1 step up * * = false --> 1 step down * * Returns frequency code from next or previous * * element of freqCode[] array * ************************************************/ uint16_t handleCoarseMode(bool dir) { if (dir) { //Get next frequency in freqArray if (ind < 32) { ind++; if (ind == 16) {range = 1;} } } else { //Get previous frequency in freqArray if (ind > 0) { ind--; if (ind == 15) {range = 0;} } } return freqCode[ind]; } /************************************************* * Handle frequency change in Fine tuning mode * * dir = true --> 1% of current frequency up * * = false --> 1% of current frequency down * * Returns frequency code increased or decreased * * by small steps * *************************************************/ #define fineModeStep 1.01 // Step is 1% uint16_t handleFineMode(bool dir, uint16_t oldFCode) { uint16_t newFCode = oldFCode; if (dir) { //Clockwise rotation - step up if (oldFCode < 4000) { //upper limit newFCode = oldFCode * fineModeStep; if (newFCode == oldFCode) { newFCode++; //add 1 if float result was truncated //to the same integer value as oldFCode } if ((newFCode > 3100) && (range == 0)) { //switch to range 1 range = 1; newFCode = newFCode / 37.43; } } } else { //Counterclockwise rotation - step down if (fCode > 50) { //lower limit newFCode = oldFCode / fineModeStep; if ((newFCode < 80) && (range == 1)) { //switch to range 0 range = 0; newFCode = newFCode * 37.43; } } } return newFCode; } /********************************************* * Set new fCode and range on encoder events * * dir = true --> 1 step up * * = false --> 1 step down * *********************************************/ void setFreq(bool dir) { if (stepMode == 0) { //Coarse mode fCode = handleCoarseMode(dir); //Get new fCode value } else { //Fine mode fCode = handleFineMode(dir, fCode); //Get new fCode value } } /********************************************** * Set frequency by sending new frequency code * * and range to Generator module * **********************************************/ void setVoltageAndRange(uint16_t newFreq, uint8_t newRange) { dac.setVoltage(newFreq, false); //Set DAC output voltage digitalWrite(rangeOut, newRange); //Write range to rangeOut df = frequency_changed; } void setup() { pinMode(rangeButtonPin, INPUT_PULLUP); pinMode(rangeOut, OUTPUT); pinMode(encSW, INPUT); //Use INPUT_PULLUP if encoder doesn't //have pullup resistor pinMode(encA, INPUT); pinMode(encB, INPUT); Serial.begin(9600); Serial.println("---- GENERATOR START ----"); if(dac.begin(DACaddr)) { Serial.println("----- DAC is ready ------"); fCode = freqCode[ind]; setVoltageAndRange(fCode, range); lastStateA = digitalRead(encA); //Get initial state of CLK } else{ Serial.println("-> DAC error. Press RESET"); while (true){}; //stop program operation } } void loop() { // ==================================== // Read range button and toggle range // ==================================== if (digitalRead(rangeButtonPin) == LOW) { waitButtonRelease(rangeButtonPin); range = range ^ 0x1; //invert range value setVoltageAndRange(fCode, range); } // ==================================== // Read encoder button and toggle step // ==================================== if (digitalRead(encSW) == LOW) { waitButtonRelease(encSW); stepMode = stepMode ^ 0x1; //Invert stepMode value df = stepMode_changed; if (stepMode == 0) { //If we have returned from fine mode fCode = freqCode[ind]; //to сoarse - recall old frequency setVoltageAndRange(fCode, range); } } // ==================================== // Read encoder and set frequency // ==================================== currentStateA = digitalRead(encA); if ((currentStateA != lastStateA) && (currentStateA == LOW)) { setFreq(digitalRead(encB) == HIGH); //if true - clockwise //else - counterclockwise setVoltageAndRange(fCode, range); } lastStateA = currentStateA; // ==================================== // Display if any changes have happened // ==================================== switch (df) { case range_changed: Serial.print("Range="); Serial.println(range); break; case stepMode_changed: Serial.print("StepMode="); Serial.println(stepMode); break; case frequency_changed: Serial.print("Range="); Serial.print(range); if (stepMode == 0) { Serial.print(" ind="); Serial.print(ind); } Serial.print(" fCode="); Serial.println(fCode); break; } df = nothing; }
В тексте имеются достаточно подробные комментарии и, при наличии у читателя даже небольшого опыта программирования на С++ для Arduino, в нем все должно быть понятно. Остается лишь сделать несколько пояснений.
Микросхема ЦАП MCP4725 в зависимости от варианта исполнения самой микросхемы и от того, какой логический уровень подан на ее вход ADDR, может иметь один из восьми адресов в диапазоне от 0x60 до 0x67 на шине I2C (см. таблицу в комментарии в самом начале скетча). Узнать, какой вариант исполнения микросхемы достался вам, можно, вооружившись мощной лупой и попытавшись увидеть на корпусе микросхемы обозначения, начинающиеся с AJ, AP, AQ или AR, как указано в той же таблице.
Если это не удастся, есть более простой способ. Надо загрузить в микроконтроллер тестового стенда программу i2c_scanner, которую легко найти на сайте Arduino, а она автоматически просканирует все возможные адреса (их не так много) и обнаружит, на каком именно адресе откликнется ваша микросхема ЦАП. Найденный адрес надо записать в текст программы тестового стенда в строчку “#define DACaddr”.
Основная функция программы loop() в цикле выполняет следующие действия:
- читает состояние кнопки переключения диапазонов Range и, если выясняется, что она нажата, ждет ее отпускания, а затем переключает диапазон
- читает состояние кнопки энкодера и, если выясняется, что она нажата, ждет ее отпускания, а затем переключает режим установки частоты грубый-точный
- читает состояние энкодера и, если выясняется, что произошел поворот его вала, меняет частоту генератора вверх или вниз в зависимости от направления поворота
- если одно из предыдущих трех событий имело место в текущем цикле, то выдает соответствующее диагностическое сообщение в Serial Monitor среды разработки Arduino IDE.
Настраиваем модуль
Здесь уместно произнести фразу, которую все мы читали в сотнях, если не в тысячах, статей с описаниями радиолюбительских конструкций. Звучит она так: “При исправных деталях и при отсутствии ошибок в монтаже устройство начинает работать сразу”. Эта фраза в полной мере относится и к описываемому модулю. Надо будет только убедиться, что модуль выполняет все свои функции, то есть, что при нажатии на кнопку переключения диапазонов они действительно переключаются, что при нажатии на кнопку энкодера переключаются режимы грубой и точной настройки и что при повороте вала энкодера происходят соответствующие изменения частоты.
Затем надо будет совершить еще две операции:
1. Покрутить подстроечные резисторы. При этом резисторы R7 и R8 определяют форму выходного синусоидального сигнала, а резистор R11 – его амплитуду. При наличии измерителя нелинейных искажений или, что еще нагляднее, современного цифрового осциллографа с возможностью просмотра спектра сигнала, вращая резисторы R7 и R8, можно добиться минимального коэффициента гармоник. Затем надо установить резистор R11 в такое положение чтобы действующее значение напряжения на выходе модуля составляло ровно 1V, что соответствует амплитуде 1.41 V, полному размаху “от пика до пика” – 2.82 V или принятому в звукотехнике уровню 0 dBV. После этого мы должны увидеть на экране осциллографа нечто вроде вот этого:

Обратите внимание на лиловую линию на осциллограмме, обозначенную CH1 FFT (Канал 1, Быстрое Преобразование Фурье). Это спектр сигнала, и как мы видим, он содержит преимущественно первую гармонику (тонкая вертикальная линия у левого края экрана), а уровень более высоких гармоник едва заметен. Это означает что подстроечные резисторы R7 и R8 находятся в оптимальных положениях , при которых форма выходного сигнала максимально близка к синусоиде или, иначе говоря, достигнут наименьший возможный в данной схеме уровень нелинейных искажений.
Если лабораторный измеритель нелинейных искажений отсутствует (что не удивительно, поскольку это довольно редкий прибор, выпускаемый в ограниченных количествах), а цифрового осциллографа с анализатором спектра тоже поблизости не видно, то нелинейные искажения можно измерять с помощью любого персонального компьютера. Для этого исследуемый сигнал подается на вход звуковой карты, а на компьютер устанавливается специализированная программа, выполняющая все необходимые обработки и вычисления.
В интернете можно найти большое количество подобных программ, однако, автор на основании собственного опыта мог бы порекомендовать программу Visual Analyser , которая кроме измерения коэффициента нелинейных искажений и коэффициента гармоник (это разные коэффициенты, хотя, обычно, довольно близкие по значению) позволяет выполнить с помощью компьютера большое количество других измерений в области звукотехники. Возможно, в будущем мы посвятим отдельную статью этой программе, имеющей очень широкие возможности, равно как и значительное количество различных недостатков, но пока читателю важно понять, что с ее помощью он может с достаточной точностью оценить уровень нелинейных искажений сигнала в звуковом диапазоне.
2. Далее необходимо откорректировать в программе значения элементов массива freqCode[33], хранящего значения, которые надо записать в рабочий регистр ЦАП, чтобы получить стандартные частоты третьоктавного ряда. То определение массива, которое вы видите в скетче
freqCode[33] = { 81, 102, 128, 162, . . .
означает: записываем в регистр ЦАП код 81 – получаем на выходе первую частоту ряда – 16 Гц, записываем 102 – получаем 20 Гц, записываем 128 – получаем 25 Гц и так далее. К сожалению, в реальной жизни частота на выходе генератора зависит не только от этого кода, но также и от фактической емкости конденсаторов C3 и C4 во времязадающей цепи, от сопротивлений некоторых резисторов, от напряжения питания и от других обстоятельств. Поэтому скорее всего в каждом конкретном экземпляре модуля придется значения элементов этого массива немного подкорректировать.
Для этого, контролируя частоту выходного сигнала модуля частотомером, вращаем ручку энкодера в режиме точной настройки и выставляем на выходе частоту 16 Гц (или максимально близкую к ней). Смотрим в окне Serial Monitor на отладочные сообщения, присланные микроконтроллером – там видно, какой код был загружен в регистр ЦАП чтобы эта частота получилась. Записываем этот код в качестве элемента массива с индексом 0 и переходим к следующей частоте – 20 Гц. В итоге повторяем эту операцию 33 раза, для каждой из частот третьоктавного ряда и в результате получаем ваш собственный массив freqCode, соответствующий вашему конкретному экземпляру модуля. Он может выглядеть, например, вот так:
freqCode[33] = { 88, 113, 144, 191, . . .
Сохраняем этот массив данных где-нибудь в надежном месте, поскольку он вам обязательно понадобится в дальнейшем, когда вы будете использовать этот модуль в составе различных приборов.
Что получилось в результате
В завершение мы должны подвести итоги сделанного, а именно – измерить технические характеристики того, что у нас получилось. По замыслу автора эти характеристики должны выглядеть примерно так:
Диапазон частот | 10 Гц . . . 28 кГц |
Номинальное выходное напряжение | 1.0 В (RMS) |
Отклонение выходного напряжения от номинала во всем диапазоне частот | не более 20 мВ (0.2 dB) |
Выходное сопротивление | 750 Ом |
Ток потребления по цепи +12 V | не более 20 мА |
Ток потребления по цепи -12 V | не более 1 мА |
Габаритные размеры | 87 х 36 х 22 мм |
На этом создание модуля генератора сигналов GEN01 завершено и мы переходим к описанию других модулей.
Автор надеется, что описание модуля было достаточно подробным для того чтобы читатели могли его успешно воспроизвести, однако, если все-таки какие-то вопросы в процессе изготовления и настройки возникнут, то он будет рад на них ответить. Обращайтесь!
Файлы для скачивания
Файлы в формате EAGLE 7.5.0:
- принципиальная схема модуля
в формате .SCH для Eagle 7.5.0.
- печатная плата модуля
в формате .BRD для Eagle 7.5.0.
- библиотека Eagle с пользовательскими компонентами
Фотошаблон печатной платы модуля генератора для изготовления методом пленочного фоторезиста.