Atmega8 мелодії. Прості звукові сирени на МК AVR. Використання звукового модуля

У статті описано принципи синтезу музики на AVR. ПЗ, що додається, дозволяє сконвертувати будь-який midi файл в вихідний код C для мікроконтролерів AVR, щоб додати відтворення музичних фрагментів готові розробки. Розглянуто приклад використання ПЗ у музичній скриньці.

Для початку, невелике відео, як все працює:

Що дозволяє ПЗ

ПЗ для PC дозволяє отримати вихід на C для CodeVision AVR, який відтворює вибраний midi файл:

1. У свій проект підключаємо common\hxMidiPlayer.h, common\hxMidiPlayer.c. Копіюємо заготовки ATMega8Example\melody.h, ATMega8Example\melody.c, ATMega8Example\hxMidiPlayer_config.h і підключаємо.
2. Запускаємо MidiToC.exe
3. Завантажуємо Midi файл.
4. Налаштовуємо програвач: sampling rate, кількість каналів, waveform та ін ПЗ відтворює мелодію так само, як гратиме AVR.
5. Натискаємо “Create player config” і пастимо вихідник у hxMidiPlayer_config.h.
6. Натискаємо “Create melody code” і пастимо вихідник у melody.c
7. У своєму проекті реалізуємо метод Player_Output() для виведення звуку через PWM чи зовнішній ЦАП.
8. Налаштовуємо таймер на частоту Sampling rate, із переривання викликаємо Player_TimerFunc().
9. Викликаємо Player_StartMelody(&s_melody, 0).

Мелодія відтворюється із переривання таймера. Це означає, що під час відтворення мікроконтролер може займатися корисною роботою.

Як це працює

У решті статті я постараюся коротко пояснити, як усе це реалізовано. На жаль, зовсім коротко не вийде - матеріалу дуже багато. Якщо не цікаво – можна одразу перейти до розділів “Опис ПЗ” та “API плеєра”.

Що таке музика

Музика – це послідовність звуків різної частоти та тривалості. Частота основної гармоніки звуку має відповідати частоті певної ноти. Якщо частота коливань звуків відрізняється від частот нот – здається, що музикант “фальшивить”.

Таблиця. Частоти нот, Гц.

Усі ноти поділені на октави, по 7 нот у кожній + 5 напівтонів (чорні клавіші на піаніно). Частоти нот сусідніх октав відрізняються у 2 рази.

Найпростіший музичний плеєр містить таблицю з послідовністю нот (нота + тривалість) мелодії та таблицю з частотами нот. Для синтезу звуку використовується один із каналів таймера, який формує меандр:

На жаль, такий примітивний плеєр має фіксовану форму хвилі (меандр), яка не дуже схожа на реальні музичні інструменти, і може відтворювати лише одну ноту одночасно.

Реальна мелодія містить як мінімум дві партії (соло + бас), до того ж, при грі на піаніно попередня нота все ще продовжує звучати, коли почалася наступна. Це легко зрозуміти, згадавши пристрій піаніно – кожній ноті відповідає окрема струна. Ми можемо змусити звучати кілька струн одночасно, провівши рукою клавішами.

У деяких мікроконтролерах є кілька каналів таймера, їх можна використовувати для відтворення кількох нот одночасно. Але зазвичай ці канали є цінним ресурсом і використовувати їх усі – небажано. Якщо ми, звичайно, не робимо просто музичну скриньку.
Для того щоб отримати поліфонію та різні звуки музичних інструментів, потрібно використовувати синтез звуку.

Синтез звуку на AVR

hxMidiPlayer використовує синтез звуку та може відтворювати поліфонію з різними формами хвилі. Плейєр розраховує амплітуду вихідного сигналу в обробнику переривання від таймера із частотою 8-22КГц (наскільки вистачить потужності процесора; також залежить від форми хвилі та кількості каналів).

Принцип синтезу звуку можна пояснити з прикладу синтезу синусоїди.

Візьмемо таблицю розміром 64, у кожному осередку якої записані значення амплітуди синуса в точках index * 2 * PI / 64 (один період):

