Arduino. Генератор сигналов

В данной статье рассказывается о том, как с помощью Arduino генерировать сигналы. Всё написанное также верно и для клонов Arduino, например Freeduino.

В статье встречаются технические термины, для понимания которых могут быть не бесполезны статьи Использование прерываний Arduino, Порты ввода-вывода Arduino и Как подключить к Arduino...

Генератор сигналов

При подключении к Arduino динамика с регулятором громкости, в качестве микропрограммы использовался скетч toneMelody из меню Examples->Digital, проигрывающий мелодию после сброса или включения Arduino. Если посмотреть, как устроен код этого скетча, то выясняется, что ноты воспроизводятся с помощью функции tone(), которая схожа с функцией delay() тем, что пока выполняется функция, программа ждёт окончания её выполнения.

Но что, если я захочу например подключить к Arduino несколько кнопок и играть разные ноты, или подключить потенциометр и менять тональность звучащей ноты? Ведь нота должна звучать пока нажата кнопка, то есть мне нужна возможность и воспроизводить звук, и опрашивать порты ввода-вывода одновременно.

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

Методы генерации сигналов с помощью таймеров отличаются в разных режимах таймеров.

Генерация сигналов в нормальном режиме

В нормальном режиме таймер-счётчик последовательно увеличивает значение регистра TCNTn (где n - номер счётчика) на каждом такте генератора, от которого таймер-счётчик приводится в действие. Когда значение регистра TCNTn достигает максимального, то на следующем такте происходит переполнение счётчика, регистр обнуляется и вызывается прерывание по переполнению TIMERn_OVF_vect, если вызов этого прерывания включен.

Соответственно, метод генерирования сигналов в нормальном режиме применяется один из двух.

В первом случае разрешается переключение состояния вывода OCRnx (где x - это A или B). Это удобно, поскольку не требует тратить на генерацию сигнала процессорного времени, но таким образом можно получить только ограниченное число частот - по количеству значений предделителя. То есть пять частот для таймеров 0 и 1, и 7 частот для таймера 2.

Поэтому чаще в нормальном режиме применяется второй метод, который состоит в том, по прерыванию в регистр TCNTn записывается новое значение счётчика, и таким образом количество частот, которые могут быть сгенерированы, увеличивается примерно в 250 раз для 8-битных таймеров и в 65535 раз для 16-битного таймера.

Тем не менее, второй способ более похож на программную реализацию режима CTC (очистки при совпадении), поэтому этот метод применяется в основном для использования ШИМ в качестве цифро-аналогового преобразователя.

Генерация сигналов в режиме CTC

Для генерации сигналов в режиме CTC я добавил в схему подключения динамика к Arduino потенциометр R3, для того, чтобы регулировать частоту генерируемого сигнала.

Итак, я собираюсь генерировать сигнал аудио-частоты, то есть в диапазоне от 20 Гц до 22 кГц. Для этого я использую вывод Digital с номером 11, который соответствует выводу OCR2A, то есть выводу Compare Match A таймера-счётчика 2.

Для установки таймера-счётчика 2 в режим CTC я должен установить биты WGM в значение 2 (бинарное 010). Чтобы включить режим переключения состояния вывода OCR2A при совпадении (Toggle on Compare Match) мне нужно установить биты COM2A в значение 1 (бинарное 01).

Поскольку в этом режиме при совпадении значений регистров OCR2A и TCNT2 происходит обнуление регистра TCNT2, то чем меньше значение регистра OCR2A, тем выше частота сигнала, а чем больше значение регистра OCR2A, тем частота ниже. Минимальная частота сигнала, которую я смогу получить с предделителем /1024, равна согласно формуле из документации, равна

16 МГц / (2 * 1024 * 256) = 30 Гц,

а максимальная -

16 МГц / (2 * 1024 * 1) = 7812 Гц,

что вполне укладывается в диапазон аудио-частот.

Для выбора в качестве источника тактовых импульсов для таймера-счётчика 2 предделителя /1024, я должен установить биты CS2 в значение 7 (бинарное 111).

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

Таким образом, я создаю новый скетч и добавляю следующий код:

#define R3_PIN (A0)

#define SPEAKER_PIN (11)

#define T2_WGM (0b010)

#define T2_COMA (0b01)

#define T2_CS (0b111)


void setup()

{

TCCR2A = (T2_COMA << 6) | (T2_WGM & 0b011);

TCCR2B = ((T2_WGM & 0b100) << 1) | T2_CS;

OCR2A = 255;

pinMode(R3_PIN, INPUT);

pinMode(SPEAKER_PIN, OUTPUT);

}

