Я люблю старые компьютерные игры. Люблю старое железо, но не настолько, чтобы коллекционировать его дома. Другое дело – поковырять какой-нибудь старый чип и попробовать самому что-нибудь воспроизвести, совместить старое с новым. В данной статье история о том, как я подключил AVR-микроконтроллер к YM3812, применявшуюся в таких звуковых картах как Adlib, Sound Blaster и Pro AudioSpectrum. Я не создал что-то принципиально новое, просто объединил разные идеи. Возможно кому-то будет интересна моя реализация. А может мой опыт подтолкнет кого-нибудь на создание свого ретро-проекта.


Суть данного проекта


Гуляя по просторам интернета, как-то раз я наткнулся на интересный проект OPL2 Audio Board for Arduino & Raspberry Pi. Если кратко: подключаем к Arduino или Raspberry Pi плату, загружаем скетч или софт соответственно, слушаем. Заманчивая идея поковырять OPL2 чип, послушать как он звучит и попробовать сделать что-нибудь свое не покидала меня, и я заказал, собрал и начал разбираться как оно работает.


Пара слов об управлении чипом YM3812


Чтобы музыка заиграла, мы должны выставлять регистры. Какие-то отвечают за настройку инструментов, какие-то за воспроизведение нот и т.д. Адрес регистра – 8 бит. Значение регистра – 8 бит. Список регистров приведен в спецификации.


Для передачи регистров мы должны правильно выставить показания на управляющих входах CS, RD,WR и A0 и шины данных D0..D7.


Вход CS нужен для блокировки шины данных в процессе ее установки. Устанавливаем CS=1 (отключаем вход), выставляем D0..D7, устанавливаем CS=0 (включаем).


На входе RD должна быть логическая единица
Для записи адреса регистра устанавливаем WR=0, A0=0
Для записи значения регистра устанавливаем WR=0, A0=1


OPL2 Audio Board for Arduino & Raspberry Pi


Упрощенная схема


Порядок передачи регистров:


  1. При инициализации устаналиваем PB2=1, чтобы заблокировать вход YM3812
  2. Передаем адрес регистра
    2.1 PB1=0 (A0=0)
    2.2 Передаем по SPI интерфейсу байт адреса регистра. Данные сохраняются в сдвиговом регистре 74595
    2.3 PB2=0 (WR=0, CS=0). Микросхема 7404 инвертирует сигнал и подает 1 на вход ST_CP 74595, который переключает свои выходы Q0..Q7. YM3812 записывает адрес регистра
    2.4 PB2=1 (WR=1, CS=1)
  3. Передаем значение регистра
    3.1 PB1=1 (A0=1)
    3.2 Передаем по SPI интерфейсу байт данных аналолично п2.2
    3.3 PB2=0 (WR=0, CS=0). YM3812 записывает данные
    3.4 PB2=1 (WR=1, CS=1)

На инверторе 7404 и кварце XTAL1 реализован генератор прямоугольных импульсов с частотой 3.579545МГц, необходимый для работы YM3812.
YM3014B преобразует цифровой сигнал в аналоговый, который усиливается операционным усилителем LM358.
Аудио усилитель LM386 нужен, чтобы можно было подключать к устройству пассивные динамики или наушники, т.к. мощности LM358 недостаточно.


Теперь попробуем извлечь из всего этого звук. Первое о чем я (и наверное не только я) подумал, это как бы заставить все это работать в DosBox. К сожалению «из коробки» поиграть с железным аналогом Adlib не получится, т.к. DosBox не знает о нашем устройстве ничего, и передавать OPL2 команды куда-либо не умеет (пока не умеет).


Автор проекта предлагает скетч для Teensy, работающий как MIDI устройство. Естественно звук будет состоять из заранее составленных инструментов и звучание будет не то, мы получим эмуляцию MIDI устройства на OPL2 чипе. Teensy у меня нет, и попробовать этот вариант не получилось.


Работа через последовательный порт


Есть скетч SerialPassthrough. С ним мы сможем передавать команды через последовательный порт. Остается только реализовать поддержку в 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. Очень хотелось все привести к какому-нибудь лаконичному виду, убрать лишние детали и собрать все на одной плате.


OPL2-USB


Появилась идея, а почему бы не сделать самостоятельное устройство с минимумом компонентов и заморочек при подключении. Во первых можно убрать 74595 и использовать порты атмеги. Тут она используется только для уменьшения количества проводов. Во вторых можно использовать готовый кварцевый генератор и избавиться от микросхемы 7404. Аудио усилитель тоже не нужен, если подключать устройство к колонкам. И наконец можно избавиться от USB-UART, если подключить атмегу к USB напрямую, например с использованием библиотеки V-USB: https://www.obdev.at/products/vusb/index.html. Чтобы не заморачиваться с написанием драйверов и их установкой, можно сделать микроконтроллер кастомным HID-устройством.