Static const flash uint8_t s_sineTable[ 64 ] = ( 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8D, 0x8F, 0x90, 0x91, 0x93, 0x93 5, 0x95, 0x95, 0x94, 0x93, 0x93, 0x91, 0x90, 0x8F, 0x8D, 0x8C, 0x8A, 0x88, 0x86, 0x84, 0x82, 0x80, 0x7E, 0x7C, 0x7A 0 1, 0x70, 0x6F, 0x6D, 0x6D, 0x6C, 0x6B, 0x6B, 0x6B, 0x6B, 0x6B, 0x6C, 0x6D, 0x6D, 0x6F, 0x70, 0x71, 0x73, 0x74, 0x76, 0x78,0

128 (0x80) відповідає нулю, 255 (0xff) - найбільшій позитивній точці, 0 - найбільшій негативній точці.

Тепер припустимо, ми виводитимемо значення і з таблиці на зовнішній ЦАП у перериванні від таймера, що викликається з частотою 1000 Гц:

Static uint8_t s_index = 0; // Timer1 output compare A interrupt service routine interrupt void timer1_compa_isr(void) ( SetDac(s_sineTable[ s_index]); if (s_index == 63) ( s_index = 0; ) else ( s_index++; ) )

Що ми матимемо на виході? Ми отримаємо синусоїдальні коливання із частотою 1000/64 Гц.

Тепер збільшувати індекс у перериванні не на 1, а на два.
Очевидно, що частота вихідних коливань буде вже 1000/64*2 Гц.

У випадку, щоб отримати частоту F, потрібно збільшувати індекс у таблиці на:
add = F / 1000 * 64

Це число може бути дробовим, але для отримання високої швидкості роботи використовують fixed point arithmetic.

Кількість записів у таблиці та частота таймера впливають на якість синтезованого звуку. У нашому випадку достатньо 64 записи в таблиці на період, і частота таймера 12kHz. Мінімально прийнятна частота таймера – 8кГц, ідеальна – 44кГц.

Очевидно, що при частоті таймера 12кГц ми зможемо згенерувати максимум 6кГц меандр, тому що потрібно зробити щонайменше два перемикання за період. Однак, частоти вище все одно будуть впізнавані, якщо правильно розраховувати стан виходу на кожному тику таймера.

У таблицю можна внести значення для періоду коливань несинусоїдальної форми та отримати інший звук.

Згасання

Якщо музичний інструмент заснований на струнах (наприклад, піаніно), після натискання клавіші звук плавно згасає. Щоб отримати більш природне звучання синтезатора, необхідно плавно зменшити амплітуду коливань після старту ноти (обернути коливання у форму згасання - envelope).

Плеєр містить таблицю згасання, яку використовує зменшення амплітуди синуса (чи інший форми хвилі) з моменту старту ноти.
"Синус" "загорнутий" у таку оболонку нагадує звук механічної музичної скриньки.

Синтез меандру

Особлива форма хвилі меандру дозволяє значно спростити синтез. Таблиці у своїй не використовуються. Достатньо розрахувати, який стан (1 або 0) повинен мати вихід при заданій частоті на поточному тику таймера. Це робиться за допомогою цілої арифметики, працює дуже швидко, чим і пояснюється популярність використання меандру для відтворення мелодій у 8-біт приставках.

Приклад: оголошуємо лічильник:

Static uint16_t s_counter = 0;

який будемо збільшувати на 0x8000 у кожному перериванні від таймера, а в порт виводитимемо старший біт лічильника:

// Timer1 output compare A interrupt service routine interrupt void timer1_compa_isr(void) ( PORTA.0 = (s_counter >> 15) & 1; s_counter += 0x8000; )

Оскільки 0x8000 + 0x8000 = 0x10000 відбувається переповнення змінної s_counter, 17-й біт відкидається, і в змінну записується 0x0000.
Таким чином, при частоті таймера 8КГц на виході вийде меандр 4КГц.
Якщо збільшувати лічильник на 0x4000, то вийде меандр 2КГц.

Загалом, можна отримати частоту F, якщо додавати:
add = F / 8000 * 0x10000

Наприклад, щоб отримати меандр частотою 1234Гц, потрібно додавати 0x277C. Реальна частота трохи відрізнятиметься від заданої, тому що ми округляємо доданок до цілого числа. У синтезаторі це припустимо.

Синтез звуків реальних інструментів

Можна оцифрувати звук ноти До піаніно (за допомогою АЦП зберегти в пам'яті значення амплітуди звуку через рівні проміжки часу):
а потім відтворити звук (за допомогою ЦАП вивести записані значення через рівні проміжки часу).

У випадку, для синтезу ударних необхідно записати звуки барабанів і відтворювати в потрібний момент. У 8-біт приставках замість звуків барабанів використовують "білий шум". Значення амплітуди для "білого шуму" одержують за допомогою генератора випадкових чисел. Витрати пам'яті у своїй – мінімальні.
hxMidiPlayer використовує “білий шум” для синтезу ударних.

Мікшування каналів

Амплітуда звуку на даному тику таймера розраховується для кожного каналу окремо. Щоб отримати фінальне значення амплітуди, необхідно додати всі канали. По-правильному необхідно коригувати суму, так як гучність, що сприймається, підпорядковується логарифмічної залежності, але в такому простому синтезаторі доведеться обійтися простим додаванням. Тому максимальна амплітуда кожного каналу дорівнює 255/N.

Виведення звуку з AVR

Після проведення всіх необхідних обчислень, плеєр отримує рівень сигналу, який потрібно перевести на аналог. Для цього можна використовувати зовнішній DAC або PWM.
Тут слід зауважити, що в обох випадках отриманий сигнал бажано відфільтрувати – прибрати високочастотні шуми, що виникають через низьку частоту синтезу та округлень.

Виведення на зовнішній паралельний DAC

Оскільки немає сенсу використовувати точні мікросхеми DAC, у таких проектах зазвичай обходяться R2R матрицею:

За такої схеми ми просто виводимо обчислену амплітуду в порт:

PORTB = sample;

Недоліки:
1) на виході R2R матриці виходить занадто слабкий сигналвикористання аналогового підсилювача обов'язково;
2) необхідно використати як мінімум 5 висновків (а краще 8);
Цей метод виправданий лише тоді, коли немає вільних каналів PWM.

(Для економії висновків можна скористатися зовнішнім АЦП з SPI інтерфейсом).

PWM

Якщо є вільний PWM канал, то найпростіше скористатися саме цим способом.

Ініціалізація PWM (ATMega8):

// Timer/Counter 2 initialization // Clock source: System Clock // Clock value: 20000,000 kHz // Mode: Fast PWM top=0xFF // OC2 output: Non-Inverted PWM ASSR=0x00; TCCR2 = 0x69; TCNT2 = 0x00; OCR2 = 0x00; І висновок сампла: void Player_Output(uint8_t sample) (OC2 = sample.)

