Introduction

Page describes ways of interfacing Atari ST with Yamaha OPL soundchips (FM Operator Type-L) used in synthesizers, MSX computers, arcades and PC soundcards like: AdLib and Sound Blaster in 80's and early 90'. Additionally special mention has Hybrid Arts Melody Maker FM cartridge (from 1989), which wasn't previously documented, it's similar to OPL2, but is cost reduced version with predefined instrument patches.

Hardware options

NokturnFM2 / NokturnFM3 cartridges

TODO photo + description

Hybrid Arts Melody Maker FM cartridge (OPLL-X)

TODO photo + description

Serdaco’s OPL2LPT

TODO photo + description

Serdaco’s OPL3LPT

TODO photo + description

RetroWave USB OPL3 Express

TODO photo + description

Cheerful Electronics OPL2 Audio board

TODO photo + description

Cheerful Electronics OPL3 Duo!

TODO photo + description

Sound Blaster on ISA / VME

TODO photo + description

Hardware interface types

Cartridge port

Used by NokturnFM and Hybrid Arts Melody Maker FM cartridge. Plug and play, fastest interface, OPL is write only...

SPI emulation through parallel / Centronics port

Used with Cheerful Electronics OPL2 Audio board / OPL3 Duo!, OPL is write only, needs special adapter and external power supply (+5V). SPI emulation is more cpu intensive than simpler parallel / Centronics port and cartridge.

connection schematics
communication description
communication routines

Parallel / Centronics port

Used with Serdaco’s OPL2LPT / OPL3LPT, OPL is write only, needs adapter and external power supply (+5V). OPL3 is only usable on TT and better due to lack of SELIN signal, on plain ST OPL3 can only operate in OPL2 mode.

USB

Currently only RetroWave USB OPL3 Express is supported, which is registered in usb stack as a serial device. Nearly plug and play, but needs extra hardware (NetUSBee / Lighting ST interface providing USB connectivity) and installed usb stack on startup. Is most cpu intensive. Device uses special communication protocol, so sending small amount of OPL data has significant overhead (ie. data should preferably be buffered before sending).

communication description
communication routines
TODO

ISA / VME

Needs ISA->ST Bus or ISA->VME adapter. OPL soundchip state can be read (depending on card model(? to be verified)).

TODO

Hardware interface programming information

TODO

Handling OPL wait states

Depending on OPL chip type, proper timing is needed between OPL commands to set address and data, otherwise sound chip state might not be set properly. This doesn't apply to USB serial interface. We can either hardcode delays if our program is supposed to run on specific configuration only or we can calculate delays in runtime.

Sending data to OPL


Hardcoded delays

OPL3_DELAY_ADDR_WRITE / OPL3_DELAY_DATA_WRITE defines and OPL2_DELAY_ADDR_WRITE / OPL2_DELAY_DATA_WRITE below are delays required by OPL chips to succesfully perform data writes and depend on OPL chip type. Each OPL model uses different internal master clock, which is either slower or faster than Atari ST(or compatible) CPU clock, so writes are dependent on CPU bus speed, if cpu is slower, then there's no problem, this is different for faster CPU's. Currently most Atari ST computers have 8MHz or 16MHz clock's, but accelerators are more common.

For usual 8 / 16MHz cpu clocks common values are:

Copy to clipboard
8MHz OPL2_DELAY_ADDR_WRITE / OPL2_DELAY_DATA_WRITE (8 / 44) OPL3_DELAY_ADDR_WRITE / OPL3_DELAY_DATA_WRITE (1 / 1) 16 MHz OPL2_DELAY_ADDR_WRITE / OPL2_DELAY_DATA_WRITE (4 / 122) OPL3_DELAY_ADDR_WRITE / OPL3_DELAY_DATA_WRITE (4 / 4) ``` ``` ; OPL address write ... ; OPL address write delay rept OPL3_DELAY_ADDR_WRITE ; delay after address write nop endr ; OPL data write ... ; OPL data write delay rept OPL3_DELAY_DATA_WRITE ; register data write delay nop endr


As cpu ranges can vary (like with more common accelerated machines), hardcoded values might be insufficient in all cases and delay has to be computed at runtime.

OPL delays independent from cpu and bus speed

Here's routine I've used in latest version of VGM player (not published yet), it is based on FreeMint delay code written by Roger Burrows, but repurposed:

Copy to clipboard
TIMERD_VEC equ $110 MFP_REGS equ $fffffa01 MFP_IERB equ MFP_REGS+8 MFP_ISRB equ MFP_REGS+16 MFP_IMRB equ MFP_REGS+20 MFP_TCDCR equ MFP_REGS+28 MFP_TDDR equ MFP_REGS+36 XDEF _runDelayCalibration XDEF runDelayCalibration TEXT ; calibrate using MFP TimerD _runDelayCalibration: runDelayCalibration: clr.l intcount ;initialise interrupt count lea MFP_TCDCR,a0 ;a0 -> Timer C/D control reg andi.b #$f0,(a0) ;stop TimerD lea MFP_TDDR,a0 ;a0 -> TimerD data clr.b (a0) ;set count = 256 move.l TIMERD_VEC,d1 ;save old TimerD vector in d1 lea handle_timer,a0 move.l a0,TIMERD_VEC ;set up new TimerD vector lea MFP_IERB,a0 move.b (a0),ierbsave bset.b #4,(a0) ;set interrupt enable bit lea MFP_IMRB,a0 move.b (a0),imrbsave bset.b #4,(a0) ;set interrupt mask bit lea MFP_TCDCR,a0 ;a0 -> Timer C/D control reg ori.b #$02,(a0) ;set pre-scaler = divide-by-10 & start TimerD move.l 4(a7),d0 ;loopcount cal_loop: subq.l #1,d0 bpl.s cal_loop move.l intcount,d0 ;d0 = interrupt count move.l d0,intsave ;save it lea MFP_TCDCR,a0 ;a0 -> Timer C/D control reg andi.b #$f0,(a0) ;stop TimerD lea MFP_IERB,a0 ;restore interrupt bits move.b ierbsave,(a0) lea MFP_IMRB,a0 move.b imrbsave,(a0) move.l d1,TIMERD_VEC ;restore TimerD vector move.l intsave,d0 ;return interrupt count rts ; ; handle timer interrupt from calibration ; ; an interrupt occurs every 256/(2457600/10) seconds, ; i.e. every 1/960 second. ; ; overhead calculation ; -------------------- ; 68000 at 8MHz ; an interrupt occurs every 8000000/960 = 8333 cycles. ; the overhead is 116/3333, approx 1.4% ; other systems are even less, so we ignore handle_timer: ;68000: ;interrupt dispatch ;44 addq.l #1,intcount ;12+16 = 28 bclr.b #4,MFP_ISRB ;12+12 = 24 rte ;20 ;total 116 BSS intcount: ds.l 1 intsave: ds.l 1 ierbsave: ds.b 1 imrbsave: ds.b 1


