Stage 1 emulator: 6502 boot finishes without ever writing $FA00

5 min read
6502cpmsoftcardapple-iiemulationreverse-engineeringretrocomputingcpm-videx-series

The static analysis from Parts 1-10 reached a wall at the cold-boot generator at Apple $FA00. The disassembled boot loader has three documented PAGE_COPY calls ($0A00-$0FFF$BA00-$BFFF, $9700-$9CFF$0A00-$0FFF, $8000-$96FF$A300-$B9FF) and one LOAD_CPM call that reads 29 sectors into $8000-$9CFF. None of those targets $FA00. The user clarified that the SoftCard has no on-board ROM, so $FA00 cannot be ROM-mapped either. So how does the Z-80 see executable code at $FA00 when its planted reset vector says JP $FA00?

This devlog runs the 6502 boot in an emulator and watches every memory write. The result settles half the question: 6502 doesn’t do it.

What the emulator looks like

cpm-investigation/emu_softcard.py ties together three pieces:

  • nibbler.cpu.CPU6502 — pre-existing 6502 emulator with Apple soft-switch handling (Disk II controller, keyboard, LC RAM stubs).
  • nibbler.dsk_disk.DSKDisk — new this session. Mirrors the WOZDisk API (read_nibble, step_phase, motor_on, current_qtrack) but synthesizes its nibble stream from a .dsk / .po sector-order image. Each track gets a synthetic GCR layout: 40×$FF sync, address prolog D5 AA 96, 4-and-4 vol/trk/sec/cks, address epilog DE AA EB, sync gap, data prolog D5 AA AD, 343 6-and-2 nibbles, data epilog. Validated by reading back known sectors via the standard ROM decoder.
  • A P6 PROM hook at $Cn5C that synthesizes sector-loads — the boot stub’s JMP ($003E) lands there, and rather than emulate ROM execution, the hook reads the requested sector from the .dsk file directly and increments $27 to mimic the PROM’s tail behavior.

I also extracted the real Disk II P6 PROM bytes from docs/DiskII_BootROM.asm and dropped them into the slot-6 ROM page ($C600-$C6FF) so the slot scanner’s CKSUM_SLOT at $114E can actually see something stable.

What works

The boot stub’s 11 sector-load iterations all complete (120 instructions), filling $0A00-$13FF. Stage-2 enters at $1000, runs the install loops, hits the slot scanner at $1060. PREP_HANDOFF copies all execute. LOAD_CPM reads its 29 sectors. After ~20M instructions the 6502 ends up looping in the warm-boot routine at $03C0-$03D2 (where JSR $0E36 would trigger the SoftCard CPU switch on real hardware).

End-of-boot snapshot:

RegionNon-zero bytesExpected source
$0A00-$0FFF (1536 B)1443PREP_HANDOFF #2 from staging $9700-$9CFF
$A300-$B9FF (5888 B)5596PREP_HANDOFF #3 from staging $8000-$96FF
$BA00-$BFFF (1536 B)1483PREP_HANDOFF #1 from $0A00-$0FFF
$1000-$1002C3 00 FAZ-80 reset vector (JP $FA00)
$FFF9-$FFFF (6 B)6 (from log)Apple monitor reset-vector patch
$FA00-$FFF80 (zero)nothing wrote here

What doesn’t work, and what it tells us

Two things didn’t work, and both are informative:

1. The slot scanner has an apparent dead-code path. SCAN_INIT_SLOT at $1086 is the only place that increments $3E (the iteration counter the post-scan check LDA $3E; CMP #$01; BEQ DISPATCH_OK reads). The path to $1086 is BEQ $1086 at $106C, which fires when $3E = 0. But $3E is initialized to $FF at $105E (via DEY from 0 then STY $3E), and no other instruction in the loader writes $3E. So $3E never reaches 0, SCAN_INIT_SLOT never runs, $3E stays at $FF, and the post-scan check always falls into the multi-card error path. On real hardware this works, so something must clear $3E — maybe a SoftCard hardware side effect on a specific memory access. For now I patch $3E = 0 externally at $1060 and document the open question.

2. No 6502 write to $FA00-$FFFF. The write logger ran the entire boot and recorded exactly 6 writes to that region: the Apple monitor reset-vector patch at $FFF9-$FFFF. Zero writes anywhere in $FA00-$FFF8. So the BIOS proper at $FA00-$FFB7 is never written by the 6502 during the boot.

Where this leaves the BIOS-population question

The factual settlement: in 2.23, the 6502 doesn’t put the BIOS at $FA00. So either:

  • (a) The Z-80 starts somewhere other than $FA00, runs setup code that generates the BIOS at $FA00, then jumps to it. The “JP $FA00” at the planted reset vector might be for warm-boots after the cold-boot generator finishes.
  • (b) The CPU switch hardware doesn’t reset the Z-80 PC to $0000. It might pick up at the Apple-side address the 6502 was attempting to execute ($0E36), which the Z-80 reads as $1E36. The Z-80 callbacks installed at $0A00-$0FFF map to Z-80 $1A00-$1FFF, so $1E36 is in the BIOS first 1 KB region — and that’s where the dispatch / cold-boot-generator code lives.

Option (b) is consistent with the resume-prompt’s note that the polling loop is at “Z-80 $1E39” and with $1E36 being the Z-80 view of Apple $0E36. Stage 2 will add a Z-80 emulator and a SoftCard CPU-switch model, then verify by tracing the actual transition.

Files

Status

Stage-1 emulator boots reliably to handoff. $FA00-$FFFF stays at zero through the entire 6502 phase. The BIOS proper must be Z-80-generated post-handoff. Stage 2 (Z-80 emulator + SoftCard switch) is next.