Carte son USB sur YM3812

J'adore les vieux jeux informatiques. J'adore le vieux fer, mais pas assez pour le récupérer à la maison. Une autre chose est de choisir une ancienne puce et d'essayer de reproduire quelque chose vous-même, de combiner l'ancien avec le nouveau. Dans cet article, l'histoire raconte comment j'ai connecté le microcontrôleur AVR au YM3812, qui était utilisé dans des cartes son comme Adlib, Sound Blaster et Pro AudioSpectrum. Je n'ai pas créé quelque chose de fondamentalement nouveau, j'ai simplement combiné différentes idées. Peut-être que quelqu'un sera intéressé par ma mise en œuvre. Ou peut-être que mon expérience poussera quelqu'un à créer son propre projet rétro.


L'essence de ce projet


En me promenant sur Internet, un jour, je suis tombé sur un projet intéressant de carte audio OPL2 pour Arduino et Raspberry Pi . En bref: connectez une carte à Arduino ou Raspberry Pi, chargez un croquis ou un logiciel, respectivement, écoutez. L'idée tentante de choisir la puce OPL2, d'écouter son son et d'essayer de faire quelque chose de moi-même ne m'a pas quitté, et j'ai commandé, assemblé et commencé à comprendre comment cela fonctionne.


Quelques mots sur la gestion des puces YM3812


Pour que la musique puisse jouer, nous devons établir des registres. Certains sont chargés d'accorder les instruments, certains de jouer des notes, etc. L'adresse du registre est de 8 bits. La valeur du registre est de 8 bits. Une liste de registres est donnée dans la spécification .


Pour transférer les registres, il faut régler correctement les relevés sur les entrées de contrôle CS, RD, WR et A0 et sur le bus de données D0..D7.


Une entrée CS est nécessaire pour bloquer le bus de données lors de son installation. Réglez CS = 1 (désactivez l'entrée), réglez D0..D7, réglez CS = 0 (activez).


L'entrée RD doit être une unité logique
Pour écrire l'adresse du registre, réglez WR = 0, A0 = 0
Pour écrire la valeur du registre, définissez WR = 0, A0 = 1


Carte audio OPL2 pour Arduino et Raspberry Pi


Schéma simplifié


Procédure de transfert de registre:


  1. Lors de l'initialisation, définissez PB2 = 1 pour bloquer l'entrée de YM3812
  2. Nous passons l'adresse du registre
    2.1 PB1 = 0 (A0 = 0)
    2.2 Nous transmettons les octets d'adresse de registre via l'interface SPI. Les données sont stockées dans le registre à décalage 74595
    2,3 PB2 = 0 (WR = 0, CS = 0). La puce 7404 inverse le signal et fournit 1 à l'entrée de ST_CP 74595 , qui commute ses sorties Q0..Q7. YM3812 écrit l'adresse du registre
    2,4 PB2 = 1 (WR = 1, CS = 1)
  3. Nous transmettons la valeur du registre
    3.1 PB1 = 1 (A0 = 1)
    3.2 Nous transférons les octets de données via l'interface SPI de la même manière que p.2.2
    3,3 PB2 = 0 (WR = 0, CS = 0). YM3812 écrit des données
    3.4 PB2 = 1 (WR = 1, CS = 1)

Un onduleur 7404 et quartz XTAL1 met en œuvre un générateur d'impulsions rectangulaire avec une fréquence de 3,579545 MHz, qui est nécessaire au fonctionnement du YM3812 .
YM3014B convertit un signal numérique en un signal analogique, qui est amplifié par l'amplificateur opérationnel LM358 .
L'amplificateur audio LM386 nécessaire pour pouvoir connecter des haut-parleurs passifs ou un casque à l'appareil, comme LM358 puissance du LM358 n'est pas suffisante.


Essayons maintenant d'extraire le son de tout cela. La première chose à laquelle j'ai (et probablement pas seulement moi) pensé était de savoir comment faire fonctionner tout cela dans DosBox. Malheureusement, jouer hors de la boîte avec le matériel Adlib ne fonctionnera pas, car DosBox ne sait rien de notre appareil et ne sait pas comment transmettre les commandes OPL2 n'importe où (jusqu'à présent, il ne le sait pas).


L'auteur du projet propose un sketch pour Teensy, fonctionnant comme un appareil MIDI. Naturellement, le son sera composé d'instruments précompilés et le son sera différent, nous obtiendrons une émulation d'un appareil MIDI sur une puce OPL2. Je n'ai pas Teensy et je n'ai pas pu essayer cette option.


Fonctionnement du port série


Il y a un croquis SerialPassthrough . Avec lui, nous pouvons transmettre des commandes via le port série. Il ne reste plus qu'à implémenter le support dans DoxBox. J'ai utilisé la version de SVN: svn://svn.code.sf.net/p/dosbox/code-0/dosbox/trunk


Dans le src/hardware/adlib.cpp nous modifions l'implémentation d'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; }; } 

Avant l'assemblage, remplacez le numéro de port COM par celui actuel.


Si vous supprimez le commentaire dans la ligne //adlib_write(reg,val); , le son sera lu simultanément via l'émulateur et l'appareil.