Звичайна практика використання PWM має на увазі згладжування вихідного сигналу за допомогою RC фільтра:

На жаль, після фільтрації сигнал дуже сильно слабшає, тому для підключення динаміка доводиться робити аналоговий підсилювач.

Щоб спростити схему, краще залишатися у цифрі до самого динаміка. Оскільки дешевий динамік все одно не може відтворювати частоти вище за 30кГц – фільтрувати їх не потрібно. Дифузор сам "відфільтрує" високі частоти PWM.

Якщо потрібно отримати більший струм, можна застосувати підсилювач на транзисторі. R1 вибирається так, щоб забезпечити необхідний струм на динаміці.

Так можна підключати невеликі динаміки з іграшок:

Для динаміків краще зібрати розгойдування на 2-х транзисторах і поставити LC - фільтр, щоб прибрати шуми:

Конденсатор С1 служить обмеження струму через динамік, коли ШІМ не працює. Також завдяки включенню послідовного конденсатора, динамік потрапляє сигнал, симетричний щодо нуля. Таким чином, дифузор динаміка рухатиметься щодо центрального “розслабленого” положення, що позитивно позначається на якості звуку.
У цьому випадку транзистори працюють у ключовому режимі, тому компенсувати зміщення бази не потрібно.

PWM, підключення до двох висновків

Недоліком перших двох схем і те, що у динамік подається струм одного напрями. Якщо ми змінюватимемо напрям струму, то гучність можна збільшити в 2 рази не перевищуючи допустиму потужність. Для цього динамік підключається до двох висновків мікроконтролера – неінвертованого та інвертованого, наприклад OC1A та /OC1A. Якщо неінвертований висновок відсутній, можна використовувати другий канал в режимі інвертування (OC1B):

// Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 24500,000 kHz // Mode: Fast PWM top=0x00FF // OC1A output: Non-Inv. // OC1B output: Inverted // Noise Canceler: Off // Input Capture on Falling Edge // Timer1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: Off // Compare B Match Interrupt: Off TCCR1A =0xB1; TCCR1B = 0x09; TCNT1H = 0x00; TCNT1L=0x00; ICR1H = 0x00; ICR1L = 0x00; OCR1AH=0x00; OCR1AL = 0x00; OCR1BH = 0x00; OCR1BL=0x00; void Player_Output(uint8_t sample) ( OCR1A = sample; OCR1B = sample; )

PWM, два висновки, підсилювач D-класу

Недоліком запропонованих схем є споживаний струм під час тиші.
"Тиша" у нас відповідає рівню сигналу 128, тобто ШИМ з 50% заповненням - струм через динамік тече завжди!

Трохи змінивши програмну частину, можна отримати досить потужний програмно-апаратний підсилювач класу D:

