USB-Soundkarte am YM3812

Ich liebe alte Computerspiele. Ich liebe altes Eisen, aber nicht genug, um es zu Hause zu sammeln. Eine andere Sache ist, einen alten Chip auszuwählen und zu versuchen, etwas selbst zu reproduzieren, das Alte mit dem Neuen zu kombinieren. In diesem Artikel geht es darum, wie ich den AVR-Mikrocontroller an den YM3812 angeschlossen habe, der in Soundkarten wie Adlib, Sound Blaster und Pro AudioSpectrum verwendet wurde. Ich habe nichts grundlegend Neues geschaffen, ich habe einfach verschiedene Ideen kombiniert. Vielleicht interessiert sich jemand für meine Umsetzung. Oder vielleicht wird meine Erfahrung jemanden dazu bringen, sein eigenes Retro-Projekt zu erstellen.


Die Essenz dieses Projekts


Als ich eines Tages durch das Internet ging, stieß ich auf ein interessantes Projekt OPL2 Audio Board für Arduino & Raspberry Pi . Kurz gesagt: Schließen Sie ein Board an Arduino oder Raspberry Pi an, laden Sie eine Skizze oder Software, und hören Sie zu. Die verlockende Idee, den OPL2-Chip auszuwählen, zu hören, wie er sich anhört, und zu versuchen, etwas Eigenes zu tun, ließ mich nicht los, und ich bestellte, montierte und begann herauszufinden, wie es funktioniert.


Ein paar Worte zum YM3812 Chip Management


Damit Musik abgespielt werden kann, müssen Register gesetzt werden. Einige sind für das Stimmen der Instrumente verantwortlich, andere für das Spielen von Noten usw. Die Registeradresse beträgt 8 Bit. Der Wert des Registers beträgt 8 Bits. Eine Liste der Register ist in der Spezifikation angegeben .


Um die Register zu übertragen, müssen die Messwerte an den Steuereingängen CS, RD, WR und A0 sowie am Datenbus D0..D7 korrekt eingestellt werden.


Der CS-Eingang wird benötigt, um den Datenbus während der Installation zu blockieren. Setze CS = 1 (schalte den Eingang aus), setze D0..D7, setze CS = 0 (ein).


Der RD-Eingang muss eine logische Einheit sein
Um die Adresse des Registers zu schreiben, setzen Sie WR = 0, A0 = 0
Um den Wert des Registers zu schreiben, setzen Sie WR = 0, A0 = 1


OPL2 Audio Board für Arduino & Raspberry Pi


Vereinfachtes Schema


Übertragungsvorgang registrieren:


  1. Setzen Sie während der Initialisierung PB2 = 1, um den Eingang von YM3812 zu blockieren
  2. Wir übergeben die Registeradresse
    2.1 PB1 = 0 (A0 = 0)
    2.2 Wir übertragen die Registeradressbytes über die SPI-Schnittstelle. Die Daten werden im Schieberegister 74595 gespeichert
    2,3 PB2 = 0 (WR = 0, CS = 0). Der Chip 7404 invertiert das Signal und liefert 1 an den Eingang von ST_CP 74595 , der seine Ausgänge Q0 ... 74595 schaltet. YM3812 schreibt die Registeradresse
    2.4 PB2 = 1 (WR = 1, CS = 1)
  3. Wir übergeben den Wert des Registers
    3.1 PB1 = 1 (A0 = 1)
    3.2 Wir übertragen Datenbytes über die SPI-Schnittstelle ähnlich wie in S.2.2
    3,3 PB2 = 0 (WR = 0, CS = 0). YM3812 schreibt Daten
    3.4 PB2 = 1 (WR = 1, CS = 1)

Ein Inverter 7404 und Quarz XTAL1 realisieren einen Rechteckimpulsgenerator mit einer Frequenz von 3.579545 MHz, der für den Betrieb des YM3812 notwendig ist.
YM3014B wandelt ein digitales Signal in ein analoges Signal um, das vom Operationsverstärker LM358 verstärkt wird.
Der Audioverstärker LM386 benötigt, damit LM386 passive Lautsprecher oder Kopfhörer an das Gerät angeschlossen werden können LM358 Leistung des LM358 reicht nicht aus.


Versuchen wir nun, den Klang aus all dem herauszuholen. Das erste, worüber ich (und wahrscheinlich nicht nur ich) nachdachte, war, wie alles in DosBox funktioniert. Leider funktioniert das Ausspielen mit der Adlib-Hardware nicht, weil DosBox weiß nichts über unser Gerät und weiß nicht, wie OPL2-Befehle irgendwo übertragen werden sollen (bisher nicht).


Der Autor des Projekts bietet eine Skizze für Teensy an, die als MIDI-Gerät arbeitet. Natürlich wird der Sound aus vorkompilierten Instrumenten bestehen und der Sound wird anders sein, wir werden eine Emulation eines MIDI-Geräts auf einem OPL2-Chip erhalten. Ich habe keinen Teensy und konnte diese Option nicht ausprobieren.


Serielle Schnittstelle


Es gibt eine Skizze SerialPassthrough . Damit können wir Befehle über die serielle Schnittstelle übertragen. Es bleibt nur die Unterstützung in DoxBox zu implementieren. Ich habe die Version von SVN verwendet: svn://svn.code.sf.net/p/dosbox/code-0/dosbox/trunk


In der src/hardware/adlib.cpp ändern wir die Implementierung von 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; }; } 

Ersetzen Sie vor dem Zusammenbau die COM-Anschlussnummer durch die aktuelle.


Wenn Sie den Kommentar in der Zeile entfernen //adlib_write(reg,val); , dann wird der Ton gleichzeitig über den Emulator und das Gerät abgespielt.


