Projects /

Microsoft SoftCard CP/M on a Videx

Reverse-engineering why Microsoft SoftCard CP/M 2.20 won't boot on a Videx Videoterm 80-column card and 2.23 does. 11 bytes of 6502 code that read the Pascal 1.1 signature byte the Videx ROM has exposed since 1980 — a 1982 fix that's Videx-shaped but not Videx-specific.

Active April 2026 – present Apple ][ + Microsoft Z-80 SoftCard + Videx Videoterm
apple-ii6502z80cpmvidexsoftcardpascal-firmwarereverse-engineeringoperating-systemsretrocomputingcpm-videx-series
↗ Repository

Microsoft’s Z-80 SoftCard was the peripheral that made CP/M software run on the Apple ][. The Videx Videoterm was the 80-column card that made the Apple ][ a serious business machine. They were contemporaries, sold to overlapping customers in the same era. They should work together. Microsoft SoftCard CP/M 2.20 doesn’t boot when a Videx is installed; 2.23 does. Why?

What was broken

When you boot Microsoft CP/M 2.20 on an Apple ][+ with a Z-80 SoftCard and a Videx Videoterm in slot 3, the load sequence hangs partway through. The 6502 boot loader gets started, reads sectors from the disk, hands off to the Z-80, and then nothing. No A> prompt. No Z-80 panic. Just stop.

The same hang happens on real Videx hardware and on the A2FPGA emulation of one — the emulation is faithful; the operating system is the broken side. Microsoft fixed it in CP/M 2.23 (1982). The fix has been there ever since, but the why of it has not been documented anywhere I could find.

What the research found

The change is 11 bytes of new 6502 code in the boot loader’s slot scanner. Functionally:

  • CP/M 2.20 looks for the Apple Pascal 1.0 firmware ID bytes ($Cn05=$38, $Cn07=$18). When it finds them, it routes the slot through the Pascal 1.0 device I/O path.
  • CP/M 2.23 looks for the Apple Pascal 1.1 firmware ID bytes ($Cn05=$38, $Cn07=$18, and $Cn0B=$01). When it finds those, it routes the slot through the Pascal 1.1 device I/O path.

The Pascal protocol is additive: a 1.1 card declares both the 1.0 ID bytes and the 1.1 ones. But Pascal 1.1 cards do not implement the Pascal 1.0 I/O entry points. They speak only the 1.1 calling convention. So when 2.20 sees a Videx, it sees a Pascal 1.0 card (because the Videx truthfully declares $38, $18) and tries to call a 1.0 entry point the Videx doesn’t expose. The streams get crossed. Fortunately, instead of total protonic reversal, the computer just hangs.

2.23 fixed it by reading the third ID byte and routing Pascal 1.1 cards through the 1.1 path that they actually implement. Not strictly Videx-specific — any Pascal 1.1 card benefits — but the Videx is the dominant 1.1 card on the Apple ][ in 1982, so it’s the visible beneficiary.

What’s settled and what’s open

Settled (and documented in the Orchard repo):

  • The 6502 boot stub at $0801-$083C (byte-identical between 2.20 and 2.23)
  • The CP/M sector skew used by the boot stub
  • The stage-2 language card switch and Apple monitor calls at $1000
  • The install loops that stage Z-80 code into Apple $0200-$03FF
  • The slot scanner including the 11-byte Pascal 1.1 detection branch
  • The Z-80 reset vector planting at Apple $1000-$1002 (JP $FA00 for 2.23, JP $DA00 for 2.20)
  • The Z-80 BIOS load addresses for both versions ($FAB8 vs $DACC)

Open (in priority order):

  • The actual SoftCard CPU-switch instruction. The 6502 loader contains no writes to $C0Bx — the switch happens elsewhere, likely in the disk-load callee at Apple $0E36 or in the Z-80 fragments that run after the switch.
  • Annotation of the disk I/O block at Apple $0A00-$0FFF (RWTS-style routines).
  • A Z-80 disassembler — needed to crack open the install fragments at $0200-$03FF (Z-80 $1200-$13FF) and the BIOS code itself.
  • The Z-80 BIOS for both versions — its CONOUT and BOOT routines are where the device-code consumer logic lives.
  • The complete diff: enumerate every difference between 2.20 and 2.23, not just the Videx-related ones. The 8 KB BIOS shift suggests substantial reorganization beyond a focused fix.

Articles

The investigation is being written up as a multi-part article series — each part covers a distinct phase of the work and is published as it’s drafted. The ultimate destination is a complete description and disassembly of CP/M 2.20 and 2.23 from the boot sector (6502) to the A> command prompt (Z-80), with both versions diffed throughout.

  • Part 1 — Why Microsoft CP/M Didn’t Recognize an 80-Column Card (published) — The starting question. Why does 2.20 hang on a Videx and 2.23 boot? Identifies the 11-byte detection delta in the 6502 slot scanner and the Pascal 1.0 vs 1.1 calling-convention mismatch.
  • Part 2 — From the Disk II ROM to the Z-80’s First Instruction (published) — Beginning-to-end narrative of the 6502 boot stage. From the Disk II PROM through the boot stub’s CP/M-skewed sector loads, the stage-2 language card switch, the slot scanner, the Z-80 reset vector planting, and the SoftCard handoff.
  • Part 3 — Apple Memory, Through Z-80 Eyes (published) — The SoftCard memory model (the address-line XOR), CP/M’s CCP/BDOS/BIOS layering, the 2.23 BIOS jump table, and the per-device dispatch table that the slot scanner ultimately feeds.
  • Part 4 — The Handoff: 6502 to Z-80 (published) — What the 6502 does in its final breath: the boot-finalization sequence, page copies that put CCP+BDOS in their final position, the embedded Z-80 install fragments inside the loader (270 bytes at $143A-$1547, 2.23-specific), and the still-open SoftCard CPU-switch trigger.
  • Part 5 — The BIOS That Half-Exists (published) — The Z-80 has woken up. About half the BIOS isn’t on the disk at all. The other half is runtime-generated by a BIOS factory — the code-generation pattern that explains why 2.20 and 2.23 differ so structurally.
  • Part 6 — The BIOS Factory (published) — The cold-boot generator at $FB3A (2.23) / $DB6E (2.20). 2.23 dispatches device codes 3, 4, and 6 (Pascal 1.1); 2.20 only handles 3 and 4. The structural reason an 11-byte slot-scanner fix isn’t enough on its own — and how the rest of the fix lives in the generator.
  • Part 7 — From the Reset Vector to the Device Scan (published) — The Z-80 boot path from JP $FA00 through cold-boot setup at $FB70, the BIOS factory generator at $FB3A, and into the device-scan dispatch. Honest accounting of which mechanisms are still open frontiers.
  • Part 8 — The Cooperative-CPU Round-Trip (published) — How the SoftCard’s two CPUs share a disk drive. The Z-80 sync polling loop at $1E39 (six instructions); the $E000/$E010 flag pair; the connection back to the original Videx hang via JSR $Cn07 on the 6502 side.
  • Part 9 — Every Difference: A Complete Inventory (published) — The 21-byte Videx fix inside ~8 KB of total change. Categorical inventory across boot stub, loader, BIOS, and CCP+BDOS, including the underlying CP/M 2.0 → 2.2 base bump that drives most of the diff.
  • Part 10 — The CPU Switch, and What’s Left (published) — The last open mechanism: the 6502’s perpetual 24-byte loop at Apple $03C0 and the JSR $0E36 instruction the SoftCard hardware uses as the CPU-switch trigger. Also the project’s status snapshot — what’s settled and what stays open.

Dev Logs

The investigation journal runs in parallel with the articles — one entry per topic / approach / area of investigation, including the dead ends. The cpm-videx dev logs document the path as it actually happened, mistakes and pivots included.

Reference

The Apple II Pascal 1.1 firmware protocol (Apple Technical Note Misc #8 — the only canonical document on this protocol) is captured here, with the source PDF preserved for offline reference.

Repository

All the disassembly artifacts, the side-by-side annotated diff, the extraction scripts, and the source disk images live in the Orchard repository under cpm-investigation/ and docs/CPM_Videx_Difference.md. The 6502 disassembler used is nibbler, also in the same repo.

The per-physical-sector reference map — for every sector of CPMV233.DSK, what it contains and how it reaches memory — is at docs/CPM_DiskSectorMap.md.

The annotated .asm source listings for both versions — the artifact form of this investigation — and the build tool that packs them back into a byte-identical .DSK are tracked under the companion project Building Microsoft SoftCard CP/M from Source.

Dev Logs

2.20 hang settled in the emulator: Case B (Z-80 stack overflow on $E5 spam) Apr 30, 2026 The cold-boot generator runs and dispatches CALL $FDB0 for the Videx Apr 30, 2026 The SoftCard CPU-switch trigger: JSR $0E36 Apr 29, 2026 2.20's BIOS architecture is fundamentally different from 2.23's Apr 29, 2026 End-to-end emulator-confirmed: 2.23 detects Videx as Pascal 1.1, 2.20 doesn't Apr 29, 2026 2.20 also makes two LOAD_CPM calls — not unique to 2.23 Apr 28, 2026 2.20's BIOS dispatch table has 6 entries vs 2.23's 4 Apr 28, 2026 Factual byte-level trace of the v2.20 hang Apr 28, 2026 The 6502 loader carries 270 bytes of embedded Z-80 code at $143A-$1547 Apr 28, 2026 Stage 1 emulator: 6502 boot finishes without ever writing $FA00 Apr 28, 2026 Stage 2 emulator: BIOS jump table is at Z-80 $1A00, not $FAB8 Apr 28, 2026 Byte-by-byte loader diff: 2.20 vs 2.23 differ in 74% of bytes Apr 28, 2026 Walking the 6502 RWTS the SoftCard borrowed from Apr 28, 2026 The inter-CPU sync polling loop at Z-80 $1E39 Apr 28, 2026 Microsoft CP/M 2.20 vs 2.23: Digital Research's CP/M base also changed (2.0 → 2.2) Apr 27, 2026 2.20 has inline init where 2.23 has runtime code generation Apr 27, 2026 BIOS jump tables read: cold-boot is BOOT (offset 0), not LIST Apr 27, 2026 The cold-boot generator: $FB3A in 2.23, $DB6E in 2.20 — and the device-code 6 branch only 2.23 has Apr 27, 2026 2.20 and 2.23 BIOSes have identical device-scan loops Apr 27, 2026 Real Z-80 code at trk2:physA — past where LOAD_CPM reads Apr 27, 2026 $FDB0 (the Pascal 1.1 callee) is a RET stub — driver init lives somewhere else Apr 27, 2026 Loader $191E: a second JSR $BBEB before the CPU switch Apr 27, 2026 2.20 ships static device handlers in BIOS; 2.23 generates them at runtime Apr 27, 2026 Two versions, two BIOS layouts: 2.20 vs 2.23 staging compared Apr 26, 2026 Some BIOS routines disassemble cleanly; others are dispatch tables in disguise Apr 26, 2026 Half the BIOS is zero on disk because it's generated at runtime Apr 26, 2026 The Z-80 callbacks read and write the BIOS second half Apr 26, 2026 Tracing CONOUT into the BIOS hits the extraction wall Apr 26, 2026 The 6502 plants the Z-80's reset vector Apr 26, 2026 The cold-boot generator hides at the LIST jump-table entry Apr 26, 2026 LOAD_CPM cracked: 29 sectors, $8000 staging, then Z-80 disk callbacks at $0A00 Apr 26, 2026 The runtime-code marker: FF FF 00 00 / F7 F7 00 00 Apr 26, 2026 $C800 isn't the Videx — it's the shared expansion-ROM window Apr 26, 2026 Z-80 disassembler online; CONOUT loads HL with the Videx VRAM window Apr 26, 2026 The BIOS that wouldn't byte-diff Apr 25, 2026 What does device code 4 mean? What does device code 6 mean? Apr 25, 2026 Three disks, two versions, one shared boot stub Apr 25, 2026 Eleven bytes that recognize a Videx Apr 25, 2026 Microsoft CP/M doesn't boot on a Videx — and never did Apr 24, 2026
← All Projects