Как‑то раз долгие годы назад у меня умер Спектрум. Уже и не вспомню, что с ним случилось, но возиться с ремонтом желания не было, ибо на замену давно хотелось новенький ZX Evolution.
Все более‑менее полезные и выглядящие целыми детали были сняты, в том числе и музыкальный сопроцессор YM2149F. И как раз в нужный момент попалась статья @Z80A о сборке плеера на базе Arduino.
Попытка собрать его на макетке привела к какофонии, поэтому мы с другом собрались и за ночь под пиво запилили плату аж при помощи фоторезиста. Ну, точнее, он пилил какую‑то свою, а я свою, поэтому она вышла такая рукожопая :-)
С тех пор проект законченным назвать было сложно — усилитель для наушников был безжалостно выброшен, т.к. из‑за неправильного включения слишком сильно шумел, да и устройство получилось не особо компактным, чтобы носить его с собой. До корпуса и кнопок переключения дорожек руки тоже не дошли. Так и валялась плата с кучей проводов на стойке с аудиотехникой, подключенная в линейный выход — на случай, если захочется послушать спектрумное поппури.
Однако, недавно мне на барахолке попалась вот такая вундервафля — Casio FD-1. Спереди — кнопки воспроизведения, справа — дисковод, сзади — MIDI‑выход.
Как только я её увидел, в голове сразу что-то щёлкнуло: дисковод есть — можно сборники составлять, кнопки выбора трека тоже есть, да и пианино мигающее на панели прикольным было бы. До кучи, можно использовать его и с другими MIDI-синтезаторами, которые у меня есть.
С некоторым трудом плата AY-плеера была выкопана из барахла родственниками и отправлена в мою сторону. К счастью, доехала как будто бы целой — а попробуй пойми, если она и изначально вся кривая :-)
Роадмап проекта был намечен максимально коротким, чтобы не откладывать в долгий ящик — придумываем протокол, пишем новую прошивку, поверх навесом на макетке наворачиваем MIDI-интерфейс, всё это в коробочку и в стойку.
Зачем? Чтобы что?
Понятно, что с современными эмуляторами отличить на слух реальный чип весьма сложно, если вообще возможно, но просто греет душу тот факт, что оно исполняется на реальном железе, срукожопленном самолично, плюс дисплей в стойке прикольно моргает :-)
Эдакая извращённая аудиофилия, эволюционирующая в разумную форму жизни, ну и плюс шанс руки занять вечерами.
Протокол
Чтобы играть музыку по нотам, нужно бы поддерживать различные MIDI-события типа Note On/Note Off, однако в случае с AY это несколько сложно — ведь для создания красивых тембров трекеры манипулируют регистрами несколько раз в секунду. То есть это надо писать по сути свой драйвер с тембрами, таблицей нот, о которых по сей день возникают холивары, и всё вот это вот.
Так как в этом проекте мне хотелось чисто слушать готовую музыку из демок и игр, то можем ограничиться двумя действиями — выставлением тактовой частоты и записью в регистр музыкального сопроцессора. Посылать их будем через System Exclusive (SysEx) — сообщения, являющиеся по сути сырым потоком данных на MIDI-шине.
Тут нам попадается первый подводный камень — в MIDI все байты, не являющиеся статусом (т.е. теми же Note On/Off и иже с ними) должны быть с нулевым старшим битом.
Для начала, посмотрим в даташит на музыкальный сопроцессор и убедимся, что 4 бита нам хватит на номер регистра:
Команды у нас пока что тоже две, но возьмём для них два бита, чтобы с запасом. Остался один бит — в него можно как раз запихать старший бит значения регистра. По итогу вписываем команду записи регистра всего в два байта:
MSB |
6 |
5 |
4 |
3 |
2 |
1 |
LSB |
|
0 |
Запрещён |
№ команды WRITE PAIR (0b10) |
№ регистра |
MSB значения |
||||
1 |
Остальные биты значения |
То есть, например, если мы хотим записать в регистр #1 значение #FC, то пакет будет выглядеть так:
F0 : маркер начала SysEx-сообщения
A7 : маркер, что это пакет нашего формата. В стандарте MIDI указан как зарезервированный.
43 : (0b10 << 5) | (0x1 << 1) | (0xFC >> 7)
^ ^ ^— значение регистра (старший бит)
| +— номер регистра
+— константа команды WRITE_PAIR
7C : оставшиеся биты от значения регистра (0xFC & 0x7F)
F7 : маркер конца SysEx-сообщения
Очень удачно на такой формат первого байта ложится и команда выставления частоты тактовки сопроцессора:
MSB |
6 |
5 |
4 |
3 |
2 |
1 |
LSB |
|
0 |
Запрещён |
№ команды SET CLOCK (0b11) |
MSB байта №1 |
MSB байта №2 |
MSB байта №3 |
MSB байта №4 |
Признак ACB-Stereo |
|
1 |
Первый байт частоты в UInt32-LE без MSB |
|||||||
2 |
Второй байт частоты в UInt32-LE без MSB |
|||||||
3 |
Третий байт частоты в UInt32-LE без MSB |
|||||||
4 |
Четвёртый байт частоты в UInt32-LE без MSB |
В свободный бит я добавил флаг выбора альтернативной стерео-раскладки, так как в моей коллекции есть композиции как в ABC, так и в ACB-стерео. Так как каналы у сопроцессора абсолютно идентичны, то менять их местами можно будет программно.
Прошивка
Как там в фидошных времён песне-то было? "Свыклись с мощной машиной, отвыкли от всякого риска" — вот и я взял и с лёгкой руки наваял всё поначалу на FreeRTOS с мьютексами и приоритетами. Совершенно при этом не подумав, что в 2048 байтах оперативной памяти у лежащей в основе платформы ATMEGA328P такое будет вертеться как слон в посудной лавке :-)
Вдоволь насмотревшись на быстро мигающий светодиод при stack overflow и медленно мигающий при out of memory, решил всё сделать попроще, и та портянка со скрина выше стала более лаконичной:
void status_regi_notify(uint8_t regi, uint8_t valu) {
if(regi > 0xF) return;
register_dump[regi] = valu;
}
uint8_t status_regi_get_blocking(uint8_t regi) {
uint8_t val = register_dump[regi];
return val;
}
Рисуем остаток совы Пишем остальной код, заливаем — работает! Делаем от балды MIDI-файл, который зацикленно играет арпеджио из трёх нот, выводим с компа — работает! почти... Лагает безбожно с частотой обновления экрана.
Готовим салат «Асинхронный» — вам потребуются: помиогурдоры, цымайон, ез.
Библиотека для работы с MIDI была написана весьма сносно, а вот для работы с дисплеем — в лучших традициях Ардуины: «Есть библиотеки, чтобы сделать что угодно, но не больше одной вещи за раз»
Выяснилось, что библиотека LiquidCrystal_I2C, которую я изначально взял для управления экраном, содержит кучу задержек, оставшихся от её предка с параллельным интерфейсом. Но даже после того, как они все были убраны, дисплей стал инициализироваться нестабильно, но тормоза никуда не ушли.
>разбираем родную библиотеку Wire на части
>внутри провода задержки
Поиски привели к библиотеке AsyncI2CMaster от cskarai, которая позволяет асинхронно управлять I2C-шиной. Однако, если просто заменить Wire на неё, сама библиотека LiquidCrystal_I2c будет забивать очередь весьма быстро, да и никаких средств контроля таймингов в ней нет, а надеяться на медленность своего кода — подход не особо хороший :-)
Поэтому, делаем свою эрзац-кооперативную многозадачность поверх имеющегося кода.
Реализация простая — добавляем функцию, которая в цикле дёргается из основной программы, рядом с функцией опроса MIDI-интерфейса. Она проверяет флаг "занято" и если он не выставлен, то берёт следующий элемент очереди. Формат элементов очереди делаем максимально компактным, чтобы даже при перерисовке всего экрана не закончилась память:
Операция |
u8 |
SEND (посыл байта) либо WAIT (задержка более чем) |
Атрибуты |
u16 |
Минимальное время задержки, мкс |
либо | ||
u8 |
Байт для отправки в дисплей |
|
u8 |
Режим отправки (доп. битовая маска от библиотеки LiquidCrystal) |
Если из очереди был вытащен элемент типа WAIT, то функция выставляет флаг "занято" и записывает текущее системное время в переменную, а при последующих дёрганиях проверяет, не превысила ли разница запрошенное время задержки.
Если же там оказался элемент типа SEND, то она запускает следующий конечный автомат, который дёргается из основной программы аналогичным образом.
Сейчас |
Делаем |
Дальше |
WILL_SET_BUS |
Записываем полубайт в I2C-регистр дисплея |
DID_SET_BUS |
DID_SET_BUS |
Записываем то же самое значение, но со включенным битом EN, что говорит дисплею считать его с шины |
DID_EN_HIGH |
DID_EN_HIGH |
Сохраняем значение системного таймера |
WAIT_EN_LOW |
WAIT_EN_LOW |
Если с момента сохранения системного таймера прошло больше 2мкс, отключаем бит EN и отправляем значение в I2C-регистр |
DID_EN_LOW |
DID_EN_LOW |
I2C-регистр обработал наш запрос, сохраняем таймер опять |
WAIT_SETTLE |
WAIT_SETTLE |
При условии, что прошло больше 50мкс (время, нужное дисплею на обработку команды целиком): * Если закончили отправлять первый полубайт, и флаг "идёт инициализация дисплея" выключен, то поменять местами половины байта в очереди и перейти снова к WILL_SET_BUS * В противном случае отключить флаг ожидания, чтобы на следующем "дёрге" основная функция могла продолжить обрабатывать очередь |
Пробуем — вуаля, всё шустро и быстро! До кучи выяснилось, что конкретно моему дисплею и шаг WILL_SET_BUS не особо нужен, и разгон шины I2C со 100 кГц до 850 кГц он переживает спокойно (выше — уже нет :-)
Скорее всего, словами это описание понять сложно, поэтому вот тут можно посмотреть код.
Конвертер
Работающий плеер — это, конечно, хорошо, но какой с него толк, если для него нет музыки?
Поэтому был написан модуль, которому скармливаешь попарно записи в регистры AY, а он взамен выдаёт в описанном нами протоколе SysEx'ы, и до кучи Note On/Off на 4 канале, чтобы на панели FD-1 мигали клавиши пианино.
Поверх него были собраны две программы для, собственно, конвертирования — из формата PSG, записанных через ZXTune, и из формата VGM.
Тут появляется ещё один подводный камень — в MIDI тайминги задаются двумя значениями: tempo и timebase; а события расставляются по третьему, из них вычисляемому — по тикам.
Связаны они между собой уравнением:
В связи с особенностью прошивки Casio FD-1, в нашем случае BPM не может быть больше чем 255.
Для PSG нам нужны задержки с дискретностью в 20мс и 80мс, поэтому для tick = 10ms нам подойдёт 120 BPM при Timebase = 50:
VGM — более сложный случай, т.к. в нём используется частота дискретизации 44100 Гц, то есть нам нужен тик в .
Экспериментально по методу Подгониана были выбраны Timebase = 16000 и BPM = 165, так как инструментарий, которым я пользовался для отладки MIDI-файлов, не очень любит файлы с большими Timebase:
И скорости света мало
Всё это время я отлаживал воспроизведение через связку loopMIDI + Hairless MIDI-Serial по штатному USB-порту ардуины. Теперь пришло время подключить её по-нормальному, старым добрым DIN5-кабелем.
По стандарту MIDI вход должен быть гальванически развязан через оптопару, срабатывающую при токе через диод в 5мА. Ничтоже сумняшеся я вытащил из загашников TLP621 и какую-то подобранную в гугл-картинках схему:
На коленке собираем:
Втыкаем в комп, проверяем — работает! Да как же так, вот прям с первого раза?
Как раз тут и началась магия. С Yamaha MU50 в роли интерфейса для компа — всё приходит и играется замечательно. С FD-1 напрямую, или даже через ямаховский THRU-порт — сплошной шум да мусор, вешающий контроллер. (Вот и прошивку пофаззили заодно) Значит, пора доставать осциллограф, и любоваться:
Задержку между фронтами считаем по клеточкам, как в школе. Посередине верхнего графика на входе импульс шириной около 32 мкс, как раз на скорости в 31250 бод одна единичка получается.
А вот под ним из оптопары вылетает нечто длиной в 10-15 мкс. Как это прочитается UART'ом? Да как повезёт, так и прочитается — поэтому вместо данных и получаем на входе мусор.
Так как в последнем радиомагазине в городе даже резисторы и конденсаторы уже не всегда в наличии, пользуемся читом «звонок другу» и на выходных получаем в руки новенькую 6N138. Она имеет существенно большее быстродействие по сравнению с TLP621. Помимо этого, на выходе у неё составной транзистор, который сам по себе хоть медленнее обычного, но на корпус этой оптопары выведена в том числе и база его выходного транзистора:
Подтянув её через 10кОм к земле, мы можем ещё сильнее улучшить быстродействие оптопары.
К счастью, после этого задержка между фронтами входного и выходного сигнала упала более чем в два раза, и всё идеально заработало.
Сыграем в ящик
Раз уж проект заканчивается спустя столько лет, то захотелось собрать первое в жизни полноценное устройство.
Не коробочку, из которой торчат провода. Не корпус от заводского девайса, из которого вываливаются модули на термосоплях. А именно полноценную, пусть даже кривую и косую, но вещь в себе.
Для начала был куплен корпус из гнутого листового алюминия:
Процесс сверления и пиления не фиксировал, поэтому сразу итоговый результат:
Ужасно? Да это вы его ещё в лицо не видели!
Поговорка про «семь раз отмерь, один раз отрежь», оказывается, подразумевает, что нужно ещё подумать, где измерять %)
Поначалу затупил и начал пилить отверстие по внешнему размеру корпуса дисплея. Ну а потом понял, как его закрепить так, чтобы не торчал, но было уже слишком поздно.
Подписи к разъёмам напечатал на принтере этикеток, чтобы через пару месяцев/лет не гадать, что это за дичь и куда она втыкается.
На экран вывел все отвечающие за звук регистры, визуализатор громкостей, значки активного обмена каналов (ACB → ABC) и приёма валидных данных.
Демо!
Talk is cheap, show me the code
Естественно, всё это было выложено на гитхаб, если кто-то захочет зачем-то такое повторить :-)
В планах, по порядку убывания вероятности:
Поменять ни за чем не успевающий ЖК-экран на вакуумно-люминисцентный
... и прикрыть дырень под него полупрозрачным тёмным стеклом :-)
Развести и заказать новую плату, попутно впилить ещё один сопроцессор и поддержку TurboSound
Полноценный синтезатор (Note On, Note Off, вот это вот всё)
Backend для ZXTune для вывода в MIDI-порт в этом формате?
Ну а пока что как-то так! Подписывайтесь, ставьте лайки, вот это вот всё, или как там сейчас принято, и спасибо за внимание.
И главное, помните — лучше уродливое решение, которое выполняет вашу задачу, чем идеально красивое, но так и не собранное :-)
Комментарии (8)
Speccyfan
00.00.0000 00:00+3Музыка в видео из Demo Ecology :-) бальзам на сердце.
vladkorotnev Автор
00.00.0000 00:00+2Да, приятная демка.
Когда у товарища был кассетный лейбл, мы хотели перевести ее на английский и выпускать как альбом — с одной стороны программа, с другой музыка.
Успели даже получить у Noro и Busy разрешение, нарисовать полиграфию, но потом и авторы как-то пропали со связи как раз на моменте выяснения, как от нас им роялти отправлять, и у нас в жизни завал начался, а там и лейбл лопнул :/
dlinyj
Проект крутейший, спасибо за статью.
Мои нежные эстетические чувства задела передняя панель. Не знаю, как теперь её развидеть.
Из личных рекомендаций, можно всё это закрыть листом акрила, будет этакая стилизованная накладка. А так, просто круто!
vladkorotnev Автор
Да, это в планах после замены экрана, как и стойки у плат резьбой вовнутрь повернуть, чтобы нижестоящие приборы не царапало.
Просто зная себя, не запостил бы как есть — не запостил бы в итоге вообще %)
dlinyj
Правильный подход
Javian
Брутализм.
kovserg
Блин, есть же натфили.