Im DosBox-Setup müssen Sie die Verwendung von OPL2 angeben:


 [sblaster] oplemu=compat oplmode=opl2 

So habe ich es bekommen:



Es sieht ziemlich sperrig aus. Auch wenn Sie Arduino anstelle des Steckbretts verwenden, müssen Sie die Drähte anschließen. Die Portnummer auf dem System kann sich ändern und Sie müssen die DosBox neu erstellen. Ich wollte wirklich alles auf den Punkt bringen, unnötige Teile entfernen und alles auf einer Platine zusammenbauen.


OPL2-USB


Es entstand die Idee, ein unabhängiges Gerät mit einem Minimum an Komponenten und Problemen beim Anschließen zu erstellen. Zunächst können Sie den 74595 entfernen und die Atmega-Ports verwenden. Hier wird es nur verwendet, um die Anzahl der Drähte zu reduzieren. Zweitens können Sie einen vorgefertigten Quarzoszillator verwenden und den 7404 Chip 7404 . Ein Audioverstärker wird auch nicht benötigt, wenn Sie das Gerät an die Lautsprecher anschließen. Und schließlich können Sie USB-UART loswerden, wenn Sie den atmega direkt an USB anschließen, z. B. über die V-USB-Bibliothek: https://www.obdev.at/products/vusb/index.html . Um das Schreiben und Installieren von Treibern zu vermeiden, können Sie den Mikrocontroller zu einem benutzerdefinierten HID-Gerät machen.


USB-OPL2 vereinfachte Schaltung


Die Ports B und C sind teilweise mit dem ISP-Programmierer und dem Quarz verbunden. Port D blieb völlig frei, wir nutzen es für die Datenübertragung. Ich habe die verbleibenden Ports im PCB-Design-Prozess zugewiesen.


Das vollständige Schema kann hier eingesehen werden: https://easyeda.com/marchukov.ivan/opl2usb


LED1 mit Widerstand ist optional und wurde bei der Montage nicht LED1 . Die U4-Sicherung wird benötigt, um den USB-Anschluss nicht versehentlich zu beschädigen. Es kann auch nicht gesetzt, sondern durch einen Jumper ersetzt werden.


Um das Gerät kompakt zu gestalten, habe ich mich entschlossen, es auf SMD-Bauteilen zu montieren.


Leiterplatten und fertiges Gerät



Option "Sicher" beim Schrumpfen 50 / 25mm


Digitalteil links, Analogteil rechts.


Für mich war dies die erste Erfahrung beim Entwerfen und Zusammenbauen eines fertigen Geräts und konnte nicht ohne Pfosten auskommen. Zum Beispiel sollten die Löcher in den Ecken der Platte einen Durchmesser von 3 mm für die Gestelle haben, es stellte sich jedoch heraus, dass sie 1,5 mm betragen.


Die Firmware kann auf Github eingesehen werden . In der früheren Version wurde ein Befehl in einem USB-Paket gesendet. Dann stellte sich heraus, dass DosBox auf dynamischen Tracks aufgrund des hohen Overheads und der geringen Geschwindigkeit von USB 1.0 langsamer wird. DosBox bleibt beim Senden eines Pakets und beim Empfangen einer Antwort hängen. Ich musste eine asynchrone Warteschlange erstellen und Befehle stapelweise senden. Dies führte zu einer leichten Verzögerung, die sich jedoch nicht bemerkbar macht.


V-USB-Setup


Wenn wir bereits früher herausgefunden haben, dass wir Daten an das YM3812 senden, muss USB daran basteln.


usbconfig-prototype.h in usbconfig.h und fügen Sie es hinzu (im Folgenden sind nur die Änderungen aufgeführt):


 //   .   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 

In der Datei main.c definieren wir die 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; 

Deklarieren Sie ein Handle für 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 }; 

Eventhandler:


 //    .         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(); } 

Ich empfehle diese russischsprachigen Artikel über 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-Unterstützung


Der Code für DosBox kann im selben Repository angezeigt werden.


Um mit dem Gerät auf der PC-Seite zu arbeiten, habe ich die Bibliothek hidlibrary.h verwendet (ich habe leider keine Links zum Original gefunden), die ein wenig modifiziert werden musste.


Ich beschloss, den OPL-Emulator nicht zu berühren, sondern eine eigene Klasse zu implementieren. Das Umschalten auf USB in Configs sieht jetzt so aus:


 [sblaster] oplemu=usb 

adlib.cpp im Konstruktor des Adlib-Moduls in adlib.cpp die Bedingung hinzu:


  else if (oplemu == "usb") { handler = new OPL2USB::Handler(); } else { 

Und in dosbox.cpp neue Konfigurationsoption:


 const char* oplemus[]={ "default", "compat", "fast", "mame", "usb", 0}; 

Die kompilierte exe kann hier abgeholt werden: https://github.com/deadman2000/usb_opl2/releases/tag/0.1


Video


Bereit Gerät in Aktion

Anschluss:



Sound aufgenommen über eine Soundkarte:





Ergebnisse und Pläne


Ich war mit dem Ergebnis zufrieden. Das Gerät lässt sich problemlos anschließen. Natürlich werden meine DosBox-Modifikationen niemals in die offizielle Version und in populäre Zweige gelangen, wie Dies ist eine sehr spezifische Lösung.


Als nächstes kommt die OPL3. Es gibt immer noch eine Idee, einen Tracker auf OPL-Chips aufzubauen


Ähnliche Projekte


VGM-Player


Soundkarte OPL2 auf ISA Bus

Source: https://habr.com/ru/post/de478666/


All Articles