Void Player_Output(uint8_t sample) ( if (sample >= 128) ( TCCR2=0x21; //normal, clear on compare match TCCR2=0x21 | 0x80; //CLEAR OC2 PORTC.0 = 0; TCCR2=0x69; //non -inverting PWM OCR2 = (sample-128) * 2;) else // if (sample< 128) { TCCR2=0x31; //normal, set on compare match TCCR2=0x31 | 0x80; //SET OC2 PORTC.0 = 1; TCCR2=0x79; //inverting PWM OCR2 = (128-sample) *2; } }

При цьому одна пара транзисторів підключається до виходу PWM, друга до звичайного цифрового виходу.

Як видно з коду, ми розглядаємо сигнал вище 128 як струм, спрямований в одну сторону, а сигнал нижче 128 як струм, спрямований в іншу сторону. При рівні 128 обидва виведення динаміка підключені до того самого висновку джерела живлення, і струм відсутній. При відхиленні від рівня 128 заповнення PWM збільшується і через динамік тече струм відповідної полярності.

p align="justify"> Важливим моментом реалізації є примусове перемикання виведення PWM в потрібний стан в момент перемикання другого (звичайного цифрового) виводу (PORTC.0). Запис у регістр OCR2 буферизується, щоб унеможливити “глюки” PWM. Нам необхідно переключити висновок PWM відразу, не чекаючи закінчення періоду.

Остання схема IMHO є найкращим варіантом у плані простоти, економії енергії та вихідної потужності.

Виведення звуку з формою хвилі SquareWave

При синтезі меандру застосовуються спрощені алгоритми.

Кожен канал (включаючи ударні) виводить на вихід або 0 або 1. Таким чином, 3-х канальний програвач виводить на вихід значення в діапазоні 0..3. Тому під час використання PWM процедура виведення виглядає:

Void Player_Output(uint8_t sample) ( OCR2 = sample * (255 / HXMIDIPLAYER_CHANNELS_COUNT); )

Якщо ж не використовувати PWM, для виведення 3-х канальної мелодії достатньо двох звичайних цифрових виходів і 2-х бітової R2R матриці.

Формат MIDI

Якщо подивитися на отриманий код мелодії, то легко помітити, що в масиві використовуються цифри, що повторюються, з невеликого діапазону. Це і зрозуміло: у мелодії використовується обмежена кількість нот у межах 1-2 октав, темп мелодії фіксований – однакові затримки, кількість каналів знаходиться в діапазоні 0..15.
Все це означає, що отриманий масив можна значно зменшити, застосувавши якийсь алгоритм стиснення.
Алгоритми типу ZIP дають хороший стиск, але також вимагають багато пам'яті для роботи (словник ZIP - 64Кб). Ми можемо застосувати дуже простий метод стиснення, що практично не вимагає пам'яті, суть якого полягає в наступному.

В одній байті всі числа поступово розподілені в діапазоні 0 ... 255, і кожне число представляється 8-ма бітами. У нашому випадку деякі числа зустрічаються набагато частіше, ніж інші. Якщо кодувати числа, що часто зустрічаються, меншою кількістю бітів, а рідше зустрічаються - більшим, можна отримати виграш по пам'яті.

Вибираємо фіксований спосіб кодування: комбінації бітів 000,001 і 010 (довжина - 3 біта) будуть представляти 3 числа, що найчастіше зустрічаються. Комбінації бітів 0110, 0111 (довжина - 4 біти) - наступні 2 числа, що найчастіше зустрічаються, і т.д:

//000..010 - 0..2 //011 x 3..4 //100 xx 5..8 //101 xxx 9..16 //110 xxx 17..24 //111 immediate

Комбінація, що починається з 111 (довжина – 11 біт) кодуватиме всі інші числа.
Спосіб кодування бітами може бути іншим. Я перепробував кілька методів і вибрав цей, як той, що дає найкращі результати на таких даних.

Процедура стиснення виглядає так:
1. Підрахувати загальну кількість числа X у потоці X = .
2. Відсортувати зменшення частоти появи в потоці.
3. Взяти перші 25 чисел. Вони кодуватимуться меншою кількістю біт.
4. Закодувати вхідний потік.

На виході отримуємо масив з 25 чисел, що найчастіше зустрічаються, і бітовий потік.
Така компресія дозволяє отримати 50% стиск при мізерних витратах пам'яті та продуктивності. На жаль, код плеєра при цьому збільшується, тому для коротких мелодій застосовувати стиснення не рекомендується.

Зберігання частот нот

Досить накладно пам'яті зберігати частоти всіх нот у таблиці. Насправді існує формула для визначення частоти ноти за її midi номером:

F = 2 ^ ((N - 69) / 12) * 440, Гц

Але обчислювати дробовий ступінь досить складно. Натомість плеєр зберігає 12 частот нот верхньої октави. Частоти нот нижніх октав визначаються зменшенням частоти в 2^Y більше, де Y – кількість октав вниз.

Подальший розвиток компресії

У мелодії часто зустрічаються фрагменти, що повторюються (“приспіви”, “куплети”). Знайшовши фрагменти, що повторюються, і представивши мелодію у вигляді фрагментів, можна зменшити мелодію ще відсотків на 50%, майже не витрачаючи оперативну пам'ятьта продуктивність. Я не став реалізовувати такий алгоритм, щоби не ускладнювати проект.

Опис ПЗ

Основне вікно програми-конвертора:

Кнопка Load Midi дозволяє завантажити файл midi. Програма відразу починає відтворювати файл з поточними вибраними параметрами, імітуючи звук, який буде в "залізі".

Вікно інформації (4) відображає:
– Length – довжина вибраного фрагмента мелодії у мс;
– Max Active syntezer channels – максимальна кількість одночасно активних каналів синтезатора;
- Max active drum channels - максимальна кількість одночасно активних каналів синтезатора, що відтворюють "ударні";
– Max active stereo notes – максимальна кількість каналів, що відтворюють ту саму ноту (див. нижче);
– Estimated size, bytes – розмір мелодії у байтах. У режимі “Custom Sample” розмір відображається як A+B, де А – розмір мелодії, B – розмір семпла. Розмір коду плеєра тут не вказується.

Вікно прогресу відображає поточне положення відтворення.
Можна натиснути на крок bar, щоб почати відтворення з вказаного моменту.
Input Box ліворуч і праворуч дозволяють вказати початок і кінець фрагмента мелодії в мс.

Мітка “Not enought channels to play melody” червоним кольором вказує, що відтворення мелодії при поточних налаштуваннях недостатньо каналів синтезатора. Якщо програвач не знаходить вільний канал, то вимикає найдавнішу ноту. У багатьох випадках це працюватиме нормально. Збільшувати кількість каналів можна тільки тоді, коли мелодія на слух звучить неправильно.

Налаштування умовно можна розділити на налаштування програвача та налаштування обробки midi файлу. Програвач зможе відтворити отриманий код мелодії, якщо конфігурація програвача та код мелодії були створені з однаковими параметрами програвача. Крім того, програвач зможе відтворити мелодію, код якої створено для програвача з меншою (але не більшою) кількістю каналів.

Апаратні установки програвача включають:

– Sampling Rate – частота синтезу. Максимальна частота синтезу визначається експериментально. У розрахунку на Atmega 16MHz, можна почати з 12000Гц для програвача з 6 каналами, і підвищувати за бажанням, поки в апаратному програвачі на слух не помітні спотворення мелодії. Максимальна частота залежить від кількості каналів, форми хвилі та складності самої мелодії.

– Waveform – форма хвилі:
- Square wave - меандр;
- Sine - синус;
- Sine + Envelope - синус із загасанням;
- Waveform * + Envelope - різні варіанти несинусоїдальних хвиль із загасанням і без;
– Custom Sample – використовувати семпл інструмента.

Кнопка Load Sample дозволяє завантажити семпл з WAV файлу. WAV файл має бути у форматі PCM 8-bit mono, 4173Гц, нота До-5. Hint: Можна підвищити частоту та знизити ноту, але в налаштуваннях програвача змінити Pitch. Жодних перевірок формату не проводиться – якщо формат інший, то звук відтворюватиметься неправильно.
Pitch – дозволяє змінити висоту звуку. Наприклад, щоб грати на 1 октаву вище, потрібно виставити Pitch+12.

Use compression – використовувати стиснення мелодії.
Enable drums synteser – увімкнути синтезатор ударних.

Player Channels: кількість каналів синтезатора (максимальна кількість нот, які звучатимуть одночасно).

Налаштування обробки midi файлу включають:

Зазвичай, таке тонке налаштування не потрібно. Ці параметри можна залишити за замовчуванням.

API плеєра

Реалізація плеєра знаходиться у файлах CommonhxMidiPlayer.c і CommonhxMidiPlayer.h. Ці файли потрібно підключити до проекту. Також необхідно створити файл hxMidiPlayer_config.h, до якого потрібно помістити конфігурацію.
Плеєр написаний на C без асемблерних вставок, що дозволить легко портувати його інші мікроконтролери.

Extern void Player_StartMelody(const flash TMelody* _pMelody, uint16_t _delay);

Почати відтворення мелодії. _delay задає початкову затримку перед відтворенням, 255 одиниць = 1 секунда.

Void Player_Stop();

Припинити відтворення мелодії.

Extern bool Player_IsPlaying();

Повертає false, якщо відтворення мелодії завершено.

Extern void Player_WaitFinish();

Зачекати, поки відтворення мелодії завершиться.

Extern void Player_TimerFunc();

Цю функцію потрібно викликати у перериванні від таймера із частотою семплювання, заданою у конфігурації. Коли відтворення мелодії завершено, дзвінки можна не робити.

Extern void Player_Output(uint8_t sample);

Повинна бути реалізована користувачем. Викликається плеєром, коли необхідно вивести наступний семпл.

Extern void Player_Started();

Повинна бути реалізована користувачем. Викликається, коли плеєр починає відтворювати мелодію. Можна використовувати для налаштування переривань від таймера.

Extern void Player_Finished();

Повинна бути реалізована користувачем. Викликається, коли плеєр завершив відтворення мелодії. Може бути використана, щоб вимкнути переривання від таймера або запустити відтворення іншої мелодії.

//#define NOTES_TO_EEPROM //#define SINETABLE_TO_EEPROM //#define ENVELOPE_TO_EEPROM

Ці рядки потрібно розкоментувати у файлі hxMidiPlayer_config.h, якщо таблицю нот, таблицю синуса та таблицю згасання необхідно розташувати у eeprom.

Проекти-приклади

ATMega644Example - проект для ATMega644, 25MHz, виведення PWM на PB3.

Вимоги до пам'яті

Таблиця. Розмір плеєра та мелодій у flash.

*при додаванні плеєра в наявний непустий проект, розмір коду буде меншим

**для нормального відтворення мелодії не вистачає каналів

Мелодія 1: bach_minuet_in_g.mid, 35 сек
Мелодія 2: yiruma-river_flows_in_you.mid, 165 сек
Мелодія 3: Franz Schubert – Serenade.mid, 217 сек

Як видно з таблиці, мінімальної конфігурації можна втиснути досить довгу мелодію навіть в ATTiny2313. Компресія може дати більш ніж дворазове зменшення мелодії, але розмір коду плеєра при цьому збільшується на 600 байт.

Таблиці нот синуса та згасання можна помістити в EEPROM, секнономів приблизно 16, 50 та 100 байт flash відповідно.

При використанні семпла з wav файлу до розміру коду плеєра потрібно додати власне розмір семпла в байтах.

Приклад використання

Як приклад використання плеєра розглянемо процес створення музичної скриньки.

Беремо готову скриньку із МДФ:

Як мікроконтролер беремо ATTiny85 в SO-8 корпусі як найбільш дешевий з досить великою кількістю пам'яті. Ми розженемо його до 27МГц, щоб отримати частоту синтезу 18Кгц при 4-х каналах Sine+Envelope.

Підсилювач буде D-класу на 4-х транзисторах, щоб заощаджувати батареї.

Транзистори працюють у ключовому режимі та можуть бути будь-які. Дросель L1 та конденсатор C6 підбираються до смаку для отримання звуку без високочастотних шумів. R1 і R2 можна підняти до 2К, щоб знизити гучність і зменшити брязкіт динаміка.

Кінцевий вимикач з дисководу підходить ідеально, ніби спеціально для скриньки та створений (працює на розмикання – при відкриванні кришки на плату подається живлення):

Вихідники прошивки знаходяться в каталозі ATTiny85MusicBox.

У 8Кб помістились:
1) плеєр: 18000Гц, 4 канали, Sine+Envelope, Pitch+12, стиснення, відтворює мелодії по черзі (остання зберігається в EEPROM)
2) Yiruma - River Flows in You
3) Франц Шуберт - Серенада
4) П.І. Чайковський "Жовтень"

