Part 5 of this series. Part 1 argued why this investigation needed a running emulator instead of more disassembly; Part 2 built the two-CPU machine over one address space; Part 3 wired in the disk, the Monitor ROM, and the keyboard; Part 4 modeled the Videx Videoterm card until its window-ownership rules became visible. Here the finished emulator runs the whole system: the run that confirmed a wrong theory, and the run that proved it wrong.
The finished emulator is a complete Apple ][ with the Z-80 SoftCard, the Videx Videoterm, and a Disk II controller, faithful enough to boot an unmodified CP/M disk to its A> prompt. Run 2.20 with the Videoterm installed and it should hang, the way real hardware does. The first time I ran it, it booted clean instead. Below: that wrong result, the bus rule the emulator was missing, and the second run that exposed it. Then an earlier build that confirmed a theory I now know is wrong, and the boundary of this work, because there is later SoftCard CP/M that was never in scope.
The overview series tells the story without the byte-level detail, across four parts: The Card That Wouldn’t Boot CP/M, The Answer That Agreed With Itself, Two Right Parts, One Dead Machine, and Two Tools and a Whole Distribution. This part has the bytes underneath all four.
The emulator booted 2.20 when it should have hung
I built the emulator to reproduce a failure. CP/M 2.20 with a Videoterm present hangs before it ever prints a banner. That failure is real. It was reported on the A2FPGA re-creation of the Videoterm, and I have a physical PAL Videoterm board on the bench that hangs the same way on 2.20. So when I pointed the finished emulator at 2.20 with the Videoterm installed and it booted clean to the banner and ran DIR to completion, the emulator was wrong somewhere. It was treating 2.20 more gently than the real hardware does.
Real hardware hangs, so the failure is in the software, and the only thing left to be wrong was the emulator. The part it got wrong was ownership of the shared $C800-$CFFF expansion-ROM window. Part 4 is the long version. The short version: this 2 KB window is the one region every slot card competes for, and only one card owns it at a time. My first model let whoever claimed it last keep it forever. The real bus does not work that way. An access to a different slot’s $Cnxx page knocks the current owner loose, and the next card has to re-claim before anything in that window can be trusted.
The SoftCard’s CPU switch is a write to $C700, slot 7’s I/O page. From the Z-80 side that same address is $E700 through the I/O window the SoftCard maps. So switching processors is itself an access to another slot’s page, and on a faithful bus that access releases whoever owns $C800-$CFFF. The emulator was missing that rule, and without it 2.20 booted.
With the rule modeled, the two versions diverge
Once the v2 emulator had the other-slot release rule and logged every claim and release against $C800-$CFFF, 2.20 and 2.23 diverged, the way the field reports said they should.
The CPU-handoff loop that drives the switching is the installed copy at Apple $03C0 in 2.23:
$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 (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
$03CF: 20 58 FF JSR $FF58 ; operand at $03D0/$03D1 is PATCHED
; by the Z-80 to the service routine
$03D2: 8D 81 C0 STA $C081
$03D5: 78 SEI
$03D6: 20 4A FF JSR $FF4A ; save A,X,Y,P to $45-$48
$03D9: 4C C0 03 JMP $03C0
The STA $C700 at $03C6 is the flip. The operand at $03D0/$03D1 is the cooperative-CPU handshake: the Z-80 writes a 6502 service-routine address there before touching the switch, the 6502 wakes up, runs that routine via the patched JSR, leaves results in the Monitor’s $45-$48 save slots, and switches back. That part works in both versions. The difference that kills 2.20 is which side of the $C700 flip the window gets claimed on.
2.20 drives the Videoterm through Apple’s older Pascal 1.0 fixed firmware entries. Those entries live at fixed addresses inside the $C800-$CFFF window: $C800 for INIT, $C84D for READ/CONIN, $C9AA for WRITE/CONOUT. And 2.20’s Z-80-side setup does the documented ownership dance before each console call, at Z-80 $DCEA/$DCEE which is Apple $FCEA, in this shape: LD A,($EFFF) to deselect every expansion ROM (Apple $CFFF), then LD A,(HL) with HL=$E300 to select the Videoterm (Apple $C300), then back to the dispatcher that patches the operand and flips the bus. So the claim happens on the Z-80 side, then the flip happens, and the flip releases the claim it just made. The 6502 wakes up and calls JSR $C800 into a window nobody owns.
2.23 detects Pascal 1.1 cards (the 11-byte check that reads $Cn0B for $01 and classifies the card as device 6) and routes console I/O through a 6502-side island that does the same dance but after the flip:
$0E30: ... $CFFF ; deselect all expansion ROMs (6502 side, post-flip)
$0E33: ... $C330 ; claim the Videoterm slot 3 window
... ; then the $Cn0D-$Cn10 vector dispatch into firmware
Same card, same slot, same window, same firmware. 2.23 sets up ownership a moment after the switch, so the claim is still live when the firmware fetch happens.
The numbers the emulator produced with the arbitration rule on, floating reads returning $FF, faults logged rather than fatal:
- 2.20 + Videx: 38,069,715 window faults, 0 characters reaching the Videoterm screen. Every VRAM write is discarded as unowned. The first faults are 6502 fetches at $C800, $C803, and onward, the INIT entry being executed from a window nobody owns. The ownership log shows the kill sequence repeating forever:
claim z80 $C300 (PC=$DD03)thenrelease z80 $C700 (PC=$DB41). - 2.23 + Videx: 0 faults, full banner, interactive
DIR, with a cleanrelease $CFFF (PC=$0E30)/claim $C330 (PC=$0E33)pair on every console call. - 2.20 alone, no Videx: 0 faults once I added the 40-column Monitor stubs (HOME/VTAB/CLREOP family) so the 40-column console path had something to call.
Those three runs are the field matrix from the 2026 bug reports, reproduced cell for cell: 2.20 plus Videx dead, 2.20 alone fine, 2.23 plus Videx fine. The matrix is now a regression test in the repo.
The wrong answer, and why an earlier run confirmed it
The overview series covered this without the byte-level detail, in The Answer That Agreed With Itself. Here are the bytes. The discredited model claimed a Z-80 story: 2.20 classifies the Videx as device 4, dispatches to a handler at $DFBE, lands in $E5 filler bytes, executes a PUSH HL into a wrapping stack, and dies. An earlier build of this same emulator produced a byte-for-byte confirmation of that chain.
It confirmed the chain because it was running on the wrong memory map. The investigation’s Z-80-to-Apple address model used a bit-12 XOR above $2000, and the SoftCard does not translate that way. The real translation is the four contiguous windows that Part 2 lays out.
Under the XOR map, the Z-80 executed through a scrambled view of memory. The filler it appeared to land in, the $DFBE dispatch target, and the wrapping stack were all artifacts of reading the wrong bytes at the wrong addresses. Rebuild the map correctly and none of those addresses hold what the old reading said. $FDB0, which the old reading called a one-byte RET stub, is delegation code at the true address: LD A,E / LD ($F047),A / JP $FB45. The device-4 path never executes Z-80-side handler code at the locations the old story named.
So the emulator that once confirmed the device-4 stack-wrap theory was correct in the way a calculator is correct on wrong inputs. The fix was the address translation that Part 2 builds. With that in place the same instrumentation produced the $C800 ownership result above. One detail of the old answer held up after the correction: a processor fetching where there is no real code. That processor is the 6502, not the Z-80; the place is an unowned expansion window; and the bytes are floating-bus reads, not $E5 filler.
What is demonstrated, and what is still open
Two pieces of this are settled and two are not.
The 38-million-fault differential is demonstrated for the A2FPGA’s arbitration rule, on real disk bytes, against the real Videoterm ROM 2.4 executing in the model. The protocol violation underneath it is documented: the Videoterm manual mandates the $CFFF-deselect plus $C300-select dance, plus screen-hole setup, immediately before every entry into $C800 space, and 2.20 cannot satisfy that behind a SoftCard while 2.23 satisfies it by construction. Both hold regardless of the exact electrical trigger.
The exact electrical trigger on the physical card is open. The A2FPGA releases the window on any other-slot access, and its own source comments call that an FPGA-bus-mux-specific addition, noting the physical card’s PAL16L8 releases on $CFFF only. The manual schematic’s activation corner does not have the inputs to decode other slots’ pages. Under a $CFFF-only release rule, the emulator boots 2.20 fine. Yet my real PAL Videoterm board does hang on 2.20, and the original report confirmed the hang on real hardware with firmware 2.4, the same ROM the emulator runs, which rules out a ROM revision. So the symptom is confirmed on real hardware and the precise kill step on the physical card is not pinned. The live candidates are board-revision variance (early 4013-based boards versus later PAL16L8 boards) and SoftCard bus-phase timing hiding the Z-80-side claim from a clock-qualified release. Settling it needs a scope on the bench rather than another emulator run.
The scope of all of this is the original SoftCard with CP/M 2.20 and 2.23. The later SoftCard CP/M releases, 2.25, 2.26, and 2.28B, shipped for SoftCard II and Premium SoftCard hardware that has on-board ROM and RAM. Those are architecturally different cards. The no-on-board-ROM reasoning that this entire investigation rests on does not apply to them, and they were never in the emulator’s scope. Covering them would mean growing the model to a different card, and I have not done that.
You can run any cell of the matrix yourself. The emulator is the softcard_emu package in the Orchard repository:
git clone https://github.com/BrentRector/apple-ii-orchard
cd orchard
python -m softcard_emu CPMV220-Disk1.po --videx --keys "DIR\r" # 38M faults, dead
python -m softcard_emu CPMV233.DSK --videx --keys "DIR\r" # clean DIR
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.