- PVSM.RU - https://www.pvsm.ru -
Я люблю старые компьютерные игры. Люблю старое железо, но не настолько, чтобы коллекционировать его дома. Другое дело – поковырять какой-нибудь старый чип и попробовать самому что-нибудь воспроизвести, совместить старое с новым. В данной статье история о том, как я подключил AVR-микроконтроллер к YM3812, применявшуюся в таких звуковых картах как Adlib, Sound Blaster и Pro AudioSpectrum. Я не создал что-то принципиально новое, просто объединил разные идеи. Возможно кому-то будет интересна моя реализация. А может мой опыт подтолкнет кого-нибудь на создание свого ретро-проекта.
Гуляя по просторам интернета, как-то раз я наткнулся на интересный проект OPL2 Audio Board for Arduino & Raspberry Pi [1]. Если кратко: подключаем к Arduino или Raspberry Pi плату, загружаем скетч или софт соответственно, слушаем. Заманчивая идея поковырять OPL2 чип, послушать как он звучит и попробовать сделать что-нибудь свое не покидала меня, и я заказал, собрал и начал разбираться как оно работает.
Чтобы музыка заиграла, мы должны выставлять регистры. Какие-то отвечают за настройку инструментов, какие-то за воспроизведение нот и т.д. Адрес регистра – 8 бит. Значение регистра – 8 бит. Список регистров приведен в спецификации [2].
Для передачи регистров мы должны правильно выставить показания на управляющих входах CS, RD,WR и A0 и шины данных D0..D7.
Вход CS нужен для блокировки шины данных в процессе ее установки. Устанавливаем CS=1 (отключаем вход), выставляем D0..D7, устанавливаем CS=0 (включаем).
На входе RD должна быть логическая единица
Для записи адреса регистра устанавливаем WR=0, A0=0
Для записи значения регистра устанавливаем WR=0, A0=1
Порядок передачи регистров:
YM3812
74595
74595
, который переключает свои выходы Q0..Q7. YM3812
записывает адрес регистраYM3812
записывает данныеНа инверторе 7404
и кварце XTAL1
реализован генератор прямоугольных импульсов с частотой 3.579545МГц, необходимый для работы YM3812
.
YM3014B
преобразует цифровой сигнал в аналоговый, который усиливается операционным усилителем LM358
.
Аудио усилитель LM386
нужен, чтобы можно было подключать к устройству пассивные динамики или наушники, т.к. мощности LM358
недостаточно.
Теперь попробуем извлечь из всего этого звук. Первое о чем я (и наверное не только я) подумал, это как бы заставить все это работать в DosBox. К сожалению «из коробки» поиграть с железным аналогом Adlib не получится, т.к. DosBox не знает о нашем устройстве ничего, и передавать OPL2 команды куда-либо не умеет (пока не умеет).
Автор проекта предлагает скетч для Teensy, работающий как MIDI устройство. Естественно звук будет состоять из заранее составленных инструментов и звучание будет не то, мы получим эмуляцию MIDI устройства на OPL2 чипе. Teensy у меня нет, и попробовать этот вариант не получилось.
Есть скетч SerialPassthrough [3]. С ним мы сможем передавать команды через последовательный порт. Остается только реализовать поддержку в DoxBox. Я использовал версию из SVN: svn://svn.code.sf.net/p/dosbox/code-0/dosbox/trunk
В файле src/hardware/adlib.cpp
меняем реализацию OPL2:
#include "serialport/libserial.h"
namespace OPL2 {
#include "opl.cpp"
struct Handler : public Adlib::Handler {
virtual void WriteReg( Bit32u reg, Bit8u val ) {
//adlib_write(reg,val);
if (comport) {
SERIAL_sendchar(comport, reg);
SERIAL_sendchar(comport, val);
}
}
virtual Bit32u WriteAddr( Bit32u port, Bit8u val ) {
return val;
}
virtual void Generate( MixerChannel* chan, Bitu samples ) {
Bit16s buf[1024];
while( samples > 0 ) {
Bitu todo = samples > 1024 ? 1024 : samples;
samples -= todo;
adlib_getsample(buf, todo);
chan->AddSamples_m16( todo, buf );
}
}
virtual void Init( Bitu rate ) {
adlib_init(rate);
LOG_MSG("Init OPL2");
if (!SERIAL_open("COM4", &comport)) {
char errorbuffer[256];
SERIAL_getErrorString(errorbuffer, sizeof(errorbuffer));
LOG_MSG("Serial Port could not be opened.");
LOG_MSG("%s", errorbuffer);
return;
}
if (!SERIAL_setCommParameters(comport, 115200, 'n', SERIAL_1STOP, 8)) {
LOG_MSG("Error serial set parameters");
SERIAL_close(comport);
return;
}
}
~Handler() {
if (comport) SERIAL_close(comport);
}
private:
COMPORT comport;
};
}
Перед сборкой номер COM порта заменить на актуальный.
Если убрать комментарий в строчке //adlib_write(reg,val);
, то звук будет играть одновременно через эмулятор и девайс.
В настройке DosBox надо будет указать использование OPL2:
[sblaster]
oplemu=compat
oplmode=opl2
Вот как это получилось у меня:
Выглядит довольно громоздко. Даже если использовать Arduino вместо макетки, нужно подключать провода. Номер порта в системе может измениться и придется пересобирать DosBox. Очень хотелось все привести к какому-нибудь лаконичному виду, убрать лишние детали и собрать все на одной плате.
Появилась идея, а почему бы не сделать самостоятельное устройство с минимумом компонентов и заморочек при подключении. Во первых можно убрать 74595
и использовать порты атмеги. Тут она используется только для уменьшения количества проводов. Во вторых можно использовать готовый кварцевый генератор и избавиться от микросхемы 7404
. Аудио усилитель тоже не нужен, если подключать устройство к колонкам. И наконец можно избавиться от USB-UART, если подключить атмегу к USB напрямую, например с использованием библиотеки V-USB: https://www.obdev.at/products/vusb/index.html [4]. Чтобы не заморачиваться с написанием драйверов и их установкой, можно сделать микроконтроллер кастомным HID-устройством.
Порты B и С частично заняты подключением к программатору ISP и кварцу. Полностью свободным остался порт D, его используем для передачи данных. Оставшиеся порты я назначил в процессе проектирования печатной платы.
Полную схему можно изучить тут: https://easyeda.com/marchukov.ivan/opl2usb [5]
Светодиод LED1
с его резистором опциональны и при сборке я не стал их устанавливать. Предохранитель U4 нужен, чтобы не сжечь случайно USB-порт. Его тоже можно не ставить, а заменить на перемычку.
Чтобы устройство было компактным, я решил попробовать собрать его на SMD-компонентах.
"Безопасный" вариант в термоусадке 50/25мм
Цифровая часть слева, аналоговая справа.
Для меня это был первый опыт проектирования и сборки готового устройства и не обошлось без косяков. Например отверстия по углам платы по задумке должны быть диаметром 3мм для стоек, но получились 1,5мм.
Прошивку можно посмотреть на github [6]. В ранней версии одна команда отправлялась одним USB-пакетом. Потом выяснилось, что на динамичных треках DosBox начинает тормозить из-за большого оверхеда и низкой скорости USB 1.0, DosBox висит на отправке пакета и получении ответа. Пришлось сделать асинхронную очередь и отправлять команды пачкой. Это добавило небольшую задержку, но она не ощутима.
Если с отправкой данных в YM3812 мы уже разобрались раньше, то с USB придется повозиться.
Переименовываем usbconfig-prototype.h
в usbconfig.h
и дописываем его (ниже только правки):
// Указываем частоту контроллера. Установка глобального define в настройках проекта мне не помогла
#define F_CPU 12000000UL
// Порты согласно подключению
#define USB_CFG_IOPORTNAME B
#define USB_CFG_DMINUS_BIT 0
#define USB_CFG_DPLUS_BIT 1
#define USB_CFG_HAVE_INTRIN_ENDPOINT 1
// Максимальное потребление тока 20 мА
#define USB_CFG_MAX_BUS_POWER 20
// Говорим, что у нас есть функция usbFunctionWrite
#define USB_CFG_IMPLEMENT_FN_WRITE 1
// Опредлеяем обработчик перезагрузки устройства (в нем будем перезагружать OPL2)
#define USB_RESET_HOOK(resetStarts) if(!resetStarts){hadUsbReset();}
// Идентифицируем устройство. По этим данным мы его будем находить снаружи
#define USB_CFG_DEVICE_ID 0xdf, 0x05 /* VOTI's lab use PID */
#define USB_CFG_VENDOR_NAME 'd', 'e', 'a', 'd', '_', 'm', 'a', 'n'
#define USB_CFG_VENDOR_NAME_LEN 8
#define USB_CFG_DEVICE_NAME 'O', 'P', 'L', '2'
#define USB_CFG_DEVICE_NAME_LEN 4
// Сообщаем, что у нас HID-устройство
#define USB_CFG_DEVICE_CLASS 0
#define USB_CFG_INTERFACE_CLASS 3
// Размер дескриптора usbHidReportDescriptor
#define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH 22
// По умолчанию настроено на прерывание INT0, мы же поменяли порт на PCINT0
#define USB_INTR_CFG PCICR
#define USB_INTR_CFG_SET (1 << PCIE0)
#define USB_INTR_CFG_CLR 0
#define USB_INTR_ENABLE PCMSK0
#define USB_INTR_ENABLE_BIT PCINT0
#define USB_INTR_VECTOR PCINT0_vect
В файле main.c
определяем структуры данных посылок
// Количество регистров в одной посылке
#define BUFF_SIZE 16
// Пара адрес-значение для регистра
struct command_t
{
uchar address;
uchar data;
};
// Лист регистров
struct dataexchange_t
{
uchar size;
struct command_t commands[BUFF_SIZE];
} pdata;
Объявляем дескриптор для HID
PROGMEM const char usbHidReportDescriptor[] = { // USB report descriptor
0x06, 0x00, 0xff, // USAGE_PAGE (Vendor Defined Page)
0x09, 0x01, // USAGE (Vendor Usage 1)
0xa1, 0x01, // COLLECTION (Application)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, sizeof(struct dataexchange_t), // REPORT_COUNT
0x09, 0x00, // USAGE (Undefined)
0xb2, 0x02, 0x01, // FEATURE (Data,Var,Abs,Buf)
0xc0 // END_COLLECTION
};
Обработчики событий:
// Посылка может приходить частями. Тут мы запоминаем сколько приняли и сколько осталось
static uchar currentAddress;
static uchar bytesRemaining;
// Прием посылки
uchar usbFunctionWrite(uchar *data, uchar len)
{
if (bytesRemaining == 0)
return 1;
if (len > bytesRemaining)
len = bytesRemaining;
uchar *buffer = (uchar*)&pdata;
memcpy(buffer + currentAddress, data, len);
currentAddress += len;
bytesRemaining -= len;
if (bytesRemaining == 0)
{
for (int i = 0; i < pdata.size; ++i) {
struct command_t cmd = pdata.commands[i];
if (cmd.address == 0xff && cmd.data == 0xff) // Для софтварного ребута OPL2 мы просто передаем в посылке FFFF
opl_reset();
else
opl_write(cmd.address, cmd.data);
}
}
return bytesRemaining == 0;
}
// При получении запроса USBRQ_HID_SET_REPORT мы должны подготовиться к получении посылки
usbMsgLen_t usbFunctionSetup(uchar data[8])
{
usbRequest_t *rq = (void*)data;
if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) {
if (rq->bRequest == USBRQ_HID_SET_REPORT) {
bytesRemaining = sizeof(struct dataexchange_t);
currentAddress = 0;
return USB_NO_MSG;
}
}
return 0; /* default for not implemented requests: return no data back to host */
}
// Обработчик команды на перезапуск устройства
extern void hadUsbReset(void) {
opl_reset();
}
Рекомендую эти рускоязычные статьи про V-USB:
http://microsin.net/programming/avr-working-with-usb/avr-v-usb-tutorial.html [7]
http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-2-hid-class-na-v-usb.html [8]
Код для DosBox можно посмотреть во все том же репозитории [9].
Для работы с устройством на стороне PC я использовал библиотеку hidlibrary.h
(ссылки на оригинал, к сожалению, не нашел), которую пришлось немного доработать.
Эмулятор OPL я решил не трогать, а реализовать свой отдельный класс. Переключение на USB в конфигах теперь выглядит так:
[sblaster]
oplemu=usb
В констркуторе модуля Adlib в adlib.cpp
добавляем условие:
else if (oplemu == "usb") {
handler = new OPL2USB::Handler();
} else {
И в dosbox.cpp
новый вариант настройки:
const char* oplemus[]={ "default", "compat", "fast", "mame", "usb", 0};
Скомпилированный exe можно забрать тут: https://github.com/deadman2000/usb_opl2/releases/tag/0.1 [10]
Подключение:
Звук записанный через звуковую карту:
Результатом я остался доволен. Подключать устройство легко, проблем никаких. Само собой мои модификации DosBox'а никогда не попадут в официальную версию и популярные ветки, т.к. это очень специфическое решение.
Далее на очереди ковыряние OPL3. Есть еще идея собрать трекер на OPL-чипах
Звуковая карта OPL2 на шине ISA [12]
Автор: dead_man
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/staroe-zhelezo/338771
Ссылки в тексте:
[1] OPL2 Audio Board for Arduino & Raspberry Pi: https://hackaday.io/project/18995-opl2-audio-board-for-arduino-raspberry-pi
[2] спецификации: https://pdf1.alldatasheet.com/datasheet-pdf/view/84281/YAMAHA/YM3812.html
[3] SerialPassthrough: https://github.com/DhrBaksteen/ArduinoOPL2/blob/master/examples/SerialPassthrough/SerialPassthrough.ino
[4] https://www.obdev.at/products/vusb/index.html: https://www.obdev.at/products/vusb/index.html
[5] https://easyeda.com/marchukov.ivan/opl2usb: https://easyeda.com/marchukov.ivan/opl2usb
[6] github: https://github.com/deadman2000/usb_opl2
[7] http://microsin.net/programming/avr-working-with-usb/avr-v-usb-tutorial.html: http://microsin.net/programming/avr-working-with-usb/avr-v-usb-tutorial.html
[8] http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-2-hid-class-na-v-usb.html: http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-2-hid-class-na-v-usb.html
[9] все том же репозитории: https://github.com/deadman2000/usb_opl2/tree/master/DosBox
[10] https://github.com/deadman2000/usb_opl2/releases/tag/0.1: https://github.com/deadman2000/usb_opl2/releases/tag/0.1
[11] Проигрыватель VGM-файлов: https://github.com/AidanHockey5/YM3812_VGM_Player
[12] Звуковая карта OPL2 на шине ISA: http://www.malinov.com/Home/sergeys-projects/isa-opl2-card
[13] Источник: https://habr.com/ru/post/478666/?utm_campaign=478666&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.