Результат на відео:

Подальший розвиток

В принципі, плеєр можна й надалі «навертати», довівши до повноцінного Midi або MOD плеєра. Я особисто вважаю, що для отримання високоякісної мелодії простіше буде підключити картку SD і грати з неї будь-які WAV файли з набагато найкращою якістючим взагалі можна отримати програмним синтезом. І такий плеєр програмно та апаратно на порядок простіше. Ніша hxMidiPlayer – додавання гарного звуку в готові проекти, коли залишилося кілька ніжок і трохи місця у flash. З цим завданням він справляється на "відмінно" вже у існуючому вигляді.

Думаю, на цьому питання створення будь-яких музичних скриньок/дзвінків на AVR можна закрити 🙂

Продовження уроку затяглося, воно і зрозуміло, довелося освоїти роботу з картками пам'яті та файловою системою FAT. Але все-таки, воно відбулося, урок готовий - практично новорічне диво.

Щоб не перевантажувати статтю інформацією, я не буду описувати структуру формату wav файлу, інформації в пошукових системах більш ніж достатньо. Досить сказати, що якщо відкрити файл, будь-яким Hex редактором, то в перших 44 байтах міститься вся інформація про тип файлу, частоту дискретизації, кількість каналів та ін. Якщо потрібно аналізувати файл, читайте цей заголовок і буде вам щастя.

