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
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:
- Set the first SID's pitch and waveform register to $00.
- Read the first SID's RNG (random number generator) and store that as the 'last played' value.
- Set the second SID's pitch to max ($ff) and waveform to noise ($81)
- Read the first SID's RNG value and compare to last played value.
- 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.
- 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:
- Set the first SID's waveform and pitch to $00. We probably don't need to use this at all but might as well.
- Set the second SID's pitch to max ($ff) and waveform to noise ($81)
- Read 16 values from the second SID's RNG register, one per frame.
- Scan through the sampled values for any instances of zero, and add them to a tally.
- If the tally is under a certain threshold (I used 3 or less) we probably have a SID there.
- 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:
; 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