Arduino based LPT2MIDI device for my retro PC...
Arduino code:
void receive() {
Serial.write((PIND>>3)+(PINB<<5)&255);
}
void setup() {
DDRB = 0;
DDRD = 0;
Serial.begin(31250); // MIDI uses 31.25 kbps serial
attachInterrupt(digitalPinToInterrupt(2), receive, RISING);
}
If you want to increase the serial buffer size, then change the parameter SERIAL_TX_BUFFER_SIZE in HardwareSerial.h to your liking. This arrangement has the added benefit that if the MIDI messages are arriving too fast at certain point in time, but not others, then Arduino buffers them and one may avoid some issues that would otherwise be present (although LPT isn't particularly fast port so the benefit might not be that big).
You can trap port operations on 386 or higher using the following code in DOS. Requires EMM386 (loadhigh works). This version is rather simplistic and doesn't implement all the functions such as those of port 331h so it only works in some cases (like my custom Supaplex hack below) where basic writes to 330h are sufficient. However, it should be fairly easy to implement the missing functions or just add this device to existing project like SOFTMPU.
; nasm -fbin trapmidi.asm -o trapmidi.com
org 100h
mov ax, 4a15h ; EMM386 I/O trap
mov bx, 0
mov dx, 330h
shl edx, 16
mov dx, 330h
mov cx, 1
mov si, io_dispatch_table
mov di, end
int 2fh
mov ax, 0x3100
mov dx, 512/16
int 21h
align 16
handler330:
mov ah, al
mov al, 255
mov dx, 0x37a ; 1st parallel port status register
out dx, al
times 4 in al, dx
mov al, ah
mov dx, 0x378 ; 1st parallel port data register
out dx, al
times 4 in al, dx
xor al, al
mov dx, 0x37a ; status register is used to signal
out dx, al ; port operation on rising edge
times 8 in al, dx
retf
align 16
io_dispatch_table:
dw 0x0330
dw handler330
end:
While we're at it, let's hack away a few bugs in roland.snd of my favorite DOS era game, Supaplex.
; nasm -fbin roland.asm -o sp_org\roland.snd
incbin "roland.snd", $, 0x0085-$ ; remove port delays
retn
incbin "roland.snd", $, 0x0098-$ ; remove port delays
mov dx, 330h
mov al, ah
out dx, al
retn
incbin "roland.snd", $, 0x0189-$ ; remove volume op (causes clipping)
times 3 nop
incbin "roland.snd", $, 0x018e-$ ; remove volume op (causes clipping)
times 3 nop
incbin "roland.snd", $, 0x0193-$ ; remove volume op (causes clipping)
times 3 nop
incbin "roland.snd", $, 3968
Sound Blaster output is also not working right (on my particular setup) so let's rewrite blaster.snd.
; nasm -fbin blaster.asm -o sp_org\blaster.snd
%macro waitdsp 0
%%wait:
in al, dx
or al, al
js %%wait
%endmacro
push ax
push bx
push cx
push dx
cmp ax, 0
jne skip0
mov cx, explosion
mov di, infotron - explosion - 1
jmp short start
skip0:
cmp ax, 1
jne skip1
mov cx, infotron
mov di, push - infotron - 1
jmp short start
skip1:
cmp ax, 2
jne skip2
mov cx, push
mov di, zonk - push - 1
jmp short start
skip2:
cmp ax, 3
jne skip3
mov cx, zonk
mov di, bug - zonk - 1
jmp short start
skip3:
cmp ax, 4
jne skip4
mov cx, bug
mov di, base - bug - 1
jmp short start
skip4:
cmp ax, 5
jne skip5
mov cx, base
mov di, exit - base - 1
jmp short start
skip5:
cmp ax, 6
jne skip6
mov cx, exit
mov di, end - exit - 1
jmp short start
skip6:
jmp skip
start:
mov dx, 0x0a ; DMA: write mask register
mov al, 15; ; channel 1 disabled
out dx, al
mov dx, 0x0c ; DMA: clear byte pointer flip-flop
mov al, 0
out dx, al
mov dx, 0x0b
mov al, 0x49 ; single-cycle playback on channel 1
out dx, al
mov bx, cs
shl bx, 4
add bx, cx ; offset
mov dx, 0x02 ; DMA: channel 1 address
mov al, bl
out dx, al
mov al, bh
out dx, al
mov ax, cs
mov bx, cx
shr bx, 4
add bx, ax
shr bx, 12 ; DMA: page in bx
mov dx, 0x83 ; DMA: channel 1 page
mov al, bl
out dx, al
mov bx, di
mov dx, 0x03 ; DMA channel 1 count
mov al, bl
out dx, al
mov al, bh
out dx, al
mov dx, 0x0a
mov al, 1 ; DMA 1 channel enabled
out dx, al
mov dx, 0x22c ; sound blaster (A220) DSP write data
waitdsp
mov al, 0x40 ; sample rate
out dx, al ; SB
waitdsp
mov al, 256 - 1000000/8333
out dx, al ; SB
waitdsp
mov al, 0x14 ; 8-bit PCM output
out dx, al ; SB
waitdsp
mov al, bl ; lo(size)
out dx, al ; SB
waitdsp
mov al, bh ; hi(size)
out dx, al ; SB
skip:
pop dx
pop cx
pop bx
pop ax
iret
state:
db 0
explosion:
incbin "explode.raw"
infotron:
incbin "infotron.raw"
push:
incbin "push.raw"
zonk:
incbin "zonk.raw"
bug:
incbin "bug.raw"
base:
incbin "base.raw"
exit:
incbin "exit.raw"
end:
We also need to make changes to the main binary and while we're at it, let's convert it to a .COM just for kicks.
; nasm -fbin supaplex.asm -o sp_org\supaplex.com
org 100h
mov dx, 0x226 ; reset sound blaster
mov al, 1
out dx, al
sub al, al
delay:
dec al
jnz delay
out dx, al
sub cx, cx
empty:
mov dx, 0x22e
in al, dx
or al, al
jns nextattempt
sub dl, 4
in al, dx
cmp al, 0xaa
je resetok
nextattempt:
loop empty
resetok:
mov dx, 0x22c ; enable sound blaster dac
wait2:
in al, dx
and al, 0x80
jnz wait2
mov al, 0xd1
out dx, al
mov ax, cs
add ax, 0x58d4 ; initial ss
add ax, (512+0x0100)/16 ; skip org and loader
mov ss, ax
mov sp, 0x0080 ; initial sp
mov ax, cs
add ax, 0x0aff ; initial cs
add ax, (512+0x0100)/16 ; skip org and loader
push ax
mov ax, 0x0010 ; initial ip
push ax
retf ; jumps to start
times 512-($-$$) nop
incbin "supaplex\supaplex.exe", $, 0x0526-$ ; 50:70 timing
db 0xa0, 0x92, 0x0d ; mov al, [0xd92]
inc al
and al, 7
db 0xa2, 0x92, 0x0d ; mov [0xd92], al
cmp al, 3
db 0x74, 0x6d ; je 0x5a1
cmp al, 0
db 0x74, 0x69 ; je 0x5a1
nop
nop
incbin "supaplex\supaplex.exe", $, 0x05c3-$ ; no PIT
db 0xb0, 0x70 ; mov al, 0x70
int 0x21
times 12 nop
incbin "supaplex\supaplex.exe", $, 0x55a5-$ ; remove mouse
retn
incbin "supaplex\supaplex.exe", $, 0x5632-$ ; use menu with backspace
db 0x80, 0x3e, 0x7b, 0x16, 0x01
incbin "supaplex\supaplex.exe", $, 0x56E2-$
in al, dx
test al, 0x8
db 0x74, 0xed
mov dx, 0x03c0
mov al, 0x33
out dx, al
db 0xA0, 0x96, 0x0D
out dx, al
int 0x70
pop ax
pop dx
ret
incbin "supaplex\supaplex.exe", $, 0x5b47-$ ; blaster.snd filesize
mov cx, 36123
incbin "supaplex\supaplex.exe", $, 0x5b7a-$ ; blaster.snd filesize
mov cx, 36123
incbin "supaplex\supaplex.exe", $, 0x8970-$ ; crack
db 0
incbin "supaplex\supaplex.exe", $, 45948-$
MOVING.DAT also has some minor imperfections in graphics, but let's fix those later...
A few other observations related to Supaplex include:
1) Supaplex needs at least 66 MHz 486 to run 70 Hz without minor tearing. Part of the reason why that much CPU power is needed is that the graphics seem to be drawn in reverse order so there's relatively speaking less time before vsync is to occur. Scrolling is "independent" so it may not tear even if sprite animation does (for example on MiSTer).
2) Music playback seems to cause minor video jerking no matter how fast a CPU (occasional duplicated frame). This jerking does not appear on the FPGA (ao486 and MiSTer FPGA) so it's probably caused by slow port operations screwing with the vertical sync (on the FPGA the port operations are much faster than on a real PC).
3) Unfortunately the FPGA is otherwise a bit too slow (minor tearing) and has some minor scrolling bug in the VGA implementation (right edge of the screen is missing varying number of pixels while scrolling). It would be nice to fix these two issues with ao486 on the FPGA at some. Proper roland CM-32L emulation would also be nice so one wouldn't need to plug in a real module (MUNT at the moment is just unusable because it's too laggy).
4) One can remove port in-based delay and the jerking is gone on the PC (works with the FPGA), but so is the music because adlib and roland are too slow and require their port delay so one needs some kind of buffering. Unfortunately my original idea of using the parallel port + arduino to do this buffering didn't quite work because the parallel port is also not quite fast enough (it helps a little compared to straight roland or adlib, but not enough).
5) I also tried rewriting the whole music routine using faster interrupts (64x 50 Hz) to get rid of port delays and use counters instead and while this worked to some degree, it causes some other timing issues with the main code which are a bit difficult to fix due to lack of game source code.
6) I also tried running the music routine during the 70 Hz vblank. That works to eliminate the jerking, but also results in the music playing way too fast.
7) Perhaps I just have to roll my own ISA card, although arduino nano doesn't have enough pins for that, at least not without some auxiliary chips, but there are alternatives.
8) I could also write the music routines natively for arduino and have them run independently of PC. That should fix any music related timing issues.
9) Or one could rewrite the whole damn game... but that's a lot of trouble...
On the PC the way to get rid of the jerking by entirely removing Programmable Interval Timer (PIT) and instead do timing with vsync. I've solved the problem of music playing too fast by skipping call to music routine 2 frames out of 7 so it plays at normal rate. SBMIDI calls are still too slow and 6+35 in's for ADLIB are too slow, but on OPL3 (SBPRO+) 1+4 in's are enough so with OPL3 we get away without any jerking on stock hardware using this 70-50 solution and SBMIDI can be replaced with my LPT2MIDI and again we get away without any jerking when dealing with the 70-50 case.