Корисні дані починаються з 44 байти, по суті вони містять рівні напруги, з яких формується звук. Ми вже говорили про щаблі напруги, в минулій частині уроку. Таким чином, все просто потрібно ці сходинки вивести на динамік з частотою дискретизації файлу.

Як фізично змусити динамік тремтіти? Потрібно виводити ці рівні напруги за допомогою ШІМ або використовувати R2R. У будь-якому випадку, використовувати дуже просто, прочитав число, засунув його або в OCR, або в PORTx. Далі через певний час підставив таке значення і так до кінця файлу.

Приклад, якийсь wav файл, дані йдуть з 44 = 0х2С байта, там записано число 0х80, відтворюємо звук наприклад ШИМ першого таймера, пишемо OCR1A = 0х80; Припустимо, частота дискретизації волки 8кГц, відповідно переривання має бути налаштовано на цю частоту. У перериванні підставляємо наступне значення 0x85 через 1/8000=125мкс.

Як налаштувати переривання на 8кГц? Згадуємо, якщо таймер працює на частоті 250кГц, то регістр порівняння переривання слід підставити (250/8)-1=31-1 або 0x1E. З ШИМом теж все просто, чим вища частота на якій він працює тим краще.

Щоб прошивка працювала, умовимося, що флешка відформатована у FAT32, використовується либа PetitFat з уроку 23.2. Файл у форматі wav або 8кГц або 22,050кГц, моно. Назва файлу 1.wav. Аналізуємо прошивку.

#include #include "diskio.h" #include "pff.h" unsigned char buffer [512]; /* буфер в який копіюється інфа з флешки */ volatile unsigned int count; //лічильник скопійованих даних interrupt [ TIM2_COMP ] void timer2_comp_isr(void ) //переривання в якому підставляються значення(OCR1A = buffer [count]; //виводимо звук на динамік if (++ count >= 512) //збільшуємо лічильник count = 0; //якщо 512 обнулюємо) void main(void ) ( unsigned int br; /* лічильник читання/запису файлу */ unsigned char buf = 0; //Змінна визначальна яка частина буфера читається FATFS fs; /* Робоча область (file system object) для логічних дисків */ PORTB = 0x00; DDRB = 0x02; //Дригаємо шимом ocr1a // Timer/Counter 1 initialization// Clock source: System Clock // Clock value: 8000,000 kHz // Mode: Fast PWM top=0x00FF // OC1A output: Non-Inv. TCCR1A = 0x81; TCCR1B = 0x09; TCNT1 = 0x00; OCR1A = 0x00; // Timer/Counter 2 initialization// Clock source: System Clock // Clock value: 250,000 kHz // Mode: CTC top=OCR2 TCCR2= 0x0B; TCNT2 = 0x00; // OCR2 = 0x1E; //Налаштування регістру порівняння для 8кГц OCR2 = 0xA; //для 22кГц #asm("sei") // Timer(s)/Counter(s) Interrupt(s) initialization if (disk_initialize() == 0 ) //ініціалізуємо флешку( pf_mount(& fs) ; //монтуємо файлову систему pf_open("1.wav"); //відкриваємо вавку pf_lseek(44); //переміщуємо покажчик на 44 pf_read(buffer, 512, & br); //Вперше заковтуємо відразу 512байт TIMSK = 0x80; //врубаємо музон while (1 ) ( if (! buf && count> 255 ) //якщо відтворилося більше 255 байт,(pf_read (& buffer [0], 256, & br); //то читаємо в першу половину буфера інфу з флешки buf = 1; if (br< 256 ) //якщо буфер не містить 256 значень означає кінець файлу break; ) if (buf && count< 256 ) { pf_read(& buffer[ 256 ] , 256 ,& br) ; // читаємо до другої частини буфера з флешки buf = 0; if (br< 256 ) break ; } } TIMSK = 0x00 ; //глушим все pf_mount(0x00 ) ; //демонтуємо фат) while (1 ) ( ) )

#include #include "diskio.h" #include "pff.h" unsigned char buffer; /* буфер у який копіюється інфа з флешки */ volatile unsigned int count; //лічильник скопійованих даних interrupt void timer2_comp_isr(void) //переривання в якому підставляються значення (OCR1A = buffer; //виводимо звук на динамік if (++count >= 512) //збільшуємо лічильник count = 0; //якщо 512 обнуляем ) void main(void) ( unsigned int br; /* лічильник читання/запису файлу */ unsigned char buf = 0; // змінна визначальна яка частина буфера читається FATFS fs; /* Робоча область (file system object) для логічних дисків */ PORTB = 0x00;DDRB = 0x02; output: Non-Inv.TCCR1A=0x81;TCCR1B=0x09;TCTC1=0x00;OCR1A=0x00; OCR2 TCCR2 = 0x0B, TCNT2 = 0x00; // OCR2 = 0x1E; // настроювання регістру порівняння для 8кГц OCR2 = 0xA; s) initialization if(disk_initialize()==0) // ініціалізуємо флешку (pf_mount(&fs); //монтуємо файлову систему pf_open("1.wav"); //відкриваємо вавку pf_lseek(44); //переміщуємо покажчик на 44 pf_read(buffer, 512, br); //Вперше заковтуємо одночасно 512байт TIMSK=0x80; //врубаємо музон while(1) ( if(!buf && count>255) //якщо відтворилося більше 255 байт, ( pf_read(&buffer, 256,&br);//то читаємо в першу половину буфера інфу з флешки buf=1 ; if (br< 256) //если буфер не содержит 256 значений значит конец файла break; } if(buf && count<256) { pf_read(&buffer, 256,&br); // читаем во вторую часть буфера с флешки buf = 0; if (br < 256) break; } } TIMSK = 0x00; //глушим все pf_mount(0x00); //демонтируем фат } while (1) { } }

