The SoftCard CPU-switch trigger: JSR $0E36
Detail for Part 4 — The Handoff: 6502 to Z-80 and Part 8 — The Cooperative-CPU Round-Trip.
Address correction up front: earlier devlogs and articles labeled the boot-finalization at “Apple $1900-$194B.” The loader file
loader_223.bincovers Apple$0800-$13FF(boot stub plus stage-2), but my disassembler invocations used--base 0x1000, which displays addresses 0x800 higher than they actually are. The boot-finalization is at Apple $1100-$114B, not $1900-$194B. The bytes are unchanged; the labels were wrong. Carry that correction through.
The boot-finalization at Apple $1100-$114B ends with JMP $03D2 — entering mid-loop in a routine at $03C0. That routine is installed by three copy loops earlier in the loader:
Apple $1044: LDA $1200,Y / STA $0200,Y ; copy 256 bytes $1200→$0200
Apple $104F: LDA $12FF,Y / STA $02FF,Y ; copy ~241 bytes $1300→$0300 (Y=$F1..1)
Apple $10F1: LDA $13EF,Y / STA $03EF,Y ; copy 16 bytes $13F0→$03F0
Net: Apple $1200-$13FF (which is loader-binary content) gets installed at Apple $0200-$03FF. The bytes at loader file offset 0xBC0 (= Apple $13C0 source) become Apple $03C0 after install — the warm-boot routine.
The warm-boot routine
$03C0: AD 83 C0 LDA $C083 ; bank in LC RAM (read+write, bank 1)
$03C3: AD 83 C0 LDA $C083 ; (twice — Apple LC bank-switch protocol)
$03C6: 8D FF FF STA $FFFF ; touch LC RAM top page
$03C9: AD 81 C0 LDA $C081 ; bank to LC bank 2 (read-only)
$03CC: 20 36 0E JSR $0E36 ; ← CPU-SWITCH TRIGGER
$03CF: 20 58 FF JSR $FF58 ; IORTS (return-RTS in monitor ROM)
$03D2: 8D 81 C0 STA $C081 ; touch LC switch
$03D5: 78 SEI ; disable interrupts
$03D6: 20 4A FF JSR $FF4A ; PREAD or similar monitor routine
$03D9: 4C C0 03 JMP $03C0 ; loop forever
The boot-finalization’s JMP $03D2 enters mid-loop. The first execution does STA $C081 / SEI / JSR $FF4A / JMP $03C0, then falls through to the top: enable LC RAM, touch $FFFF, bank back, JSR $0E36.
What’s special about $0E36
Apple $0E36 contains the bytes C3 39 FB. Those are Z-80 instructions (JP $FB39) — not valid 6502 code. They got there as part of the BIOS first 1 KB that PREP_HANDOFF copied to Apple $0C00-$0FFF ($0E36 is inside that range).
When the 6502 executes JSR $0E36:
- The 6502 pushes the return address
$03CEonto the stack. - The 6502’s program counter is set to
$0E36. - The 6502 fetches the byte at
$0E36:$C3— an illegal 6502 opcode (treated as KIL on the 6502; a zombie state on most variants).
If nothing else happened, the 6502 would halt or wander. But the SoftCard hardware monitors the address bus. The combination of (a) the JSR-targeted address being in the BIOS-shared region and (b) the byte fetched not being a valid 6502 instruction is what the SoftCard uses as the CPU-switch trigger.
The exact electrical signaling — whether the SoftCard latches on the address read, the opcode fetch, or some other timing detail — needs a hardware document to confirm. But the mechanism is clear: JSR $0E36 is the instruction that flips the bus.
Z-80’s view
After the switch, the Z-80 is alive. Its program counter starts at its own reset vector $0000 (= Apple $1000 under bit-12 XOR), where the 6502 had earlier planted JP $FA00. The Z-80 reads C3 00 FA and jumps to $FA00. From there cold-boot proceeds as covered in Part 4 and Part 7.
The cooperative loop, completed
The warm-boot routine’s perpetual loop is what the 6502 runs during the entire CP/M session. It’s not a one-shot trigger; it’s the 6502’s main function under CP/M.
Sequence in steady state:
[Z-80 is running, CP/M is active]
Z-80 needs disk service (or hangs at JSR $Cn07)
Z-80 reaches the polling loop at $1E39: LD A,($E000) / RLA / JR NC, ...
SoftCard detects $E000 read → switches to 6502
[6502 wakes at $03CF: JSR $FF58]
$FF58: monitor IORTS → immediate RET
$03D2: STA $C081, SEI, JSR $FF4A: monitor cleanup
$03D9: JMP $03C0
[6502 services request via runtime-installed handler dispatch]
(the actual disk read happens here — 6502 runs RWTS routines preserved at $BA00-$BFFF)
Eventually the 6502 sets $E000's high bit
$03C0: LDA $C083, LDA $C083, STA $FFFF, LDA $C081, JSR $0E36
← CPU SWITCH back to Z-80
[Z-80 wakes; the polling loop's LD A,($E000) now succeeds]
Z-80 writes $E010 to acknowledge
Z-80 returns to BIOS routine that requested disk
[Z-80 continues with disk data available]
Where exactly the 6502 services the request between JMP $03C0 and the next JSR $0E36 is the open piece — the warm-boot loop as written doesn’t visibly do disk I/O. The dispatch must be hooked elsewhere, probably via the monitor routines $FF58 and $FF4A having been patched before the boot to point into RWTS handlers, or via the STA $FFFF write triggering some side effect we haven’t traced.
What’s settled: JSR $0E36 is the CPU-switch instruction, executed in a perpetual loop at the 6502’s address $03C0-$03DC. The Z-80’s reset vector at $0000 is what the Z-80 fetches the first time the switch happens. After that, the Z-80 runs CP/M and yields back when it needs disk service via the polling loop at $1E39.
The Apple II loop has both feet on the ground: a 6502 perpetually retriggering a CPU switch in a 24-byte routine, and a Z-80 driving CP/M while perpetually polling for the 6502 to come back.
Status: SoftCard CPU-switch trigger identified and decoded. The boot-finalization address correction is logged. The “what runs in the 6502 between yields” is the last open piece, and likely sits inside whatever the JSR $FF58 / JSR $FF4A or STA $FFFF actually do in this configuration — possibly with monitor-vector patches that I haven’t yet traced through.