Wednesday, 5 August 2020

YM3812

I had one old broken Sound Blaster ISA card and decided to pull the music chip from it to see if it worked. This was the Yamaha YM3812 or otherwise known as OPL2. It's the same chip that was used in Adlib cards. Pretty much everyone who played DOS games back in the day knew the sound of this chip.


This chip has a rather peculiar sample format, it's 13 bit floating point with 10 bits for mantissa and 3 bits for exponent. The chip requires 3.58 MHz input, uses 8 data bits and one address bit. I used a cheap FPGA board to generate the clock for the chip and fed the digital samples back to the FPGA. I converted the floating point number into regular 16-bit integer and used a software delta-sigma modulator running at 50 MHz to convert back to analog signal.


The chip contrary to the datasheets 16 bits seemed to be output 18 bits during a single SYNC cycle (SYNC is cyan in the above image, CLK is yellow). First 5 extra bits were irrelevant. According to the datasheet only first 3 bits were supposed to be irrelevant. It also appears this chips seemed to work fine with 3.3 V where in the datasheet the minimum voltage was listed as 4.5 V. Operating it with 3.3 V was convenient as my FPGA supplied this voltage and could also only accept 3.3 V logic.


The above is the signal directly measured from the 1-bit digital FPGA pin which is sigma-delta modulated (1st order) digital-to-analog conversion of the signal generated by the YM3812 chip. The sampling frequency is just so high (50 MHz) that it looks very good analog signal when low pass filtered.

Taking a running average (a kind of low pass filter) of the delta-sigma modulated signal yields an analog signal. The higher the "oversampling" rate, the more faithful the filtered signal is to the original (up to some high frequency cutoff).

Second order delta-sigma modulation shapes the noise in order to improve the signal-to-noise ratio even further at the expense of the S/N at the higher frequencies.

Looking at the spectrum, one can easily see that the quantization noise slope is much steeper for higher order delta-sigma modulations and thus can yield better signal fidelity at lower frequencies. The low frequency peak's "skirt" is due to finite sampling. The signal is actually just a delta-peak in frequency space and has no skirt (other than some vanishingly small one related to the phase noise of the digital clock, but that's another story and not relevant for this context). 
DSM is of course not limited to just 1-bit, but one can use it to improve the SNR of n-bit converters as well if they are capable of operating fast enough relatively to the signal of interest. The image is from 4-bit converter modulated by 2nd order DSM.


The effect of higher order DSMs on quantization noise.

Below, 1-bit 1st order DSM and 4-bit 2nd order DSM demonstration code in Matlab.

out = 0;
int = 0;
for x = 1:length(t)
    int = int + sin(pi*t(x)) - out(x);
    if int > 0
        out(x+1) = 1;
    else
        out(x+1) = -1;
    end
end

%%

out2 = 0;
int1 = 0;
int2 = 0;
for x = 1:length(t)
    int1 = int1 + 6*sin(pi*t(x)) - out2(x);
    int2 = int2 + int1 - out2(x);
    out2(x+1) = round(int2);
end

Saturday, 1 August 2020

EGA to PVM

I bought an EGA display card for ISA recently. EGA games run without scan doubling so they looked a bit different originally on real EGA monitor when compared to VGA monitor and I hadn't actually ever seen this so I figured I'd try. Unfortunately I don't have an EGA monitor or CGA monitor (low resolution EGA is compatible with CGA-monitors) and they are quite difficult to come by. EGA as well as CGA uses digital TTL signals instead of analog RGB like VGA monitors. VGA monitors are also incompatible with CGA and EGA timings so I would have to figure something out.



I do have a PVM capable of displaying NTSC signals (basically a TV), including RGB input and since most EGA games are running in the low resolution mode with NTSC compatible timing, I figured it might be relatively easy to botch something up. PVM only accepts CSYNC so I would have to convert VSYNC and HSYNC. Unfortunately they need to be inverted so I had to use an active logic circuit for that instead of just something passive. Luckily the logic is quite simple. Another thing is that TTL levels are too high for PVM, but simply placing 1 kohm resistor in series divides the voltage down sufficiently (into 75 ohm input). Maybe this division is even a bit too much, but it's what I had at hand. Below is the schematic I used.



Unfortunately the CGA/EGA color brown is handled in a "nonlinear" fashing and would require a bit more logic to display it correctly. Here it instead of being brown looks more yellow. However, the other 15 colors are fine.


This is how it would look with the correct shade of brown.


My botched board is maybe a bit nasty, but it works fine.


Monday, 20 July 2020

FPGA to ISA-bus

I bought a while ago a few of the cheap FPGA boards from ebay for 10 € a pcs and decided to test whether one could interface the old ISA bus with it. One issue is that this FPGA board is not 5 V tolerant so I decided to try with 10 kohm resistors in series with the pins and this seemed to be all that was needed or at least so far everything seems ok for standard port writes even if I'm not entirely sure if this is safe in the long run.


