Многоголосая "музыкальная шкатулка" на ATtiny13

Даже на микроконтроллерах с ограниченными ресурсами можно реализовать многоголосую музыкальную шкатулку.

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(&notes[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 Indien

const 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 Mer

const 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/).





8 ms; mod: Mon, 11 Jan 2021 09:39:36 GMT; gen: Thu, 28 Mar 2024 21:27:49 GMT