cpm-videx-series — Part 9
Every Difference: A Complete Inventory of CP/M 2.20 vs 2.23
The Videx fix is 21 bytes. The release that contains it changes 8 KB. This article catalogs every category of difference between Microsoft SoftCard CP/M 2.20 and 2.23 — boot stub, loader, BIOS, CCP+BDOS — and shows how the small fix sits inside a much larger version-bump-driven rewrite.
Part 9 of the cpm-videx series. The series opened with an 11-byte slot-scanner delta in Part 1. The eight articles since then traced the architectural substrate those 11 bytes plug into. This article inventories every category of difference between 2.20 and 2.23 — boot stub, loader, BIOS, CCP+BDOS — and shows where the Videx fix sits in the larger release.
The headline number
If you diff everything Microsoft shipped in CP/M 2.20 against everything they shipped in 2.23 — boot stub, loader, CCP, BDOS, BIOS — and count the bytes, you get something like 8 KB of change in a roughly 12 KB system. About two-thirds of the binary is different.
The Videx fix is 21 bytes of that. 11 in the 6502 loader’s slot scanner; about 10 in the Z-80 BIOS cold-boot generator’s dispatch branch. So roughly 0.25% of the byte-level delta between the two releases is the Videx fix proper. The other 99.75% is the version bump — Microsoft updating the underlying CP/M from Digital Research’s 2.0 to 2.2, plus a coordinated rewrite of the BIOS to a runtime-generated architecture, plus internal cleanup.
This article catalogs all of it.
What’s identical
The boot stub at Apple $0801-$083C is byte-for-byte identical across the two versions. That’s the 60-byte fragment the Disk II PROM loads from sector 0 of track 0 to bring up the loader. It’s the smallest possible bootstrap: enough to read 10 more sectors of track 0 in CP/M skew order, then JMP $1000. Microsoft didn’t change it because they didn’t need to — the next-stage loader at $1000 is what does the real work, and 2.20 vs 2.23 differences live there.
The 8-byte Pascal-signature table in the loader is also byte-for-byte identical:
F2 03 18 38 <- bytes expected at $Cn05
48 3C 38 18 <- bytes expected at $Cn07
It sits at $1176-$117D in 2.20 and $11BE-$11C5 in 2.23 (different positions because the surrounding code shifted), but the bytes themselves are unchanged. Microsoft didn’t add a new signature; they added new logic that consults one more byte ($Cn0B) when the existing Pascal-1.0 signature matches.
The Apple Pascal 1.1 firmware protocol bytes that the Videx exposes in its expansion ROM ($CB05=$38, $CB07=$18, $CB0B=$01, $CB0C=$82) are unchanged in the Videx — that hardware is what it is. The change is in the OS, reading the third of those bytes that 2.20 ignores.
The loader diff at byte level
Both loaders are 3072 bytes (12 sectors of track 0/1) at Apple $1000-$1BFF. Direct byte diff:
| Run | Apple range | Length | What it is |
|---|---|---|---|
| 1 | $1060-$10FF | 160 | Copyright string region + slot scanner with the 11-byte Pascal 1.1 detection branch. 2.20 has $FF filler where 2.23 has ' COPYRIGHT (C) 1982 MICROSOFT - CP ' followed by zeros. The slot-scanner delta is inside this range. |
| 2 | $1200-$1232 | 51 | Code change near loader entry — early disk-prep state machine. |
| 3 | $1234-$128D | 90 | Code change. |
| 4 | $128F | 11 | Small code change — corresponds to a structural shift in how the slot loop is set up. |
| 5 | $129B-$1340 | 166 | Substantial slot-handling code change. |
| 6 | $1342-$1365 | 36 | Code change. |
| 7 | $1367-$177E | 1048 | Massive change — most of the middle of the loader. Includes the rewritten LOAD_CPM setup (in 2.23, $1407: LDA #$1D for 29 sectors and $1416: JMP $BBE9; 2.20 has different code at $1407 and a different LOAD_CPM address — see the 2.20-also-has-second-load devlog for the corrected picture), the post-load page copies, the slot-info table assembly logic. |
| 8 | $1782-$17B2 | 49 | Code change. |
| 9 | $17B4-$17FF | 76 | Code change — install-fragment staging area. |
| 10-30 | various | 1-49 | Many smaller changes through $1800-$1BFF. |
| 31-40 | $1B4A-$1BEB | small runs | Constants/data tables at the end of the loader. |
Total: 40 differing runs, 2280 bytes (74% of the 3 KB loader).
The 11-byte Pascal 1.1 detection branch — the visible Videx fix on the 6502 side — is one byte-pattern (E0 04 D0 0A A0 0B B1 3C C9 01 D0 02 plus A2 06) inside a 160-byte run that also contains the 1982 copyright string and small offset-shifts in the slot-scanner outer loop. The Videx-specific bytes are 11 bytes out of 2280 differing bytes in the loader. Less than 0.5% of the loader’s diff is Videx-related.
The BIOS diff: structural
The BIOSes are sized differently. 2.20 BIOS is 2 KB at $DACC-$E2CB. 2.23 BIOS is ~1.35 KB at $FAB8-$FFFF. Both use a 256-byte page interleave with code pages alternating with filler/runtime-generator pages — but with different fill bytes:
| Aspect | 2.20 | 2.23 |
|---|---|---|
| BIOS base address | $DACC | $FAB8 |
| BIOS size | 2 KB (8 pages) | ~1.35 KB (5+ pages) |
| Code pages | 4 (pages 0, 2, 4, 6) | 3 (pages 0, 2, 4) |
| Filler/runtime-generator pages | 4 (pages 1, 3, 5, 7) | 2 full + 1 partial (pages 1, 3, 5) |
| Filler byte | $E5 (PUSH HL; CP/M filesystem deleted-file marker) | FF FF 00 00 / F7 F7 00 00 (RST $38/RST $30 traps) |
| Static device-handler page | Yes ($E0CC-$E1CB) | No — removed |
| BDOS final position | $CC06 | $9C06 (12 KB shift) |
| Cold-boot generator at | $DB6E | $FB3A |
| Pascal 1.1 (device-code-6) handler | None | CALL $FDB0 (currently a stub — driver-install path is elsewhere) |
The BIOS jump tables are the same 17-entry CP/M layout. Targets differ because the code pages live at different addresses. Two routines worth flagging:
- 2.20 SETSEC is at
$DD89(code page 2 — a static routine). 2.23 SETSEC is at$FBF4(in a runtime-generator page). 2.23 generates the SETSEC routine at boot rather than shipping it statically. - 2.20 SETDMA is at
$DD8E(also static). 2.23 SETDMA is at$FBF9(also runtime-generated). - HOME, SELDSK, SETTRK, READ, WRITE follow the same pattern: static in 2.20, runtime-generated in 2.23 — many of them in the NOP slide at the start of code page 4.
The biggest structural removal: 2.20’s page 6 ($E0CC-$E1CB) holds 256 bytes of static per-device handler routines — code with hardwired references to Pascal 1.0 entry points ($Cn05, $Cn07), the shared expansion-ROM window ($C800, $C9AA, $C84D), and slot-base arithmetic (A = E*16). 2.23 has no equivalent page. The static handlers are gone. Equivalent functionality in 2.23 lives in code that gets installed into the runtime-generator pages at boot — the BIOS factory pattern from Part 6.
That removal isn’t reductive cleanup. It’s an architectural change: 2.20’s BIOS dispatches into static code that knows about specific device protocols at compile time. 2.23’s BIOS dispatches into runtime-installed code parameterized by what the slot scanner detected. The static-to-generated flip is what makes the device-code-6 dispatch useful — the dispatch can route to a Pascal 1.1 handler that 2.20 simply doesn’t have static slots for.
The CCP+BDOS diff: a base-version bump
sysimg_220.bin and sysimg_223.bin are 5888 bytes each — CCP + BDOS + (optional) boot banner. They differ in 5807 of 5888 bytes (98.6%).
Even accounting for absolute-address relocation (BDOS at $CC06 vs $9C06), 98.6% is too high. The simplest explanation, evidenced by the boot banner string Softcard CP/M / 60K Ver. 2.23 / (c) 1980,1982 Microsoft at the end of sysimg_223.bin, is that Microsoft bumped the underlying Digital Research CP/M from 2.0 to 2.2. CP/M 2.2 (1982) was a substantial revision of CP/M 2.0 (1980); the BDOS internals were largely rewritten. Adopting the 2.2 base inevitably changes nearly every byte of the CCP+BDOS image.
Indirect evidence:
- Both
sysimgfiles have the same CCP error strings (READ ERROR,NO FILE,ALL (Y/N)?,NO SPACE,FILE EXISTS,BAD LOAD,Bdos Err On : $Bad Sector$Select$File R/O$) at similar offsets — confirming both are CCP+BDOS, not different things. sysimg_220.binhas no boot banner string (last 64 bytes are$E5filler).sysimg_223.binends with the 2.23 banner.- The “1982 Microsoft” date in the 2.23 banner aligns with Digital Research’s CP/M 2.2 release timing.
So the CCP+BDOS change is overwhelmingly Microsoft taking a fresh upstream, not Microsoft authoring new BDOS code. The Videx fix is not in the BDOS at all — it’s entirely in the loader (slot scanner) and the BIOS (cold-boot generator).
Categorizing the changes
Putting it all together:
| Category | Magnitude | Driver |
|---|---|---|
| Boot stub | 0 bytes | (unchanged) |
| Loader: copyright string addition | ~35 bytes | 1982 release branding |
| Loader: slot-scanner Pascal 1.1 branch | 11 bytes | Videx fix |
| Loader: rest of the diffs | ~2230 bytes | Internal rewrite tied to BIOS architecture change and CP/M base bump |
| BIOS: cold-boot generator device-6 branch | ~10 bytes | Videx fix |
| BIOS: removal of 2.20’s static handler page | -256 bytes | Architectural shift to runtime-generated handlers (refined: 2.20 already had 2 runtime slots in its dispatch table — 2.23 expanded it to all 4 slots; see the 2.20 dispatch table devlog) |
| BIOS: code-page rewrites | hundreds of bytes | New runtime-generator architecture |
BIOS: trap-marker fill change ($E5 → FF/F7/00) | hundreds of bytes | Safety upgrade |
| CCP+BDOS | 5807 bytes | Digital Research CP/M 2.0 → 2.2 base bump |
| Total estimated diff | ~8 KB | (mostly architectural and base-version, ~21 bytes Videx-specific) |
The Videx fix is ~21 bytes inside ~8 KB of change. The rest is the connective tissue.
Why Microsoft shipped it like this
Could Microsoft have shipped a “minimum diff” 2.21 release that only added the 11-byte slot-scanner branch and a corresponding Pascal-1.1 dispatch in 2.20’s static handler page? In principle, yes. They didn’t.
Why? Probably because the “minimum diff” version doesn’t actually exist in a coherent architecture. 2.20’s BIOS dispatches by static device-code → static handler. To support a new device class (Pascal 1.1), you’d need to either (a) add a fifth static handler in BIOS page 6 (within the 256-byte size constraint), or (b) augment the device-scan logic to special-case device code 6 with embedded handler code. Both paths increase 2.20’s BIOS size and complexity in unprincipled ways. Microsoft instead invested in a runtime-generator architecture that admits arbitrary new device classes by dispatching to code installed at boot rather than baked in.
That investment dominates the diff. The 11-byte slot-scanner branch wouldn’t be possible without it. The 11 bytes are the user-facing difference; the architectural rewrite is the engineering difference. They shipped together because they had to.
The CP/M 2.0 → 2.2 base bump was a separate, parallel change Microsoft was going to do anyway in 1982 (CP/M 2.2 had been out since 1981 and was the new standard). Pulling all three changes — slot-scanner fix, BIOS architecture rewrite, CP/M base bump — into one release was the practical decision. So a Videx user got more than a Videx fix; they got 2.2 features in the BDOS, a more extensible BIOS, the safety upgrade in the trap-marker pattern, and the actual Pascal 1.1 detection.
What the inventory leaves open
The inventory above is by category, not byte-for-byte. To make it byte-for-byte, the still-open mechanisms have to be resolved.
Update (2026-04-30): Part 10 closed the SoftCard CPU-switch trigger —
JSR $0E36in a 24-byte loop at Apple$03C0. The other items in the list below stayed open at the time of this article and are summarized in Part 10’s final status table.
- The SoftCard CPU-switch trigger. Some 6502 instruction or memory access flips the bus from 6502 to Z-80. The loader contains zero
$C0Bxwrites (the conventional SoftCard switch port). The trigger is somewhere in the warm-boot routine at Apple$03C0or in a side-effect of the LC RAM bank-switch sequence. Until it’s identified, the byte-level account of the boot path has a hole. - The initial population of
$FA00-$FAB7(cold-boot area below BIOS). The Z-80’s planted reset vector (JP $FA00) lands in this 184-byte region. Static disk content has trap markers there. Something writes real code before the Z-80 fetches its first instruction; what does is open. - Runtime population of trap-marker pages. The 2.23 BIOS has trap-marker pages with addresses that static code calls into. Strong candidate: the loader’s second
JSR $BBEBreads additional sectors past LOAD_CPM’s main 29. The destination of those reads, and the path by which those bytes reach the trap-marker pages, isn’t fully traced.
These are the connective tissue that complete the boot pipeline. The categorical inventory still works without them — we know what kinds of changes happened in each region, even if we don’t yet know every transit step.
A final practical note
Once the boot pipeline is fully traced, the natural deliverable is a disk-sector-by-disk-sector map: for every physical sector of the disk, what it contains, where it gets loaded in memory (Apple address before/after PREP_HANDOFF, Z-80 address), and which mechanism loads it. The map closes the loop on what the disk image actually is — beyond just “CP/M for the SoftCard,” down to “this sector becomes this byte at this Z-80 address in the running system.” That’s the eventual capstone for this work.
For now: the visible Videx fix is 21 bytes. The release that contains it is an 8 KB rewrite, two thirds new code, with the Videx fix as one small architectural payoff among several. The fix proper is small, targeted, and surgical. The substrate that makes it work is most of the project.
Deep dives
The byte-level diffs that fed this inventory:
- Two versions, two BIOS layouts: 2.20 vs 2.23 staging compared — first 2.20-vs-2.23 layout comparison once both staging dumps existed.
- Microsoft CP/M 2.20 vs 2.23: Digital Research’s CP/M base also changed (2.0 → 2.2) — the CCP+BDOS diff (5807/5888 bytes differ) that establishes the underlying CP/M version bump.
- Byte-by-byte loader diff: 2.20 vs 2.23 differ in 74% of bytes — the 40-run breakdown of the loader diff, with the 11-byte Videx fix located as one specific island.
- 2.20’s BIOS dispatch table has 6 entries vs 2.23’s 4 — refines the static-vs-runtime framing: 2.20 has 4 static + 2 runtime; 2.23 has 0 static + 4 runtime.