I ordered some rather blank ISA boards from JLCPCB for 2 € for 5 boards and soldered some tests leads to 8 data pins and 10 address pins.



The 8 data pins are located at A9-A2 and the 20 address pins are located at A31-A12. Additionally we need nIOW for I/O write which is located at B13 and GND that is located at B1.


This is how the thing looked like. I suppose one could have powered the FPGA board directly from the ISA slot, assuming it can supply enough current. That way one could make a rather standalone board, but so far I didn't try this. Actually, in this picture I have only 9 address pins connected, but it would be more standard to use 10 address pins.


I wrote a quick VERILOG to control the FPGA board LEDs. Basically all this does is set the LEDs to match the data bits when I/O write occurs and the address is 511.

module C2(LED, ADDR, DATA, nIOW);

output  [2:0] LED;
input   [8:0] ADRR;
input   [7:0] DATA;
input         nIOW;

reg     [2:0] leds;
assign LED = leds;

always @(negedge nIOW)
begin
if(ADDR == 511)
leds <= DATA;
end

endmodule

I also wrote a quick PASCAL program to test with a binary counter.

var
  a, b : byte;
  c : word;
begin
  for b := 0 to 4 do
    for a := 0 to 7 do
      for c := 0 to 32768 do ;
      for c := 0 to 32768 do port[511] := a;
    end;
  end;
end.

All seemed to work well on the first try so it appears making a rudimentary ISA device is very easy.

[http://www.hardwarebook.info/ISA]

Sunday, 14 June 2020

Phosphorescence decay of LIT

I bought this phosphorescent material already a while ago, because I was fascinated by the long decay time compared to most other materials. One could faintly see the glow even after a week from just 1 second of illumination by a 1 W 445 nm laser. Even moderately higher temperature like touch by a human hand also seemed to expedite the release of the stored energy in this material significantly and this effect was clearly visible even hours after the first excitation. After long period in the dark and drained from stored energy, application of heat did not cause emissions as expected.

The seller claims it can be excited by heat, but this is of course nonsense. In general only wavelengths shorter than the emission spectrum can excite so infrared and heat can only expedite the release of the already stored energy, but not cause excitations. Excitation by heat under equilibrium condition is forbidden by the laws of thermodynamics.

It it is still true that under nonequilibrium conditions such as high intensity infrared irradiation can a process called frequency upconversion occur, but these are very rare events, do not apply to this case and do not come into conflict with the laws of thermodynamics.


I made a quick detector from a photomultiplier tube and a metal can and the proceeded to log the tube output using a raspberry pi and a digital multimeter.


It seems this material doesn't exhibit typical exponential decay one would normally expect, but rather it seems to decay first at a faster rate and then slow down. The measurements below indicate that the intensity will drop to approximately 1:10 of the initial intensity during the first hour, but will take something like 18 hours to drop to 1:100. Human eye adjusted to dark can still detect the output probably even if it's only 1:1000 of the initial. I don't know what exactly is the cause of this non-exponential behavior in this material, but I suspect there is some kind of statistical mix of different decay times at play here. Unfortunately I couldn't find my strontium aluminate right now for comparision. I guess some events are fast, some are slow and these then together will lead to this decay curve. I know I'm far from the dark counts of the tube and it's not saturating either so all should be good. The quantitative results also seem to match the qualitative results as far as my eye can tell so this really is the way this material behaves. Thiugh, after long enough it seems to settle to exponential decay so perhaps the assumption is correct. Perhaps I should also check how the emission spectrum looks like, but for that I'll have to wait for my spectrometer to arrive.


This is the same detector which can count individual photons. Although, the quantum efficiency is only slightly above 20%.


...got my spectrometer now and measured a bunch of things...


Monitor red doesn't appear to be very natural. Because I excited quinine with 404 nm laser, the peak is seen in the fluorescence spectrum as well as fluorescence (fluorescence lifetime is so short that afterglow can't be measured directly this way). LIT phosphorescence spectrum doesn't show the 445 nm excitation as the excitation is off when collecting the spectrum as phosphorescence afterglow decays slow enough. My 445 nm laser seems to be frequency upconverted from IR and the IR peak is visible as well (barely at the edge of the range for my spectrometer).

Spectrum lines of my monitor displaying all white image

Sunday, 12 January 2020

Sync issues solved...

No more dropped frames, the solution was...
1) LPT2MIDI allows lower delay (only times 4 in al, dx) as opposed to SBMIDI.
2) The timer is reset every 69th frame.
3) OPL3 delay (1+3 times in al, dx) is used.


I released the sources...
1) https://github.com/eigenco/LPT2MIDI
2) https://github.com/eigenco/supaplex

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.