Saturday, 14 December 2019

LPT2MIDI

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.