Tarjeta de sonido USB en YM3812

Me encantan los viejos juegos de computadora. Me encanta el hierro viejo, pero no lo suficiente como para recogerlo en casa. Otra cosa es elegir un chip viejo e intentar reproducir algo usted mismo, combinar lo viejo con lo nuevo. En este artículo, la historia trata sobre cómo conecté el microcontrolador AVR al YM3812, que se utilizó en tarjetas de sonido como Adlib, Sound Blaster y Pro AudioSpectrum. No creé algo fundamentalmente nuevo, simplemente combiné diferentes ideas. Quizás alguien esté interesado en mi implementación. O tal vez mi experiencia empujará a alguien a crear su propio proyecto retro.


La esencia de este proyecto.


Caminando por Internet, un día me encontré con un interesante proyecto de OPL2 Audio Board para Arduino & Raspberry Pi . En resumen: conecte una placa a Arduino o Raspberry Pi, cargue un boceto o software, respectivamente, escuche. La idea tentadora de elegir el chip OPL2, escuchar cómo suena e intentar hacer algo por mi cuenta no me dejó, y ordené, ensamblé y comencé a descubrir cómo funciona.


Algunas palabras sobre la gestión de chips YM3812


Para que suene la música, debemos establecer registros. Algunos son responsables de afinar los instrumentos, algunos de tocar notas, etc. La dirección de registro es de 8 bits. El valor del registro es de 8 bits. Se proporciona una lista de registros en la especificación .


Para transferir los registros, debemos establecer correctamente las lecturas en las entradas de control CS, RD, WR y A0 y el bus de datos D0..D7.


Se necesita la entrada CS para bloquear el bus de datos durante su instalación. Configure CS = 1 (apague la entrada), configure D0..D7, configure CS = 0 (encienda).


La entrada RD debe ser una unidad lógica
Para escribir la dirección del registro, establezca WR = 0, A0 = 0
Para escribir el valor del registro, establezca WR = 0, A0 = 1


Placa de audio OPL2 para Arduino y Raspberry Pi


Esquema simplificado


Procedimiento de transferencia de registro:


  1. Durante la inicialización, configure PB2 = 1 para bloquear la entrada de YM3812
  2. Pasamos la dirección de registro
    2.1 PB1 = 0 (A0 = 0)
    2.2 Transmitimos los bytes de la dirección de registro a través de la interfaz SPI. Los datos se almacenan en el registro de desplazamiento 74595
    2.3 PB2 = 0 (WR = 0, CS = 0). El chip 7404 invierte la señal y suministra 1 a la entrada de ST_CP 74595 , que conmuta sus salidas Q0..Q7. YM3812 escribe dirección de registro
    2.4 PB2 = 1 (WR = 1, CS = 1)
  3. Pasamos el valor de registro
    3.1 PB1 = 1 (A0 = 1)
    3.2 Transferimos bytes de datos a través de la interfaz SPI de manera similar a la p.2.2
    3.3 PB2 = 0 (WR = 0, CS = 0). YM3812 escribe datos
    3.4 PB2 = 1 (WR = 1, CS = 1)

Un inversor 7404 y XTAL1 cuarzo implementan un generador de pulso rectangular con una frecuencia de 3.579545 MHz, que es necesario para el funcionamiento del YM3812 .
YM3014B convierte una señal digital en una señal analógica, que es amplificada por el amplificador operacional LM358 .
El amplificador de audio LM386 necesario para poder conectar altavoces o auriculares pasivos al dispositivo, como LM358 potencia del LM358 no es suficiente.


Ahora intentemos extraer el sonido de todo esto. Lo primero que pensé (y probablemente no solo yo) fue cómo hacer que todo funcione en DosBox. Desafortunadamente, jugar fuera de la caja con el hardware Adlib no funcionará, porque DosBox no sabe nada acerca de nuestro dispositivo y no sabe cómo transmitir comandos OPL2 a ningún lado (hasta ahora no lo sabe).


El autor del proyecto ofrece un boceto para Teensy, que funciona como un dispositivo MIDI. Naturalmente, el sonido consistirá en instrumentos precompilados y el sonido será diferente, obtendremos una emulación de un dispositivo MIDI en un chip OPL2. No tengo Teensy, y no pude probar esta opción.


Operación de puerto serie


Hay un boceto SerialPassthrough . Con él, podemos transmitir comandos a través del puerto serie. Solo queda implementar el soporte en DoxBox. svn://svn.code.sf.net/p/dosbox/code-0/dosbox/trunk la versión de SVN: svn://svn.code.sf.net/p/dosbox/code-0/dosbox/trunk


En el src/hardware/adlib.cpp cambiamos la implementación de 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; }; } 

Antes del ensamblaje, reemplace el número de puerto COM con el actual.


Si elimina el comentario en la línea //adlib_write(reg,val); , el sonido se reproducirá simultáneamente a través del emulador y el dispositivo.


En la configuración de DosBox, deberá especificar el uso de OPL2:


 [sblaster] oplemu=compat oplmode=opl2 

Así es como lo obtuve:



Se ve bastante voluminoso. Incluso si usa Arduino en lugar de la placa de pruebas, debe conectar los cables. El número de puerto en el sistema puede cambiar y tendrá que reconstruir el DosBox. Realmente quería llevar todo a una apariencia concisa, eliminar partes innecesarias y ensamblar todo en una placa.


OPL2-USB


Surgió una idea, y por qué no hacer un dispositivo independiente con un mínimo de componentes y problemas cuando está conectado. Primero, puede quitar el 74595 y usar los puertos atmega. Aquí se usa solo para reducir la cantidad de cables. En segundo lugar, puede usar un oscilador de cristal listo para usar y deshacerse del chip 7404 . Tampoco se necesita un amplificador de audio si conecta el dispositivo a los altavoces. Y finalmente, puede deshacerse de USB-UART si conecta el atmega a USB directamente, por ejemplo, utilizando la biblioteca V-USB: https://www.obdev.at/products/vusb/index.html . Para no molestarse en escribir controladores e instalarlos, puede hacer que el microcontrolador sea un dispositivo HID personalizado.


Circuito simplificado USB-OPL2


Los puertos B y C están parcialmente ocupados conectándose al programador ISP y al cuarzo. El puerto D permaneció completamente libre, lo usamos para la transferencia de datos. Asigné los puertos restantes en el proceso de diseño de PCB.


El esquema completo se puede estudiar aquí: https://easyeda.com/marchukov.ivan/opl2usb


LED1 con su resistencia es opcional y durante el montaje no los LED1 . Se necesita el fusible U4 para no quemar accidentalmente el puerto USB. Tampoco se puede configurar, sino que se reemplaza por un puente.


Para hacer el dispositivo compacto, decidí intentar ensamblarlo en componentes SMD.


Placas de circuito impreso y dispositivo terminado



Opción "segura" en termocontracción 50 / 25mm


Parte digital a la izquierda, analógica a la derecha.


Para mí, esta fue la primera experiencia en el diseño y montaje de un dispositivo terminado y no podría funcionar sin jambas. Por ejemplo, los orificios en las esquinas del tablero deben tener un diámetro de 3 mm para los bastidores, pero resultaron ser de 1,5 mm.


El firmware se puede ver en github . En la versión anterior, se envió un comando en un paquete USB. Luego resultó que en las pistas dinámicas DosBox comienza a disminuir debido a la gran sobrecarga y la baja velocidad de USB 1.0, DosBox se cuelga al enviar un paquete y recibir una respuesta. Tuve que hacer una cola asincrónica y enviar comandos en lotes. Esto agregó un ligero retraso, pero no se nota.


Configuración de V-USB


Si ya descubrimos el envío de datos al YM3812 anteriormente, entonces USB tendrá que jugar.


Cambie el nombre de usbconfig-prototype.h a usbconfig.h y agréguelo (a continuación se muestran solo las ediciones):


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

En el archivo main.c , definimos las estructuras de datos de parcela


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

Declarar un identificador para 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 }; 

Manejadores de eventos:


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

Recomiendo estos artículos en ruso sobre 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


Soporte de DosBox


El código para DosBox se puede ver en el mismo repositorio .


Para trabajar con el dispositivo en el lado de la PC, usé la biblioteca hidlibrary.h (desafortunadamente, no encontré enlaces al original), que tuvo que modificarse un poco.


Decidí no tocar el emulador OPL, sino implementar mi propia clase separada. Cambiar a USB en configuraciones ahora se ve así:


 [sblaster] oplemu=usb 

En el constructor del módulo Adlib en adlib.cpp agregue la condición:


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

Y en dosbox.cpp nueva opción de configuración:


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

El exe compilado se puede recoger aquí: https://github.com/deadman2000/usb_opl2/releases/tag/0.1


Video


Dispositivo listo en acción

Conexión:



Sonido grabado a través de una tarjeta de sonido:





Resultados y planes


Estaba satisfecho con el resultado. Es fácil conectar el dispositivo, no hay problemas. Por supuesto, mis modificaciones de DosBox nunca entrarán en la versión oficial y las ramas populares, como Esta es una solución muy específica.


El siguiente en la línea es elegir el OPL3. Todavía existe la idea de construir un rastreador en chips OPL


Proyectos similares


Jugador VGM


Tarjeta de sonido OPL2 en bus ISA

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


All Articles