Многоголосая "музыкальная шкатулка" на ATtiny13
Описание
Исходный код для avr-gcc Быстрое умножение на ассемблере Другие варианты мелодии Joe Dassin - L'ete Indien Vangelis - La petite Fille de la Mer Формат мелодии Проект для AtmelStudio 6 Даже на микроконтроллерах с ограниченными ресурсами можно реализовать многоголосую музыкальную шкатулку. ATtiny13 обладает 1 килобайтом флеш-памяти (чего хватает на 512 программных инструкций, включая вектора прерываний) и 64 байтами оперативной памяти ОписаниеДля вывода звука используется широтно-импульсная модуляция (ШИМ, PWM), микроконтроллеры AVR реализуют широтно-импульсную модуляцию при помощи таймеров. Поскольку ATtiny13 обладает только одним таймером, его приходится использовать как для генерации ШИМ-сигнала, так и для синхронизации по времени при генерировании звука. Частота ШИМ должна быть не меньше 20кГц, чтобы его шум не был слышен. При работе микроконтроллера на частоте 9,6МГц, 8-битный таймер переполняется через каждые 256 тактов, 37500 раз в секунду. Однако, эта частота слишком высока для формирования звука, поэтому используется делитель. В прерывании таймера некая переменная уменьшается на 1, а формирование очередного сэмпла звука происходит в основном теле программы, когда значение этой переменной достигнет нуля, после чего её значение увеличивается на величину делителя (в данном случае - 4). Такой подход позволяет выравнивать звучание, если формирование очередного сэмпла заняло больше времени чем 4 цикла таймера, при этом не сбивается периодичность вызыва обработчика прерывания по переполнению таймера. Для того чтобы придать формируемому звуку живость, чтобы он не казался монотонным, сигнал, формирующий его, разбит на две части: форманта - основная частота в виде синусоиды, и обертона - дополнительные высокие частоты, задающие тембр звука. Амплитуда этих двух сигналов меняется различным образом во времени, и, вместе с тем, изменяется тембр звука. Поскольку ATtiny не обладают аппаратной поддержкой операции умножения, то умножение при вычислении амплитуды сигнала является узким местом. Для того чтобы ускорить операцию умножения, функция, реализующая его, написана на ассемблере. Используемые формы сигналов, а также значения частот и мелодия хранятся во флеш-памяти. Исходный код для avr-gcc#define F_CPU 9600000UL #include <avr/io.h> #include <avr/interrupt.h> #include <avr/pgmspace.h> #ifdef __AVR_MEGA__ // Если у нас процессор со встроенным умножением (мега) - то его и используем inline int8_t mul_s_u_shr8(int8_t s, uint8_t u) { return (s * u) >> 8; } #else // Для tiny - вызовем функцию которая не более чем за 78 тактов сделает то же самое extern int8_t mul_s_u_shr8(int8_t, uint8_t); #endif // Тип с фиксированной точкой - для позиции в вейв-форме и скорости // старшие 6 бит кодируют целую часть, младшие 10 - дробную typedef uint16_t fixed_6_10; const int8_t wave1[] PROGMEM = { // Форманта (синусоида) 0, 6, 12, 19, 24, 30, 36, 41, 45, 49, 53, 56, 59, 61, 63, 64, 64, 64, 63, 61, 59, 56, 53, 49, 45, 41, 36, 30, 24, 19, 12, 6, 0, -6, -12, -19, -24, -30, -36, -41, -45, -49, -53, -56, -59, -61, -63, -64, -64, -64, -63, -61, -59, -56, -53, -49, -45, -41, -36, -30, -24, -19, -12, -6 }; const int8_t wave2[] PROGMEM = { // Обертоны 0, 27, 47, 59, 62, 59, 53, 45, 37, 28, 20, 12, 6, -1, -6, -11, -14, -18, -20, -22, -23, -23, -23, -22, -21, -19, -17, -15, -12, -10, -6, -3, 0, 3, 6, 10, 12, 15, 17, 19, 21, 22, 23, 23, 23, 22, 20, 18, 14, 11, 6, 1, -6, -12, -20, -28, -37, -45, -53, -59, -62, -59, -47, -27 }; // Определяем скорость, для частоты в 1 герц #define SOUND_ONE_HERTZ_SPEED (65536 / (F_CPU / 256 / 4)) const fixed_6_10 notes[] PROGMEM = { // Значения полей waveSpeed для разных нот. Интервал - полутон. 0, // далее - начиная от фа# малой октавы (fixed_6_10)(0.5 + 185.00 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 196.00 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 207.65 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 220.00 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 233.08 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 246.94 * SOUND_ONE_HERTZ_SPEED), // до первой октавы и далее (fixed_6_10)(0.5 + 261.63 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 277.18 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 293.66 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 311.13 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 329.63 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 349.23 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 369.99 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 392.00 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 415.30 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 440.00 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 466.16 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 493.88 * SOUND_ONE_HERTZ_SPEED), // до второй октавы и далее (fixed_6_10)(0.5 + 523.25 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 554.37 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 587.33 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 622.25 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 659.26 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 698.46 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 739.99 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 783.99 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 830.61 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 880.00 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 932.33 * SOUND_ONE_HERTZ_SPEED), (fixed_6_10)(0.5 + 987.77 * SOUND_ONE_HERTZ_SPEED), // до третьей октавы (fixed_6_10)(0.5 + 1046.50 * SOUND_ONE_HERTZ_SPEED) }; const uint8_t song[] PROGMEM = { // Несколько тактов из Yiruma - River flows in you 0x30, 0x55, 0xC8, 0x74, 0x45, 0xC8, 0x1C, 0xA1, 0x44, 0xD0, 0x5B, 0x88, 0xC8, 0x1C, 0xC8, 0x8D, 0xC8, 0x50, 0xC8, 0x1B, 0xC8, 0x5C, 0xC8, 0x89, 0xC8, 0x50, 0xC8, 0x17, 0x84, 0xC8, 0x5C, 0xC8, 0x8B, 0xC8, 0x10, 0xC8, 0x55, 0x89, 0xC8, 0x10, 0xC8, 0x54, 0x84, 0xD0, 0x15, 0x8B, 0xD0, 0x17, 0x50, 0x88, 0xD0, 0x14, 0xD0, 0x12, 0x4F, 0x83, 0xD0, 0x86, 0xD0, 0x8B, 0xD0, 0x10, 0xC8, 0x4F, 0xC8, 0x10, 0x81, 0xD0, 0x88, 0xD0, 0x8D, 0xC8, 0x4B, 0xC8, 0x10, 0xC8, 0x52, 0xC8, 0x14, 0x89, 0xD0, 0x84, 0xD0, 0x8B, 0xD0, 0x14, 0xC8, 0x55, 0xC8, 0x17, 0x84, 0xD0, 0x8B, 0xD0, 0x88, 0xD0, 0x55, 0xC8, 0x14, 0xC8, 0x52, 0x8B, 0xD0, 0x86, 0xD0, 0x8F, 0xD0 }; typedef struct { fixed_6_10 wavePos; // Позиция в массивах звука. fixed_6_10 waveSpeed; // Скорость приращения позиции, кодируется также. uint8_t wave1amp; // Множетель громкости первого звука (форманты) uint8_t wave2amp; // Множетель громкость второго звука (обертоны) uint8_t volMark; // Маркировка громкости по каналу. } chan_info; #define NUM_CHANNELS 3 chan_info chans[NUM_CHANNELS]; #define WAVE1_FADE_SPEED 1 #define WAVE2_FADE_SPEED 3 volatile uint8_t amp = 128; volatile int8_t vec = 0; volatile int8_t itskip = 3; uint8_t runTick = 0; uint8_t songPos = 0; uint8_t ticksToNote = 1; inline void doTick(); inline void doSample(); // Прерывание при переполнении таймера 0 // выполняется 9.6 MHz / 256 = 37500 раз в секунду #ifdef TIMER0_OVF_vect ISR(TIMER0_OVF_vect) { #else ISR(TIM0_OVF_vect) { #endif uint8_t is = itskip; if (is > 0) { itskip = is - 1; uint8_t a = amp + vec; amp = a; OCR0A = a; } } // Просчитывает один сэмпл и выводит его. Выполняется 37500/4 = 9375 раз в секунду. inline void doSample() { int16_t sum = 127; for (chan_info * ch = &chans[0]; ch < &chans[NUM_CHANNELS]; ch++) { uint8_t wp = ch->wavePos >> 10; ch->wavePos += ch->waveSpeed; sum += mul_s_u_shr8(pgm_read_byte(&wave1[wp]), ch->wave1amp); sum += mul_s_u_shr8(pgm_read_byte(&wave2[wp]), ch->wave2amp); } if (sum > 255) sum = 255; if (sum < 0) sum = 0; vec = (sum - amp - 1) >> 2; if (runTick--) return; doTick(); // Каждые 256 сэмплов } // Прорабатывает коэффициенты звучащих каналов. Выполняется 9375/256 = 36,6 раз в секунду inline void doTick() { for (chan_info * ch = &chans[0]; ch < &chans[NUM_CHANNELS]; ch++) { if (ch->wave1amp >= WAVE1_FADE_SPEED) { ch->wave1amp -= WAVE1_FADE_SPEED; } else { ch->wave1amp = 0; } if (ch->wave2amp >= WAVE2_FADE_SPEED) { ch->wave2amp -= WAVE2_FADE_SPEED; } else { ch->wave2amp = 0; } } ticksToNote--; while (ticksToNote == 0) { uint8_t n = pgm_read_byte(&song[songPos++]); if ((n & 0xC0) == 0xC0) { // Пауза if ((n & 0x3F) == 0) { // Специальный случай ticksToNote = 64; } else { ticksToNote = n & 0x3F; } } else { chan_info * ch = &chans[n >> 6]; if (n & 0x20) { // Присутствует доп.байт задающий громкость ch->volMark = pgm_read_byte(&song[songPos++]); } uint8_t tone = n & 0x1F; if (tone == 0) { ch->waveSpeed = 0; ch->wave1amp = 0; ch->wave2amp = 0; } else { ch->waveSpeed = pgm_read_word(¬es[tone]); ch->wave1amp = (ch->volMark & 0xF) << 4; ch->wave2amp = ch->volMark & 0xF0; } ch->wavePos = 0; } if (songPos >= sizeof(song)) { songPos = 0; } } } int main(void) { DDRB = 0b00000001; // PB0 (вывод OC0A) настраиваем на выход TCCR0A = 0b10000011; // 1 x pwm, fast pwm TCCR0B = 0b00000001; // включаем таймер 0, без предмножителя TIMSK0 = TIMSK0 | (1 << TOIE0); // включаем прерывание по переполнению таймера 0 sei(); // Включаем прерывания, запускаем шарманку. while(1) { while (itskip > 0) ; itskip += 4; doSample(); } } Быстрое умножение на ассемблере#include <avr/io.h> .global mul_s_u_shr8 // Ассемблерная функция должна быть объявлена global // Функция максимум за 78 таков выполняет умножение знакового и беззнакового 8-битных чисел, // сдвигает произведенеи вправо на 8 разрядов (возвращает старший байт результата) mul_s_u_shr8: // r24 - первый множитель (int8_t), r22 - второй множитель (uint8_t), результат (int8_t) в r24 mov r21, r24 clr r23 // r24:r23 - сумма clr r24 clr r20 //r21:r20 - второй параметр расширенный до 16 бит, сдвигаемый на каждом шаге вправо арифметически tst r22 breq end // Если ноль, то сразу выходим cycle: brpl no_bit // Если старший бит не установлен, то ничего не делаем // Иначе прибавляем к сумме наш аргумент asr r21 ror r20 add r23, r20 adc r24, r21 lsl r22 brne cycle // Продолжаем пока не вытолкаем все биты ret no_bit: asr r21 ror r20 lsl r22 brne cycle // Продолжаем пока не вытолкаем все биты end: ret Другие варианты мелодииПоскольку память очень ограничена, мелодия может вмещать в себя всего несколько тактов звучания. В программе используется несколько тактов из мелодии "River flows in you" композитора Yiruma. Предлагаю несколько альтернативных фрагментов мелодий: Joe Dassin - L'ete Indienconst uint8_t song[] PROGMEM = { // Joe Dassin - L'ete Indien 0x33, 0x97, 0x64, 0x66, 0xA7, 0x66, 0xD4, 0x10, 0xCA, 0x0B, 0xF2, 0x44, 0x87, 0xDE, 0x0B, 0xCA, 0x10, 0xCA, 0x13, 0xCA, 0x12, 0xCA, 0x10, 0xCA, 0x12, 0x43, 0x86, 0xD4, 0x0F, 0xCA, 0x0B, 0xF2, 0x43, 0x86, 0xE8, 0x43, 0x86, 0xE8, 0x10, 0x44, 0x88, 0xCA, 0x11, 0xCA, 0x10, 0xCA, 0x0E, 0xCA, 0x0B, 0xCA, 0x09, 0xCA, 0x08, 0xD4, 0x44, 0x88, 0xDE, 0x08, 0xCA, 0x09, 0xD4, 0x0B, 0xD4, 0x0E, 0x44, 0x89, 0xCA, 0x0C, 0xCA, 0x0B, 0xCA, 0x0C, 0xF2, 0x44, 0x89, 0xE8, 0x44, 0x89, 0xE8, 0x18, 0x45, 0x89, 0xD4, 0x15, 0xCA, 0x11, 0xF2, 0x45, 0x89, 0xDE, 0x11, 0xCA, 0x15, 0xCA, 0x18, 0xCA, 0x17, 0xCA, 0x15, 0xCA, 0x17, 0x44, 0x87, 0xD4, 0x13, 0xCA, 0x10, 0xF2, 0x44, 0x87, 0xE8, 0x44, 0x87, 0xE8, 0x13, 0x44, 0x8A, 0xD4, 0x10, 0xCA, 0x16, 0xF2, 0x44, 0x8A, 0xF2, 0x13, 0xCA, 0x12, 0xCA, 0x10, 0xCA, 0x12, 0x43, 0x8B, 0xE8, 0x43, 0x8B, 0xE8, 0x43, 0x8B, 0xE8, 0x43, 0x8B, 0xE8 } Vangelis - La petite Fille de la Merconst uint8_t song[] PROGMEM = { // Vangelis - La petite Fille de la Mer 0x3B, 0x76, 0xAB, 0x66, 0xE4, 0x83, 0xE4, 0x88, 0xE4, 0x83, 0xE4, 0x76, 0x76, 0x87, 0xD2, 0x17, 0xD2, 0x59, 0x8A, 0xD2, 0x1B, 0xD2, 0x57, 0x8B, 0xE4, 0x83, 0xE4, 0x88, 0xE4, 0x16, 0x83, 0xE4, 0x87, 0xE4, 0x8A, 0xE4, 0x1B, 0x8B, 0xE4, 0x83, 0xE4, 0x88, 0xE4, 0x83, 0xE4, 0x56, 0x87, 0xD2, 0x17, 0xD2, 0x59, 0x8A, 0xD2, 0x1B, 0xD2, 0x57, 0x8B, 0xE4, 0x83, 0xE4, 0x88, 0xE4, 0x16, 0x83, 0xE4, 0x87, 0xE4, 0x8A, 0xE4, 0x54, 0x88, 0xE4, 0x18, 0x83, 0xE4, 0x5B, 0x86, 0xE4, 0x1E, 0x88, 0xE4, 0x5C, 0x83, 0xE4, 0x1B, 0x86, 0xE4, 0x1B, 0x81, 0xE4, 0x84, 0xE4, 0x88, 0xE4, 0x59, 0x81, 0xE4, 0x84, 0xE4, 0x88, 0xE4, 0x12, 0x86, 0xE4, 0x56, 0x81, 0xE4, 0x19, 0x84, 0xE4, 0x5C, 0x86, 0xE4, 0x1B, 0x81, 0xE4, 0x59, 0x84, 0xE4, 0x59, 0x89, 0xE4, 0x81, 0xE4, 0x84, 0xE4, 0x18, 0x88, 0xE4, 0x83, 0xE4, 0x8C, 0xE4 } Формат мелодииВы можете создавать свои мелодии, формат довольно простой: Каждая нота кодируется одним байтом, старшие 2 бита (7й и 6й) кодируют номер канала (0, 1, 2), или паузу (3). Если старшие два бита приняли значения 0, 1, или 2, то они кодируют номер канала (голоса) и: - Если следующий за ними бит (5й) установлен, значит за этой нотой идёт байт кодирующий громкость по этому каналу. Старшие 4ре бита кодируют начальную громкость обертонов (от 0 до 15), нижние - форманты. Громкость запоминается для канала, и эта и все последующие ноты будут звучать с заданной громкостью. - Младшие 5ть бит (от 4го по 0й) кодируют ноту. 0 - означает остановка звучания на канале. Остальные значения (от 1 до 31) кодируют ноту с шагом в полутон, начиная от "фа#" малой октавы, заканчивая "до" третьей окатвы. Если старшие два бита - оба единицы, то это означает паузу в тиках до следующей команды. Один тик - это 1/36,6 секунды (=23,7 мс). То есть допустимы паузы от 27,3 мс до 1,72 секунды. Проект для AtmelStudio 6Исходный код проекта доступен для скачивания здесь: zip-файл, 33 кБ. Помещённый здесь код является свободным. То есть, допускается его свободное использование для любых целей, включая коммерческие, при условии указания ссылки на автора (Погребняк Дмитрий, http://aterlux.ru/). 5 ms; mod: Mon, 11 Jan 2021 09:39:36 GMT; gen: Tue, 03 Dec 2024 16:48:37 GMT |