cpm-videx-series — Part 4

The Handoff: 6502 to Z-80

What happens between the 6502's last instruction and the Z-80's first. The boot-finalization sequence, the embedded Z-80 install fragments inside the 6502 loader, and the still-mysterious SoftCard CPU-switch trigger that flips the bus between processors.

13 min read advanced
apple-ii6502z80cpmsoftcardreverse-engineeringoperating-systemsretrocomputingcpm-videx-seriesdeep-dive

Part 4 of the cpm-videx series. Part 2 traced the 6502 path from the boot PROM to the last instruction before the CPU switch. Part 3 covered the SoftCard memory model the Z-80 will see when it wakes. This article sits in the gap between them — what the 6502 does in its final breath and what the Z-80 finds when it comes alive.


The puzzle

The 6502 boot loader runs to completion and triggers the SoftCard CPU switch. The Z-80 begins fetching instructions. Between those two moments, several things have to be true:

  1. The Z-80 reset vector at Z-80 $0000-$0002 has to hold a valid JP $FA00 (in 2.23) or JP $DA00 (in 2.20).
  2. The cold-boot area at Z-80 $FA00-$FAB7 (in 2.23) has to hold real Z-80 code, not the trap markers it has on disk.
  3. CCP+BDOS have to be at their final positions (Apple $8000-$96FF after the loader’s third page copy — corresponds to BDOS at Z-80 $9C06).
  4. The BIOS first 1 KB has to be at Apple $0C00-$0FFF (mapped to Z-80 $FAB8-$FEB7 via SoftCard’s high-address RAM).
  5. The Z-80 disk callbacks have to be at Apple $0A00-$0BFF (Z-80 $1A00-$1BFF after bit-12 XOR).
  6. The slot-info table at TPA-area $F3B8-$F3BF has to hold the device codes from the slot scanner.
  7. The SoftCard hardware has to be in a state where it can accept the CPU-switch trigger and resume the Z-80 from a clean reset.

The 6502 loader does all of this. Some of it I can show concretely; some is still under investigation.

Boot finalization, step by step

The last 6502 code to run, at Apple $1900-$194B1:

$1901: STA $1001                ; Z-80 reset target low = $00
$1904: LDA #$FA                 ; high byte = $FA (2.23) — was $DA in 2.20
$1906: STA $1002                ; $1000-$1002 now = $C3 $00 $FA = JP $FA00
$1909: LDA #$0A
$190B: STA $53
$190D: LDA #$BA
$190F: STA $51
$1911: LDA #$00
$1913: STA $52
$1915: STA $50
$1917: LDX #$06
$1919: JSR $115C                 ; copy 6 pages: $BA00-$BFFF -> $0A00-$0FFF
$191C: LDA #$80
$191E: JSR $BBEB                 ; second LOAD_CPM call (parameter A = $80)
$1921: LDA #$0A
$1923: STA $BC08                 ; sector parameter
$1926: LDA #$97
$1928: STA $53
$192A: LDA #$0A
$192C: STA $51
$192E: LDX #$06
$1930: JSR $115C                 ; copy 6 pages: $0A00-$0FFF -> $9700-$9CFF
$1933: LDA #$80
$1935: STA $53
$1937: LDA #$A3
$1939: STA $51
$193B: LDX #$17
$193D: JSR $115C                 ; copy 23 pages: $A300-$B9FF -> $8000-$96FF
                                ; ← THE CCP+BDOS RELOCATION
$1940: LDY #$06
$1942: LDA $116C,Y                ; from a small constants table
$1945: STA $FFF9,Y                ; patch Apple monitor reset vectors
$1948: DEY
$1949: BNE $1942
$194B: JMP $03D2                 ; jump to warm-boot routine — switch happens

The page copies are the most important moves. The third copy ($1933-$193D) is the CCP+BDOS relocation — it moves the staged image from $A300-$B9FF to $8000-$96FF, where the BDOS final position $9C06 lands inside.

The second JSR $BBEB at $191E is harder to interpret. It’s a second call to LOAD_CPM-equivalent, with A = $80 as parameter. We’ve established that 2.20 also makes two LOAD_CPM calls (at $1608 and $17D0), so the dual-load mechanism is shared between versions. What the second call reads — sector range, destination — isn’t fully traced yet.

The very last 6502 instruction before the switch is JMP $03D2. That target is in the warm-boot routine the loader installed at Apple $03C0+. From prior work, the warm-boot routine roughly does: enable LC RAM, touch a $Cn00 address (self-modified), disable LC RAM, then JSR $0E36. The JSR $0E36 is what enters the disk-callback / CPU-switch territory at Apple $0E36 (= Z-80 $1E36 after bit-12 XOR).

The embedded Z-80 inside the 6502 loader

A finding I hadn’t documented before: the 6502 loader at Apple $1000-$1BFF contains substantial Z-80 code in the range $143A-$1547 (about 270 bytes). This isn’t 6502 code that happens to disassemble as Z-80 — it’s deliberately Z-80-encoded, with calls into BIOS routine addresses ($FECA, $FEC6, $FEC3, $AD25) and BIOS state references ($FED1, $FED2, $FED3, $FED6, $FED7, $FED9, $FEDA, $FEDB, $FEDC, $FEDD, $FE03, $FE07, $FE0E).

Sample bytes at $143A:

af 32 dd fe 3e 02 21 da fe 77 23 77 23 77 18 48
61 2e 00 22 da fe 79 fe 02 20 0f 2e 08 3a d6 fe
67 22 dd fe 2a d1 fe 22 df fe 21 dd fe 7e b7 28
21 35 3a d6 fe 23 be 20 19 3a d1 fe 2a df fe bd
20 10 3a d2 fe bc 20 0a 24 22 df fe af 32 dc fe
18 06 21 01 00 22 dc fe 3a d2 fe 5f b7 1f 21 4a
ad 85 6f 4e 21 d8 fe 7e 36 01 b7 28 1b 2a d6 fe
7d bc 20 0d 2a e0 f3 3a d1 fe bd 20 04 79 bc 28
42 3a d9 fe b7 c4 25 ad 3a d6 fe 32 d7 fe 47 e6
01 3c 32 e4 f3 78 e6 0e 87 87 87 2f c6 61 32 e6
f3 3a d1 fe fe 23 38 0b 6f 3a e3 fe fe 8b 20 04
7d d6 23 6f 61 22 e0 f3 3a dc fe b7 c4 2c ad af
32 d9 fe 7b 21 00 f8 1f cb 1d ed 5b e1 fe 01 80
00 3a da fe b7 20 05 3c 32 d9 fe eb ed b0 3a db
fe 1f 3e 00 30 03 cd 25 ad c3 ca fe af 32 d9 fe
3e 02 21 3e 01 32 eb f3 21 00 08 22 e8 f3 21 03
0e cd c3 fe 3a ea f3 b7 c8 d1 fe 10 20 db c3 c6
fe

Decoded as Z-80, it’s a structured routine that:

  • Stores constants into $FEDD, $FEDA-$FEDC state slots (af 32 dd fe / 3e 02 / 21 da fe / 77 23 77 23 77).
  • Tests state at $FEDA and dispatches.
  • Reads slot-info table at $F3E0 — the same area the cold-boot generator’s slot scan uses.
  • Calls into BIOS routines at $AD25, $AD2C, $FECA, $FEC6, $FEC3.
  • Has a LDIR (block move) at $1515-$1516 — the Z-80 instruction that copies BC bytes from (HL) to (DE). This is the exact primitive a runtime code generator would use.
  • References absolute addresses $F800 (21 00 f8) and $0E03 (21 03 0e) as setup parameters.

So the loader carries a substantial chunk of Z-80 code. Two questions: (a) where does this code execute, and (b) how does it get there.

Where the embedded Z-80 code runs

Static analysis bounds it. The Z-80 calls reference 2.23 BIOS addresses ($FECA, $FEC6, etc.), so this code runs after the BIOS is in place — meaning after the SoftCard CPU switch. The code references Z-80-mapped low-memory addresses too ($AD25, $AD2C are in the BDOS area at Z-80 $AD00-ish).

The execution-address question doesn’t resolve from static disassembly alone. Three possibilities:

(a) The fragment gets copied to a known Z-80 address by the boot finalization. Candidates: Apple $0A00-$0BFF (Z-80 disk-callback area), or the BIOS trap-marker pages. The boot-finalization page copies don’t visibly target a 270-byte range, but a smaller subset could be moved by code I haven’t fully traced.

(b) The fragment is meant to execute in place at Apple $143A (Z-80 $043A after bit-12 XOR). The Z-80 maps Apple $1xxx to Z-80 $0xxx, so Apple $143A becomes Z-80 $043A. This is in TPA territory, which CP/M user programs use — not a typical BIOS spot, but possible if the cold-boot path JPs to it.

(c) The fragment is a template that the cold-boot generator copies into the trap-marker pages. The presence of LDIR is suggestive, but LDIR could also live inside the fragment to copy something else, rather than being the means of installing this fragment.

I lean toward (a) — the fragment gets installed somewhere by a copy mechanism that hasn’t yet been fully traced, and runs from there. The LDIR inside the fragment is then itself a code-generation primitive that would populate the runtime regions of BIOS at boot time.

This connects to the trk2:physA finding — disk sectors past LOAD_CPM’s main read also contain Z-80 code with BIOS calls. The picture forming is: the runtime BIOS extends beyond what 6502-LOAD_CPM stages, and the additional bytes come from multiple sources: the embedded Z-80 fragment in the loader, sectors past the main LOAD_CPM range, possibly more.

