Музыка в Arduino может быть фоновой. У меня появилась идея написать новую музыкальную программу или библиотеку для музыкальной программы.
Идеи появляются не на пустом месте. Первый музыкальный автомат я собрал 30 лет назад, в нём был генератор, подстроечные резисторы и дешифратор. Недостатки этой схемы побудили меня разработать свою схему и тогда я разработал цифровой таймер-счётчик на D-триггерах, управляемый по программе из прожигаемого ПЗУ. Последние 8 лет я программирую микроконтроллеры. Первый мой музыкальный автомат был разработан на Ассемблере для м-к ATtiny13. Тогда, почему-то, захотелось написать генератор нот без использования таймеров-счётчиков м-к. И это получилось! Писал для Arduino с использованием таймеров-счётчиков м-к. Это просто. Недавно я закончил серьёзный проект на ATmega328. В этом проекте задачи добавлялись несколько раз так, что в конце, чтобы всё заработало, пришлось оптимизировать программу по объёму, и главное по использованию памяти, переписать насколько библиотек и разработать 2 свои. Теперь я более оптимистично смотрю на ресурсы Ардуино.
Сегодня появилась идея к которой не приступишь в лоб. Нужна концепция. Вот об этом и буду писать.
- Музыка в Arduino может быть фоновой, она может звучать в то время как выполняется основная программа. Должна быть многозадачность. Значит надо будет использовать прерывания.
- Одного байта достаточно чтобы хранить код ноты и её длительность. Отведём 3 бита под длительности 1, 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, "Конец". 5 бит под коды нот (31 нота + пауза). Номер паузы 0.
- Так как массив нот будет храниться в шифрованном виде, готовить этот массив надо будет во вспомогательной программе, например в Open Office.
- Так как оперативной памяти в м-к мало, коды нот будем хранить в Flash памяти программ м-к.
- Так как музыкальное произведение чаще всего состоит из повторяющихся фрагментов, в программе должны быть предусмотрены вызовы этих фрагментов.
- В программе должны быть предусмотрены команды управления воспроизведением (вперёд, назад, стоп, по номеру, повтор, повтор всего).
Кажется, с концепцией определились.
Возможная техническая реализация.
Длительность ноты.
Пусть WDT (Watchdog) отвечает за длительность ноты. Распишем коды длительностей:
Длительность | Сек | Команда | Код команды | Код 3 бита | Инверсный код |
---|---|---|---|---|---|
1/64 | 0.015625 | WDTCSR = (1 << WDIE ); | 0b01000000 | 000 | 7 |
1/32 | 0 .03125 | WDTCSR = (1 << WDP0) | (1 << WDIE ); | 0b01000001 | 001 | 6 |
1/16 | 0.0625 | WDTCSR = (1 << WDP1) | (1 << WDIE ); | 0b01000010 | 010 | 5 |
1/8 | 0.125 | WDTCSR = (1 << WDP0) | (1 << WDP1) | (1 << WDIE ); | 0b01000011 | 011 | 4 |
1/4 | 0.25 | WDTCSR = (1 << WDP2) | (1 << WDIE ); | 0b01000100 | 100 | 3 |
1/2 | 0.5 | WDTCSR = (1 << WDP0) | (1 << WDP2) | (1 << WDIE ); | 0b01000101 | 101 | 2 |
1 | 1 | WDTCSR = (1 << WDP1) | (1 << WDP2) | (1 << WDIE ); | 0b01000110 | 110 | 1 |
Конец | 111 | 0 |
Табл. 1. Коды длительностей нот.
; ***** WATCHDOG *********************
; WDTCSR - Watchdog Timer Control Register
.equ WDP0 = 0 ; Watch Dog Timer Prescaler bit 0
.equ WDP1 = 1 ; Watch Dog Timer Prescaler bit 1
.equ WDP2 = 2 ; Watch Dog Timer Prescaler bit 2
.equ WDE = 3 ; Watch Dog Enable
.equ WDCE = 4 ; Watchdog Change Enable
.equ WDP3 = 5 ; Watchdog Timer Prescaler Bit 3
.equ WDIE = 6 ; Watchdog Timeout Interrupt Enable
.equ WDIF = 7 ; Watchdog Timeout Interrupt Flag
Лист. 1. Биты регистра WDTCSR микроконтроллера ATmega328
Частота ноты.
Пусть 16-ти битный таймер отвечает за частоту ноты (высоту тона).
В м-к ATmega328 один 16-ти битный таймер-счётчик - это "Timer/Counter1". Timer/Counter1 (ТС1) может работать в режиме генератора частоты.
Таймер-счётчик ТС1 микроконтроллера, работая в режиме СТС (сравнение и сброс таймера при совпадении, один из четырёх режимов), ведёт подсчёт тактовых импульсов до совпадения с числом, заданным программистом в любом из регистров совпадения(OCR1A или OCR1B). В случае совпадения, сигнал, на выходе счётчика меняется на противоположный и счёт начинается сначала. Таймер/счётчик TC1 имеет выходы на порты микроконтроллера PB1 и PB2 (9 и 10 pin Arduino или 15 и 16 pin м-к ATmega328-PU). Таймер-счётчик приводятся в действие от системного тактового генератора, работающего на частоте 16 МГц. Если записать в регистр совпадения 0, Таймер-счётчик делит частоту на входе на 2. Если мы хотим получить с помощью таймера-счётчика, заданную частоту, мы должны записать в 16-ти битный регистр совпадения (OCR1A или OCR1B) коэффициент деления, который можно вычислить по формуле, Формуле. 1.
Формула. 1. Коэффициент деления ТС1,
В datashit на м-к ATmega328 в формуле 1 записано (1+k), но это в нашей задаче не принципиально.
Частота любой музыкальной ноты F может быть вычислена по формуле Ф.2, где N порядковый номер ноты.
Формула. 2. Частота любой музыкальной ноты.
Нота ЛЯ первой октавы имеет порядковый номер 48 и её частота, вычисленная по этой формуле равна 440 Гц. В октаве 7 нот (ДО, РЕ, МИ, ФА, СОЛЬ, ЛЯ, СИ), однако, они распределены по частоте неравномерно. На фортепианной клавиатуре вы видели что есть белые и чёрные клавиши. Белые клавиши мы уже перечислили, это основные ноты. Чёрные клавиши дополняют белые так, что все ноты в октаве, а их теперь 12, распределяются по частоте равномерно, следуя математическому закону по Формуле.2.
Перечислим все 12 нот одной октавы: ДО ДО# РЕ РЕ# МИ ФА ФА# СОЛЬ СОЛЬ# ЛЯ ЛЯ# СИ Знак # читается как диез и ставится на нотной линейке перед нотой или в начале строки. Отменяется знаком бекар ????. Теперь должно быть понятно, что нота ЛЯ второй октавы имеет номер 60 и её частота 880 Гц. Очевидно, что частота двух одноименных нот из соседних октав отличается в ровно 2 раза. Эти ноты воспринимаются на слух, как очень похожие, только одна выше другой. В программе на языке Си для Ардуино, чтобы извлечь желаемую ноту, мы должны будем записать (присвоить) в 16-ти битный регистр совпадения (OCR1A или OCR1B) коэффициент деления вычисленный для этой ноты.
Для автоматизации расчёта коэффициентов деления для всех нот мы воспользовались электронной таблицей Google документы (Можно использовать LibreOffice Calc). В результате получилась таблица табл. 2.
A | B | C | D | E | F | |
---|---|---|---|---|---|---|
1 | № | N | Октава | Нота | F Gz | К дел. |
2 | 1 | 27 | 0 | До | 131 | 61156 |
3 | 2 | 28 | 0 | До# | 139 | 57724 |
4 | 3 | 29 | 0 | Ре | 147 | 54484 |
5 | 4 | 30 | 0 | Ре# | 156 | 51426 |
6 | 5 | 31 | 0 | Ми | 165 | 48540 |
7 | 6 | 32 | 0 | Фа | 175 | 45815 |
8 | 7 | 33 | 0 | Фа# | 185 | 43244 |
9 | 8 | 34 | 0 | Соль | 196 | 40817 |
10 | 9 | 35 | 0 | Соль# | 208 | 38526 |
11 | 10 | 36 | 0 | Ля | 220 | 36364 |
12 | 11 | 37 | 0 | Ля# | 233 | 34323 |
13 | 12 | 38 | 0 | Си | 247 | 32396 |
14 | 13 | 39 | 1 | До | 262 | 30578 |
15 | 14 | 40 | 1 | До# | 277 | 28862 |
16 | 15 | 41 | 1 | Ре | 294 | 27242 |
17 | 16 | 42 | 1 | Ре# | 311 | 25713 |
18 | 17 | 43 | 1 | Ми | 330 | 24270 |
19 | 18 | 44 | 1 | Фа | 349 | 22908 |
20 | 19 | 45 | 1 | Фа# | 370 | 21622 |
21 | 20 | 46 | 1 | Соль | 392 | 20408 |
22 | 21 | 47 | 1 | Соль# | 415 | 19263 |
23 | 22 | 48 | 1 | Ля | 440 | 18182 |
24 | 23 | 49 | 1 | Ля# | 466 | 17161 |
25 | 24 | 50 | 1 | Си | 494 | 16198 |
26 | 25 | 51 | 2 | До | 523 | 15289 |
27 | 26 | 52 | 2 | До# | 554 | 14431 |
28 | 27 | 53 | 2 | Ре | 587 | 13621 |
29 | 28 | 54 | 2 | Ре# | 622 | 12856 |
30 | 29 | 55 | 2 | Ми | 659 | 12135 |
31 | 30 | 56 | 2 | Фа | 698 | 11454 |
32 | 31 | 57 | 2 | Фа# | 740 | 10811 |
33 | 32 | 58 | 2 | Соль | 784 | 10204 |
34 | 33 | 59 | 2 | Соль# | 831 | 9631 |
35 | 34 | 60 | 2 | Ля | 880 | 9091 |
36 | 35 | 61 | 2 | Ля# | 932 | 8581 |
37 | 36 | 62 | 2 | Си | 988 | 8099 |
38 | 37 | 63 | 3 | До | 1047 | 7645 |
39 | 38 | 64 | 3 | До# | 1109 | 7215 |
40 | 39 | 65 | 3 | Ре | 1175 | 6810 |
41 | 40 | 66 | 3 | Ре# | 1245 | 6428 |
42 | 41 | 67 | 3 | Ми | 1319 | 6067 |
43 | 42 | 68 | 3 | Фа | 1397 | 5727 |
44 | 43 | 69 | 3 | Фа# | 1480 | 5405 |
45 | 44 | 70 | 3 | Соль | 1568 | 5102 |
46 | 45 | 71 | 3 | Соль# | 1661 | 4816 |
47 | 46 | 72 | 3 | Ля | 1760 | 4545 |
48 | 47 | 73 | 3 | Ля# | 1865 | 4290 |
49 | 48 | 74 | 3 | Си | 1976 | 4050 |
Табл. 2. Коэффициенты деления для всех нот.
В табл. 2 в ячейку E2 мы вставили формулу =ОКРУГЛТ(27,5*2^(B2/12); 1) и растянули её на всю колонку E. В ячейку F2 мы вставили формулу =ОКРУГЛТ(8000000/(27,5*2^(B2/12)); 1) и растянули её на всю колонку F.
На основании табл. 2 мы можем составить массив коэффициентов деления {61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817, 38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270, 22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431, 13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099, 7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050}.
С таким же успехом, можно использовать 12 коэффициентов деления, но тогда прерывания будут выполнятся дольше, за счёт лишних операций по вычислению коэффициентов деления.
Замечание, так как мы планируем использовать в любом музыкальном произведении только 31 ноту (2,5 октавы), то у нас будет необходимость транспонировать, повышать или понижать тональность произведения, что мы легко сможем сделать сдвигая цифры в колонке A вверх или вниз.
Управление таймером-счётчиком TC1.
void setup() {
// инициализация Timer/Counter-1
pinMode (9, OUTPUT); // PB1/OC1A как Output порт
TCCR1A = 0;
TCCR1B = 0;
TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12)); // Таймер остановлен.
TCCR1B |= (1 << WGM12); // CTC режим.
TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0)); // Порты PB1/PB2 от OC1A/OC1B отключены.
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Порт PB1 подключен к OC1A и обнуляется при совпадении.
TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0); // Порт PB1 подключен к OC1A и инвертируется при совпадении.
OCR1A = 18182; // установка регистра совпадения. Нота Ля 440 Гц.
TCCR1B |= (1 << CS10); // запуск с таймера с делителем на 1.
}
void loop() {
}
Лист. 2. Программное управление таймером счётчиком TC1.
Рис. 1. Осциллограмма снятая на 9 ножке платы Arduino Nano.
Принципиальная электрическая схема.
В программе лист. 2 мы организовали подключение выхода таймера-счётчика TC1 на порт микроконтроллера PB1, который, в свою очередь, подключён к pin 9 платы Arduino NANO или UNO.
Рис. 2. Принципиальная электрическая схема музыкального автомата.
На принципиальной электрической схеме рис. 2 однокаскадный импульсный усилитель на транзисторе 2N2222 своим входом подключён к pin 9 платы Arduino, а нагрузкой этого усилителя является высокоомный динамик. У транзистора 2N2222 типичный коэффициент усиления по току в схеме с общим эмиттером равен 300. В ключевом режиме работы ток коллектора транзистора примерно равен 5/30=0,16 Ампер. Где 5 Вольт напряжение питания схемы, а 30 Ом сопротивление динамика. Сопротивлением открытого транзистора можно пренебречь. Для полного открывания транзистора ток базы этого транзистора должен быть не менее 0,16/300=0,00055 Ампер. Где 0,16 Ампер ток коллектора транзистора, а 300 коэффициент усиления по току. Ограничивающий ток базы резистор должен быть не более 4,5/0,00055=8100 Ом. Где 4,5 Вольт напряжение единичного импульса на выходе микроконтроллера.
Например, Вы не нашли высокоомный динамик, типичное сопротивление маломощных динамиков 8 Ом, и такой у Вас есть. Задача: рассчитать сопротивление резистора в цепи базы транзистора. 5/8=0,625А. 0,625/300=0,002А. 4,5/0,02=2160 Ом.
Нотная грамотность.
Хороший программист, создавая свой программный продукт для целей автоматизации какого либо рода деятельности человека в производстве, науке или искусстве должен разбираться в этой области на уровне специалиста. Создавая программу музыкального автомата, программист должен, как минимум, изучить музыкальную нотацию. Не плохо было бы подтянуть себя по сольфеджио.
Рис. . Основы музыкальной нотации.
Лист. 3. Ноты Рождественской песенки Jingle Bells.
Закодируем эти ноты в соответствии с таблицами 1 и 2, для начала, каждой ноте будет соответствовать 2 цифры № ноты и её длительность. Коды сразу пишем в таблицу Google документы или LibreOffice Calc.
A | B | C | |
---|---|---|---|
1 | № ноты | Длительность | Код |
3 | 17 | 3 | 139 |
5 | 17 | 3 | 139 |
7 | 17 | 2 | 138 |
9 | 20 | 3 | 163 |
11 | 13 | 4 | 108 |
13 | 17 | 1 | 137 |
14 | 0 | 0 | 0 |
Табл. 3. Коды нот первых четырёх тактов песенки Jingle Bells.
В ячейку C3 табл. 3 мы поместили формулу =A2*8+B2 и растянули её на все следующие ячейки в колонке C.
В результате, у нас есть массив из кодов нот первых четырёх тактов песенки Jingle Bells и их длительностей:
{139, 139, 138, 139, 139, 138, 139, 163, 107, 108, 124, 137, 0}
Управление прерываниями Watchdog таймера (WDT).
Напишем программу инициализации прерываний WDT с примером использования этих прерываний.
// Массив из кодов нот первых четырёх тактов песенки Jingle Bells и их длительностей
byte volatile x[] = {1, 139, 139, 138, 139, 139, 138, 139, 163, 107, 108, 124, 137, 0};
// Массив коэффициентов деления для таймера-счётчика TC1
int volatile y[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817, 38526, 36364, 34323, 32396, 30578, 28862, 27242,
25713, 24270, 22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431, 13621, 12856, 12135, 11454,
10811, 10204, 9631, 9091, 8581, 8099, 7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
};
ISR(WDT_vect) { // Обработчик прерываний WDT
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Звук выкл.
int static n = 0; // Смещение в массиве из кодов нот
byte t = 0; // Код длительности ноты
if (x[n] != 0) { // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
if ((x[n] / 8) != 0) { // Если Если только код ноты = 0 это ПАУЗА в муз. произведении
if (x[n] / 8 == x[n - 1] / 8) { // Если № ноты = № предыдущей ноты, надо организовать СТАККАТО
for (int i = 0; i < 500; i++) { // Задержка между соседними нотами
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
}
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
t = x[n]; // Вычисляем код длительности ноты
t = ~t & 0b00000111; // Код длительности ноты - это инвертированные младшие три бита кода ноты
asm("wdr"); // Сбрасываем WDT
WDTCSR |= (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = t | (1 << WDIE ); // Устанавливаем длительность и разрешаем прерывания от WDT
OCR1A = y[x[n] / 8]; // Выбор ноты
TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0); // Звук вкл.
}
n++; // Приращение смещения в массиве, только если это не КОНЕЦ муз. фрагмента
}
}
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode (9, OUTPUT);
// инициализация WDT
cli(); // Запрт прерываний
asm("wdr"); // Сбрасываем WDT
WDTCSR |= (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = (1 << WDP2) | (1 << WDIE ); // Время 1/4 и разрешение прерывания от WDT
sei(); // Разрешение прерываний
// инициализация таймера-счётчика TC1
TCCR1A = 0;
TCCR1B = 0;
TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12)); // Таймер остановлен.
TCCR1B |= (1 << WGM12); // CTC режим.
TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0)); // Порты PB1/PB2 от OC1A/OC1B отключены.
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Порт PB1 подключен к OC1A и обнуляется при совпадении.
TCCR1B |= (1 << CS10); // запуск с таймера с делителем на 1.
}
void loop() {
}
Лист. 4. Программа исполняющая первые четыре такта песенки Jingle Bells.
В результате разработки программы лист. 4, был выработан формат массива кодов нот музыкального фрагмента и массива коэффициентов деления для таймера-счётчика TC1. Массив кодов нот музыкального фрагмента должен заканчиваться "0". Массив коэффициентов деления для таймера-счётчика TC1 должен начинаться с "0".
Размещение массива данных в Flash-памяти м-к.
Программа лист. 4 использует память м-к следующим образом:
Скетч использует 1360 байт (4%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 123 байт (6%) динамической памяти, оставляя 1925 байт для локальных переменных. Максимум: 2048 байт.
Если хранить коды нот в динамической памяти, то мы сможем записать в м-к всего 1-2 мелодии. Flash память программ в м-к может хранить в десятки раз больше мелодий, но доступ к данным в Flash памяти специфичен. Перепишем программу лист. 4 так, что-бы массивы данных размещались в Flash памяти.
// Массив из кодов нот первых четырёх тактов песенки Jingle Bells и их длительностей
PROGMEM const byte notaCode[] = {139,139,138,139,139,138,139,163,107,108,124,137,147,
147,147,148,148,147,139,139,140,140,139,123,123,139,
122,162,139,139,138,139,139,138,139,163,107,108,124,
137,147,147,147,148,148,147,139,139,140,140,163,163,
147,123,105, 67,139,123,107, 66, 67, 68, 68, 67,139,
123,107, 81, 83,147,139,123, 97,163,163,147,123,137,
67,139,123,107, 81, 67,139,123,107, 82, 83, 83, 83,
147,139,123,163,163,163,163,179,163,147,123,105, 0
};
// Массив коэффициентов деления для таймера-счётчика TC1
PROGMEM const word timerCounter[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817,
38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270,
22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431,
13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099,
7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
};
ISR(WDT_vect) {
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Звук выкл.
int static nNote = 0; // Номер ноты в музыкальном фрагменте
boolean static staccato = 0;
staccato = !staccato; // Если 0, задержка между соседними нотами - СТАККАТО
if (staccato) {
byte lengthNote = pgm_read_byte_near(notaCode + nNote); // Код ноты и её длительности
if (lengthNote) { // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
byte tmp = lengthNote; // Длительность ноты
tmp = ~tmp & 0b00000111; // Код длительности ноты - это инвертированные младшие три бита кода ноты
asm("wdr"); // Сбрасываем WDT
WDTCSR = (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = tmp; // Устанавливаем длительность
WDTCSR = 0 | (1 << WDIE ); // Запрещаем RESET и разрешаем прерывания от WDT
nNote++; // Приращение номера ноты в музыкальном фрагменте
lengthNote = lengthNote / 8; // Вычисляем номер ноты в массиве timerCounter
if (lengthNote) { // Если номер ноты не равен 0
OCR1A = pgm_read_word_near(timerCounter + lengthNote); // Выбор ноты
TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0); // Звук вкл.
}
}
}
else { // Задержка между соседними нотами - СТАККАТО
asm("wdr"); // Сбрасываем WDT
WDTCSR = (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = 0 | (1 << WDIE ); // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
}
}
void setup() {
pinMode (9, OUTPUT);
// инициализация WDT
cli(); // Запрт прерываний
asm("wdr"); // Сбрасываем WDT
WDTCSR |= (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = 0 | (1 << WDIE ); // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
sei(); // Разрешение прерываний
// инициализация таймера-счётчика TC1
TCCR1A = 0;
TCCR1B = 0;
TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12)); // Таймер остановлен.
TCCR1B |= (1 << WGM12); // CTC режим.
TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0)); // Порты PB1/PB2 от OC1A/OC1B отключены.
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Порт PB1 подключен к OC1A и обнуляется при совпадении.
TCCR1B |= (1 << CS10); // запуск с таймера с делителем на 1.
}
void loop() {
}
Лист. 5. Программа исполняющая песенку Jingle Bells с использованием Flash памяти для хранения констант.
Программа лист. 5 использует меньше динамической памяти чем программа лист. 4.
Скетч использует 1026 байт (3%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 12 байт (0%) динамической памяти, оставляя 2036 байт для локальных переменных. Максимум: 2048 байт.
Управление воспроизведением музыкальных фрагментов.
Массив кодов нот песенки Jingle Bells, не содержит повторяющихся частей. Но, практически, любая песня содержит куплет и припев и, как правило, ноты всех куплетов не отличаются друг от друга, а у припева, и подавно.
Но, для дальнейшего программирования, мы возьмём ещё боле сложное и интересное музыкальное произведение чем песенка.
Лист. 6. Ноты фортепианной пьесы Людвига ван Бетховена «К Элизе» (нем. Für Elise).
Обратите внимание на лист. 6, мы нашли в произведении Людвига ван Бетховена «К Элизе» один фрагмент мелодии повторяющийся 3 раза (окрашено в розовый цвет) и один такт повторяющийся 2 раза. Но это ещё не все повторения в этом произведении. Первая половина произведения «К Элизе» повторяется 2 раза. Причём, сначала звучит 9 тактов (в девятом такте нота ля длительностью 1/4). Обратите внимание, девятый и первый такт не полные. После звучания ноты ля длительностью 1/4 из девятого такта, начинается воспроизведение с первого такта в котором 2 ноты длительностью 1/16 каждая. В результате девятый такт вместе с первым имеют длительность 3/8.
Потом звучит 2, 3, 4, 5, 6, 7, 8 такты, а далее 10, 11 .... и до конца произведения.
Программист все повторения может просто запрограммировать. Но, для начала нам надо закодировать все музыкальные фрагменты произведения Людвига ван Бетховена «К Элизе» без повторений.
A | B | C | D | |
---|---|---|---|---|
1 | № ноты | Длительность | Код | Адрес фрагмента |
2 | 0 | 2 | 2 | 0 |
3 | 29 | 4 | 236 | 1 |
4 | 28 | 4 | 228 | 2 |
5 | 0 | 0 | 0 | 3 |
6 | 29 | 4 | 236 | 4 |
7 | 28 | 4 | 228 | 5 |
8 | 29 | 4 | 236 | 6 |
9 | 24 | 4 | 196 | 7 |
10 | 27 | 4 | 220 | 8 |
11 | 25 | 4 | 204 | 9 |
12 | 22 | 3 | 179 | 10 |
13 | 0 | 4 | 4 | 11 |
14 | 13 | 4 | 108 | 12 |
15 | 17 | 4 | 140 | 13 |
16 | 22 | 4 | 180 | 14 |
17 | 0 | 0 | 0 | 15 |
18 | 24 | 3 | 195 | 16 |
19 | 0 | 4 | 4 | 17 |
20 | 17 | 4 | 140 | 18 |
21 | 21 | 4 | 172 | 19 |
22 | 24 | 4 | 196 | 20 |
23 | 25 | 3 | 203 | 21 |
24 | 17 | 4 | 140 | 22 |
25 | 29 | 4 | 236 | 23 |
26 | 28 | 4 | 228 | 24 |
27 | 0 | 0 | 0 | 25 |
28 | 24 | 3 | 195 | 26 |
29 | 0 | 4 | 4 | 27 |
30 | 17 | 4 | 140 | 28 |
31 | 25 | 4 | 204 | 29 |
32 | 24 | 4 | 196 | 30 |
33 | 0 | 0 | 0 | 31 |
34 | 22 | 2 | 178 | 32 |
35 | 0 | 0 | 0 | 33 |
36 | 22 | 3 | 179 | 34 |
37 | 0 | 4 | 4 | 35 |
38 | 24 | 4 | 196 | 36 |
39 | 25 | 4 | 204 | 37 |
40 | 27 | 4 | 220 | 38 |
41 | 29 | 3 | 235 | 39 |
42 | 0 | 4 | 4 | 40 |
43 | 20 | 4 | 164 | 41 |
44 | 30 | 4 | 244 | 42 |
45 | 29 | 4 | 236 | 43 |
46 | 27 | 3 | 219 | 44 |
47 | 0 | 4 | 4 | 45 |
48 | 18 | 4 | 148 | 46 |
49 | 29 | 4 | 236 | 47 |
50 | 27 | 4 | 220 | 48 |
51 | 25 | 3 | 203 | 49 |
52 | 0 | 4 | 4 | 50 |
53 | 17 | 4 | 140 | 51 |
54 | 27 | 4 | 220 | 52 |
55 | 25 | 4 | 204 | 53 |
56 | 24 | 3 | 195 | 54 |
57 | 0 | 4 | 4 | 55 |
58 | 17 | 4 | 140 | 56 |
59 | 29 | 4 | 236 | 57 |
60 | 28 | 4 | 228 | 58 |
61 | 0 | 0 | 0 | 59 |
62 | 22 | 2 | 178 | 60 |
63 | 0 | 0 | 0 | 61 |
Табл. 4. Коды нот произведения Людвига ван Бетховена «К Элизе» без повторений.
В программе на языке C++ мы должны предусмотреть возможность воспроизводить некоторые музыкальные фрагменты по несколько раз.
// Массив инструкций переходов
PROGMEM const word order[] {4, 16, 4, 26, 32, 1, 4, 16, 4, 26, 34, 4, 26, 60, 0};
// Массив из кодов нот и их длительностей
PROGMEM const byte notaCode[] = {2, 236, 228, 0, 236, 228, 236, 196, 220, 204, 179, 4, 108, 140, 180, 0,
195, 4, 140, 172, 196, 203, 140, 236, 228, 0, 195, 4, 140, 204, 196, 0,
178, 0, 179, 4, 196, 204, 220, 235, 4, 164, 244, 236, 219, 4, 148, 236,
220, 203, 4, 140, 220, 204, 195, 4, 140, 236, 228, 0, 178, 0
};
// Массив коэффициентов деления для таймера-счётчика TC1
PROGMEM const word timerCounter[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817,
38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270,
22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431,
13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099,
7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
};
ISR(WDT_vect) {
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Звук выкл.
int static nOrder = 0; // Номер инструкции в массиве Order
int static nNote = 0; // Номер ноты в музыкальном фрагменте
boolean static staccato = 0;
staccato = !staccato; // Если 0, задержка между соседними нотами - СТАККАТО
if (staccato) {
byte lengthNote = pgm_read_byte_near(notaCode + nNote); // Код ноты и её длительности
if (lengthNote) { // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
byte tmp = lengthNote; // Длительность ноты
tmp = ~tmp & 0b00000111; // Код длительности ноты - это инвертированные младшие три бита кода ноты
asm("wdr"); // Сбрасываем WDT
WDTCSR = (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = tmp; // Устанавливаем длительность
WDTCSR = 0 | (1 << WDIE ); // Запрещаем RESET и разрешаем прерывания от WDT
nNote++; // Приращение номера ноты в музыкальном фрагменте
lengthNote = lengthNote / 8; // Вычисляем номер ноты в массиве timerCounter
if (lengthNote) { // Если номер ноты не равен 0
OCR1A = pgm_read_word_near(timerCounter + lengthNote); // Выбор ноты
TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0); // Звук вкл.
}
}
else { // КОНЕЦ муз. фрагмента
if (pgm_read_word_near(order + nOrder)) { // Если не конец произведения
nNote = pgm_read_word_near(order + nOrder);
nOrder ++;
}
}
}
else { // Задержка между соседними нотами - СТАККАТО
asm("wdr"); // Сбрасываем WDT
WDTCSR = (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = 0 | (1 << WDIE ); // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
}
}
void setup() {
pinMode (9, OUTPUT);
// инициализация WDT
cli(); // Запрт прерываний
asm("wdr"); // Сбрасываем WDT
WDTCSR |= (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = 0 | (1 << WDIE ); // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
sei(); // Разрешение прерываний
// инициализация таймера-счётчика TC1
TCCR1A = 0;
TCCR1B = 0;
TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12)); // Таймер остановлен.
TCCR1B |= (1 << WGM12); // CTC режим.
TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0)); // Порты PB1/PB2 от OC1A/OC1B отключены.
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Порт PB1 подключен к OC1A и обнуляется при совпадении.
TCCR1B |= (1 << CS10); // запуск с таймера с делителем на 1.
}
void loop() {
}
Лист. 7. Программа исполняющая фортепианную пьесу Людвига ван Бетховена «К Элизе».
Массивы в музыкальной программе.
Массив order в программе лист. 7 содержит адреса первых нот музыкальных фрагментов в массиве notaCode. Фактически, в массиве order записаны индексы массива notaCode. Каждый элемент массива order - это инструкция, в каком порядке следует исполнять музыкальные фрагменты. Но, в нашей музыкальной программе так же нужны инструкции, позволяющие использовать в музыкальном произведении более 31 ноты. То есть, нужна инструкция, позволяющая тридцать одному номеру ноты поставить в соответствие различные наборы нот.
В программе лист. 7 в строке lengthNote = lengthNote / 8 мы из онобайтного кода ноты и её длительности вычисляем код ноты. Если к полученному коду ноты прибавить число (смещение), то мы получим новый набор соответствий кодов нот (номеров нот) и их частоте (высоте ноты). Чтобы можно было гибко выбирать музыкальный диапазон из 31 ноты, нам нужны смещения от 1 до 49 - 31 = 18 и мы охватим 3 октавы (можно и больше). Для 18 смещений создавать новый массив команд, или изменять структуру массива order, считаю, не рационально. Можно в массиве order считать число от 1 до 18 командой изменить высоту нот в музыкальном фрагменте, а числа от 20 до 32767 считать адресом музыкального фрагмента. Такой подход наложит не существенные ограничения на использование младших 20 адресов в массиве notaCode.
А что если использовать в массиве order значения меньше 32000 как адреса музыкального фрагмента, а числа от 32001 до 32018 как команду изменить высоту нот. Это рационально так как старшие адреса Flash памяти микроконтроллера не будут использоваться для хранения массива notaCode. Значит, так и поступим.
Лист. 8. Ноты Полонеза Огинского.
// Массив инструкций переходов
PROGMEM const word order[] {32016, 1, 1, 0};
// Массив из кодов нот и их длительностей
PROGMEM const byte notaCode[] = {0, 105, 100, 108, 115, 107, 75, 76, 68, 50, 75, 107,
107, 146, 107, 131, 123, 114, 91, 163, 164, 148, 139,
140, 116, 107, 108, 92, 107, 108, 92, 74, 75, 51, 68,
76, 68, 52, 68, 92, 116, 108, 92, 76, 68, 76, 52, 76,
108, 148, 170, 147, 164, 172, 164, 148, 164, 188, 212,
204, 188, 172, 164, 148, 140, 148, 164, 140, 162, 146, 0
};
// Массив коэффициентов деления для таймера-счётчика TC1
PROGMEM const word timerCounter[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817,
38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270,
22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431,
13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099,
7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
};
ISR(WDT_vect) {
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Звук выкл.
byte static hTone = 0; // Высота тона, тональность
int static nOrder = 0; // Номер инструкции в массиве Order
int static nNote = 0; // Номер ноты в музыкальном фрагменте
boolean static staccato = 0;
staccato = !staccato; // Если 0, задержка между соседними нотами - СТАККАТО
if (staccato) {
byte lengthNote = pgm_read_byte_near(notaCode + nNote); // Код ноты и её длительности
if (lengthNote) { // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
byte tmp = lengthNote; // Длительность ноты
tmp = ~tmp & 0b00000111; // Код длительности ноты - это инвертированные младшие три бита кода ноты
asm("wdr"); // Сбрасываем WDT
WDTCSR = (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = tmp; // Устанавливаем длительность
WDTCSR = 0 | (1 << WDIE ); // Запрещаем RESET и разрешаем прерывания от WDT
nNote++; // Приращение номера ноты в музыкальном фрагменте
lengthNote = lengthNote / 8; // Вычисляем номер ноты в массиве timerCounter
if (lengthNote) { // Если номер ноты не равен 0
OCR1A = pgm_read_word_near(timerCounter + lengthNote + hTone); // Выбор ноты
TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0); // Звук вкл.
}
}
else { // КОНЕЦ муз. фрагмента
if (pgm_read_word_near(order + nOrder)) { // Если не конец произведения
// hTone = 0; // Восстановить тональность
if (pgm_read_word_near(order + nOrder) >= 32000) { // Если order[n] команда изменить высоту нот
hTone = pgm_read_word_near(order + nOrder) - 32000; // Изменить тональность
nOrder ++;
}
nNote = pgm_read_word_near(order + nOrder);
nOrder ++;
}
}
}
else { // Задержка между соседними нотами - СТАККАТО
asm("wdr"); // Сбрасываем WDT
WDTCSR = (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = 0 | (1 << WDIE ); // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
}
}
void setup() {
pinMode (9, OUTPUT);
// инициализация WDT
cli(); // Запрт прерываний
asm("wdr"); // Сбрасываем WDT
WDTCSR |= (1 << WDCE) | (1 << WDE); // Разрешить изменение значения предделителя WDT
WDTCSR = 0 | (1 << WDIE ); // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
sei(); // Разрешение прерываний
// инициализация таймера-счётчика TC1
TCCR1A = 0;
TCCR1B = 0;
TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12)); // Таймер остановлен.
TCCR1B |= (1 << WGM12); // CTC режим.
TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0)); // Порты PB1/PB2 от OC1A/OC1B отключены.
TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1); // Порт PB1 подключен к OC1A и обнуляется при совпадении.
TCCR1B |= (1 << CS10); // запуск с таймера с делителем на 1.
}
void loop() {
}
Лист. 9. Программа исполняющая первую часть Полонеза Огинского.