Инициализация таймера-счётчика завершена, и теперь я добавляю опрос состояния потенциометра R3 для изменения частоты сигнала путём изменения значения регистра OCR2A. Поскольку функция analogRead() возвращает значение от 0 до 1023, то мне нужно его смасштабировать функцией map() в диапазион от 0 до 255.

Соответственно, код функции loop() в этом скетче выглядит так:

void loop()

{

OCR2A = map(analogRead(R3_PIN), 0, 1023, 0, 255);

}

Готово.

Добавив к этому скетчу библиотеку Simple Dumping Monitor, я также могу узнать текущее значение регистра OCR2A при изменении положения регулятора потенциометра R3.

Генерация сигналов в режиме быстрого ШИМ (Fast PWM)

Использование микроконтроллера Arduino как генератора сигналов в режиме CTC довольно удобно тем, что для этой задачи не требуется процессорное время, но в то же время у этого режима есть существенное ограничение - с его помощью можно генерировать сигналы только определённого набора частот. Методом генерации сигналов в режиме быстрого ШИМ возможно получить большее количество частот, чем позволяет режим CTC.

В режиме быстрого ШИМ вывод широтно-импульсного модулятора используется в качестве цифро-аналогового преобразователя, на который генерируемый сигнал выводится как последовательность значений из волновой таблицы.

Ниже приведён скетч, который демонстрирует генерирование нот на выводе ШИМ Digital 11, задаваемых пользователем по последовательному порту. Темп игры (длина ноты) задаётся с помощью потенциометра, подключенного к выводу Analog In 0.

Поскольку с конденсатором усилитель класса D звучал несколько странно, я немного изменил схему, удалив из неё конденсатор:

#define PWMOUTPIN 11 // Digital 11 - вывод ШИМ OC2A

#define LEDPIN 13 // Digital 13 - светодиод

#define TEMPOINPUT A0 // Analog In 0 - потенциометр, задающий темп


enum { WAITING, PLAYING } state = WAITING; // Текущее состояние (ожидание/проигрывание ноты)

volatile bool waitForInterrupt = true; // Флаг ожидания прерывания

unsigned int playCounter = 0; // Счётчик проигрывания ноты

// (0 - окончание проигрывания)

unsigned int waveCounter = 0; // Счётчик генератора волны


unsigned int notes[14] = { // Таблица приращений счётчика волны

0, 1096, 1161, 1230, 1304, 1381, 1463, // согласно частот для нот первой октавы (0 - пауза)

1550, 1643, 1740, 1844, 1953, 2069, 2192 };


char keys[14] = { // Таблица клавиш нот первой октавы (пробел - пауза)

' ', 'z', 's', 'x', 'd', 'c', 'v',

'g', 'b', 'h', 'n', 'j', 'm', ',' };


unsigned int note = 0; // Приращение счётчика для текущей проигрываемой ноты


void setup() // Инициализация микроконтроллера

{

pinMode(LEDPIN, OUTPUT); // Контакт светодиода в режим вывода

pinMode(PWMOUTPIN, OUTPUT); // Контакт ШИМ OC2A в режим вывода

setupPWMOutput(); // Настройка таймера 2 в режим быстрого ШИМ

sei(); // Enable interrupts // Разрешить прерывания

Serial.begin(57600); // Инициализация последовательного порта

}


void loop() // Основной цикл скетча

{

switch(state) { // Машина состояний

case WAITING: // Обработка ожидания

stateWaiting();

break;

case PLAYING: // Обработка проигрывания ноты

statePlaying();

break;

}

}


void setupPWMOutput() // Настройка таймера 2 в режим быстрого ШИМ

{

TCCR2A = 0;

TCCR2A |= _BV(WGM21) | _BV(WGM20); // Режим быстрого ШИМ (TOP = 0xFF)

TCCR2A |= _BV(COM2A1); // Сброс OCR2A при совпадении,

// установка в нижней точке (не-инверсный режим)

TCCR2B = 0;

TCCR2B |= _BV(CS20); // Частота тактового сигнала - предделитель /1

TIMSK2 |= _BV(OCIE2A); // Разрешить прерывание по совпадению TIMER2_COMPA_vect

}


ISR(TIMER2_COMPA_vect) { // Обработка прерывания TIMER2_COMPA_vect

static unsigned char div = 0; // Делитель частоты

++div; // Увеличить значение делителя на 1

div &= 3; // Делим частоту на 4

if (div == 0) {

waitForInterrupt = false; // Сброс флага ожидания прерывания

}

}


void stateWaiting() // Обработка ожидания

