Кто помнит Tetris 2 на Спектруме? Там были куча уровней, возможность играть вдвоём и классная музыка.
Недавно мы сделали 8-битный компьютер для несложных игр, но никаких звуковых возможностей в нём не предусмотрели. И вот захотелось добавить туда какую-нибудь 8-битную музыку. Мне вспомнилась именно мелодия из Tetris 2 (много за ним часов проведено), поэтому я стал с ней ковыряться.
Процессор ATmega328P в нашем компьютере большую часть времени занят отрисовкой изображения, поэтому времени на синтезирование нормальной музыки совсем нет. Значит нам понадобится звуковой процессор YM2149F (он же AY-3-8910), такой же как в ZX Spectrum и других компьютерах.
Подключаем YM2149F к Arduino
Начал я с подключения звукового синтезатора к плате Arduino и с вывода на него простых нот. YM управляется с помощью записи значений в один из 15 (на самом деле мы используем 13) регистров. В регистры записываются частоты звука в каждом из трёх каналов, частота шума, уровень громкости, частота и форма огибающей. Для адресации и передачи данных используется 8 сигналов. Ещё парочка нужны для управления режимом шины — выбор регистра или загрузка значения.
Найденная в интернете схема подключения
Для тактирования YM используется встроенный таймер процессора с делителем частоты. В итоге на вход звукового чипа приходит 2МГц, что неправильно с точки зрения соответствия оригиналу, но для наших тестов пока сойдёт.
Код для инициализации генератора тактового сигнала
// Sets a 4MHz clock OC2A (PORTB3)
void set_ym_clock(void) {
// PB3 - output
DDRB |= 0x01 << PORTB3;
// Set Timer 2 CTC mode with no prescaling. OC2A toggles on compare match
//
// WGM22:0 = 010: CTC Mode, toggle OC
// WGM2 bits 1 and 0 are in TCCR2A,
// WGM2 bit 2 is in TCCR2B
// COM2A0 sets OC2A (arduino pin 11 on Uno or Duemilanove) to toggle on compare match
//
TCCR2A = ((1 << WGM21) | (1 << COM2A0));
// Set Timer 2 No prescaling (i.e. prescale division = 1)
//
// CS22:0 = 001: Use CPU clock with no prescaling
// CS2 bits 2:0 are all in TCCR2B
TCCR2B = (1 << CS20);
// Make sure Compare-match register A interrupt for timer2 is disabled
TIMSK2 = 0;
// Divide the 16MHz clock by 8 -> 2MHz
OCR2A = 3;
}
Тестовые ноты проигрались успешно и надо было двигаться дальше.
Я раньше не очень интересовался спектрумовской музыкой отдельно от самого Спектрума, но всё же слышал про формат AY. Ещё в одной из статей я видел упоминание формата PSG. Он похож на всякие WAV в MP3 в том смысле, что там содержится линейная последовательность действий с регистрами музыкального сопроцессора. Поэтому файлы получаются большие и в память ATmega не влезают.
Файлы в формате AY намного меньше. В чём же там секрет? А в том, что это куски кода из игр или демок для проигрывания мелодии, а также некоторые массивы данных для этого кода. Обычно проигрыватели просто эмулируют центральный процессор Спектрума, чтобы выполнить эту программу и так проиграть мелодию.
Почему бы не сэмулировать Z80 на AVR?
… подумал я и поискал какую-нибудь библиотеку-симулятор процессора Z80 на языке C. Такой симулятор нашёлся. Ему надо было подсунуть только функции чтения/записи памяти и портов ввода-вывода.
Небольшая сложность возникла с памятью — ведь у ZX Spectrum 48 килобайт ОЗУ, а у ATmega328P всего 2 — напрямую создать массив памяти для функций чтения/записи не получится. Пришлось сделать массив адресов и ячеек, а в нем искать значения при обращениях от процессора.
Оказалось (кто бы мог подумать!), что эмулировать один 8-битный компьютер на другом 8-битном — не очень хорошая идея. Какой-то звук выводится, но всё происходит настолько медленно, что мелодией это назвать сложно. Тогда я решил разобраться с кодом проигрывателя и переписать его на C.
Декомпиляция или закат Солнца вручную
Код оказался немного запутанным. Это был интерпретатор какого-то байт-кода, управляющего музыкальным сопроцессором. В байт-коде даже есть поддержка циклов и подпрограмм. Каждый канал YM управляется отдельной программой с отдельным стеком. Получилось, что изначально я эмулировал компьютер, который эмулирует другой трехпроцессорный компьютер. И хоть программа получалась небольшая (и запускалась лишь 50 раз в секунду), всё равно это было очень медленно.
После того, как я почти раскусил формат этого байт-кода, я случайно наткнулся на его описание. Это был Fuxoft AY Language. Его разработал Frantisek Fuka (Fuxoft), который написал и сам тетрис, и музыку к нему. Этот язык используется в нескольких десятках композиций. И их код даже извлечён из игр в виде файлов FXM. Тот код, что я уже проанализировать, пришлось выкинуть, чтобы начать всё заново (но его по-прежнему можно увидеть в истории изменений репозитория).
Проигрыватель FXM
Краткое описание формата и его декодер мне попались в исходниках бульбовского проигрывателя. Сравнивая его с дизассемблированным кодом, я нашёл всего пару небольших отличий, но семантика структур данных стала намного понятнее.
Теперь проигрыватель работает достаточно быстро. Музыка играется, можно объединять компьютер и прогрыватель. Каждая композиция занимает всего пару килобайт, поэтому если взять Arduino Mega (на процессоре ATmega2560), то можно уместить в его память все существующие мелодии в формате FXM.
Что дальше?
Осталось добавить правильный кварцевый генератор, чтобы частота работы микросхемы была как на Спектруме. Ещё можно написать декодеры других трекерных форматов, добавить SD-карту и получится аппаратный плеер спектрумовской музыки. А когда подсоединим это к нашей игровой коробке — получится настоящий игровой автомат.
Правда пока почему-то не получается, чтобы громкость динамика была нормальной, сейчас всё очень тихо. Может кто-нибудь знает как заставить такой динамик из игрушечного телефона звучать? Китайский телефон с этим справляется, а выходы YM2149F почему-то не очень. Поэтому видео работы плеера я пока не выкладываю.