-
-
Save mgcaret/022bd0bb3ee71f28429972523556416e to your computer and use it in GitHub Desktop.
Apple IIc Plus accelerator code disassembly/analysis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; 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