{

digitalWrite(LEDPIN, HIGH); // Включить светодиод

OCR2A = 0; // Выключить звук

if (Serial.available()) { // Если есть данные в последовательном порту

unsigned char cmd = Serial.read(); // Прочитать символ

state = PLAYING; // Состояние проигрывания ноты

note = getNote(cmd); // Определить приращение счётчика генератора волны

playCounter = (analogRead(TEMPOINPUT)+1) << 3; // Определить темп проигрывания

waveCounter = 0; // Инициализировать счётчика генератора волны

}

}

void statePlaying() // Обработка проигрывания ноты

{

digitalWrite(LEDPIN, LOW); // Выключить светодиод

--playCounter; // Уменьшить значение счётчика проигрывания ноты на 1

if (playCounter == 0) {

state = WAITING; // Перейти в состояние ожидания по окончании

} // проигрывания ноты

else {

waveCounter += note; // Генерация пилообразной волны

unsigned int out = ((waveCounter >> 8) & 0xFF) * (playCounter>>5); // Спад ноты (decay)

while(waitForInterrupt); // Ожидание прерывания

OCR2A = (out >> 8) & 0xFF; // Вывод текущего значения волны на вывод ШИМ OC2A

waitForInterrupt = true; // Установка флага ожидания прерывания

}

}


unsigned int getNote(char key) // Преобразование клавиши в приращение счётчика

{ // генератора волны

for(unsigned char n = 0; n < 14; ++n) {

if (keys[n] == key) {

return notes[n];

}

}

return notes[0]; // Если клавиша не определена, проиграть паузу

}

Скачать скетч можно, кликнув по ссылке.

Скомпилировав и загрузив скетч в память Arduino, откройте окно Serial Monitor и установите скорость обмена, заданную в скетче - 57600 бод. Чтобы проиграть гамму, отправьте в Arduino строку символов zxcvbnm, (символ запятая соответствует ноте до второй октавы). Соответственно, рок-н-ролл можно сыграть, отправив строку zcb.zcb.zcbnjnbc. (точка в данном скетче неопределённый символ, поэтому вместо неё будет проиграна пауза).

Прослушать демо можно, кликнув по ссылке.

Записывая демо, я обратил внимание, что громкость нот зависит от их длительности. Спросив себя, почему так происходит, я вспомнил, что для спада ноты используется значение переменной playCounter, которое тем меньше, чем быстрее темп проигрывания нот. Таким образом, я добавил в скетч переменные decayCounter и decay, на основе которых реализовал спад ноты таким образом, чтобы громкость нот не зависела от их длины.

Исправленную версию скетча можно скачать по ссылке.

Прослушать демо исправленной версии скетча можно, кликнув по ссылке.

Чтобы облагородить звучание, я добавил в третью версия скетча эффект изменения частоты тона в начале проигрывания ноты. Для этого я добавил переменные счётчика тона toneCounter и приращения тона toneDelta. В третьей версии скетча при проигрывании ноты частота тона увеличивается до частоты ноты. Благодаря этому эффекту звучание стало похоже на электронный бас.

Эту версию скетча можно скачать по ссылке. Прослушать демо - кликнув по ссылке.

И чтобы стало совсем похоже на настоящий синтезатор, в четвёртую версию я добавил эффект реверберации. Размер буфера определяется константой BUFSIZE, размер которой я установил равным 1096 байт, что совпадает с приращением счётчика генератора волны для ноты до. Это даёт частоту цикла воспроизведения буфера равной 14.26 Гц для частоты дискретизации 15625 Гц. Такой размер буфера даёт более музыкальный саунд, нежели например буфер размером 1024 байта.

В целом звучание получилось весьма винтажным, прослушать демо можно по ссылке.

Версия скетча с ревербератором.

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

Для регулировки фильтра я добавил в схему ещё один потенциометр, подключив его к выводу Analog In 1.

В шестой версии скетча я решил сделать ревербератор стереофоническим, но для этого мне нужно было добавить к Arduino стереофонический линейный выход. Чтобы не слишком заморачиваться, я взял имевшийся под рукой низковольтовый однополярный операционный усилитель мощности KA2209 и на его основе собрал схему, практически повторяющую схему стерео-усилителя, приведённую в даташите.

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

Тем не менее, поскольку линейный вход АЦП звуковой карты обычно оснащён автоматической подстройкой нуля (это сделано чтобы не возникало так называемых смещений несущей), то такой вариант как вариант дешёво и сердито я для этой проекта счёл вполне приемлемым.

Версия скетча со стереофоническим ревербератором.

Демо скетча со стереофоническим ревербератором.

Автор: Андрей Шаройко <vanyamboe@gmail.com>