The Videx Videoterm card lives almost entirely in a 2 KB block of memory that every expansion slot has to share, and only one card at a time may drive it. Whether the card works comes down to whether it owns that block at the instant the CPU fetches an instruction from it. In the three parts before this one I built an emulator that runs unmodified CP/M disk bytes: one 64 KB array with two CPUs taking turns, a disk primitive and a handful of monitor entries serviced at their documented contracts. That emulator boots CP/M. It does not yet show why CP/M 2.20 boots with a Videoterm installed and 2.23 hangs. For that I need the Videoterm modeled precisely enough that its ownership of the shared block is something I can watch, count, and fault on. The 6502 fetches from an unowned window are what hang 2.20, and tracing them is Part 5.
A short orientation for anyone who never opened an Apple ][. The machine has eight expansion slots. Each slot gets a private 256-byte page of ROM address space at $Cn00, where n is the slot number: slot 3 is $C300-$C3FF, slot 7 is $C700-$C7FF. That little page is too small for real firmware, so the designers added a shared 2 KB window at $C800-$CFFF that every slot can use, with the constraint that only one card at a time may drive it. Whoever owns it answers reads there; everyone else stays quiet. The Videoterm’s firmware is far bigger than 256 bytes, so the card lives almost entirely in that shared window. Whether the card works comes down to whether it owns the window at the instant the CPU fetches an instruction from it.
The Videoterm ownership rule, as the firmware and the A2FPGA re-creation implement it:
- A read from the card’s own slot page (
$C300-$C3FF) claims the window. - A read of
$CFFFreleases it (the deselect strobe). - On the A2FPGA, an access to a different slot’s
$Cn00page also releases it. - A system reset releases it.
That third rule is the one that matters for the SoftCard, and the one I have to be careful about: it is demonstrated for the A2FPGA and not pinned on the physical card. More on that at the end.
Running the real firmware, not a description of it
One early decision paid for itself: the model executes the genuine 1 KB Videoterm firmware ROM (revision 2.4), the same image the A2FPGA loads. I did not write a Python function that imitates the card’s INIT routine. The 6502 fetches and runs the actual $C800 INIT path, the actual $C84D read entry, the actual $C9AA write entry. Those three fixed addresses are the Pascal 1.0 firmware entry points (device code 4), and they sit inside the shared window, which is why ownership of that window is load-bearing for them. The 2.23 path instead enters through the slot-page vectors $Cn0D-$Cn10, the Pascal 1.1 convention (device code 6). Both conventions run real ROM bytes in the model, so any ownership behavior the firmware exhibits is the firmware’s, not my paraphrase of it.
The rendering side mirrors the A2FPGA work: the model carries the MC6845 CRTC register file and derives the screen from it. The 6845 is the chip that scans VRAM out to the display; registers R12 and R13 hold the start-of-display address, and a screen decoder reads the character cells out of CRTC state the way real hardware does. The Videx screen is never a side buffer I write into. It is always the consequence of VRAM contents plus CRTC registers. A Videoterm that doesn’t own its window produces a blank screen by the same mechanism real hardware would: the writes never land.
The paged VRAM behind a 512-byte window
The Videoterm has 2 KB of video RAM, but it exposes only 512 bytes of it at a time, through a window at $CC00-$CDFF. Which 512-byte page you see is selected by address bits A2-A3 of an access into the $C0B0-$C0BF control range (slot 3’s device-select strip). So a write to the 80-column screen is: select the page via $C0Bx, then write your character into $CC00+offset. The model implements the paging exactly that way: four banks of 512 bytes, page register driven by the $C0Bx access, the window at $CC00. The VRAM window is gated by the same ownership flag as the rest of $C800-$CFFF. If the card doesn’t own the window, a write to $CC00 goes nowhere. That gating is what makes the fault count meaningful: an unowned Videoterm silently eats every screen write, the failure mode I’m hunting.
The arbiter, and the fault log it feeds
The state machine at the center of this part is small: one boolean, c8_owned, and a switch (--flat-c800) that disables arbitration entirely so I can A/B test against an always-owned window. The transitions:
claim on read in $C300-$C3FF (this card's slot page)
release on read of $CFFF (deselect strobe)
release on access to another slot's $Cn00 page (A2FPGA rule)
release on reset
When the window is unowned, two things happen. A read in $C800-$CFFF returns the floating-bus value ($FF in the model, what an undriven bus tends to read as on this hardware), and a write into the window, including a VRAM write at $CC00, is discarded. Every one of those unowned accesses is logged with the responsible CPU and the program counter that did it. That log is what this whole part exists to produce. A working configuration produces zero faults; a broken one produces a flood, and the first few entries name the exact addresses where execution walked into a dead window.
One implementation wart shaped how the fault log gets populated. The 6502 core fetches opcodes straight out of the memory array, bypassing the per-access read hooks that data-side accesses go through. So for fetch-side ownership violations I can’t rely on the read hook. I mirror the firmware bytes into the fetch plane and lay a blanket of PC breakpoints across $C100-$CFFF; a fetch landing in that range while the window is unowned is caught by the breakpoint and logged the same way a data read would be. It’s roughly seventeen hundred breakpoint dictionary entries plus the per-access hooks, and the full boot still finishes in seconds, so the arrangement cost me tedium and almost no runtime.
Arming the bus flip without tripping the arbiter
The arbiter and the CPU switch from Part 2 intersect in a way that, handled wrong, manufactures a fake result. The SoftCard’s bus flip is itself an access to another slot’s page. The CPU-handoff loop hands the bus to the Z-80 with STA $C700 (slot 7), and the Z-80 hands it back with LD ($E700),A, which is Apple $C700 through the Z-80’s I/O window. Under the A2FPGA rule, $C700 is an other-slot access, so it releases the Videoterm claim.
But the boot also includes a slot scanner that probes every slot’s $Cn00 page looking for cards. Those probe reads are detection, not the SoftCard’s bus switch, and they must not be treated as bus flips, or the emulator never boots at all. So the flip is armed only when it originates from the CPU-handoff loop, with the program counter below $0400. The scanner’s $Cn00 touches drive the ownership arbiter (a $C700 probe does release the window, correctly) but they do not flip CPU ownership. Two different behaviors keyed off the same address range, told apart by which code is making the access. Conflating them would have either broken boot or hidden the interaction I needed to see.
What this part can already show, and what it deliberately leaves loaded
With arbitration on and faults logged, the model distinguishes the configurations before I narrate the mechanism at all. The 2.23 path, traced live, does its ownership work on the 6502 side, after the bus flip: a $CFFF deselect at $0E30, a $C330 claim at $0E33, then the slot-page vector dispatch into the firmware. The claim is made after the switch, so it survives to the fetch. The 2.20 path does its ownership work on the Z-80 side, before the switch: LD A,($EFFF) to deselect, LD A,(HL) with HL=$E300 to select the Videoterm, then it flips the bus via $E700, and that flip is the $C700-class access that tears down the claim it just made. The 6502 then enters the fixed firmware entries $C800/$C84D/$C9AA with the window unowned.
Part 5 picks up there. The arbiter is built, the fault log is wired, the real firmware is running, the VRAM is gated by ownership. I leave the kill mechanism loaded and unfired here on purpose. The overview series stated the conclusion in Two Right Parts, One Dead Machine without the byte-level detail; Part 5 spends it byte by byte, including the stretch where the first version of this instrument reported something false and I believed it for a while.
One thing to carry forward, because it constrains how strongly Part 5 gets to claim anything. The other-slot release rule is what the A2FPGA implements and what I modeled, and under it the difference between the two CP/M versions is stark. The physical Videoterm’s release trigger is a separate question. The A2FPGA’s own source comments call other-slot release an FPGA-bus-mux addition and say the physical card’s PAL releases on $CFFF only; the card’s schematic doesn’t even have the inputs to decode other slots’ pages. Yet a real Videoterm with firmware 2.4 does fail to come up under 2.20. So the symptom is confirmed on real hardware and the precise electrical trigger on the physical card is open. The instrument I built here is exact for the A2FPGA. Where I lean on it in Part 5, I’ll say so.

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