Формирование звука с помощью микроконтроллера не составляет большого труда. Достаточно взять за основу программу мигающего светодиода и подключить вместо него динамик, а в самой программе поменять константу задержки таким образом, чтобы частота на выходе повысилась до звукового диапазона. Человек может услышать звуки с частотой от 50Hz до 15kHz. Светодиод в одном из наших уроков мигает с частотой 4Hz, а если уменьшить время задержки в 1000 раз, то можно получить частоту на выходе 4kHz.

Задача: разработаем программу, при помощи которой микроконтроллер Atmega8 будет воспроизводить простую однотональную мелодию. Тактовая частота микроконтроллера 8МГц.

Воспроизведение однотональных мелодий с помощью AVRМузыкальный ряд делится на октавы, каждая октава делится на 12 нот, это 7 основных(До, Ре, Ми, Фа, Соль, Ля, Си) и пять дополнительных(До диез, Ре диез, Фа диез, Соль диез, Ля диез). Частоты двух соседних нот отличаются друг от друга в одинаковое количество раз, а частоты двух одноименных нот двух соседних октав отличаются в 2 раза.

Для формирования звука используем шестнадцатиразрядный таймер/счетчик1, он будет работать в режиме СТС (сброс при совпадении), для управления коэффициентом пересчета используем регистр ICR1. Режим СТС позволяет осуществлять непосредственное управление частотой сигнала. Для активизации этого режима биты WGM13 и WGM12 устанавливаем в единицу.

Для того чтобы в режиме СТС на выходе формировался периодический сигнал, необходимо настроить выход OC1A таким образом, чтобы при каждом совпадении сигнал на выходе менял свое значение на противоположное. Для этого установим бит COM1A0 в единицу и подключим к нему динамик, а также настраиваем порт PB1 на выход.

Частота сигнала на выходе OC1A определяется по формуле:



где N - коэффициент пересчета предварительного делителя, вместо OCR1A в нашем случае ICR1.

Исходя из этой формулы расчитываем коэффициенты для ICR1, зная частоты основных нот. Так как для большинства разобраться с нотной портитурой не очень просто, названия нот я определил как в редакторе мелодий старого телефона Сименс. Коды таких мелодий еще можно найти в интернете и в дальнейшем поменять в исходном тексте. Также важно понятие как длительность тона - это время звучания одной ноты, она выражается долями от целой. В общем за проигрывание ноты у нас отвечает функция play_note.

Кроме нот любая мелодия содержит паузы, это промежуток времени когда ни один звук не звучит. За паузы отвечает функция set_tempo, она попросту в нужное время отключает таймер/счетчик.

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

// Воспроизведение однотональных мелодий с помощью AVR
#include <avr/io.h>

// 1 октава 
#define A2      9008 // Ля
#define Ais2    8580 // Ля диез
#define B2      8098 // Си
// 2 октава
#define C3      7644 // До
#define Cis3    7214 // До диез
#define D3      6810 // Ре
#define Dis3    6427 // Ре диез
#define E3      6066 // Ми
#define F3      5726 // Фа
#define Fis3    5404 // Фа диез
#define G3      5101 // Соль
#define Gis3    4815 // Соль диез
#define A3      4544 // Ля
#define Ais3    4289 // Ля диез
#define B3      4049 // Си
// 3 октава
#define C4      3821 // До
#define Cis4    3607 // До диез
#define D4      3404 // Ре
#define Dis4    3213 // Ре диез
#define E4      3033 // Ми
#define F4      2862 // Фа
#define Fis4    2702 // Фа диез
#define G4      2550 // Соль
#define Gis4    2407 // Соль диез
#define A4      2272 // Ля
#define Ais4    2144 // Ля диез
#define B4      2024 // Си
// 4 октава
#define C5      1910 // До
#define Cis5    1803 // До диез
#define D5      1702 // Ре
#define Dis5    1606 // Ре диез
#define E5      1516 // Ми
#define F5      1431 // Фа
#define Fis5    1350 // Фа диез
#define G5      1275 // Соль
#define Gis5    1203 // Соль диез
#define A5      1135 // Ля
#define Ais5    1072 // Ля диез
#define B5      1011 // Си
// 5 октава
#define C6      955 // До
#define Cis6    901 // До диез
#define D6      850 // Ре
#define Dis6    803 // Ре диез
#define E6      757 // Ми
// Длительность тона 
#define LE32    1*3 // 1/32
#define LE16    2*3 // 1/16
#define LE16D   3*3
#define LE16T   2*2
#define LE8     4*3 // 1/8
#define LE8D    6*3
#define LE8T    4*2
#define LE4     8*3 // 1/4
#define LE4D    12*3
#define LE4T    8*2
#define LE2     16*3 // 1/2
#define LE2D    24*3
#define LE1     32*3 // 1

