CUSTOMELECTRONICS.RU
Информационно-учебный блог о разработке электроники
Эл. почта: info@customelectronics.ru

AVR. Работа с пьезоизлучателем. Проигрывание мелодии

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

Подготовка к работе

На нашем TutorShield’е установлен пьезоизлучатель. Для его подключения установите перемычку между отмеченными выводами. Эти контакты подписаны как «buz». При их замыкании пьезоизлучатель подключается к выводу микроконтроллера PC5.

Подключение пьезоизлучателя

Подключение пьезоизлучателя

Пьезоизлучатели бывают двух типов — со встроенным генератором и без. Зуммеры со встроенным генератором излучают фиксированный тональный сигнал сразу после подачи на них номинального напряжения. Они не могут воспроизводить произвольный сигнал. Их обычно используют для простого звукового оповещения. Если требуется проиграть мелодию, или в разных ситуациях по-разному «пищать», то используют пьезоизлучатели без встроенного генератора и генерируют сигнал отдельно.
На нашем шилде установлен зуммер без встроенного генератора и мы сможем воспроизводить различные мелодии. Если вы не используете наш шилд, то можете просто подключить зуммер между выводом PC5 и землей, и все примеры будут работать и у вас тоже на микроконтроллере Atmega8.

Первая программа

Для начала попробуем воспроизвести простой тональный сигнал, периодически меняющий свою частоту. Скомпилируйте и загрузите следующий код:

#include <avr/io.h>
#define F_CPU 16000000UL //16MHz
#include <util/delay.h>

int main(void) {
    DDRC  |= 1<<5;
    PORTC &= ~1<<5;
    while(1) {
        for(int i=0; i<1000; i++) {
            PORTC |= 1<<5;
            _delay_us(500);
            PORTC &= ~1<<5;
            _delay_us(500);
        }
        for(int i=0; i<500; i++) {
            PORTC |= 1<<5;
            _delay_ms(1);
            PORTC &= ~1<<5;
            _delay_ms(1);
        }
    }
}

Чтобы разобраться с этой программой нужно иметь представление о физике звука. Это механические колебания с относительно низкой частотой, слышимые ухом. Чем выше частота колебаний, тем выше воспроизводимая нота.
В начале происходит настройка вывода PC5 на выход, а в основной части программы прокручиваются два цикла.
В первом цикле на 500мкс выставляется высокий уровень напряжения на выводе, а затем на 500мкс и цикл этот повторяется тысячу раз. Другими словами период сигнала составляет 1000мкс (или 1мс), а частота 1кГц. Если мы повторяем этот сигнал 1000 раз, то звучать он будет ровно одну секунду. В результате работы этого цикла в течении одной секунды будет воспроизводиться звук с частотой 1кГц.
Во втором цикле период сигнала будет 2мс, частота 500Гц, а повторяться он будет 500 раз, то есть в течении одной секунды.
В результате работы всей программы вы будете слышать, что частота сигнала меняется раз в секунду.

Star Wars — Main Theme

При желании можно прописать любую мелодию по аналогии с первой программой. Это потребует много усилий, но задача выполнимая. Такой код будет очень сложно поддерживать и если вы захотите записать другую мелодию, вам придется писать все заново.
Чтобы записать все в удобном виде и получить возможность простого изменения мелодии нам придется освоить много новых тонкостей программирования. Для начала просто, не задумываясь над кодом, запишите в микроконтроллер следующую программу:

#include <avr/io.h>
#define F_CPU 16000000UL //16MHz
#include <util/delay.h>

#define F4 349
#define A4S 466
#define F5 698
#define C5 523
#define D5 587
#define D5S 622
#define A5S 932
#define LOOPS_PAUSE 1000 //between loops, ms
#define NOTES_PAUSE 1 //between notes, ms
#define SEQU_SIZE 19 //notes quantity
#define TEMPO 108 //quarter in minute
#define WHOLE_NOTE_DUR 240000/TEMPO //ms
uint16_t notes[]={F4, F4, F4, A4S, F5, D5S, D5, C5, A5S, F5,
    D5S, D5, C5, A5S, F5, D5S, D5, D5S, C5 };
//whole note = 255
uint16_t beats[]={21, 21, 21, 128, 128, 21, 21, 21, 128, 64,
    21, 21, 21, 128, 64, 21, 21, 21, 128};
uint16_t note_duration[SEQU_SIZE];
uint32_t signal_period[SEQU_SIZE];
uint32_t elapsed_time;
uint8_t i;

void VarDelay_us(uint32_t takt) {
    while(takt--) {
        _delay_us(1);
    }
}

int main(void) {
    DDRC  |= 1<<5;
    PORTC &= ~1<<5;
    //converting notes to signal period, us
    for (i = 0; i < SEQU_SIZE; i++) {
        signal_period[i] = 1000000 / notes[i];
    }
    //converting beats to notes duration, ms
    for (i = 0; i < SEQU_SIZE; i++) {
        note_duration[i] = (WHOLE_NOTE_DUR*beats[i])/255;
    }
    while(1) {
        for (i = 0; i < SEQU_SIZE; i++) {
            elapsed_time = 0;
            while (elapsed_time < 1000*((uint32_t)note_duration[i])) {
                PORTC |= 1<<5;
                VarDelay_us(signal_period[i]/2);
                PORTC &= ~(1<<5);
                VarDelay_us(signal_period[i]/2);
                elapsed_time += signal_period[i];
            }
            _delay_ms(NOTES_PAUSE);
        }
        _delay_ms(LOOPS_PAUSE);
    }
}

Если все сделано правильно, будет воспроизводиться мелодия из фильма "Звездные войны".

Теперь давайте разбираться с программой.
После подключения библиотек сразу идет объявление частот используемых в мелодии нот. Это удобно с человеческой точки зрения, тем более что найти соответствие между нотой и частотой очень легко. Мы занесли только те ноты, которые используются в мелодии.
Затем задаются некоторые константы, о которых мы поговорим позже, после чего объявляются переменные.
Сразу хочется обратить внимание на способ объявления переменных. В таком виде очень удобно задавать целочисленные переменные с явно заданной длинной.

  • uint8_t (unsigned integer) — беззнаковая (всегда положительная) целочисленная переменная длиной 8 бит. Может принимать значения от 0 до 255 (0b11111111)
  • int8_t (signed integer) — знаковая переменная от минус 127 до 127
  • uint16_t — от 0 до 65535
  • int16_t — от минус 32767 до 32767
  • и т.д.

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

В программе объявляются два массива. Массив с нотами notes[] содержит простое перечисление нот. Этим нотам сопоставляется длительность звучания в массиве beats[]. Длительность в музыке определяется делителем ноты по отношению к целой ноте. За целую ноту принимается значение 255. Половинки, четверти, восьмые получаются путем деления этого числа.
Обратите внимание, что длительность первой же ноты не получается путем деления 255 на степень двойки. Тут придется переключиться на теорию музыки. Ноты исходной мелодии можно посмотреть здесь. Эти ноты объединены в триоли. При таком объединении три ноты по одной восьмой звучат также, как одна четвертая. Поэтому их относительная длительность 21.
Также пользователю необходимо явно указать количество нот в последовательности директивой:

#define SEQU_SIZE 19

В основной программе в первую очередь происходит пересчет массивов частот и длительность в периоды сигналов и длительность нот.
С периодами сигналов (массив signal_period[]) все просто. Чтобы получить длительность периода в микросекундах достаточно разделить 1000000 на частоту сигнала.
Для расчета абсолютной длительности звучания нот необходимо, чтобы был указан темп музыкального произведения. Делается это директивой

#define TEMPO 108

Темп в музыке, это количество четвертей за минуту. В строке

#define WHOLE_NOTE_DUR 240000/TEMPO

рассчитывается длительность целой ноты в миллисекундах. Теперь достаточно по формуле пересчитать относительные значения из массива beats[] в абсолютные массива note_duration[].
В основном цикле, переменная elapsed_time инкрементируется после каждого периода воспроизводимого сигнала на длительность этого периода до тех пор, пока не превысит длительность звучания ноты. Стоит обратить внимание на эту запись:

while (elapsed_time < 1000*((uint32_t)note_duration[i]))

Переменная elapsed_time 32ух-битная, а элементы массива notes_duration[] 16ти-битная. Если 16ти битное число умножить на 1000, то гарантированного наступит переполнение и переменная elapsed_time будет сравниваться с мусором. Модификатор (uint32_t) преобразует элемент массива notes_duration[i] в 32ух-битное число и переполнение не наступает.
В цикле воспроизведения звука вы можете увидеть еще одну особенность. В нем не получится использовать функцию _delay_us(), так как ее аргументом не может быть переменная.
Для создания таких задержек используется функция VarDelay_us(). В ней цикл с задержкой в 1мкс прокручивается заданное количество раз.

void VarDelay_us(uint32_t takt) {
    while(takt--) {
        _delay_us(1);
    }
}

При воспроизведении мелодии используется еще две задержки. Если ноты будут воспроизводиться без пауз, то они будут сливаться в одну. Для этого между ними вставлена задержка 1мс, заданная директивой:

#define NOTES_PAUSE 1

После каждого полного цикла проигрывания мелодии программа делает паузу в 1с и начинает воспроизведение заново.
В итоге мы получили код, в котором легко изменить темп, поправить длительности или полностью переписать мелодию. Для этого достаточно будет только преобразовывать только часть программы с директивами и объявлением переменных.

Индивидуальные задания

  1. В предложенной мелодии попробуйте изменить темп исполнения и сделайте паузу 5 секунд между повторами.
  2. Элементы массива beats[] принимают значения только от 0 до 255. Измените разрядность элементов массива и посмотрите в выводе компилятора, как это повлияет на объем памяти, занимаемой программой.
  3. Теперь попробуйте самостоятельно изменить мелодию. Например, вот “Имперский марш” из того же кинофильма:
    int notes[] = {A4, R,  A4, R,  A4, R,  F4, R, C5, R,  A4, R,  F4, R, C5, R, A4, R,  E5, R,  E5, R,  E5, R,  F5, R, C5, R,  G5, R,  F5, R,  C5, R, A4, R};
    int beats[]  = {50, 20, 50, 20, 50, 20, 40, 5, 20, 5,  60, 10, 40, 5, 20, 5, 60, 80, 50, 20, 50, 20, 50, 20, 40, 5, 20, 5,  60, 10, 40, 5,  20, 5, 60, 40};
    

    В этой мелодии присутствуют паузы R. Дополните код так, чтобы обработать эту особенность.

Остальные статьи цикла можно найти здесь.

Метки: , , , , , Просмотров: 12454