cpm-videx-series — Part 10
The CPU Switch, and What's Left
The last open mechanism, found. The 6502's main loop is a 24-byte routine at Apple $03C0 that perpetually retriggers a CPU switch via JSR $0E36 — a fetch into Z-80 memory the SoftCard intercepts. Plus an honest accounting of what the investigation closed and what stays open.
Part 10 of the cpm-videx series. The previous nine articles traced the boot pipeline from the Disk II PROM through to the cooperative-CPU disk model, with one mechanism left explicitly open: how the SoftCard actually flips the bus between processors. This article closes that question and inventories what the investigation settled.
The last open mechanism
Part 4 ended with the 6502 doing JMP $03D2 — a jump into a routine at Apple $03C0 that I knew existed but couldn’t extract. Part 8 traced the Z-80’s side of the cooperative loop at $1E39 (six instructions: LD A,($E000) / RLA / JR NC / LD ($E010),A / CCF / RRA / RET) and described three possible hardware mechanisms that could turn that polling into a CPU switch. None of them was verified.
The missing piece was the 6502’s side. What was the 6502 doing while the Z-80 was running CP/M? What instruction kicked the SoftCard to transfer control between them?
The answer is one routine, 24 bytes, at Apple $03C0.
How $03C0 gets there
The loader doesn’t write to $03C0 directly with a series of STA instructions. Three copy loops in the loader stage-2 code at Apple $1044, $104F, and $10F1 together copy the bytes Apple $1200-$13FF (which is loader-binary content that arrived from the boot-stub sector loads) into Apple $0200-$03FF. The bytes at loader-source $13C0-$13DC become the warm-boot routine at runtime $03C0-$03DC.
Apple $1044: LDA $1200,Y / STA $0200,Y ; copy 256 bytes — covers $0200-$02FF
Apple $104F: LDA $12FF,Y / STA $02FF,Y ; loop with Y = $F1..$01 — covers $0300-$03F0
Apple $10F1: LDA $13EF,Y / STA $03EF,Y ; loop with Y = $10..$01 — covers $03F0-$03FF
Net: 512 bytes of install fragments end up at Apple $0200-$03FF. The warm-boot routine is one of several pieces of code installed there.
The warm-boot routine, decoded
The bytes at Apple $03C0 after install:
$03C0: AD 83 C0 LDA $C083 ; (1) bank in LC RAM (read+write, bank 1)
$03C3: AD 83 C0 LDA $C083 ; (2) twice — Apple LC bank-switch protocol
$03C6: 8D FF FF STA $FFFF ; (3) touch LC RAM top page
$03C9: AD 81 C0 LDA $C081 ; (4) bank to LC bank 2 (read-only)
$03CC: 20 36 0E JSR $0E36 ; (5) ← THE CPU-SWITCH TRIGGER
$03CF: 20 58 FF JSR $FF58 ; (6) IORTS in monitor (return-RTS)
$03D2: 8D 81 C0 STA $C081 ; (7) touch LC switch
$03D5: 78 SEI ; (8) disable interrupts
$03D6: 20 4A FF JSR $FF4A ; (9) monitor routine (PREAD-area)
$03D9: 4C C0 03 JMP $03C0 ; (10) loop back to start
Twenty-four bytes. A perpetual loop. The 6502 enters this routine via JMP $03D2 at the end of the loader’s boot-finalization (mid-loop, skipping the LC enable on first pass). After that, the 6502 spends the rest of the CP/M session running this routine.
What JSR $0E36 does
Step 5 — the trigger — needs unpacking. Apple $0E36 is in the BIOS first 1 KB area that PREP_HANDOFF copied to Apple $0C00-$0FFF. Z-80 sees those same physical bytes at $1E36 (under SoftCard’s bit-12 XOR mapping for low addresses), where they’re the head of the inter-CPU sync polling loop covered in Part 8. The Z-80 instructions there are C3 39 FB — JP $FB39, into the 2.23 BIOS code page.
When the 6502 executes JSR $0E36:
- It pushes the return address
$03CEonto the stack. - It sets its program counter to
$0E36. - It reads the byte at
$0E36:$C3— an illegal 6502 opcode.
If nothing else happened, the 6502 would either halt or wander into undefined behavior. But the SoftCard hardware is monitoring the address bus. The combination of (a) the JSR landing in a watched memory range and (b) the byte being unrecognized by the 6502 is what flips the bus to Z-80 mode.
The Z-80 then runs from its own program counter. On first boot, that’s $0000 (= Apple $1000 under bit-12 XOR), where the 6502 had earlier planted JP $FA00. The Z-80 jumps to $FA00 and starts BIOS cold-boot. On subsequent yields-back-from-CP/M, the Z-80’s program counter is wherever it left off — typically inside the polling loop at $1E39, which expects to see $E000’s high bit set and then return to its caller.
The cooperative loop, end-to-end
With both sides of the switch concrete, the cooperative-CPU model resolves into a single small picture:
[Z-80 running CP/M]
Z-80 BIOS routine needs disk → calls into disk-callback at $1A00
Disk-callback writes parameters to BIOS state, calls $1E39
$1E39: LD A,($E000) / RLA / JR NC, $1E39
→ SoftCard intercepts the $E000 read, flips to 6502
[6502 wakes at $03CF, mid-warm-boot-loop]
$03CF: JSR $FF58 → monitor's IORTS → immediate RET
$03D2: STA $C081, SEI, JSR $FF4A → monitor cleanup
$03D9: JMP $03C0 → top of loop
(somewhere in here the 6502 services the disk request,
probably via monitor-vector patches that route through
the preserved RWTS at $BA00-$BFFF; the exact dispatch
path isn't visible from the 24-byte loop alone)
$03C0: LDA $C083, LDA $C083, STA $FFFF, LDA $C081
$03CC: JSR $0E36
→ SoftCard intercepts the $0E36 fetch, flips to Z-80
[Z-80 wakes at $1E39 (the polling-loop instruction it left off at)]
Reads $E000 — high bit now set
Falls through: LD ($E010),A — acknowledge
CCF, RRA, RET — return to disk-callback caller
[Z-80 BIOS routine resumes with disk data available]
Two CPUs. One bus. Twenty-four bytes of perpetual-loop on one side, six instructions of polling on the other. The SoftCard hardware orchestrates the rest.
What the investigation settled
A summary of the project by category, marking what’s settled and what’s not:
| Stage | Status |
|---|---|
| Disk II P6 PROM behavior | Settled (Part 2) |
6502 boot stub at $0801-$083C | Settled (Part 2) |
| Stage-2 loader, slot scanner, Pascal 1.1 detection | Settled (Part 1, Part 2) |
| LOAD_CPM dual-call mechanism (29 + extra sectors) | Settled (Part 4) |
| PREP_HANDOFF page copies and CCP+BDOS relocation | Settled (Part 4) |
Z-80 reset vector planting (JP $FA00) | Settled (Part 2) |
| Embedded Z-80 install fragment in 6502 loader | Settled (270 bytes at loader $143A — see Part 4) |
| SoftCard CPU-switch trigger | Settled (this article — JSR $0E36) |
| Warm-boot routine bytes and address | Settled (this article — copied from loader $1300+ to Apple $03C0) |
| SoftCard memory model (bit-12 XOR for low addresses, LC RAM for high) | Settled (Part 3) |
| BIOS layout (256-byte interleave, code + runtime-generated pages) | Settled (Part 5, Part 6) |
Cold-boot generator at $FB3A (2.23) / $DB6E (2.20) | Settled (Part 6) |
| Device-code-6 dispatch (Pascal 1.1) | Settled — the Z-80 side of the Videx fix (Part 6) |
$FDB0 (the device-6 init callee) being a RET stub | Settled — the actual driver-install path is elsewhere |
Cooperative-CPU sync polling loop at $1E39 | Settled (Part 8) |
| Why 2.20 hangs on a Videx (concrete trace) | Settled (Part 8) |
| Categorical inventory of every 2.20 vs 2.23 difference | Settled (Part 9) |
Initial population of $FA00-$FAB7 (cold-boot below BIOS) | Open |
| Runtime population of trap-marker pages with handler code | Open — strong candidate sectors identified |
| What the 6502 actually does between yields (disk service dispatch) | Open — the JSR $FF58 / JSR $FF4A / STA $FFFF chain isn’t fully traced |
Three open items remain. None of them is blocking the explanation: 2.20 hangs, 2.23 boots, and we can describe both at the byte level. The remaining unknowns are about how SOME bytes reach memory and what some specific subroutines do — connective tissue, not load-bearing structure.
The first two open items would be settled by booting the system in a Z-80 emulator and dumping memory at specific points. The third would be settled by extracting the bytes at Apple $0E00-$0EFF post-PREP_HANDOFF and reading what the disk-callback dispatch actually looks like. Both are doable — they just require either dynamic execution or another extraction pass.
What the Videx fix actually was
Rolling everything together, the answer to “why does 2.20 hang on a Videx and 2.23 boot?” has three parts:
Part one — detection. 2.23 added an 11-byte branch in the 6502 loader’s slot scanner that reads the Pascal 1.1 signature byte ($Cn0B == $01) and tags the slot with device code $06 instead of $04.
Part two — dispatch. 2.23 added a corresponding branch in the BIOS cold-boot generator at $FB3A that handles device code $06 by setting HL = $0DD0 and calling $FDB0. About 10 bytes.
Part three — the substrate that makes (1) and (2) work. This part is most of the project. The runtime-generated handler architecture, the 256-byte BIOS page interleave, the cooperative-CPU disk model with the polling loop and the JSR $0E36 trigger, the BIOS factory pattern, the embedded Z-80 fragment in the 6502 loader, the dispatch table populated from runtime-installed pages — none of this was changed for the Videx fix specifically; all of it had to exist for the Videx fix to be 21 bytes instead of a wholesale rewrite.
So the “fix” answer has two layers. At the surface: 21 bytes across two CPUs. Underneath: an 8 KB rewrite from CP/M 2.0 to 2.2 plus an architectural redesign of the BIOS into a runtime-generator pattern, into which those 21 bytes plug as one specific feature among many. Microsoft shipped the architecture and the fix together because they had to.
Where this leaves the project
The series is at its natural end. The boot pipeline is mapped. The Videx fix is located byte-by-byte. The cooperative-CPU model is concrete on both sides. The remaining open mechanisms are bounded.
The next deliverable — per the project notes — is a comprehensive disk-sector map: for every physical sector of the 2.23 disk image, what it contains, what address it ends up at after PREP_HANDOFF and the second LOAD_CPM, and which mechanism gets it there. That’s a reference document, not a narrative article. It’s the natural capstone to the byte-level work. It won’t be a Part 11 of the series; it’ll live alongside the series as a structured reference.
For now: the Videx hang has a 31-byte answer, a 24-byte CPU-switch routine, and most of an Apple ][ early-1980s operating system traced from disk to A> prompt. Joshua’s original observation — “the SoftCard CP/M won’t boot with a Videx” — turned out to be the visible tip of three years of architectural decisions Microsoft made between 1980 and 1982.
It’s been an investigation. The system is mostly comprehensible now.
Deep dives
- The SoftCard CPU-switch trigger: JSR $0E36 — the byte-level extraction of the warm-boot routine, the install-loop math that puts it at Apple
$03C0, and the address-labeling correction footnoted in Part 4.