Skip to content

Instantly share code, notes, and snippets.

@badvision
Last active February 20, 2017 21:54
Show Gist options
  • Save badvision/837c8e2449b449069c3b to your computer and use it in GitHub Desktop.
Save badvision/837c8e2449b449069c3b to your computer and use it in GitHub Desktop.
Mockingboard sound driver
*= $4000
!cpu 65c02
!sl "globals.txt"
;-----------
reg_ORB = 0 ;Output Register B
reg_ORA = 1 ;Output Register A
reg_DDRB = 2 ;Data direction reg B
reg_DDRA = 3 ;Data direction reg A
reg_T1CL = 4 ;T1 low-order latches (low-order counter for read operations)
reg_T1CH = 5 ;T1 high-order counter
reg_T1LL = 6 ;T1 low-order latches
reg_T1LH = 7 ;T1 high-order latches
reg_T2CL = 8 ;T2 low-order latches (low-order counter for read operations)
reg_T2CH = 9 ;T2 high-order counter
reg_SR = 10 ;Shift register
reg_ACR = 11 ;Aux control register
reg_PCR = 12 ;Perripheral control register
reg_IFR = 13 ;Interrupt flag register
reg_IER = 14 ;Interrupt enable register
reg_ORAH = 15 ;Output Register A (no handshake)
bus_OFF = 4
bus_READ = 5
bus_WRITE = 6
bus_LATCH = 7
int_ENABLE = $80
int_DISABLE = $00
int_TIMER1 = $40
int_TIMER2 = $20
anyInterruptEnabled = %11100000 ; AND with IER and BNE to test if interrupts are enabled
IRQ_VECTOR = $3fe
dataPtr = $FA ;Must not be affected by anything else
sequenceTablePtr = $FC ;Must not be affected by anything else
stackSize = 16 ; Size of stack for active commands per channel
;-----------
!set TRUE=100
!set FALSE=1
;TODO: MOVE TO NEW FILE
!set debug = TRUE
!set trace = FALSE
!set traceRestore = FALSE
!macro special p1, p2 {
!if debug = TRUE {
!byte $fc, p1, p2
}
}
!macro disableTrace {
+traceOff
!set traceRestore = FALSE
}
!macro pauseTrace {
!if trace = TRUE {
+traceOff
!set traceRestore = TRUE
}
}
!macro restoreTrace {
!if traceRestore = TRUE {
+traceOn
}
!set traceRestore = FALSE
}
!macro traceOn {
!set trace = TRUE
+special $65, 1
}
!macro traceOff {
!set trace = FALSE
+special $65, 0
}
!macro dumpRegs {
+traceOn
NOP
+traceOff
}
!macro showMemoryMap {
+pauseTrace
+special $64, $da
}
!macro printNum n {
+pauseTrace
+special $50, n
}
!macro printRegA {
+pauseTrace
+special $51, 0
}
!macro printRegX {
+pauseTrace
+special $51, 1
}
!macro printRegY {
+pauseTrace
+special $51, 2
}
!macro printRegSP {
+pauseTrace
+special $51, 3
}
!macro printProgramCounter {
+pauseTrace
+special $51, 4
}
!macro printlnNum n {
+pauseTrace
+special $5B, n
}
!macro printChr n {
+pauseTrace
+special $5c, n
}
!macro println {
+printChr 13
}
!macro printChr n1, n2 {
+printChr n1
+printChr n2
}
!macro printChr n1, n2, n3 {
+printChr n1
+printChr n2, n3
}
!macro printChr n1, n2, n3, n4 {
+printChr n1
+printChr n2, n3, n4
}
!macro printChr n1, n2, n3, n4, n5 {
+printChr n1
+printChr n2, n3, n4, n5
}
!macro printChr n1, n2, n3, n4, n5, n6 {
+printChr n1
+printChr n2, n3, n4, n5, n6
}
!macro printChr n1, n2, n3, n4, n5, n6, n7 {
+printChr n1
+printChr n2, n3, n4, n5, n6, n7
}
!macro printChr n1, n2, n3, n4, n5, n6, n7, n8 {
+printChr n1
+printChr n2, n3, n4, n5, n6, n7, n8
}
!macro printChr n1, n2, n3, n4, n5, n6, n7, n8, n9 {
+printChr n1
+printChr n2, n3, n4, n5, n6, n7, n8, n9
}
START
;-----------
sei
lda #<data
sta dataPtr
lda #>data
sta dataPtr+1
setup_playback
php
pha
phx
phy
verify_header
; TODO: Verify header is 3C 51 C0
; If not, exit and print error message
update_pointers
; All of the song pointers are relative, before playback they need to be converted to absolute
;Get # of patterns
ldy #6
lda (dataPtr),y
+printRegA
+printChr ' ','p','a','t','t','e','r','n','s'
+println
tax
lda #$01
and status
bne detect_card
;For each pattern, add the base dataPtr address and overwrite
;Because this modifies the data area, it's important to keep track if this has been done already
;That information is tracked on bit 0 of the status register
+printChr 'A','d','j',' ','t','b','l'
+println
- iny
+printRegX
+println
clc
lda (dataPtr),y
adc #<data
sta (dataPtr),y
iny
lda (dataPtr),y
adc #>data
sta (dataPtr),y
dex
bne -
init_vars
+printChr 'I','n','i','t',' ','S','e','q'
+println
; (dataPtr),y is now at the end of the pattern offset table
; The sequence table is next, we should keep track of it
iny
tya
clc
adc dataPtr
sta sequenceTablePtr
lda #0
adc dataPtr+1
sta sequenceTablePtr+1
detect_card
; TODO: Figure out what slot the mockingboard is in and store in X
ldx #$04
install_handler
; Update internal routines to point to card location
clc
txa
adc #$c0
sta regWrite1 + 2
sta regWrite2 + 2
sta regRead1 + 2
sta regRead2 + 2
;Setup all playback variables
ldy #3
lda (dataPtr),y
sta speed
ldy #reg_T1LL
jsr regWrite1
ldy #4
lda (dataPtr),y
sta speed+1
ldy #reg_T1LH
jsr regWrite1
ldy #5
lda (dataPtr),y
sta ticksPerBeat
+printRegA
+printChr ' ','t','p','b'
+println
; Set to first sequence
lda #0
jsr change_sequence
; Register IRQ handler
; TODO: Support prodos IRQ handler or at least preserve it
lda #<playback_main
sta IRQ_VECTOR
lda #>playback_main
sta IRQ_VECTOR+1
; Init both 6522 chips
; Set port A for output
lda #$ff
ldy #reg_DDRA
jsr regWriteBoth
; Set port B for output
lda #$0F
ldy #reg_DDRB
jsr regWriteBoth
; Set timer 1 to free-running mode
lda #$40
ldy #reg_ACR
jsr regWrite1
; Enable interrupts
lda #(int_ENABLE | int_TIMER1)
ldy #reg_IER
jsr regWrite1
; Note that playback has started
lda #$81
sta status
+printChr 'P','l','a','y'
+println
; Complete setup routine, leave interrupts enabled
ply
plx
pla
plp
cli
rts
;..... Playback support routines
; Call order:
; Get AY ready with JSR reset1/2
; Load AY register # and JSR writeOra and then JSR latch
; Load register value and JSR writeOra and the JSR write
!zone Playback_Routines
writeOra1
ldy #reg_ORA
regWrite1
sta $c000,y
rts
reset1
lda #bus_OFF
bra command1
write1
jsr writeOra1
lda #bus_WRITE
bra command1
latch1
jsr writeOra1
lda #bus_LATCH
command1
ldy #reg_ORB
bra regWrite1
; lda #bus_OFF
; bra regWrite1
regWriteBoth
jsr regWrite1
jmp regWrite2
writeOra2
ldy #reg_ORA
regWrite2
sta $c080,y
rts
reset2
lda #bus_OFF
bra command2
write2
jsr regWrite2
lda #bus_WRITE
bra command2
latch2
jsr regWrite2
lda #bus_LATCH
command2
ldy #reg_ORB
bra regWrite2
; lda #bus_OFF
; bra regWrite2
regRead1
lda $c000,y
rts
regRead2
lda $c080,y
rts
;----------
!zone Playback_Main
playback_main
sei
php
pha
phx
phy
; Check to see if the first 6522 via triggered the interrupt
ldy #reg_IFR
jsr regRead1
ora #int_TIMER1
; No interrupt? Exit. TODO: Support prodos IRQ handling
bne playback_tick
+printChr 'o','d','d',' ','i','r','q'
+println
finish_irq_service
ply
plx
pla
plp
cli
+printChr '-','T','i','c','k','-'
+println
+traceOff
rti
playback_tick
; Clear interrupt
lda #$FF
ldy #reg_IFR
jsr regWrite1
; advance tick counter
dec currentTick
; New row?
; No: Still on same note, continue effects
bne do_tick
; Yes: advance row counter
lda ticksPerBeat
sta currentTick
lda currentRow
inc
cmp patternLength
; End of pattern?
bcs .advance_sequence
; No: Still on same pattern, play next row
sta currentRow
jmp play_row
; Yes: Advance sequence counter
.advance_sequence
lda (sequenceTablePtr)
cmp currentSequence
; End of song?
beq stop_playback
; No: play first row of next pattern
lda currentSequence
inc
jsr change_sequence
jmp play_row
; Yes: Song over, stop playback
stop_playback
;Note that playback has stopped
lda status
and #$7f
sta status
+printChr 'S','t','o','p'
+println
; Clear interrupts
lda #(int_DISABLE | int_TIMER1)
ldy #reg_IER
jsr regWrite1
jmp finish_irq_service
do_tick
; Look at active effects and handle them accordingly
jmp finish_irq_service
!macro processChannel channelNumber, ayNumber {
!zone processChannel {
!set fineTune = (channelNumber - 1) * 2
!set coarseTune = fineTune + 1
!set vol = channelNumber + 7
!set trackingData = ay1_params + (3+stackSize) * (channelNumber - 1 + (3*(ayNumber-1)))
; Get pitch/macro to play
jsr read_data
bpl +
; This is some non-pitch command
;11xxxxxx = Macro X playback
;101----- = Note off
jmp ++
+
+printChr 'A','y',' '
+printNum ayNumber
+printChr ',','C','h',' '
+printNum channelNumber
+printChr ',','P','i','t','c','h',' '
+printRegA
+println
asl
tax
; Process fine tune parameter
lda #fineTune
!if ayNumber=1 {
jsr latch1
} else {
jsr latch2
}
lda pitch_table,x
sta trackingData + 1
!if ayNumber=1 {
jsr write1
} else {
jsr write2
}
; Process coarse tune parameter
lda #coarseTune
!if ayNumber=1 {
jsr latch1
} else {
jsr latch2
}
lda pitch_table+1,x
sta trackingData + 2
!if ayNumber=1 {
jsr write1
} else {
jsr write2
}
++
jsr read_data
; Process volume
; $20 = Envelope generator
; $FF = No change
; $80-$FE = Commands
cmp #32
bcs +
+printChr 'v','o','l',' '
+printRegA
+println
pha
lda #vol
!if ayNumber=1 {
jsr latch1
} else {
jsr latch2
}
pla
!if ayNumber=1 {
jsr write1
} else {
jsr write2
}
+
-
jsr read_data
+printChr 'S','k','i','p',' '
+printRegA
+println
; TODO: Handle non-zero values as extra commands, not implemented at the moment
bne -
.end
}
}
!macro processRow ayNumber {
!zone processRow {
sta channelFlags
+printChr 'C','h',' ','f','l','g',':'
+printRegA
+println
.checkExtendedCommands
lda #1
bit channelFlags
bne .hasExtendedCommands
jmp .checkEnvelopeCommands
.hasExtendedCommands
; TODO: Process extended commands
.checkEnvelopeCommands
lda #2
bit channelFlags
bne .hasEnvelopeCommands
jmp .checkNoiseCommands
.hasEnvelopeCommands
; TODO: Process envelope commands
.checkNoiseCommands
lda #4
bit channelFlags
bne .hasNoiseCommands
jmp .checkACommands
.hasNoiseCommands
; TODO: Process noise commands
.checkACommands
lda #8
bit channelFlags
bne .hasACommands
jmp .checkBCommands
.hasACommands
+processChannel 1, ayNumber
.checkBCommands
lda #16
bit channelFlags
bne .hasBCommands
jmp .checkCCommands
.hasBCommands
+processChannel 2, ayNumber
.checkCCommands
lda #32
bit channelFlags
bne .hasCCommands
jmp .done
.hasCCommands
+processChannel 3, ayNumber
.done
} ; -- End Zone processRow
} ; -- End Macro processRow
play_row
lda currentRow
+printChr 'r','o','w',' '
+printRegA
+printChr ' ','o','f',' '
lda patternLength
+printRegA
+println
; Read next row of data and start playing it
jsr read_data
bpl .hasSomeData
; If the high-bit is set then there is nothing on this row, move on.
jmp finish_irq_service
.hasSomeData
+processRow 1
jsr read_data
+processRow 2
jmp finish_irq_service
;-------------------
; Set sequence (A has sequence #)
change_sequence
sta currentSequence
+printChr 'S','e','q',' '
+printRegA
+println
tay
iny
lda (sequenceTablePtr),y
sta currentPattern
+printChr 'p','a','t',' '
+printRegA
+println
asl
adc #7
tay
lda (dataPtr),y
sta read_data+1
iny
lda (dataPtr),y
sta read_data+2
stz currentRow
lda ticksPerBeat
sta currentTick
jsr read_data
sta patternLength
+printChr 'p','a','t',' ','l','e','n',' '
+printRegA
+println
rts
read_data
lda read_data
inc read_data+1
bne +
inc read_data+2
+ ora #$00 ; Reset flags based on value of A
rts
; End program section
;--------------------------------------------------------------------------------------------
; Variables
; If hi-bit set then song is playing, otherwise not playing
; Bit 0 = Location still relative (1 = location offsets already processed)
status !byte 0
ticksPerBeat !byte 0 ; Number of ticks per beat (affects tempo and effects)
speed !word 0 ; Duration of each tick
currentSequence !byte 0
currentPattern !byte 0
patternLength !byte 0
currentRow !byte 0
currentTick !byte 0
channelFlags !byte 0
ay1_params
ay1_aAmp !byte 0
ay1_aFreq !word 0
ay1_aStack !fill stackSize
ay1_bAmp !byte 0
ay1_bFreq !word 0
ay1_bStack !fill stackSize
ay1_cAmp !byte 0
ay1_cFreq !word 0
ay1_cStack !fill stackSize
ay1_envPeriod !word 0
ay1_envStack !fill stackSize
ay2_params
ay2_aAmp !byte 0
ay2_aFreq !word 0
ay2_aStack !fill stackSize
ay2_bAmp !byte 0
ay2_bFreq !word 0
ay2_bStack !fill stackSize
ay2_cAmp !byte 0
ay2_cFreq !word 0
ay2_cStack !fill stackSize
ay2_envPeriod !word 0
ay2_envStack !fill stackSize
;-----------------------
; Note values: Generate lookup tables for frequencies
clockRate = 1020484
!macro note baseFreq, octave {
!set freq = (2^octave) * baseFreq
!set period = clockRate / freq
!word int(period + 0.5)
}
pitch_table
!for octave,0,5 {
+note 261.625625, octave ; 0: C
+note 277.1825, octave ; 1: C#
+note 293.664375, octave ; 2: D
+note 311.126875, octave ; 3: D#
+note 329.6275, octave ; 4: E
+note 349.228125, octave ; 5: F
+note 369.994375, octave ; 6: F#
+note 391.995625, octave ; 7: G
+note 415.305, octave ; 8: G#
+note 440, octave ; 9: A
+note 466.16375, octave ; 10:A#
+note 493.883125, octave ; 11:B
}
;-------------------------------------------------------------------------------------------------------------------
;Sample 1-pattern song, plays 4 repeats of the same test pattern
data
!byte $3C, $51, $C0 ;Header bytes: MU SI C0
!byte 0, $F0 ;Global speed (LSB, MSB)
!byte 16 ;16 ticks per row
!byte 1 ;Number of patterns
;Offset of pattern 0 (LSB, MSB)
!word (pattern0-data)
!byte 1 ;Sequence length: 4
!byte 0 ;Sequence is pattern 0 repeated 4 times
pattern0
; !byte 16 ;Pattern length is 16 notes
!byte 2 ;Pattern length is 16 notes
;Row 1
!byte %00001000 ;Bit pattern indicating Channel A has data (AY 1)
; %--CBANEX ; X = Extended commands
; E = Envelope generator commands
; N = Noise generator commands
; A/B/C = Channels A/B/C
!byte 12 ;C5
; If this byte has the high-bit clear then it is a note pitch
; If first byte has high-bit set then it is either a macro, a note off, or indicating no command changes
; 10000000 No pitch change
; 1M###### Macro 0-31, next byte specifies pitch offset in semitones (+/- 72)
; 101----- Note off ($c0)
!byte 10 ;Amplitude is 10
; If high-bit is set then it is a command of some sort:
; 32: Set the amplitude to the enveloper generator
; 11111111 No change to volume
; 101SS### Raise (D=1) or Lower (D=1) the amplitude by ### every tick for this row
; 1001#### Tremolo (rapid raise and lowering of amplitude, alternating every SS ticks by ### amount each tick)
!byte 0 ; No more data for channel A
!byte %00001000 ;Bit pattern indicating Channel A has data (AY 2)
!byte 12 ;C5
!byte 10 ;Amplitude is 10
!byte 0 ;No more data
;Row 2
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 3
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 4
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 5
!byte %00011000 ;Bit pattern indicating Channels A and B have data (AY 1)
!byte %10000000 ;No pitch change
!byte 7 ;Set amplitude to half
!byte 0 ;No more data
!byte 7 ;Channel B pitch to G4
!byte 15 ;Amplitude is 15
!byte 0 ;No more data
!byte %00011000 ;Bit pattern indicating Channels A and B have data (AY 2)
!byte %10000000 ;No pitch change
!byte 7 ;Set amplitude to half
!byte 0 ;No more data
!byte 7 ;Channel B pitch to G4
!byte 15 ;Amplitude is 15
!byte 0 ;No more data
;Row 6
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 7
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 8
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 9
!byte %00011000 ;Bit pattern indicating Channels A and B have data (AY 1)
!byte %10100000 ;Channel A off
!byte 0 ;No more data (note off takes no amplitude byte)
!byte %10100000 ;Channel B off
!byte 0 ;No more data (note off takes no amplitude byte)
!byte %00011000 ;Bit pattern indicating Channels A and B have data (AY 2)
!byte %10100000 ;Channel A off
!byte 0 ;No more data (note off takes no amplitude byte)
!byte %10100000 ;Channel B off
!byte 0 ;No more data (note off takes no amplitude byte)
;Row 10
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 11
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 12
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 13
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 14
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 15
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
;Row 16
!byte %10000000 ;Bit pattern indicating no data for row (AY 1/AY 2)
@badvision
Copy link
Author

Changed debugging statements to use new fake NOP commands so output is STDOUT in JACE and doesn't rely on in-emulator display.

@badvision
Copy link
Author

Some of the more sillier bugs have been found but there is still no sound playing yet. :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment