A SoftCard machine has one memory, two processors, and only one of those processors ever running. A SoftCard is a Z-80 on a card that plugs into an Apple ][; CP/M runs on the Z-80, and the Apple’s 6502 still handles the disk, the keyboard, and the screen. The emulator is built around those three facts and almost nothing else. No scheduler, no clock, no cycle interleaving. Just a 64KB array, a 6502 core, a Z-80 core, an address-translation function the Z-80 looks through, and a rule for when the turn changes. Get any of those three facts wrong and everything downstream is wrong too, in ways that still look like findings.
I got the address map wrong the first time. The overview series, in The Answer That Agreed With Itself, walks through a confident conclusion I reached by reasoning about Z-80 code through a scrambled view of memory; with the map wrong, the bad reasoning stayed internally consistent. The corrected map is below, because the next article hooks routines at specific addresses that only make sense once you can translate a Z-80 address to the Apple address it actually reaches.
One array, two lenses
The Apple’s RAM is a single 64KB bytearray. The 6502 core reads and writes it directly, the way the 6502 does on real hardware. The Z-80 core reads and writes the same array, but every Z-80 access passes through a translation function first, because the SoftCard does not give the Z-80 its own memory. The SoftCard is a Z-80 on a card with no RAM of its own worth speaking of and, importantly, no ROM (the original card has none, which matters constantly when you go looking for where Z-80 code came from). When the Z-80 reads address X, the card rewrites X into an Apple address and the read lands in the Apple’s RAM.
So both cores see the same bytes. The Z-80 reads what the 6502 wrote, and vice versa, through different windows onto the one array. There is no “copy the BIOS to the Z-80’s space” step and no “where did the second copy come from” mystery, because there is no second copy. The Z-80 is looking at the same page the 6502 staged, just at a different number. The earlier investigation invented both of those questions for itself, and neither one exists.
The four-window map
The real translation is four windows, four different offsets:
| Z-80 address | Apple address | Offset | What lives there |
|---|---|---|---|
$0000-$AFFF | $1000-$BFFF | +$1000 | the contiguous TPA (where CP/M programs run) |
$B000-$DFFF | $D000-$FFFF | +$2000 | the memory behind the I/O page (Apple ROM, or bankable RAM on the larger systems) |
$E000-$EFFF | $C000-$CFFF | -$2000 | the Apple I/O page and slot space |
$F000-$FFFF | $0000-$0FFF | -$F000 | zero page, the 6502 stack, the text screen, screen holes |
This is not a single XOR or a single add. It is four piecewise regions with three different deltas, and the boundaries are not powers of two. The map I shipped first modeled the upper half as a bit-12 flip, which agrees with the real map in the TPA but diverges above $2000 where the BIOS and the I/O page and the stack live; the byte-level account of that wrong answer is in the last article in this series.
A few concrete reads pin the map down. The slot-info table the boot code writes at Apple $03B8 is reached by the Z-80 as $F3B8 (the -$F000 window: $F3B8 minus $F000 is $03B8). When the Z-80 reads the Videx’s detected device code, it reads $F3BB, which is Apple $03BB, which holds $06. The 2.23 BIOS jump table sits at Apple $0A00, which the Z-80 calls as $FA00. The reset vector at Apple $1000 (the bottom of the TPA window, Z-80 $0000) holds C3 00 FA, a JP $FA00, and after the first cold boot the warm-boot path rewrites it to C3 03 FA, a JP $FA03, which lands three bytes into the table on the WBOOT entry instead of the cold BOOT entry. The “off by three” between cold and warm entry is just the gap between table slot 0 and table slot 1.
For 2.20 the BIOS lives in a different window. Its jump table is at Z-80 $DA00, the +$2000 window, so Apple $FA00, in the bankable RAM behind the I/O page. The 6502 loader writes it there directly. The post-boot dump at Apple $FA00 reads C3 A8 DE C3 CC DA C3 08 DB, a table of JP $DEA8 / JP $DACC / JP $DB08 and so on, all Z-80 addresses in the $DA00-$DF00 range. The “8KB shift” between the two CP/M versions that earlier docs puzzled over is not a relocation. It is the same BIOS structure mapped through two different SoftCard windows, $FA00 in one release and $DA00 in the other.
The address bases, for the record: BIOS jump table at $FA00 for 2.23 and $DA00 for 2.20. And the distribution is three releases, not two: the 2.23 44K layout, the 2.23 60K layout, and 2.20, all rebuilt in the Decompiler series, in Part 6.
Two cores that never overlap
Both CPU cores are instruction-complete and neither is cycle-accurate. They execute real opcodes against the array and produce correct results, they just do not track how many cycles each took. Nothing in this investigation needed cycle counts, and pretending to have them would have been a second place for me to be wrong.
The two cores are never concurrent, and that is what makes a non-cycle-accurate model legitimate here. On the real SoftCard the 6502 and the Z-80 are not two processors racing a shared bus with arbitration logic deciding who wins each cycle. They take strict turns. One runs, hands the bus over, stops, and the other runs. There is no window where both are mid-instruction. A piecewise address map and a turn-taking loop are enough because the hardware does not interleave, so there is no interleaving to get wrong.
The run loop executes the active core until it performs the access that means “your turn is over,” then makes the other core active and executes it until it does the same, forever. Each core resumes at the instruction after the one that triggered the switch, the way the real processor does when the bus comes back to it.
The switch is one memory access
The CPU switch is plain. It is a single ordinary memory access to a specific slot’s I/O page, with no interrupt or dedicated instruction involved. The installed CPU-handoff loop is the 6502’s whole job while the system runs: hand the machine to the Z-80, take it back when CP/M needs an Apple-side routine run, run that one routine, and hand it over again. Only one core runs at a time; the loop is just the 6502’s side of the trade. It looks like this:
$03C0: AD 83 C0 LDA $C083 ; bank soft-switch, read twice
$03C3: AD 83 C0 LDA $C083
$03C6: 8D 00 C7 STA $C700 ; the bus flip: access slot 7 I/O space
$03C9: AD 81 C0 LDA $C081
$03CC: 20 36 0E JSR $0E36 ; restore A,X,Y,P from $45-$48; RTS
$03CF: 20 58 FF JSR $FF58 ; operand $03D0/$03D1 patched by the Z-80
$03D2: 8D 81 C0 STA $C081
$03D5: 78 SEI
$03D6: 20 4A FF JSR $FF4A ; save A,X,Y,P into $45-$48
$03D9: 4C C0 03 JMP $03C0
The instruction that flips the bus is STA $C700 at $03C6. Touching slot 7’s I/O page is the switch. Nothing about the value stored matters; the access itself is the signal. The LDA $C083 pair before it and the LDA $C081 after it are bank soft switches that select what is visible behind $D000.
Going the other way, the Z-80 flips the bus back with LD ($E700),A. Z-80 $E700 is the -$2000 I/O window, so Apple $C700, the same slot 7 page. The switch is symmetric: each core ends its turn by touching $C700, seen as $C700 from the 6502 and as $E700 from the Z-80.
The slot number is not hardcoded in the shipped image. The switch instruction’s operand is patched at install time to the card’s slot (detail in Part 3), which on these disks resolves to slot 7 and $C700.
In the emulator the switch is a Python exception. The memory hooks watch for an access to the configured slot’s I/O page, and when they see one they raise a Yield. The run loop catches it, swaps which core is active, and resumes the other core past its triggering instruction. No scheduler, no locks, no shared-state coordination, because the hardware has none of those things to model. One detail matters here: the flip is armed only from inside the CPU-handoff loop (PC below $0400), because the stage-2 slot scanner probes every slot’s $Cn00 page looking for the card, and those probe accesses touch I/O pages too. If every $Cn00 access flipped the bus, the scanner’s own probing would throw the machine into the wrong core mid-scan. The real card only honors the flip from the configured slot, and the emulator matches that by gating on where the access comes from.
These switches are constant once the system is interactive: every console character, every keystroke, every disk sector crosses the bus this way, so a DIR listing alone runs through far more switches than the cold boot does. They all ride on the same mechanism. The 2.20 failure I was chasing shows the volume in the worst way: its dead loop spins through roughly 38 million window faults, which the last article in this series gets into. All of that traffic is one mechanism: a memory access to one address, modeled as an exception, swapping two cores that were never running at the same time anyway.
With the map and the switch in place, the emulator can take turns and reach the right bytes. It still cannot read a disk, run a monitor routine, or read the keyboard, because none of those live in the disk image. Those are contracts, and hooking them honestly is the next article.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.