Saturday 26 December 2020

2nd SID chip auto detect

I've started working on something that can use a 2nd SID chip if it exists.  While I was going to put a manual setup in I thought I'd have a go at detecting it automatically as well.   I haven't looked at other people's implementations as I thought it'd be better to try and understand the problem from scratch.
 
So far this has only been tested on an emulator, but I think the premise at least is sound if the memory mapping holds up.

SID chips in memory:

SID chips, at least as far as I'm aware, can only be in the $d000-$dfff block of memory, and within that can only occupy two areas:

  • $d400-$d7ff
  • $de00-$dfff
This is because the other VIC-II registers, timers and colour ram also occupy that same 4kb of memory in various places.

The first SID is always at $d400, with the second SID able to occupy any 32-byte block in those two memory spaces. 

Unoccupied blocks behave differently depending on which of the two areas you are trying to detect.   In the first area ($d400-$d7ff) empty blocks are mirrors of the first chip.  So, for example, writing to $d500 acts the same as writing to $d400.

In the second area they don't appear to be mirrored at all, though empty blocks seem to get some crosstalk. (at least relying on the emulator output to be accurate)  So, for example, writing to $de00 won't affect the first chip at $d400.

This means we need two similar but seperate routines to look for a second SID across the available areas.

Detection:

To detect the chip I used the 'random number generator' read register on the SID.  ($1b)  This gives you the output value of the 3rd channel's oscillator.  The oscillator can be read just by setting it's pitch and waveform registers, it doesn't need any ADSR setup or output volume so can work silently.   By setting oscillator 3 to the noise waveform and putting the frequency at max, we have a constantly changing value we can compare against to figure out where the other SID could be.

As mentioned because of the different memory banking we need two detection routines:

1) $d400-$d7ff area:

  1. Set the first SID's pitch and waveform register to $00.
  2. Read the first SID's RNG (random number generator) and store that as the 'last played' value.
  3. Set the second SID's pitch to max ($ff) and waveform to noise ($81)
  4. Read the first SID's RNG value and compare to last played value.
  5. If the RNG value doesn't match the last played value we haven't found the 2nd SID.  This is because $d41b is being overwritten by an unmapped area.
  6. If the RNG value is the same as the last played value it means the memory isn't being mirrored and there is probably another SID there.

So that's the first memory block taken care of.  Of course there is a possibilty the RNG value will match the last played value but the chances are fairly low.

Now, if the second block doesn't have mirroring, surely we can just check the RNG register on the second chip and see if there's any activity?   Well, yes and no.    This is what we're going to use, but because of the apparant crosstalk we'll need to sample a group of values and try and detect it from an average instead.

The second detection routine does exactly the same as the first, but unmapped areas don't give a constant 0 output when reading their RNG register.   If there is a SID there you'll get the stream of random values, but otherwise you'll get zeroes interspersed with $ff and sometimes other values.   The one consistent thing is that there are many more zeroes on unmapped areas than the other values.   I decided to read 16 values from the RNG (one per frame) then use that this sample to decide if a SID exists.   If there are more than an arbitary amount of zeroes in the sample then most likely there isn't a SID there.

2) $de00-$dfff area:

  1. Set the first SID's waveform and pitch to $00.  We probably don't need to use this at all but might as well.
  2. Set the second SID's pitch to max ($ff) and waveform to noise ($81)
  3. Read 16 values from the second SID's RNG register, one per frame.
  4. Scan through the sampled values for any instances of zero, and add them to a tally.
  5. If the tally is under a certain threshold (I used 3 or less) we probably have a SID there.
  6. If the tally is over that threshold there's a lot more zeros in the crosstalk and we don't have a SID there.  

I'm sure there are plenty of flaws in the above, but it'll be interesting to see what does and doesn't work on real machines when I have time.   This was tested with VICE 3.1

Source:

setup_siddetect

    ; We're using indirect addressing to read the second SID.
    ; Set the read address to the first possible one. ($d420)
    
    lda #$20
    sta $02
    lda #$d4
    sta $03
    
    ; Detection loop for $d400-$d7ff area.
    