How the actual CPU switch happens

This is the most-open piece. What I can say:

  • The 6502 loader contains zero writes to $C0Bx — the conventional SoftCard CPU-control port. So the switch isn’t triggered by a port write.
  • The warm-boot routine at Apple $03C0 does some LC RAM bank-switching, then JSR $0E36. Apple $0E36 is in the BIOS first 1 KB area (in the staged callbacks/BIOS region). From the 6502’s perspective, those bytes are Z-80 instruction encodings — not valid 6502 code.
  • One hypothesis: the SoftCard hardware monitors the address bus and triggers the switch when the 6502 attempts to fetch instructions from a specific address range (possibly $0E00-$0FFF, the disk-callback / BIOS area). The JSR $0E36 would then be the trigger by virtue of being a fetch into that range.
  • Another hypothesis: the LC RAM bank-switch sequence in the warm-boot routine has a side-effect that the SoftCard monitors. The SoftCard does have a relationship with LC RAM (since it serves the Z-80’s high-memory addresses from LC RAM), so a specific bank-switch pattern might be the trigger.

Without dynamic execution I can’t disambiguate. The structural understanding works without a definitive answer to “what specific instruction triggers the flip.” The answer is somewhere in the warm-boot routine at $03C0 or in the side-effects of the LC RAM bank-switch.

What this article doesn’t claim

  • Doesn’t claim to know exactly where the embedded Z-80 fragment at $143A-$1547 ends up running. Static analysis bounds it; dynamic execution would settle it.
  • Doesn’t claim to know the SoftCard CPU-switch trigger. The candidates are listed above; none is verified.
  • Doesn’t claim that the Z-80 fragment is the only source of runtime BIOS bytes. The disk sectors past LOAD_CPM are another source; there may be more.

What it does establish: the boot-to-Z-80 handoff isn’t a single step. It’s a sequence of page copies, a second LOAD_CPM call, an embedded Z-80 fragment, and a final jump to a warm-boot routine that triggers the switch. The 6502 leaves Z-80-readable state in many places, and the Z-80 picks up that state when it starts running.

A larger pattern

CP/M 2.23 on the SoftCard is, structurally, two operating-system layers stacked: a 6502-side layer that handles boot, disk I/O service, and inter-CPU sync; and a Z-80-side layer (CCP + BDOS + BIOS) that’s the user-facing CP/M proper. The two layers communicate through shared Apple memory and through SoftCard hardware that mediates which CPU is active.

The handoff this article describes is the moment the system transitions from “6502 driving” to “Z-80 driving.” After that moment, control oscillates rapidly: any time CP/M needs disk, the Z-80 calls a callback that signals the 6502, the SoftCard switches CPUs, the 6502 services the request, then the SoftCard switches back. The handoff in this article is the first such switch — the one that goes from 6502 to Z-80 for the first time.

The architecture has aged surprisingly well. It’s the same pattern modern systems use for hypervisor calls and userspace/kernel transitions: shared memory plus a controlled trigger that flips execution context. Microsoft and Bill Stewart designed the SoftCard in 1979-1980 and shipped CP/M 2.20 in 1980 — when the IBM PC didn’t exist yet. Three years before “DOS” was a household word, you could buy a card that ran CP/M software on your Apple ][. The handoff mechanism described here is what made that possible.

Open frontiers at the time of writing:

QuestionStatus
Where the embedded Z-80 fragment at $143A is installedNot yet pinned down
The SoftCard CPU-switch trigger (specific instruction or memory access)Open — candidates listed
Initial population of $FA00-$FAB7Connects to fragment-installation question
Sector range of the second JSR $BBEB at $191ENot yet pinned down
The exact content of the warm-boot routine at Apple $03C0Not extracted yet — would need a dump of the staged install fragments

The handoff is mostly mapped. The remaining unknowns are concrete and tractable — they need either deeper static work (extracting the warm-boot routine from the loader’s install loops) or dynamic execution (running the system in a Z-80 emulator and observing).

Part 5 traces the Z-80 side: where CP/M is actually located in memory after the handoff, and the discovery that the BIOS on disk is missing about half its runtime bytes.

Deep dives

Footnotes

  1. All $19xx addresses in this article are wrong by $0800 — they’re actually $11xx. The disassembler runs I’d done used --base 0x1000 against a loader file that starts at Apple $0800, so labels came out 2 KB high. Boot-finalization is really at Apple $1100-$114B; the $1933 / $193D page-copy is at $1133 / $113D; the second JSR $BBEB at “$191E” is at $111E. The bytes themselves and the absolute targets in the disassembly (e.g., JMP $03D2 at the end) are correct. Detail in the CPU-switch-trigger devlog.