Dans la configuration DosBox, vous devrez spécifier l'utilisation d'OPL2:


 [sblaster] oplemu=compat oplmode=opl2 

Voici comment je l'ai obtenu:



Il semble assez volumineux. Même si vous utilisez Arduino au lieu de la maquette, vous devez connecter les fils. Le numéro de port sur le système peut changer et vous devrez reconstruire la DosBox. Je voulais vraiment tout apporter à un aspect concis, supprimer les pièces inutiles et assembler le tout sur une seule planche.


OPL2-USB


Une idée est venue, et pourquoi ne pas créer un appareil indépendant avec un minimum de composants et de problèmes lorsqu'il est connecté. Tout d'abord, vous pouvez retirer le 74595 et utiliser les ports atmega. Ici, il est utilisé uniquement pour réduire le nombre de fils. Deuxièmement, vous pouvez utiliser un oscillateur à cristal prêt à l'emploi et vous débarrasser de la puce 7404 . Un amplificateur audio n'est également pas nécessaire si vous connectez l'appareil aux haut-parleurs. Et enfin, vous pouvez vous débarrasser de USB-UART si vous connectez directement l'atmega à USB, par exemple en utilisant la bibliothèque V-USB: https://www.obdev.at/products/vusb/index.html . Afin de ne pas vous embêter à écrire des pilotes et à les installer, vous pouvez faire du microcontrôleur un appareil HID personnalisé.


Circuit simplifié USB-OPL2


Les ports B et C sont partiellement occupés à se connecter au programmateur FAI et au quartz. Le port D est resté totalement libre, nous l'utilisons pour le transfert de données. J'ai attribué les ports restants dans le processus de conception de PCB.


Le schéma complet peut être étudié ici: https://easyeda.com/marchukov.ivan/opl2usb


LED1 avec sa résistance est optionnelle et lors du montage je ne les ai pas installés. Le fusible U4 est nécessaire pour ne pas brûler accidentellement le port USB. Il ne peut pas non plus être réglé, mais remplacé par un cavalier.


Pour rendre l'appareil compact, j'ai décidé d'essayer de l'assembler sur des composants SMD.


Cartes de circuits imprimés et appareil fini



Option "Safe" en thermorétractable 50 / 25mm


Partie numérique à gauche, analogique à droite.


Pour moi, c'était la première expérience dans la conception et l'assemblage d'un appareil fini et ne pouvait pas me passer de jambages. Par exemple, les trous dans les coins de la planche doivent avoir un diamètre de 3 mm pour les racks, mais ils se sont avérés être de 1,5 mm.


Le firmware peut être consulté sur github . Dans la version précédente, une commande était envoyée dans un paquet USB. Ensuite, il s'est avéré que sur les pistes dynamiques, DosBox commence à ralentir en raison de la surcharge et de la faible vitesse de l'USB 1.0, DosBox se bloque lors de l'envoi d'un paquet et de la réception d'une réponse. J'ai dû faire une file d'attente asynchrone et envoyer des commandes par lots. Cela a ajouté un léger retard, mais ce n'est pas perceptible.


Configuration V-USB


Si nous avons déjà compris l'envoi de données au YM3812 plus tôt, alors l'USB devra bricoler.


Renommez usbconfig-prototype.h en usbconfig.h et ajoutez-le (ci-dessous ne sont que les modifications):


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

Dans le fichier main.c , nous définissons les structures de données des parcelles


 //      #define BUFF_SIZE 16 //  -   struct command_t { uchar address; uchar data; }; //   struct dataexchange_t { uchar size; struct command_t commands[BUFF_SIZE]; } pdata; 

Déclarez une poignée pour 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 }; 

Gestionnaires d'événements:


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

Je recommande ces articles en russe sur 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


Prise en charge de DosBox


Le code de DosBox peut être consulté dans le même référentiel .


Pour travailler avec le périphérique côté PC, j'ai utilisé la bibliothèque hidlibrary.h (malheureusement, je n'ai pas trouvé de liens vers l'original), qui a dû être un peu modifiée.


J'ai décidé de ne pas toucher l'émulateur OPL, mais d'implémenter ma propre classe séparée. Le passage à l'USB dans les configurations ressemble maintenant à ceci:


 [sblaster] oplemu=usb 

Dans le constructeur du module Adlib dans adlib.cpp ajoutez la condition:


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

Et dans dosbox.cpp nouvelle option de configuration:


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

L'exe compilé peut être récupéré ici: https://github.com/deadman2000/usb_opl2/releases/tag/0.1


Vidéo


Appareil prêt en action

Connexion:



Son enregistré via une carte son:





Résultats et plans


J'étais satisfait du résultat. Il est facile de connecter l'appareil, aucun problème. Bien sûr, mes modifications DosBox n'entreront jamais dans la version officielle et les branches populaires, comme Il s'agit d'une solution très spécifique.


La prochaine étape consiste à choisir l'OPL3. Il y a encore une idée pour construire un tracker sur les puces OPL


Projets similaires


VGM Player


Carte son OPL2 sur bus ISA

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


All Articles