Skip to content

Instantly share code, notes, and snippets.

@mgcaret

mgcaret/accel.s Secret

Last active August 31, 2017 03:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgcaret/022bd0bb3ee71f28429972523556416e to your computer and use it in GitHub Desktop.
Save mgcaret/022bd0bb3ee71f28429972523556416e to your computer and use it in GitHub Desktop.
Apple IIc Plus accelerator code disassembly/analysis
; Apple IIc Plus Accelerator Control Code
; This is an analyzed and commented disassembly of the Apple //c accelerator control firmware described in the
; appendices of https://archive.org/details/AppleIIcTechnicalReference2ndEd
; If you wish to know how to *use* the accelerator control functions, see that document.
; Without setting fixbugs, ca65 will assemble this into a byte-for-byte match of what is in the IIc Plus firmware.
; Setting fixbugs attempts to fix them, but these are untested as of 2/10/2017.
; Undocumented features/functions/bugs/errors:
; * undocumented command $00 - executed during reset, this inits accelerator and checks for esc key
; if you issue this command and press esc during the paus, accel switches to standard
; speed
; * bug: trashes location $0000
; * bug: accelerator control word high byte is never used to config accelerator
; * bug: the reset code appears to want to restore saved state of accelerator but only does so for the slots
; * bug: write accelerator command trashes the saved accelerator control word
; * bug: some nonfunctional code in set accelerator routine
; above bugs are fixed if fixbugs is nonzero (see below)
; * docs error: accelerator control word low byte docs say:
; 1 = fast, reality: 1 = slow
; bit 7 = speaker speed, bit 0-6 = slot speed: reality: speaker at bit 0, slots at 1-7.
; * Uses MIG RAM page 2 as scratchpad and state storage (MIG RAM is 32-byte window at $CE00)
; referred to in the rest of this file as mig[offset(s)]
; The front side to the MIG RAM (not connected to the IWM) is controlled by access to $CEA0 and $CE20
; $CEA0 resets the window to page 0. $CE20 increments the page. Note the accesses to these locations
; near the beginning of the $FD00 code.
; The MIG is visible when the altnernate firmware is visible, its address space is $CE00-$CFFF
; But only $CE00-$CE1F view the RAM, everything else looks like floating bus.
; Missing features present in the hardware but unused:
; * Accelerator has clock scaler, code always sets 4.00 MHz
; uses ZP from $00 to $07 after saving it to mig[$10-$17]
; $00: temp loc
; $01: original stack pointer at entry
; $02: command number
; $03-$04: user buffer pointer (if command needs it)
; $05: exit code to be put into accumulator
; $06: unused
; $07: unused
; mig memory (at $CE00) usage:
; $00: powerup byte 1 - $33 after initialization
; $01: powerup byte 2 - $55 after initialization
; $02: current accelerator control word - low byte - speeds (1 = fast)
; b7: speaker
; b6-b0: port (slot) 7-1
; init byte is $67 (01100111 - speaker, ports 1, 2, 5, 6 = slow)
; $03: current accelerator control word - high byte - misc
; b7: reserved
; b6: paddle speed (1 = fast)
; b5: reserved
; b4*: register lock/unlock state (1 = locked, undocumented)
; b3: accelerator enable (1 = disabled) - Not set by write accelerator, see docs.
; b2-b1: reserved
; b0: reserved but in initial value for ACW high byte
; init byte appears to supposed to be $51 (01010001), but never set
; $04: keyboard key value to check for <ESC> at reset
; $05: copy of what should be in $c05c hw reg
; $06: copy of what should be in $c05d hw reg
; $07: copy of what should be in $c05e hw reg
; $08: copy of what should be in $c05f hw reg
; $10-$17: saved ZP values
; Accelerator registers - Appear to follow ZIP Chip without any customization.
; the ZIP chip 2.0 software has no problems detecting and configuring the IIc Plus
; accelerator
; $c05a - Write $A5 lock
; Write $5a * 4 to unlock
; Write anything else to slow down to 1 MHz indefinitely
; $c05b - Write anything here to enable accelerator
; Read status
; $c05c - R/W Slot/speaker speed
; $c05d - Write system speed
; $c05e - Write I/O delay bit 7 (1 disable)
; - Read softswitches
; $c05f - Paddle speed/language card cache
; ----------------------------------------------------------------------------
; ca65 setup
.psc02
; ----------------------------------------------------------------------------
; conditions
fixbugs = 0 ; if this is set to 1, fix bugs
; otherwise ca65 produces byte-for-byte match
escreverse = 0 ; if this is set to 1, reverse the function of the esc key
; equates
PAGE0 = $00
COUNTER = PAGE0+0
CALLSP = PAGE0+1
COMMAND = PAGE0+2
UBFPTRL = PAGE0+3
UBFPTRH = PAGE0+4
EXITCOD = PAGE0+5
; misc
STACK = $0100
SWRTS2 = $C784
; I/O page
IOPAGE = $C000
KBD = IOPAGE+$00
KBDSTR = IOPAGE+$10
ZIP5A = IOPAGE+$5A
ZIP5B = IOPAGE+$5B
ZIP5C = IOPAGE+$5C
ZIP5D = IOPAGE+$5D
ZIP5E = IOPAGE+$5E
ZIP5F = IOPAGE+$5F
; MIG
MIGBASE = $CE00
MIGRAM = MIGBASE
PWRUPB0 = MIGRAM+0
PWRUPB1 = MIGRAM+1
ACWL = MIGRAM+2
ACWH = MIGRAM+3
KBDSAVE = MIGRAM+4
ZIP5CSV = MIGRAM+5
ZIP5DSV = MIGRAM+6
ZIP5ESV = MIGRAM+7
ZIP5FSV = MIGRAM+8
ZPSAVE = MIGRAM+$10
MIGPAG0 = MIGBASE+$A0
MIGPAGI = MIGBASE+$20
; fixed values
PWRUPV0 = $33
PWRUPV1 = $55
ESCKEY = $9B
; ----------------------------------------------------------------------------
; Display "Normal" on the screen.
.if escreverse
.org $FC27
.proc FAST
ldy #$04 ; 4 characters
loop: lda msg-1,y ; Get message byte
sta $048F,y ; Put on screen
dey
bne loop
bit KBDSTR ; clear keyboard
rts ; done `
msg: .byte $C6, $E1, $F3, F4 ; 'Fast'
.endproc
.else
.org $FC27
.proc NORMAL
ldy #$06 ; 6 characters
loop: lda msg-1,y ; Get message byte
sta $0491,y ; Put on screen
dey
bne loop
bit KBDSTR ; clear keyboard
rts ; done `
msg: .byte $CE, $EF, $F2, $ED, $E1, $EC ; 'Normal'
.endproc
.endif
; ----------------------------------------------------------------------------
; PC arrives here from the other bank.
; .org $FCA8
; ; bunch of NOPs
; .org $FCAF
; jsr LFCB5 ; FCAF 20 B5 FC ..
; jmp LC784 ; SWRTS2
; ----------------------------------------------------------------------------
; Aux bank delay, called by accelerator init routine, presumably to give
; time for <ESC> key to be recognized. This is also what messes up the beep
; see http://quinndunki.com/blondihacks/?p=2471
; input: A = delay counter
.org $FCB5
.proc ADELAY
phy ;
ldy #$9A ; offset of Port 1 ACIA cmd register ($C09A)
phx ;
sec ;
tax ;
loop1: lda IOPAGE,y ; cause 50ms slow cycle (if port 2 is unaccelerated)
txa ;
loop2: sbc #$01 ;
bne loop2 ;
dex ;
bne loop1 ;
plx ;
ply ;
rts
.endproc
; ----------------------------------------------------------------------------
; main body of code, you arrive here by calling jsr $C7C7 while the main bank
; is active. The system switches banks and jumps to $FD00.
.org $FD00
.proc ACCEL
php ; save processr status
sei ; disable interrupts
LFD02: phy ; save y reg (label ref from jsr @ $db25, but prob data)
phx ; save x reg
bit MIGPAG0 ; set MIG page 0
bit MIGPAGI ; MIG page 1
bit MIGPAGI ; MIG page 2
; Next routine reads $0000-$0007, stores into $CE10-$CE17, and zeros them
ldx #$07 ; 8 times
@loop: lda PAGE0,x ; get ZP location
sta ZPSAVE,x ; Save to MIG
stz PAGE0,x ; Zero ZP location
dex ; next
bpl @loop ; Loops 8 times
; The following routine gets the command and parameters off of the stack
; Next combination of instructions puts the stack pointer into all 3
; registers, then increments it in y.
tsx ; get sp
txa ; copy it...
tay ; to y
iny ; now x is sp, y is sp+1
lda STACK+6,x ; reach into stack for command, 6-byte offset for return
; address and saved registers
sta COMMAND ; put into ZP $02
cmp #$05 ; $05 = Read Accelerator - first command w/buffer pointer
stx CALLSP ; original sp into $01
bcc noparm ; no buffer pointer to get
lda STACK+7,x ; buffer pointer low byte
sta UBFPTRL ; into ZP $03
lda STACK+8,x ; buffer pointer high byte
sta UBFPTRH ; into $04
iny ; Fix index registers for parameters...
iny ;
inx ;
inx ;
noparm: inx ; we go here if it's a no-param call
txs ; stack now adjusted .
ldx CALLSP ; original SP ..
lda #$05 ; 5 bytes
sta COUNTER ; into ZP $00
@loop: lda STACK+5,x ; shift stack up
sta STACK+5,y ; to remove call parameters
dex ; next from
dey ; next to
dec COUNTER ; counter decrement
bne @loop ; loop until zero
lda COMMAND ; get command
cmp #$07 ; bad command number?
bcc docmd ; no, do command
lda #$01 ; bad command exit code
sta EXITCOD ; put in $05
bra acceldn ; skip command call
docmd: asl a ; turn call into jump index
tax ; and move to x
jsr dispcmd ; call command function
acceldn:
lda EXITCOD ; get exit value
pha ; save it
; Following code attempts to restore the zero page to what it was, but
; it fails to restore $0000
ldx #$07 ; # 8?! times (see above)
@loop: lda ZPSAVE,x ; FD60 BD 10 CE ...
sta PAGE0,x ; FD63 95 00 ..
dex ; FD65 CA .
.if ::fixbugs
bpl @loop
.else
bne @loop ; Bug: Loops only 7 times! I bet this should be bpl
; like save loop, this is why $0000 gets trashed.
.endif
pla ; get return code
plx ; get saved x
ply ; y
plp ; p
; What follows is *NOT* a difference from the docs, now that I have
; gotten back up to speed on the 6502. However, the intent is clear
; that C should be cleared if A is $00, set if it isn't. The docs say:
; "If the command number is valid, the firmware performs lhe function
; specified by the command and returns to the calling routine with a
; value of $00 in the A register (accumulator). If the command number
; is not valid, the firmware returns the value $01 in the A register
; to indicate an error. In either case, the c (carry) flag is set."
; Perhaps they meant the Z flag to also reflect the accumulator... if so
; that is undocumented, anyway.
.if ::fixbugs
cmp #$01
.else
clc ; Clear carry.
cmp #$00 ; is A zero? (sets carry).
beq noerr ; Yep, skip next instruction
sec ; No, set carry.
.endif
noerr: jmp SWRTS2 ; switch banks and RTS
; ----------------------------------------------------------------------------
dispcmd:
jmp (cmdtable,x) ; dispatch command
; ----------------------------------------------------------------------------
; Init Accelerator (undocumented)
.proc AINIT
ldx #$03 ; 3 times
iloop: lda #$FF ; Maximum
jsr ADELAY ; Delay
dex ; next ieration
bne iloop ; Do it again. Sheesh.
lda KBD ; Keyboard input
sta KBDSAVE ; Save into mig[4]
; following code checks for power up bytes in MIG
lda PWRUPB0 ; mig[0]
cmp #PWRUPV0 ; Is it $33
; da65 becomes confused - original disassembly:
; .byte $D0 ; FD8D D0 .
; $FD8E referenced in bank 1 from JSRs at: $F82E, $F835, $FA0B
; appears to be calling mid-instruction
;LFD8E: rmb0 $AD ; FD8E 07 AD ..
; ora ($CE,x) ; FD90 01 CE ..
bne coldst ; nope, do cold start
lda PWRUPB1 ; yep, check mig[1]
cmp #PWRUPV1 ; Is it $55?
beq warmst ; yes
coldst: lda KBDSAVE ; No match. Get saved keyboard value.
ora #$80 ; Set high bit in case keyboard strobe was cleared.
sta KBDSAVE ; And put it back.
ldx #$03 ; 3 count for register init
rinilp: lda regini,x ; $FE8C+3..1
sta ZIP5CSV,x ; $CE05+3..1
sta ZIP5C,x ; $C05C+3..1
dex ; next byte .
LFDAA: bne rinilp ; not 0, so not done (ref from $DAB8, but prob data)
; this is probably another bne/bpl bug, as an appropriate
; value *is* present.
ldx acwini+$0 ; initial bytes for accelerator control word
ldy acwini+$1 ; (this one is never written anywhere if bugs are not fixed)
bra setacc ; skip next 2 instructions
; we get straight here if mig[0] has [$33 $55]
warmst: ldx ACWL ; get saved accelerator control bytes
ldy ACWH ; (this one is never written anywhere)
setacc: jsr AUNLK ; unlock registers
jsr AENAB ; enable high speed
jsr ASETR ; set registers (restore previous state - bug, doesnt work except slots)
jsr ALOCK ; lock registers
; set powerup bytes
lda #PWRUPV0 ; first powerup byte
sta PWRUPB0 ; into mig[0]
lda #PWRUPV1 ; second powerup byte
sta PWRUPB1 ; into mig[1]
; now handle user speed selection
lda KBDSAVE ; get saved keypress from mig[4]
cmp #ESCKEY ; <ESC> key
.if ::escreverse
beq hispd
jsr ANLK
jsr ADISA
jsr ALOCK
sta KBDSTR
rts
hispd: lda #$08
trb ACWH
jmp FAST
.else
bne hispd ; Nope, leave at high speed
jsr AUNLK ; Unlock registers
LFDDA: jsr ADISA ; Set 1 Mhz - ref from code in F8 space that looks like
; it belongs in main bank ROM
jsr ALOCK ; Lock registers ..
jmp NORMAL ; print 'Normal', clear kbd, exit.
.endif
; ----------------------------------------------------------------------------
; clear keyboard strobe and return
hispd: lda #$08 ; Bit 3 (accelerator status)
trb ACWH ; clear in accelerator control word high byte
sta KBDSTR ; clear keyboard strobe
rts ; `
.endproc
; ----------------------------------------------------------------------------
; Enable accelerator high speed mode.
; assumes registers are already unlocked.
; da65 confused here by another reference
;LFDEC: .byte $A9 ; FDEC A9 .
;LFDED: php ; FDED 08 .
.proc AENAB
lda #$08 ; Refer to bit 3 (acclerator status)
sta ZIP5B ; Standard Zip ngage accelerator
trb ACWH ; Cler bit 3 in accelerator control word high byte
rts ;
.endproc
; ----------------------------------------------------------------------------
; "Indefinite Synchronous Sequence"
; turns off high speed mode.
; assumes registers are already unlocked.
.proc ADISA
lda #$08 ; Can be anything but $A5 or $5A, so refer to bit 3
sta ZIP5A ; write once
tsb ACWH ; set bit 3 in accelerator control word high byte
rts ; `
.endproc
; ----------------------------------------------------------------------------
; lock the accelerator registers
.proc ALOCK
lda #$A5 ; standard zip lock byte here
sta ZIP5A ; write once
lda #$10 ; bit 4
tsb ACWH ; set it in accelerator control word high byte
rts ;
.endproc
; ----------------------------------------------------------------------------
; unlock the accelerator registers
; this is the standard ZIP unlock sequence and also sets the state save
; in mig[3]
.proc AUNLK
lda #$5A ; standard zip unlock byte here
sta ZIP5A ; write 4 times
sta ZIP5A ;
sta ZIP5A ;
sta ZIP5A ;
lda #$10 ; bit 4
trb ACWH ; clear it in accelerator control word high byte
rts ; `
.endproc
; ----------------------------------------------------------------------------
; read accelerator
; never actually touches the accelerator except to lock or unlock the
; registers, the response comes from mig[2..3]
.proc AREAD
phy ; Save y
jsr AUNLK ; unlock registers
ldy #$00 ; First byte of user buffer
lda ACWL ; mig[2]
sta (UBFPTRL),y ; User buffer[0]
iny ; next byte of user buffer
lda ACWH ; mig[3]
sta (UBFPTRL),y ; User buffer[1]
jsr ALOCK ; lock registers
ply ; restore y
rts ; done
.endproc
; ----------------------------------------------------------------------------
; write accelerator
.proc AWRIT
.if ::fixbugs
ldy #$00
lda (UBFPTRL),y ; new ACW low
pha
iny
lda (UBFPTRL),y ; new ACW high
pha
.else
lda $03 ; user buffer pointer low byte - bug, should deref
sta ACWL ; write to mig[2]
tax ; why? .
ldy $04 ; user buffer pointer high byte - bug, should deref
sty ACWL ; write mig[2] again? Another bug?
; probaly should be $CE03. This is hidden by the
; call to set the accelerator registers, which ends
; up doing doing something usable, but then we screw
; up again below.
phx ; save x
phy ; save y
.endif
jsr AUNLK ; unlock registers
jsr AENAB ; enable accelerator
ply ; get ACW high
plx ; get ACW low
jsr ASETR ; set registers
jsr ALOCK ; lock registers
.if ::fixbugs
; do nothing
.else
lda <CALLSP ; original sp, no idea why this access - probably a bug
trb ACWH ; use value to reset bits in high byte of accel control word
; leaving them scrambled
.endif
rts ; FE53 60 `
.endproc
; ----------------------------------------------------------------------------
; set accelerator registers and saved values from x register
; other things call this with y register set, too
; saves x to ce05/c05c and doesn't use y at all. zeros ce03.
; preserves all registers
.proc ASETR
php ; Save registers
phy ;
phx ;
pha ;
stx ZIP5CSV ; Save copy of what we are putting into $c05c
stx ZIP5C ; Store slot speeds
stx ACWL ; It's also the low byte of accel control word
.if ::fixbugs
sty ACWH
tya
and #$40 ; bit 6 only
sta ZIP5F ; paddle speed
lda #$40
sta ZIP5E ; Well, initial value has bit 6 set, orig does this, too
.else
stz ACWH ; Zero high byte of accel control word - probably a bug
lda #$40 ; bit 6 on - enable paddle delay, bit 7 off enable LC cache
sta ZIP5F ; Into control register.
lda #$40 ; bit 6 on -
sta ZIP5E ; Unknown effect here, Zip docs do not have a bit 6 function
; possibly a bug
sta ZIP5F ; As above... we write here again
.endif
stz ZIP5D ; Set speed register to 4.000 MHz unconditionally
stz ZIP5E ; Enable synchronous sequences unconditionally
pla ; FE77 68 h
plx ; FE78 FA .
ply ; FE79 7A z
plp ; FE7A 28 (
rts ; FE7B 60 `
.endproc
; ----------------------------------------------------------------------------
; jump table for command functions
cmdtable:
.word AINIT ; $00 - Init (undocumented)
.word AENAB ; $01 - Enable Accelerator
.word ADISA ; $02 - Disable Accelerator
.word ALOCK ; $03 - Lock Accelerator
.word AUNLK ; $04 - Unlock Accelerator
.word AREAD ; $05 - Read Accelerator
.word AWRIT ; $06 - Write Accelerator
; original disassembler output:
;LFE7C: sei ; FE7C 78 x
; sbc LFDEC,x ; FE7D FD EC FD ...
; sbc $FD,x ; FE80 F5 FD ..
; inc $09FD,x ; FE82 FE FD 09 ...
; inc LFE1D,x ; FE85 FE 1D FE ...
; .byte $33 ; FE88 33 3
; .byte $FE ; FE89 FE .
; ----------------------------------------------------------------------------
acwini: .byte $67 ; Accelerator control word init low byte
.byte $51 ; Accelerator control word init high byte
regini: .byte $67 ; this byte is never accessed
; init routine sticks these in $c05d-$c05f if not previously initialized:
.byte $00 ; $c05d: 4 Mhz
.byte $C0 ; $c05e: bit 7 (enable I/O synchronous delay)
; bit 6 undocumented
.byte $00 ; $c05f: bit 7 (off, enable lang card cache)
; bit 6 (off, disable paddle delay)
; original confused disaassembly
;LFE8C: rmb6 L0000 ; FE8C 67 00 g.
; cpy #$00 ; FE8E C0 00 ..
.byte $80, $03, $40 ; unused bytes?
; bra LFE95 ; FE90 80 03 ..
; rti ; FE92 40 @
; ----------------------------------------------------------------------------
.byte $03 ; more unused...
.byte $20 ;
LFE95: .byte $02 ; FE95 02 label ref from da65 confusion
.endproc ; ACCEL
; end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment