Placa de som USB no YM3812

Eu amo jogos de computador antigos. Adoro ferro velho, mas não o suficiente para coletá-lo em casa. Outra coisa é pegar um chip antigo e tentar reproduzir alguma coisa você mesmo, combinar o antigo com o novo. Neste artigo, a história é sobre como eu conectei o microcontrolador AVR ao YM3812, que foi usado em placas de som como Adlib, Sound Blaster e Pro AudioSpectrum. Não criei algo fundamentalmente novo, simplesmente combinei idéias diferentes. Talvez alguém esteja interessado na minha implementação. Ou talvez minha experiência leve alguém a criar seu próprio projeto retrô.


A essência deste projeto


Andando pela Internet, um dia me deparei com um interessante projeto OPL2 Audio Board para Arduino e Raspberry Pi . Em resumo: conecte uma placa ao Arduino ou Raspberry Pi, carregue um esboço ou software, respectivamente, ouça. A idéia tentadora de escolher o chip OPL2, ouvir como ele soa e tentar fazer algo próprio não me deixou, e eu pedi, montei e comecei a descobrir como ele funciona.


Algumas palavras sobre o gerenciamento de chips YM3812


Para que a música toque, precisamos definir registros. Alguns são responsáveis ​​por afinar os instrumentos, outros por tocar notas, etc. O endereço do registro é 8 bits. O valor do registro é 8 bits. Uma lista de registros é fornecida na especificação .


Para transferir os registradores, devemos definir corretamente as leituras nas entradas de controle CS, RD, WR e A0 e no barramento de dados D0..D7.


A entrada CS é necessária para bloquear o barramento de dados durante sua instalação. Defina CS = 1 (desligue a entrada), defina D0..D7, defina CS = 0 (ligue).


A entrada RD deve ser a unidade lógica
Para escrever o endereço do registro, defina WR = 0, A0 = 0
Para escrever o valor do registro, defina WR = 0, A0 = 1


Placa de áudio OPL2 para Arduino e Raspberry Pi


Esquema simplificado


Procedimento de transferência de registro:


  1. Durante a inicialização, defina PB2 = 1 para bloquear a entrada do YM3812
  2. Passamos o endereço de registro
    2.1 PB1 = 0 (A0 = 0)
    2.2 Nós transmitimos os bytes do endereço de registro através da interface SPI. Os dados são armazenados no registro de turno 74595
    2.3 PB2 = 0 (WR = 0, CS = 0). O chip 7404 inverte o sinal e fornece 1 à entrada do ST_CP 74595 , que alterna suas saídas Q0..Q7. YM3812 grava endereço de registro
    2,4 PB2 = 1 (WR = 1, CS = 1)
  3. Passamos o valor do registro
    3.1 PB1 = 1 (A0 = 1)
    3.2 Transferimos bytes de dados através da interface SPI de maneira semelhante à p.2.2
    3.3 PB2 = 0 (WR = 0, CS = 0). YM3812 grava dados
    3,4 PB2 = 1 (WR = 1, CS = 1)

Um inversor 7404 e quartzo XTAL1 implementam um gerador de pulsos retangular com uma frequência de 3,579545 MHz, necessária para a operação do YM3812 .
YM3014B converte um sinal digital em um sinal analógico, que é amplificado pelo amplificador operacional LM358 .
O amplificador de áudio LM386 necessário para que alto-falantes ou fones de ouvido passivos possam ser conectados ao dispositivo, como LM358 energia do LM358 não é suficiente.


Agora vamos tentar extrair o som de tudo isso. A primeira coisa que pensei (e provavelmente não apenas eu) foi como fazer tudo funcionar no DosBox. Infelizmente, jogar fora da caixa com o hardware Adlib não funcionará, porque O DosBox não sabe nada sobre o nosso dispositivo e não sabe como transmitir comandos OPL2 em qualquer lugar (até o momento).


O autor do projeto oferece um esboço para Teensy, trabalhando como um dispositivo MIDI. Naturalmente, o som será composto por instrumentos pré-compilados e o som será diferente; obteremos uma emulação de um dispositivo MIDI em um chip OPL2. Não tenho Teensy e não pude experimentar esta opção.


Operação de porta serial


Há um esboço SerialPassthrough . Com ele, podemos transmitir comandos através da porta serial. Resta apenas implementar o suporte no DoxBox. Eu usei a versão do SVN: svn://svn.code.sf.net/p/dosbox/code-0/dosbox/trunk


No src/hardware/adlib.cpp a implementação do 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 da montagem, substitua o número da porta COM pelo atual.


