YM3812上的USB声卡

我喜欢旧电脑游戏。 我爱旧铁,但还不足以在家中收藏。 另一件事是选择一些旧的芯片,然后尝试自己复制一些东西,将新旧结合起来。 在本文中,这个故事是关于我如何将AVR微控制器连接到YM3812的,YM3812在诸如Adlib,Sound Blaster和Pro AudioSpectrum之类的声卡中使用。 我没有创造出根本上崭新的东西,我只是结合了不同的想法。 也许有人会对我的实施感兴趣。 也许我的经验会促使某人创建自己的复古项目。


这个项目的实质


有一天,我在Internet上逛逛,遇到了一个有趣的针对Arduino和Raspberry Pi的OPL2音频板项目。 简而言之:将开发板连接到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


用于Arduino和Raspberry Pi的OPL2音频板


简化方案


注册转移程序:


  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的输入,ST_CP 74595的输出Q0..Q7进行切换。 YM3812写寄存器地址
    2.4 PB2 = 1(WR = 1,CS = 1)
  3. 我们传递寄存器的值
    3.1 PB1 = 1(A0 = 1)
    3.2我们通过SPI接口传输数据字节,类似于p.2.2
    3.3 PB2 = 0(WR = 0,CS = 0)。 YM3812写入数据
    3.4 PB2 = 1(WR = 1,CS = 1)

逆变器7404和石英XTAL1实现具有3.579545MHz的频率的矩形脉冲发生器,这对于YM3812操作是必需的。
YM3014B将数字信号转换为模拟信号,并由LM358运算放大器放大。
需要LM386音频放大器,以便无源扬声器或耳机可以连接到设备,例如 LM358功率不足。


现在,让我们尝试从所有这些中提取声音。 我(也许不仅是我)想到的第一件事是如何使其在DosBox中都能正常工作。 不幸的是,无法立即使用Adlib硬件进行播放,因为 DosBox对我们的设备一无所知,也不知道如何在任何地方传输OPL2命令(到目前为止还不知道)。


该项目的作者提供了Teensy的草图,用作MIDI设备。 自然,声音将由预编译的乐器组成,并且声音将有所不同,我们将在OPL2芯片上模拟MIDI设备。 我没有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);行中的注释//adlib_write(reg,val); ,然后声音将通过模拟器和设备同时播放。


在DosBox设置中,您将需要指定OPL2的用法:


 [sblaster] oplemu=compat oplmode=opl2 

这是我的方法:



看起来很笨重。 即使您使用Arduino而不是面包板,也需要连接电线。 系统上的端口号可能会更改,您将必须重新构建DosBox。 我真的很想使所有内容看起来简洁,删除不必要的零件并将所有内容组装在一块板上。


OPL2-USB


提出了一个主意,为什么不制造一个独立的设备,使它在连接时具有最少的组件和最少的麻烦。 首先,您可以卸下74595并使用atmega端口。 此处仅用于减少导线数量。 其次,您可以使用现成的晶体振荡器来摆脱7404芯片。 如果将设备连接到扬声器,则也不需要音频放大器。 最后,如果将atmega直接连接到USB,例如使用V-USB库: https ://www.obdev.at/products/vusb/index.html,则可以摆脱USB-UART。 为了不打扰编写驱动程序并安装它们,可以将微控制器设置为自定义HID设备。


USB-OPL2简化电路


端口B和C部分忙于连接ISP编程器和石英。 端口D保持完全空闲,我们将其用于数据传输。 我在PCB设计过程中分配了其余端口。


完整的方案可以在这里进行研究: https : //easyeda.com/marchukov.ivan/opl2usb


带电阻的LED1是可选的,在组装过程中我没有安装它们。 需要U4保险丝,以免意外烧毁USB端口。 也不能设置,而是用跳线代替。


为了使设备紧凑,我决定尝试将其组装在SMD组件上。


印刷电路板及成品



热收缩50 / 25mm中的“安全”选项


左侧为数字部分,右侧为模拟部分。


对我来说,这是设计和组装成品设备的第一次经验,没有门框就不可能做到。 例如,对于机架,板角上的孔的直径应为3毫米,但事实证明它们为1.5毫米。


可以在github上查看固件。 在早期版本中,一个命令以一个USB数据包发送。 事实证明,由于USB 1.0的开销大和速度慢,在动态轨道上DosBox开始变慢,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){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 

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: return no 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.cpp的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播放器


ISA总线上的声卡OPL2

Source: https://habr.com/ru/post/zh-CN478666/


All Articles