USB-OPL2 упрощенная схема


Порты B и С частично заняты подключением к программатору ISP и кварцу. Полностью свободным остался порт D, его используем для передачи данных. Оставшиеся порты я назначил в процессе проектирования печатной платы.


Полную схему можно изучить тут: https://easyeda.com/marchukov.ivan/opl2usb


Светодиод LED1 с его резистором опциональны и при сборке я не стал их устанавливать. Предохранитель U4 нужен, чтобы не сжечь случайно USB-порт. Его тоже можно не ставить, а заменить на перемычку.


Чтобы устройство было компактным, я решил попробовать собрать его на SMD-компонентах.


Печатные платы и готовое устройство



"Безопасный" вариант в термоусадке 50/25мм


Цифровая часть слева, аналоговая справа.


Для меня это был первый опыт проектирования и сборки готового устройства и не обошлось без косяков. Например отверстия по углам платы по задумке должны быть диаметром 3мм для стоек, но получились 1,5мм.


Прошивку можно посмотреть на github. В ранней версии одна команда отправлялась одним USB-пакетом. Потом выяснилось, что на динамичных треках DosBox начинает тормозить из-за большого оверхеда и низкой скорости USB 1.0, DosBox висит на отправке пакета и получении ответа. Пришлось сделать асинхронную очередь и отправлять команды пачкой. Это добавило небольшую задержку, но она не ощутима.


Настройка V-USB


Если с отправкой данных в 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
http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-2-hid-class-na-v-usb.html


Поддержка в DosBox


Код для DosBox можно посмотреть во все том же репозитории.


Для работы с устройством на стороне 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


Видео


Готовое устройство в действии

Подключение:



Звук записанный через звуковую карту:





Итоги и планы


Результатом я остался доволен. Подключать устройство легко, проблем никаких. Само собой мои модификации DosBox'а никогда не попадут в официальную версию и популярные ветки, т.к. это очень специфическое решение.


Далее на очереди ковыряние OPL3. Есть еще идея собрать трекер на OPL-чипах


Похожие проекты


Проигрыватель VGM-файлов


Звуковая карта OPL2 на шине ISA

