modemwars/disassembly/boot/c64CartridgeStub8000.s
2026-08-23 02:09:40 -05:00

193 lines
15 KiB
ArmAsm

; ============================================================================
; Pseudo-cartridge image copied to $8000 by the C128 boot sector (bytes $89-$FF of track 1 sector 0)
; ============================================================================
; $8000/$8002 = cold/warm start vectors ($8009), $8004 = "CBM80". The C64 KERNAL reset code jumps to
; $8009, which initialises I/O, clears low RAM, sets MEMTOP and LOADs the normal boot file.
.setcpu "6502"
.include "c64.inc"
.include "kernal.inc"
.include "zeropage.inc"
; ---- references to code/data outside this file ----
D_0200 := $0200
D_0282 := $0282
D_0288 := $0288
L_02B8 := $02B8
D_0300 := $0300
sub_0B45 := $0B45
sub_0B55 := $0B55
; Contents
; --------
; $8009 cartColdStart Cold/warm start of the CBM80 pseudo-cartridge that the C128 boot sector plants
; at $8000 before GO64.
; $800F clearLowRamLoop Not a callable routine, just the body of the clear loop inside cartColdStart.
; $8065 cartLoadEaFile Tail of cartColdStart, reached by the JMP at $805E that steps over the inline
; filename.
.org $8000
; cartHeader ($8000-$8008) - the 9-byte autostart cartridge header the C64 KERNAL looks for.
; $8000/$8001 = cold-start vector, $8002/$8003 = warm-start (NMI) vector, $8004-$8008 = the
; signature 'CBM80'. Both vectors point at $8009, so RESET and RESTORE do exactly the same thing.
; This is RAM, not ROM: the C128 boot sector at $0B27 copies 256 bytes from $0B89 to $8000-$80FF and
; then calls GO64 ($FF4D). The C64 reset code ($FCE4: SEI / TXS / CLD) calls the signature check at
; $FD02, finds CBM80 here, and takes the JMP ($8000) at $FCEC - which skips IOINIT, RAMTAS, RESTOR and
; CINT entirely. cartColdStart has to redo all four by hand. On the plain C64 path (LOAD"EA",8,1)
; none of this exists and $8000 is just free RAM.
cartHeader:
.addr cartColdStart ; 8000 cold-start vector -> cartColdStart ($8009); taken by the KERNAL reset at $FCEC
.addr cartColdStart ; 8002 warm-start vector, also $8009: the KERNAL NMI handler ($FE47) jumps here when RESTORE is pressed, so RESTORE restarts the whole boot
; cbm80Signature ($8004-$8008) - the five magic bytes $C3 $C2 $CD $38 $30 = 'C'|$80, 'B'|$80, 'M'|$80,
; '8', '0'. The KERNAL's cartridge test at $FD02 compares these against its own copy at
; $FD10; a match is what makes both the reset and the NMI vector through $8000/$8002. Not a
; string in the game's own format (no bit-7 terminator) - the high bits are part of the magic.
cbm80Signature:
.byte $C3,$C2,$CD,$38,$30 ; 8004 ...80 'CBM80' with bit 7 set on the first three characters - the autostart magic tested by $FD02
; ----------------------------------------------------------------------
; cartColdStart - Cold/warm start of the CBM80 pseudo-cartridge that the C128 boot sector plants at
; $8000 before GO64. Because the KERNAL reset jumps through the $8000 vector before it runs
; IOINIT/RAMTAS/RESTOR/CINT, this routine performs all of them itself (I/O init, clear $0002-$0101 and
; $0200-$03FF, MEMTOP=$A000, MEMSTR=$0800, HIBASE=$0400, RESTOR, CINT), blanks the screen in blue,
; initialises the drive with a DOS 'I' command, then SETNAMs "0:EA" and falls through cartLoadEaFile
; into the normal boot chain. It never returns.
; In: Nothing in registers (entered from the KERNAL reset at $FCEC via JMP ($8000), or from the NMI
; handler at $FE4D via JMP ($8002) if RESTORE is pressed). Requires the C128 boot sector's helper
; routines at $0B45 (open command channel) and $0B55 (send DOS command) to still be in RAM - they
; are, because the reset skipped RAMTAS and the clear loop below stops at $03FF.
; Out: $0002-$0101 and $0200-$03FF zeroed; MEMSIZ $0283/$0284 = $A000; MEMSTR high $0282 = $08; HIBASE
; $0288 = $04; KERNAL indirect vectors restored; screen editor initialised; VIC_BORDER = VIC_BG0 =
; 6 (blue); VIC_CTRL1 bit 4 (DEN) cleared so the display is blank; drive initialised and logical
; file 15 closed; stack pointer = $F0; A = 0 for the LOAD. Does not return - control ends up at
; $02B8 (eaLoadGame).
; Called from: nobody in this disassembly - only the C64 KERNAL reset/NMI, through the vectors at
; $8000/$8002. (The XREF entries for L_8059/L_805C/L_8065/L_806A come from game/mapGenerator6F00
; and game/comcenScreens6F00, which are different code loaded over these addresses at run time.)
; Note: unlike the real RAMTAS this does not set the tape buffer pointer $B2/$B3 = $033C (harmless -
; the game never touches tape), and it never executes CLI, so the I flag set by the reset's SEI at
; $FCE4 is only cleared later, inside the KERNAL's own serial routines.
; ----------------------------------------------------------------------
cartColdStart:
jsr KERNAL_IOINIT ; 8009 IOINIT ($FF84): set up CIA1/CIA2 (keyboard, serial bus, jiffy timer) and the SID/VIC defaults the reset skipped
lda #$00 ; 800C $00 = the fill value for the low-RAM clear below
tay ; 800E Y = 0: first of the 256 offsets to clear
; ----------------------------------------------------------------------
; clearLowRamLoop - Not a callable routine, just the body of the clear loop inside cartColdStart. One
; pass of Y = 0..255 writes $00 through three absolute,Y stores, clearing $0002-$0101, $0200-$02FF and
; $0300-$03FF. It is a byte-for-byte copy of the clear loop at the head of the KERNAL's RAMTAS
; ($FD54), which the reset did not run.
; In: A = 0 (fill value), Y = 0 (index)
; Out: $0002-$0101, $0200-$03FF all zero; Y = 0 again on exit; A still 0
; Called from: fall-through from $800E, and its own BNE at $8019.
; The clear deliberately stops at $03FF: $0400 upwards holds the C128 boot sector's code and its
; still-needed helpers at $0B45/$0B55, and $0000/$0001 (the 6510 CPU port) are skipped because the
; first store is at $0002.
; ----------------------------------------------------------------------
clearLowRamLoop:
sta a:$0002,y ; 800F clear $0002+Y - absolute,Y, so the last two passes spill into $0100/$0101; $00/$01 (CPU port) are never touched
sta D_0200,y ; 8012 clear $0200+Y - BASIC input buffer and the KERNAL workspace ($0282 MEMSTR, $0288 HIBASE included; both are rewritten below)
sta D_0300,y ; 8015 clear $0300+Y - the vector page; RESTOR at $802D refills $0314-$0333 afterwards
iny ; 8018 advance to the next of the 256 offsets
bne clearLowRamLoop ; 8019 loop until Y wraps back to 0, i.e. all 256 offsets of all three pages are cleared
; ----------------------------------------------------------------------
; Rebuild the power-on memory layout that RAMTAS would normally have measured and stored. The values
; are simply hardcoded rather than RAM-tested: top of memory $A000, bottom of user memory
; $0800, screen page $0400. RESTOR and CINT then repair the vectors and the editor that the clear
; loop above just wiped.
; ----------------------------------------------------------------------
ldx #$00 ; 801B top-of-memory low byte = $00
ldy #$A0 ; 801D top-of-memory high byte = $A0, i.e. MEMTOP = $A000 (the standard C64 value; $A000-$BFFF later holds the title bitmap)
clc ; 801F C = 0 selects the 'set' direction of MEMTOP (C = 1 would read it back)
jsr KERNAL_MEMTOP ; 8020 MEMTOP ($FF99): store $A000 into MEMSIZ $0283/$0284
lda #$08 ; 8023 page $08 = the standard start of user RAM
sta D_0282 ; 8025 MEMSTR high byte $0282 = $08 -> bottom of memory $0800 (its low byte $0281 is already 0 from the clear loop)
lda #$04 ; 8028 page $04 = the default text screen
sta D_0288 ; 802A HIBASE $0288 = $04 so CINT below points the screen editor at $0400
jsr KERNAL_RESTOR ; 802D RESTOR ($FF8A): reload the KERNAL indirect vectors $0314-$0333, which the clear loop had zeroed
jsr KERNAL_CINT ; 8030 CINT ($FF81): program the VIC from the KERNAL's default table, clear screen RAM at $0400 and reset the editor
; ----------------------------------------------------------------------
; Hide the load. CINT has just switched on a normal blue-on-blue text screen, so paint border and
; background the same colour and then turn the display off completely; the player sees a plain blue
; screen until the boot loader at $C145 sets VIC_CTRL1 = $3B and shows the title bitmap.
; ----------------------------------------------------------------------
lda #$06 ; 8033 colour 6 = blue
sta VIC_BORDER ; 8035 VIC_BORDER ($D020) = blue
sta VIC_BG0 ; 8038 VIC_BG0 ($D021) = blue as well, so no text can show through
lda VIC_CTRL1 ; 803B read VIC_CTRL1 ($D011) - the raster/mode register CINT just initialised to $1B
and #$EF ; 803E clear bit 4 (DEN, display enable); the other bits (RSEL, YSCROLL, raster MSB) are left alone
sta VIC_CTRL1 ; 8040 write it back: screen blanked ($D011 = $0B) for the whole loading sequence
; ----------------------------------------------------------------------
; Reset the drive. Both calls go to routines that belong to the C128 boot sector and are still
; sitting in RAM at $0B45/$0B55 - the KERNAL reset skipped RAMTAS and this stub only cleared up to
; $03FF, so they survived GO64. The SETLFS inside $0B45 (lfn 15, device 8, secondary 15) also sets
; up the logical-file variables that the LOAD at $8065 later inherits. Note that these are NOT the
; game's own $0B45/$0B55 - the main program's code at $0B00 only exists after the fast loader has run,
; long after this stub is dead.
; ----------------------------------------------------------------------
jsr sub_0B45 ; 8043 c128OpenCommandChannel ($0B45): SETNAM length 0 / SETLFS 15,8,15 / OPEN -> drive command channel open
lda #$02 ; 8046 drive-command index 2 = the string "I" at $0B87 (DOS INITIALIZE); index 0/1 are the 1571 commands the C128-mode code already sent
jsr sub_0B55 ; 8048 c128SendDriveCommand ($0B55): CHKOUT 15, send "I" byte by byte, CLRCHN - the drive re-reads the BAM and clears any pending error
lda #$0F ; 804B logical file 15 = the command channel just used
jsr KERNAL_CLOSE ; 804D CLOSE ($FFC3): shut the command channel; the KERNAL leaves the current-file variables $B8/$B9/$BA at 15 / 15 / 8, which is exactly what the LOAD below needs
; ----------------------------------------------------------------------
; Name the stage-1 file and hand over. The filename is stored inline four bytes further on, so the
; pointer passed to SETNAM points back into this routine. No SETLFS is issued: the device (8) and the
; non-zero secondary address (15) left over from the command channel are what make LOAD honour the
; load address in the file's own header.
; ----------------------------------------------------------------------
lda #$04 ; 8050 filename length = 4 characters
ldx #$61 ; 8052 filename pointer low byte $61
ldy #$80 ; 8054 filename pointer high byte $80 -> eaFileNameCart at $8061, embedded in this very routine
jsr KERNAL_SETNAM ; 8056 SETNAM ($FFBD): filename = "0:EA"
ldx #$F0 ; 8059 stack pointer value $F0 (the reset had set $FF)
txs ; 805B SP = $F0, i.e. stack top $01F0; safe because nothing on the stack is ever needed again - this code never returns. Probably just copied from EA's generic cartridge template
lda #$00 ; 805C A = 0 tells LOAD to load (A = 1 would mean verify)
jmp cartLoadEaFile ; 805E jump over the four inline filename bytes at $8061 into cartLoadEaFile
; eaFileNameCart ($8061-$8064) - 4 bytes of plain PETSCII, "0:EA": drive 0, file "EA" (the 1-block
; directory file that loads to $02A8-$030B). Length-counted by SETNAM, so there is no terminator and
; no bit-7 flag. It sits in the middle of the code and is stepped over by the JMP at $805E.
eaFileNameCart:
.byte "0:EA" ; 8061 "0:EA" - drive number, colon, filename; passed to SETNAM as X=$61 / Y=$80 / A=4
; ----------------------------------------------------------------------
; cartLoadEaFile - Tail of cartColdStart, reached by the JMP at $805E that steps over the inline
; filename. KERNAL-LOADs "0:EA" and jumps straight into it. Because the secondary address inherited
; from the command channel is 15 (non-zero), LOAD uses the two-byte load address in the file itself,
; so "ea" lands at $02A8-$030B; the JMP then enters eaLoadGame at $02B8 by hand. On the ordinary C64
; path that entry happens indirectly, through the BASIC vectors that "ea" overwrites at $0300-$030B -
; here BASIC never runs at all, which is why the jump is explicit. The carry/error result of LOAD is
; discarded: a load failure runs whatever happens to be at $02B8.
; In: A = 0 (load, not verify); filename set by the SETNAM at $8056; device 8 / secondary 15 left in
; $B8/$B9/$BA by $0B45 and the CLOSE at $804D
; Out: $02A8-$030B filled from disk; control passes to $02B8 (eaLoadGame), which SETLFS 8,8,1, loads
; the 45-block file "LOAD" to $9800-$C3FF, JSRs the boot loader at $C000 and finally JMPs to the
; game at $0800. Never returns.
; Called from: only the JMP at $805E.
; ----------------------------------------------------------------------
cartLoadEaFile:
jsr KERNAL_LOAD ; 8065 LOAD ($FFD5): pull "0:EA" from device 8; secondary 15 means 'use the file's own load address' -> $02A8-$030B
jmp L_02B8 ; 8068 enter eaLoadGame at $02B8 directly, skipping the BASIC-vector hijack that the C64 path relies on; C (LOAD error) is ignored
; cartTailFiller ($806B-$8076) - 12 dead bytes, never referenced by anything. A $00 followed by 8E 72
; 17 / 8E 73 17 / 8E 70 17 / 8E 71, which disassembles as STX $1772 / STX $1773 / STX $1770 / STX
; $1771 with the last operand byte cut off. These are simply the last bytes ($F4-$FF) of track 1
; sector 0 that the C128 boot sector's 256-byte copy loop dragged along with the real cartridge image;
; they look like leftovers in the mastering system's buffer, the same way track 1 sector 16 still
; holds fragments of assembler source text. Execution can never reach them (the routine above ends in
; a JMP), and at run time the whole $8000-$87FF block is overwritten by the $6F00 overlay's upper half
; anyway.
cartTailFiller:
.byte $00,$8E,$72,$17,$8E,$73,$17,$8E; 806B ..r..s.. leftover mastering-buffer bytes: $00, then STX $1772 / STX $1773 / STX $1770 ...
.byte $70,$17,$8E,$71 ; 8073 p..q ... and the truncated STX $1771 that ends the sector; dead filler, never referenced