USB-звукова карта на YM3812

Alex Alex 04 грудня 2019

USB-звукова карта на YM3812

Я люблю старі комп'ютерні ігри. Люблю старе залізо, але не настільки, щоб колекціонувати його будинку. Інша справа – поколупати який-небудь старий чіп і самому спробувати що-небудь відтворити, поєднати старе з новим. У даній статті історія про те, як я підключив 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)

// Ідентифікуємо пристрій. За цими даними ми його будемо знаходити зовні
#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: no return 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

Source: habr.com

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.