// Функция задержки в us 
void Delay_us(unsigned char time_us)
{ register unsigned char i;
 
  for(i = 0; i < time_us; i++)   // 4 цикла
    { asm (" PUSH  R0 ");       // 2 цикла
      asm (" POP   R0 ");       // 2 цикла
        // 8 циклов = 1 us для 8MHz
    }
}

// Функция задержки в ms 
void Delay_ms(unsigned int time_ms)
{ register unsigned int i;
 
  for(i = 0; i < time_ms; i++)
    { Delay_us(250);
      Delay_us(250);
      Delay_us(250);
      Delay_us(250);
    }
}
 
unsigned char temp;

// Функция установки темпа и паузы 
void set_tempo(unsigned char number)
{
temp = number;                      // Установка темпа
TCCR1A = (1 << COM1A0);             // Используем вывод таймера OC1A
TCCR1B = 0;                         // Звук выключен
}

// Функция проигрывания ноты 
void play_note(unsigned int note, unsigned int LE)
{
ICR1 = note;                        // Установка ICR1
TCNT1 = 0x0000;                      // Очистка Timer/Counter1
TCCR1B |= (1 << WGM13)|(1 << WGM12)  // CTC режим
                      |(1 << CS10);  // Без предделителя
 
Delay_ms(LE*temp*7);                 // Длительность ноты
TCCR1B = 0;                          // Звук выключен
}
 
int main(void)
{
DDRB |= (1 << PB1);
PORTB = 0x00;
 
Delay_ms(50);
 
set_tempo(3);       // Установка темпа
play_note(Dis4,LE8);
play_note(Cis4,LE8);
play_note(Fis3,LE4);
play_note(Fis4,LE4);
play_note(Fis4,LE4);
play_note(Dis4,LE8);
play_note(Cis4,LE8);
play_note(Fis3,LE4);
play_note(Fis4,LE4);
play_note(Fis4,LE4);
play_note(Dis4,LE8);
play_note(Cis4,LE8);
play_note(Fis3,LE4);
play_note(Fis4,LE4);           
play_note(Dis3,LE4);
play_note(Fis4,LE4);
play_note(Cis3,LE4);
play_note(F4,LE4); 
play_note(F4,LE4);

while(1);

}

Комментарии  

+1 #1 nevermoresk 08.06.2011 14:20
Подскажите пожалуйста, как можно передать мелодию по COM порту ?
Сообщить модератору
0 #2 AntonChip 09.06.2011 07:00
Написать другую программу, использовать выводы RX/TX
Сообщить модератору
+1 #3 nevermoresk 09.06.2011 15:39
А можете выкинуть небольшой примерчик, передачи некоторого сообщения, не обязательно мелодии, на COM порт, именно в AVR Studio ?
Сообщить модератору
+1 #4 AntonChip 09.06.2011 17:10
Собирался написать такую статью в течение месяца, а пока можете поискать на других ресурсах
Сообщить модератору
0 #5 tabarigenn 12.07.2011 13:45
доброе время суток.. на данном примере небольшая заминка возникла. не понятно где в примере используется вывод OC1A и почему нет обработчика прерываний??

_________________

кстати большое спасибо вашему сайту очень познавательные занятия.. много чего полезного узнал для себя. спасибо большое!!!
Сообщить модератору
0 #6 AntonChip 12.07.2011 17:50
В данном примере прерывания так таковые не используются, таймер 1 работает в режиме СТС, и его вывод меняет свое состояние с большой частотой, тут и получается звук
Сообщить модератору
0 #7 tabarigenn 12.07.2011 23:56
оооойййй ссори.. я забыл что вывод ОС1А у atmega162 на которой я содеоирую не вторая ножка порта В как в вашем примере.. побежал поправлять..
Сообщить модератору
0 #8 tabarigenn 13.07.2011 10:17
ну все заработало :-)
Сообщить модератору
0 #9 FreshMan 31.10.2013 17:42
прога хорошая, спору нет....., но вот как переводить ноты с нотного стана celtic-music.ru/.../... в понятные нам A2, B2, D3 и так далее ?
как это сделать просто без музыкального образования ? может есть какие-то программы ?
Сообщить модератору
0 #10 spyphy 11.12.2013 22:25
только здесь не все ноты определены, которые встречаются в мелодиях формата сименс. как оставшиеся добавить? я в этом совсем не шарю.
Сообщить модератору
0 #11 Артём 31.01.2014 17:18
подскажите как в Attiny13 сделать 16 разрядный программный счетчик чтобы точно также можно было выводить мелодии. Можно хотя бы просто алгоритм в двух словах, как это сделать, а дальше сам как нибудь.
Сообщить модератору
0 #12 Александр Диордица 13.03.2015 02:30
подскажите как в Attiny13 сделать точно также как нибудь...
integrator.adior.ru/.../436 - здесь теория
integrator.adior.ru/.../437 - здесь музыкальный автомат. Все на Ассемблере, но с подробными комментариями и с инструкцией по компиляции и заливке.
В примере Полонез Огинского.
Сообщить модератору
0 #13 Ренат 11.04.2016 19:17
не понимаю как паузы делать... :sad:
Сообщить модератору
0 #14 Serg26 29.06.2019 23:48
То ли лыжи не едут, то ли автор недоговаривает. ... Формула для расчета частоты на OC1A явно неверная. Там двойка в знаменателе лишняя? Я пробую пересчитывать значение для той же ноты ЛЯ (440 Гц), и, если считать по приведенной формуле, то никак не получится значение Код: #define A4 2272 // Ля. Частота 8 МГц, делитель 8. По формуле получаем 8000000/( 2 * 440 * 8 ) = 1136.
Сообщить модератору
0 #15 AntonChip 30.06.2019 18:45
Формула правильная, не так считаете
Код:
8000000/(2*1*(1+2272)) = 1759,7 Гц

частота соответствует ноте ля третей октавы
Сообщить модератору
0 #16 Gravechapa 14.02.2020 21:15
В документации atmega8 сказано, что для предделителя = 8 выставляется бит CS11. В данном коде выставляется CS10, получается предделитель = 1.
99-я страница:
ww1.microchip.com/.../...
Сообщить модератору
0 #17 AntonChip 16.02.2020 17:31
Цитирую Gravechapa:
В документации atmega8 сказано, что для предделителя = 8 выставляется бит CS11. В данном коде выставляется CS10, получается предделитель = 1

Да, действительно, без предделителя, в коде исправил, спасибо
Сообщить модератору
0 #18 jroyce 29.02.2020 00:25
Здравствуйте!
Во-первых, хотел бы отметить, что в статье опечатка: для того чтобы состояние вывода OC1A менялось на противоположное в единицу нужно установить бит COM1A0, а COM1A1 оставить нулём. В коде, кстати, правильно написано.
Во-вторых, почему используется регистр захвата ICR1, а не регистр сравнения? Я с регистром захвата только разбираюсь, но здесь, мне кажется, можно было бы с помощью блока сравнения реализовать или я ошибаюсь и не вижу каких-то подводных камней?
Заранее спасибо!
Сообщить модератору
0 #19 AntonChip 29.02.2020 08:15
Цитирую jroyce:
Хотел бы отметить, что в статье опечатка: для того чтобы состояние вывода OC1A менялось на противоположное в единицу нужно установить бит COM1A0, а COM1A1 оставить нулём. В коде, кстати, правильно написано.

Спасибо, исправил
Сообщить модератору
0 #20 AntonChip 29.02.2020 08:21
Цитирую jroyce:
Почему используется регистр захвата ICR1, а не регистр сравнения?
Хотел показать что вместо регистра сравнения в некоторых случаях можно использовать альтернативу регистр ICR1, а так пожалуйста можно использовать в коде регистр OCR1A
Сообщить модератору