Se você remover o comentário na linha //adlib_write(reg,val); , o som será reproduzido simultaneamente pelo emulador e pelo dispositivo.


Na configuração do DosBox, você precisará especificar o uso do OPL2:


 [sblaster] oplemu=compat oplmode=opl2 

Aqui está como eu entendi:



Parece bastante volumoso. Mesmo se você usar o Arduino em vez da tábua de pão, precisará conectar os fios. O número da porta no sistema pode mudar e você precisará reconstruir o DosBox. Eu realmente queria trazer tudo para uma aparência concisa, remover peças desnecessárias e montar tudo em uma placa.


OPL2-USB


Surgiu uma idéia e por que não fazer um dispositivo independente com o mínimo de componentes e problemas quando conectado. Primeiro, você pode remover o 74595 e usar as portas atmega. Aqui é usado apenas para reduzir o número de fios. Em segundo lugar, você pode usar um oscilador de cristal pronto e se livrar do chip 7404 . Um amplificador de áudio também não é necessário se você conectar o dispositivo aos alto-falantes. E, finalmente, você pode se livrar do USB-UART se conectar o atmega ao USB diretamente, por exemplo, usando a biblioteca V-USB: https://www.obdev.at/products/vusb/index.html . Para não se preocupar em escrever drivers e instalá-los, você pode tornar o microcontrolador um dispositivo HID personalizado.


Circuito simplificado USB-OPL2


As portas B e C estão parcialmente ocupadas conectando-se ao programador do ISP e ao quartzo. A porta D permaneceu completamente livre, usamos para transferência de dados. Eu designei as portas restantes no processo de design da PCB.


O esquema completo pode ser estudado aqui: https://easyeda.com/marchukov.ivan/opl2usb


LED1 com seu resistor é opcional e durante a montagem não os instalei. O fusível U4 é necessário para não queimar acidentalmente a porta USB. Também não pode ser definido, mas substituído por um jumper.


Para compactar o dispositivo, decidi montá-lo em componentes SMD.


Placas de circuito impresso e dispositivo acabado



Opção "segura" no termo-retrátil 50 / 25mm


Parte digital à esquerda, analógica à direita.


Para mim, esta foi a primeira experiência em projetar e montar um dispositivo acabado e não poderia prescindir de batentes. Por exemplo, os furos nos cantos da placa devem ter 3 mm de diâmetro para os racks, mas foram 1,5 mm.


O firmware pode ser visualizado no github . Na versão anterior, um comando foi enviado em um pacote USB. Em seguida, verificou-se que, em faixas dinâmicas, o DosBox começa a desacelerar devido à grande sobrecarga e à baixa velocidade do USB 1.0, o DosBox fica impedido de enviar um pacote e receber uma resposta. Eu tive que fazer uma fila assíncrona e enviar comandos em lotes. Isso adicionou um pequeno atraso, mas não é perceptível.


Configuração V-USB


Se já descobrimos o envio de dados para o YM3812 anteriormente, o USB terá que mexer.


Renomeie usbconfig-prototype.h para usbconfig.h e adicione-o (abaixo estão apenas as edições):


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

No arquivo main.c , definimos as estruturas de dados do pacote


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

Declarar um 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 }; 

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

Eu recomendo estes artigos em russo sobre o 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


Suporte do DosBox


O código do DosBox pode ser visualizado no mesmo repositório .


Para trabalhar com o dispositivo no lado do PC, usei a biblioteca hidlibrary.h (infelizmente, não encontrei links para o original), que precisou ser modificada um pouco.


Decidi não tocar no emulador OPL, mas implementar minha própria classe separada. A mudança para USB nas configurações agora fica assim:


 [sblaster] oplemu=usb 

No construtor do módulo Adlib em adlib.cpp adicione a condição:


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

E no dosbox.cpp nova opção de configuração:


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

O exe compilado pode ser obtido aqui: https://github.com/deadman2000/usb_opl2/releases/tag/0.1


Vídeo


Dispositivo pronto em ação

Conexão:



Som gravado através de uma placa de som:





Resultados e planos


Fiquei satisfeito com o resultado. É fácil conectar o dispositivo, sem problemas. Obviamente, minhas modificações no DosBox nunca entrarão na versão oficial e em ramos populares, como Esta é uma solução muito específica.


O próximo na fila é escolher o OPL3. Ainda existe uma ideia para construir um rastreador em chips OPL


Projetos similares


VGM Player


Placa de som OPL2 no barramento ISA

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


All Articles