Copy to clipboard
#include <mint/falcon.h> #include <mint/osbind.h> // initial 1 millisecond delay loop values #define LOOPS_68060 110000 // 68060 timing assumes 110MHz for safety #define LOOPS_68030 3800 // 68030 timing assumes 32MHz #define LOOPS_68000 760 // 68000 timing assumes 16MHz #define CALIBRATION_TIME_MS 100 // target millisecs to run calibration uint32_t gLoopcount_1_msec; uint32_t gDelay_1usec; static inline void delayInit(int32_t mcpu) { switch(mcpu) { case 60: { gLoopcount_1_msec = LOOPS_68060; } break; case 40: case 30: { // assumes 68030 gLoopcount_1_msec = LOOPS_68030; } break; default: { // assumes 68000 gLoopcount_1_msec = LOOPS_68000; } }; } // calibrate delay values: must only be called *after* interrupts are allowed // NOTE: we use TimerD so we restore the RS232 stuff extern uint32_t runDelayCalibration(uint32_t loopcount); static inline void delayCalibrate(void) { // first, we save the status of the relevant parts of the system. // since TimerD is used by the standard serial port for baud rate // control, we need to save the current interrupt status and baud // rate. the interrupt status must be saved via direct hardware // access; for the baud rate, we can use a system call. however, // since the serial port may have been remapped, we need to map it // back to the standard device first. int32_t old_device = Bconmap(6); // ok even if Bconmap() doesn't exist int32_t old_rate = Rsconf(-2,-1,-1,-1,-1,-1); // disable interrupts then run the calibration // (the calibration saves & restores the interrupt status) int32_t ret; if (Super((void *)1L) == 0L) ret = Super(0L); else ret = 0; const uint32_t loopcount = CALIBRATION_TIME_MS * gLoopcount_1_msec; const uint32_t intcount = runDelayCalibration(loopcount); if (ret) { SuperToUser((void *)ret); } Rsconf(old_rate,-1,-1,-1,-1,-1); Bconmap(old_device); // intcount is the number of interrupts that occur during 'loopcount' // loops. an interrupt occurs every 1/960 sec (see os.s). // so the number of loops per second = loopcount/(intcount/960). // so, loops per millisecond = (loopcount*960)/(intcount*1000) // = (loopcount*24)/(intcount*25). if (intcount) { // check for valid gLoopcount_1_msec = (loopcount * 24) / (intcount * 25); } gDelay_1usec = gLoopcount_1_msec / 1000; } void calibrateHardwareDelay(int32_t mcpu) { delayInit(mcpu); delayCalibrate(); }


Copy to clipboard
; OPL2 timings ; Master clock = 3,58 Mhz ; minimal time after address write mode = 12 cycles ; minimal time after data write mode = 84 cycles ; 12 / 3,58 = ~ 3,3 µs after the address write ; 84 / 3,58 = ~ 23,5 µs after the data write ; 100ns strobe hold OPL2_DELAY_ADDR_WRITE equ 4 OPL2_DELAY_DATA_WRITE equ 24 OPL2_DELAY_STROBE_HOLD equ 1 ; OPL3 timings ; Master clock = 14,32MHz ; minimal time after address write mode = 32 cycles ; minimal time after data write mode = 32 cycles ; 32 / 14,32 = ~ 2,23µs after the address write if we write data ; 32 / 14,32 = ~ 2,23µs after the data write ; 100ns strobe hold OPL3_DELAY_ADDR_WRITE equ 3 OPL3_DELAY_DATA_WRITE equ 3 OPL3_DELAY_STROBE_HOLD equ 1 XREF gDelay_1usec XREF _gDelay_1usec VAR_DELAY_1US equ _gDelay_1usec OsDelay macro labelname, reg move.l VAR_DELAY_1US,\2 .\1\@: subq.l #1,\2 bpl.s .\1\@ endm ... ; perform OPL address write ... address write ; delay after OPL3 address write rept OPL3_DELAY_ADDR_WRITE OsDelay addrWrite,d4 ; delay after address write endr ; perform data write ... data write ; delay after OPL3 data write rept OPL3_DELAY_DATA_WRITE OsDelay dataWrite,d4 ; register data write delay endr ...

On our program startup we need to pass cpu type retrieved from operating system 'Cookie Jar' to calibrateHardwareDelay() function. Afterwards we can use OsDelay m68k macro as in listing above. Doubly exported label is added in case we use newer mintelf gcc compiler for which underscores in symbols aren't needed. m68k assembly is written in Devpac / VASM syntax.