Комментарии (29)


  1. DrMefistO
    04.12.2019 11:21

    Скажите, а отличия от YM2612 очень большие?


    1. Costic
      04.12.2019 14:15

      Большие. YM2612 — попроще, ей не нужен внешний ЦАП (YM3014). И это удобно. А вот каналов в YM2612 меньше (6 каналов вместо 9), зато один из каналов может оцифрованный звук выводить (любой). Для трекеров — супер. С точки зрения программиста работа с микросхемами очень схожа — параллельный порт (8-бит), CLOCK, CS, WR, RD. Официальную документацию на YM2612 я не встречал, точного описания _всех_ регистров нет.


  1. scg
    04.12.2019 13:56
    +1

    Отличный проект. Я так понимаю, что из-за особенностей USB нельзя отправлять команды синтезатору чаще чем 1000 раз в секунду. Получается, что этого достаточно?
    P.S. За Кирандию в видео — особое спасибо :)


    1. dead_man Автор
      04.12.2019 14:34

      Я к сожалению не знаю размер заголовков у посылок. Скорость USB 1.0 = 12 мегабит. Полезная нагрузка 33 байта. Если размер каждого пакета к примеру 33+100, то мы получим 11 тысяч запросов в секунду. Но тут еще надо учесть скорость обработки запросов операционкой. В текущей реализации все успевает отправляться на самых быстрых треках


      1. scg
        04.12.2019 14:57

        В USB посылки идут кадрами с периодом 1мс. Если вы хотите контролировать время отправки команд на синтезатор, то вам либо нужно добавлять временную метку к каждой команде, либо отправлять не более одной команды за кадр. Впрочем, если вы об это не задумывались, а устройство работает как надо, то запись в регистры OPL2 действительно происходит не чаще 1000 раз в секунду. Я просто задумывался над этим, когда хотел сделать USB->ISA переходник, чтобы подключать старые звуковухи к DosBox. И получалось, что в общем виде PCM работать не будет, так как после записи в регистр, нужно еще и проверить его статус, а это можно сделать только в следующем USB кадре.


        1. dead_man Автор
          04.12.2019 15:12

          Возможно по этой причине у меня все подвисало при отправке одного регистра за раз. Сейчас по 16 и норм. Не думали о нескольких командах в одной посылке?


          1. scg
            04.12.2019 20:34

            Если они относятся к одному моменту времени — то да. А если вам нужно отправить, допустим, 8000 сэмплов на ЦАП, то просто кидать по 8 значений за раз не получится. USB девайсу уже нужно будет знать, что обрабатывать эти значения нужно не сразу, а с какой-то задержкой.


          1. SADKO
            05.12.2019 10:34

            Вот, к стати вспомнил один ньюанс, регистры которые не меняются, лучше не трогать. Я в своё время тупо апдейтил всё, и на некоторых платах звучание мягко говоря отличалось.
            После того как я стал отправлять только изменившиеся значения проблема исчезла.


        1. ser-mk
          04.12.2019 23:32

          когда хотел сделать USB->ISA переходник,

          Интересный проект. А как вы планировании сделать контроллер ISA?


          1. scg
            06.12.2019 00:19

            FPGA+CY7C68013A+преобразователи логического уровня. Но, дальше обдумывания проект не пошел. Правда, в продаже были подобные переходники, но у них, говорят, были именно эти проблемы. В режиме DMA звуковуха еще может работать. Синтезаторы — то же. А вот PCM в режиме полинга — уже никак.


  1. scg
    04.12.2019 14:57

    не туда


  1. madf
    04.12.2019 16:32

    Было бы больше популяризированно, если бы можно было в обычной винде выбрать миди или любой другой стандарт «звуковой карты».


    1. scg
      04.12.2019 20:35

      Как MIDI OPL2 (да и OPL3) звучат препаршивейше. Другое дело, если музыка написана специально для этих синтезаторов.


  1. icCE
    04.12.2019 22:18

    Надо написать драйвер для DOS и можно грузится в саму систему как в славные добрые!


    1. SADKO
      05.12.2019 09:47

      Чтобы драйвер заработал, надо чтобы его кто-нибудь использовал, а игрухи тупо пишут в порты, увы.
      А вот из эмулятора, пожалуйста...


      1. tronix286
        05.12.2019 11:34

        Если я чего правильно помню, то при помощи EMM386 и виртуального режима можно перехватывать обращения к портам в досе. Так работал эмулятор саунд бластера через PC Speaker и другой софт. По тому же принципу на vogons делают всякие приставки к LPT (OPL2, OPL3, Tandy) и пишут перенаправляющий софт. Олдфаги объяснят более подробно.


      1. icCE
        05.12.2019 20:13

        Не вижу проблем сделать виртуальный порт, тем более это делалось уже в старших классов карт и надо було уже прописывать драйвер.


  1. romanetz_omsk
    04.12.2019 22:49

    "Чтобы не заморачиваться с написанием драйверов и их установкой, можно сделать микроконтроллер кастомным HID-устройством"
    В корне неверный подход. USB Audio class включает в себя как подкласс USB MIDI, конечные точки там типа bulk (пакеты по 64 байта в кадре — сколько влезет, скорость передачи определяется быстротой выставления ACK устройством). Так оно будет играть из коробки в винде и линуксе.


    1. dead_man Автор
      05.12.2019 00:19

      Но OPL2 это не MIDI


      1. icCE
        05.12.2019 01:01

        не midi, но может


        1. SADKO
          05.12.2019 10:20

          Нет, тут именно драйвер который играет GM через OPLы. OPL не понимает миди от слова совсем, это хардварные синтаки, органы управления которых управляются регистрами, которые встроенные в винду дровишки даже трогать не позволяют.
          Что мотивировало многих людей и меня в том числе писать свои, которые нихрена не знали про инструменты GM зато пересылали в регистры значения из событий контролов миди и отыгрывали события нот…
          … ну, это я типо на пальцах пытаюсь изложить, реальность несколько сложней


          1. icCE
            05.12.2019 20:12

            ну это не совсем так. Те я понимаю что ты хочешь сказать, но я говорю как со стороны обывателя (и да я знаю разницу между FM и Midi)

            Вполне можно сформировать Midi и именно midi формат, который будет играть яерез opl. Проигрователи есть и под linux и под dos и под win.


        1. gpyra
          05.12.2019 10:57

          В DOS игрушках у каждой свой набор инструментов был. Поэтому и звучало интересно, разнообразно.
          А тут вы что будете делать? SoundFont'ы не подходят, поскольку метод синтеза другой.


          1. icCE
            05.12.2019 20:12

            Специально сформированный midi для opl.


            1. scg
              06.12.2019 00:22

              Инструменты можно грузить SysExcl, как в MT-32, но это будет тоже костыль, ничуть не лучше прямого управления регистрами.


  1. romanetz_omsk
    05.12.2019 05:30

    Софтовая реализация V-USB — это не 12, это 1,5 мегабита. И набор классов там минимальный доступен (как раз только HID, audio device class — начиная с full-speed)


  1. gpyra
    05.12.2019 10:51

    А не проще ли перейти на программный стек?

    github.com/jpcima/ADLplug

    github.com/Wohlstand/OPL3BankEditor


  1. MaxFilippov
    05.12.2019 15:24

    А гербера/файлы проекта печатки есть? В репозитории вроде не нашел.