Для перевірки на ніжку OCR1A підключаємо динамік через конденсатор 100мкФ, «+» на ніжку мікроконтролера, «-» на динамік. "-" динаміка на землю, "+" на конденсатор.

Не чекайте гучного сигналу на виході, щоб звучало гучно, потрібний підсилювач. На відео це добре видно. Для тесту залив півня 8кГц та трек 22кГц.

Бажаючі можуть сміливо збільшити частоту таймера2, щоб програвати файли 44кГц, досліди показують, що можна досягти непоганої якості звучання. На відео звук слабкий та якість погана, але насправді це через те, що знімав на фотоапарат.

Також викладаю матеріали люб'язно надані Апаратником – вихідник для GCC, з якого було написано прошивку під CAVR.

І відео із відтворенням 44кГц.

Користуючись нагодою вітаю Усіх з Настанням, бажаю щоб усі прошивки та девайси у вас працювали 🙂

Проект wav плеєра на Atmega8

Написав програмний модуль, що дозволяє додати функцію відтворення мелодій або послідовностей звуків практично будь-який проект на мікроконтролері AVR.

Особливості модуля:

Проста інтеграція з готовим проектом

Задіяний лише 8-розрядний таймер т2, при цьому залишається можливість використовувати його для опитування або формування часових інтервалів.

Модуль налаштовується практично на будь-яку частоту тактового генератора

Висота нот задається як символічних констант (С0, А2 тощо.) чи Герцах

Тривалості задаються у стандартному вигляді (чверті, восьмі і т.д.) або мілісекундах

Є можливість задавати темп відтворення мелодії та кількість її повторень

У процесі відтворення мелодія може бути поставлена ​​на паузу


Підключення звукового модуля

1. Переписуємо всі файли модуля (tone.h, sound.h, sound.c) до папки проекту.

2. Підключаємо файл sound.c до проекту.

Для IAR `a – клацнути правою кнопкою мишці у вікні workspace і вибрати Add > Add Files…

Для WINAVR приблизно те саме, тільки sound.c потрібно ще прописати в make файл:

SRC = $(TARGET).c sound.c

3. Включаємо заголовний файл sound.h у відповідний модуль. Наприклад, у main.c

#include "sound.h"

4. Задаємо налаштування модуля у файлі sound.h

//якщо закоментувати - тривалість нот буде

//розраховуватись з BPM`а заданого в мелодії

//якщо залишити, то значення заданого нижче

//#define SOUND_BPM 24

//тактова частота мк

#define SOUND_F_CPU 16U

//висновок мікроконтролера, на якому генеруватиметься звук

#define PORT_SOUND PORTB

#define PINX_SOUND 0

//кількість заданих мелодій.

#define SOUND_AMOUNT_MELODY 4

5. Додаємо в sound.c свої мелодії та прописуємо назви мелодій у масив melody.

Додавання мелодій

Мелодія є масивом 16-ти розрядних чисел і має наступну структуру

BPM (кількість четвертних нот за хвилину)– це константа, використовувана до розрахунку тривалості нот і визначальна швидкість відтворення мелодії.

BPM може приймати значення від 1 до 24, що відповідає 10 і 240 четвертним нотам за хвилину відповідно.

Якщо тривалість нот/звуків задається в мілісекундах, то BPM, прописаний у масиві, повинен дорівнювати 1.

Якщо в заголовному файлі sound.h константа SOUND_BPM закоментована, то тривалість нот розраховується в процесі виконання програми по BPM у заданому в масиві. Якщо SOUND_BPM не закоментована – тривалість нот розраховується ще етапі компіляції, виходячи зі значення цієї константи, у своїй всі мелодії відтворюватимуться однаковому темпі. Це обмежує функціонал, але дозволяє заощадити кілька байт коду.

Кількість повторень.Може приймати значення 1...254 і LOOP (255). LOOP - означає, що мелодія буде повторюватися нескінченно, доки не буде подано команду SOUND_STOP або SOUND_PAUSE.

Тривалість ноти– час протягом якого генерується заданий тон звуку або витримується пауза. Може задаватися в ms, з допомогою макросу ms(x), чи вигляді стандартних тривалостей нот – восьмих, шістнадцятих тощо. Нижче наведено список тривалостей, що підтримуються. Якщо виникне потреба у якихось екзотичних тривалостях, їх можна додати у файлі tone.h

n1 - ціла нота

n2 - половинна нота

n4 - чверть

n8 - восьма

n3 - восьма тріоль

n16 - шістнадцята

n6 – секстоль

n32 – тридцять друга

Висота нотизадається за допомогою символічних констант, описаних у файлі tone.h, наприклад C2, A1 і т.д. Також висота нот може задаватись у Герцах за допомогою макросу f(x).