setup_siddetectloop
    ; Wait for a new frame before looking for the SID just for safety.
    lda $d012
    cmp #$fe
    bne setup_siddetectloop

    ; These subroutines clear the first and second SID registers.
    jsr sid_sidchip_clear   
    jsr setup_sid2ndclear

    ; Set first SID high pitch/waveform on channel 3 to zero.
    lda #$00
    sta $d401+$0e
    lda #$00
    sta $d404+$0e

    ; Read first SID's RNG register and store it.
    lda $d41b
    sta setup_prevvalue

    ; Set second SID high pitch/waveform on channel 3 to a noise waveform at max pitch.
    ldy #$0f
    lda #$ff
    sta ($02),y
    ldy #$12
    lda #$81
    sta ($02),y
    
    ; Compare the first SID's RNG register against the previously recorded value.
    ; If it's the same the second SID must be mapped in this area, otherwise we'd be
    ; getting the second SID's random output mirrored in.
    
    lda $d41b
    cmp setup_prevvalue
    bne setup_siddetect_notfoundyet
    jmp setup_siddetect_found

    ; Move the second SID address up to the next 32-byte block.
    ; If we've reached $d800 (where the colour RAM is) we need to skip to the second
    ; detection routine instead.
    
setup_siddetect_notfoundyet   
    clc
    lda $02
    adc #$20
    sta $02
    cmp #$00
    bne setup_siddetectloop
    inc $03
    lda $03
    cmp #$d8
    beq setup_siddetectskip
    jmp setup_siddetectloop
    
setup_siddetectskip

    ; Start of second detection routine, set it to start reading from $de00.
    lda #$de
    sta $03
    lda #$00
    sta $02
    
setup_sidlastloop
    ; This is pretty much the same as the first detection routine.....
    lda $d012
    cmp #$fe
    bne setup_sidlastloop
    
    jsr sid_sidchip_clear
    jsr setup_sid2ndclear

    lda #$00
    sta $d401+$0e
    lda #$00
    sta $d404+$0e
    ldy #$0f
    lda #$ff
    sta ($02),y
    ldy #$12
    lda #$81
    sta ($02),y

    ; ...until here.  This time we take a sample of 16-bytes from the 2nd SID's
    ; RNG register.  We take one per frame to try and avoid duplicates.
    
    ldy #$1b
    ldx #$0f
setup_siddetect_sampleinput
    lda $d012
    cmp #$fe
    bne setup_siddetect_sampleinput
    lda ($02),y
    sta setup_samplecache,x
    dex
    bpl setup_siddetect_sampleinput

    ; Now we scan through the sample looking for zeros, if we find any they're
    ; added to a tally.
    
    ldx #$00
    stx setup_prevvalue
setup_siddetect_sampleanalyze
    lda setup_samplecache,x
    cmp #$00
    bne setup_siddetect_samplenozero
    inc setup_prevvalue
setup_siddetect_samplenozero   
    inx
    cpx #$10
    bne setup_siddetect_sampleanalyze

    ; If the tally is 3 or less we've probably found a SID chip.  With the RNG
    ; register going it's unlikely it'd find any plus it's default is 0 anyway.
    
    lda setup_prevvalue
    cmp #$03
    bcc setup_siddetect_found

    ; If we haven't found a SID continue moving through the memory blocks.
    ; If we reach $e000 we're at the end of the available memory
    ; space, so set the address to $ffff which we can use as a check for
    ; no second SID existing.
    
    clc
    lda $02
    adc #$20
    sta $02
    cmp #$00
    bne setup_sidlastloop
    inc $03
    lda $03
    cmp #$e0
    bne setup_sidlastloop
    
setup_siddetect_notfound
    lda #$ff
    sta $02
    sta $03

    ; When the SID address is found write it on screen as PETSCII values.
    
setup_siddetect_found

    lda $02
    sta $0400
    lda $03
    sta $0401
    rts

    ; Clear first SID's registers.

sid_sidchip_clear
    ldx #$18
    lda #$00
sid_sidchip_clearloop
    sta $d400,x
    dex
    bpl sid_sidchip_clearloop
    rts

    ; Clear potential second SID's registers.
    
setup_sid2ndclear

    ldy #$00
    tya
setup_sid2ndclearloop   
    sta ($02),y
    iny
    cpy #$1d
    bne setup_sid2ndclearloop
    rts

    ; Variables:
    ; setup_prevvalue is used to store $d41b value in first detect and as the tally in second detect.
    
setup_prevvalue

    .byte $00
    
    ; Array for second detect sample.
    
setup_samplecache
    .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00