У програмі є обмеження на мінімальну та максимальну частоту звуку!

Маркер кінця мелодії.Значення останнього елемента масиву обов'язково має бути нульовим.

Використання звукового модуля

На початку main`a потрібно обов'язково викликати функцію SOUND_Init(). Ця функція налаштовує виведення мікроконтролера на вихід, конфігурує таймер Т2 та ініціалізує змінні модулі.

Потім потрібно встановити прапор дозволу переривань - __enable_interrupt(), адже в модулі використовується переривання таймера Т2 по переповненню та збігу.

Після цього можна запускати відтворення мелодій.

Наприклад, так:

SOUND_SetSong(2);

SOUND_Com(SOUND_PLAY); //відтворити мелодію

//Встановити покажчик на 2-ю мелодію

//і запустити відтворення

SOUND_PlaySong(2);

Відтворення мелодії можна у будь-який момент зупинити, подавши команду SOUND_STOP.
Також можна поставити мелодію на паузу за допомогою команди SOUND_PAUSE. Наступна подача команди SOUND_PLAY відновлює відтворення мелодії з місця, на якому сталася зупинка.

В принципі цей функціонал не особливо потрібен (це вже я просто навернув) і при роботі з модулем достатньо функції SOUND_PlaySong (unsigned char numSong);

Файли

Приклади використання звукового модуля можна завантажити за посиланнями нижче. Схему малювати не став, бо там просто. підключено до виводу PB0, кнопка запуску мелодій підключено до виводу PD3. У проектах визначено 4 мелодії. Натискання кнопки запускає щоразу нову мелодію. Використовується мікроконтролер atmega8535. Спочатку хотів заморочитися на проект із чотирма кнопками – PLAY, STOP, PAUSE та NEXT, але потім подумав, що це зайве.

PS: Модуль не проходив розширене тестування та надається "як є". Якщо є якісь раціональні пропозиції – давайте його доопрацюємо.

У цій статті ми розглянемо спосіб відтворення тональних сигналів та навчимося програвати монофонічну мелодію.

Підготовка до роботи

У програмі оголошуються два масиви. Масив із нотами 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 , 5 , 5 20, 40, 5, 20

    Якщо у вашому автомобілі не встановлена ​​звукова сирена, і ви все ще ніяк не наважуйтесь якусь купити та встановити, то дана стаття саме для Вас. Навіщо купити дорогі сигналізації, якщо можна досить простим чином зібрати все це своїми руками?

    Уявляю дві такі прості схемина мікроконтролерах AVR ATmega8 та Attiny2313, точніше схема одна, просто реалізована для роботи на цих двох мікроконтролерах. До речі, в архіві ви знайдете два варіанти прошивок для мікроконтролера Атмега8, з яких перший відтворює звук схожий на автомобільну сигналізацію, а другий звук схожий на охоронну сигналізацію будівлі (швидкий та різкий сигнал).

    Всі прошивки можете завантажити нижче в архіві (вони всі підписані), в архіві ви також знайдете симуляцію схем в Proteus, а це означає, що послухавши всі мелодії, ви самі зі списку зможете підібрати те, що більше сподобався.

    Нижче схема сигналізації на Atmega8

    Список застосовуваних радіокомпонентів у схемі Атмега8

    U1- Мікроконтролер AVR 8-біт ATmega8-16PU, кільк. 1,
    R1- Резистор з номіналом 47 Ом, кільк. 1,
    R2, R3- Резистор з номіналом 270 Ом, кільк. 2,
    D2, D3-світлодіод, кільк. 2,
    LS1-динамік, кільк. 1,
    S1-датчик.

    А в схемі сигналізації на Attiny2313 змінено лише мк.
    U1- Мікроконтролер AVR 8-біт ATtiny2313-20PU, кільк. 1.

    Друкована платадля Atmega8 виглядає так:

    Як бачимо схема дуже проста, там всього один мікроконтролер, 3 резистори, 2 світлодіоди і ще один динамік. Замість кнопки можна застосувати геркон або інший контакт.

    Принцип роботи є наступним. Як тільки ми подаємо харчування, відразу ж загоряється або почне блимати (залежно від прошивки) світлодіод (у схемі D3), і якщо датчика не чіпатимемо, то сигналізація мовчатиме. Ось якщо спрацював датчик, то і сирена запрацює, також блиматиме світлодіод, але вже D2.

    Якщо хочете щоб при роботі сигналізації моргав також фари автомобіля, то для цього потрібно виведення мікроконтролера 24 РС1 підключити до реле через транзистор, а реле вже до фар. Щоб вимкнути сирену, необхідно вимкнути і знову включити прилад, або просто натиснути на кнопку. Для роботи мікроконтролера потрібний внутрішній генератор на 8МГЦ,

    Якщо хочете посилити звук сигналізації, то з транзисторами можете зібрати підсилювач і підключити до схеми. Я саме так і зробив, тільки у цій схемі цього не зобразив.

    Перейдемо до схеми на Attiny 2313, в ньому як і сказав раніше ті самі деталі і той же принцип роботи, тільки змінений МК, в результаті і підключені висновки. Такий мікроконтролер працює від внутрішнього генератора 4МГц, хоча можна і на 1МГц прошити.

    Нижче схема підключення вже на Attiny2313

    Для цієї мк написав лише одну версію прошивки, зібрав все на кокетній платі, перевірив, чи все нормально працює.
    А фьюзі потрібно виставити нижче поданим чином:





Top