Projects /
Building Microsoft SoftCard CP/M from Source
Annotated 6502 and Z-80 assembly sources for Microsoft SoftCard CP/M 2.20 and 2.23, with a build tool that packs them into a byte-identical .DSK. The deliverable that fell out of the cpm-videx investigation — a reproducible disk image, region by region.
The cpm-videx investigation traced what’s in Microsoft SoftCard CP/M 2.20 and 2.23 from the boot sector to the A> prompt. This project is the deliverable that falls out of it: the disk images, reconstructed from annotated assembly source.
Each region of the disk has a corresponding .asm source file with full symbol annotations, all known Apple monitor / Disk II / BDOS entry points named, and per-instruction commentary on the parts that are non-obvious. A small Python tool packs the assembled binaries into a 35-track DOS-3.3-ordered .DSK image at the right physical sectors. Round-trip verified: the packer produces an output byte-identical to the original CPMV233.DSK.
What’s in the source set
Eleven .asm files, each ORG’d to its runtime address and directly compilable by a standard 6502 or Z-80 assembler. Each instruction is in the leading column with the address and raw bytes in a trailing comment for traceability:
LDA $C083 ; $03C0 AD 83 C0
LDA $C083 ; $03C3 AD 83 C0
STA $FFFF ; $03C6 8D FF FF
LDA $C081 ; $03C9 AD 81 C0
JSR $0E36 ; $03CC 20 36 0E ← CPU-switch trigger
The disassembler in Orchard’s nibbler toolkit emits this format directly via --format asm on both the 6502 and Z-80 disassembler subcommands. No post-processing step.
6502 side
CPM223_BootLoader.asm— boot stub at$0801(60 bytes, byte-identical to 2.20), stage-2 loader at$1000(1 KB), warm-boot routine source bytes at$13C0.CPM223_InstallFragments.asm— runtime view of$0200-$03FFafter the stage-2 install. Centerpiece: the 24-byte warm-boot routine at$03C0with theJSR $0E36CPU-switch trigger fully annotated.CPM223_RWTS.asm— disk-routine block at$0A00-$0C38: WRITE_SECTOR, READ_SECTOR, SEEK_TRACK, LOAD_CPM_LOOP. Standard Apple Disk II 6-and-2 GCR pattern.CPM220_BootLoader.asm,CPM220_InstallFragments.asm,CPM220_RWTS.asm— 2.20 equivalents. Boot stub byte-identical; rest differs in 74% of bytes (the version-bump-driven rewrite Microsoft did between 1980 and 1982).
Z-80 side
-
CPM223_BIOS.asm— the 1.35 KB BIOS at$FAB8-$FFFF, fully sectioned. Jump table, dispatch table, control-char dispatch, the cold-boot generator at$FB3A(with the device-code-6 Pascal 1.1 branch — the Z-80 side of the Videx fix), trap-marker pages with their runtime-population annotations, the device-scan loop at$FF0E. -
CPM220_BIOS.asm— the 2 KB BIOS at$DACC-$E2CB. Same structural sectioning. Includes the$E0CC-$E1CBstatic-handler page that has no analog in 2.23 — that page is what 2.23 traded for runtime-installed handlers. -
CPM223_DiskCallbacks.asm— Z-80 callbacks at$1A00-$1BFF(Apple$0A00-$0BFFafter PREP_HANDOFF). BDOS-style thunks plus the bridge to the inter-CPU sync polling loop. (2.20’s callbacks are embedded inCPM220_InstallFragments.asmat$034A-$037B— different layout.) -
CPM223_SystemImage.asm,CPM220_SystemImage.asm— Z-80 source for CCP+BDOS at$8000-$96FFafter relocation. Digital Research CP/M 2.2 (in 2.23) and CP/M 2.0 (in 2.20). Structurally annotated by section: CCP entry, built-in command table, BDOS body, and the boot banner at the top. Per-instruction annotation deferred to standard CP/M reference disassemblies; what’s here is the structural layout plus pointers to the entry points the BIOS plants ($9C06 / $CC06).
All files use full symbol tables. Apple monitor entries (COUT, IORTS, SAVE), soft switches (LC_RD_RAM, LC_WR_RAM, KBD, DISK_*), per-slot Disk II registers, BDOS routine addresses, BIOS state slots, TPA-area state — all named where they appear. The Videx 2.4 ROM disassembly contributed the Apple-side symbol set; everything else came from the cpm-videx investigation itself.
The build pipeline
.asm sources --[assemble]--> .bin chunks --[pack]--> .DSK image
(ca65 / z80asm) (pack_dsk.py)
Each .asm file declares a single .ORG (or two, in the integrated boot-loader file). A 6502 or Z-80 assembler (e.g., ca65 from cc65, or z80asm for the Z-80 sources) produces the assembled binary chunk. The packer then places each chunk at the correct physical sector of the .DSK per the disk sector map.
The current pack_dsk.py skips the assembly step and reads pre-extracted .bin files directly — enough to validate the chunk map by round-trip. A complete pipeline (assemble each .asm, then pack) is a one-evening extension when needed.
What the packer does
Walk through, region by region, of how CPMV233.DSK gets composed:
- Boot stub — 256 bytes at trk0:phys0. The first sector the Disk II PROM reads.
- RWTS routines + stage-2 loader — 11 sectors of track 0, distributed across physical sectors 1, 2, 3, 4, 5, 6, 8, A, C, E in the order the boot stub reads them. The boot stub uses physical sector numbers, not logical, indexed via a 16-byte skew table at
$082D. - CCP + BDOS + boot banner + Z-80 callbacks + BIOS first 1 KB — 29 sectors read by LOAD_CPM. PHYSICAL sectors trk0:phys
$B-$F, trk1:phys$0-$F, trk2:phys$0-$7. (The CP/M skew that the loader uses is physical-sector-keyed; the DOS-3.3 logical layout in the.DSKfile just stores those bytes at the file offsets DOS-3.3 would use to satisfyphys → logicalinterleave.) - Additional BIOS handler bytes — loaded by the second
JSR $BBEBcall from the loader’s boot-finalization. Source sectors are at trk2:phys$8-$F; destination is the SoftCard’s high RAM (LC RAM at$FAB8+). The packer leaves the reference image’s bytes in those sectors (since the destination is in LC RAM, not main RAM, and the round-trip validates against the disk content directly). - Apple ProDOS / CP/M filesystem — tracks 3 onward. Mostly
CAT,STAT,DUMP,ASM, etc. — the user-visible CP/M utilities. The packer leaves these in place from the reference disk; they’re not part of the boot pipeline.
Round-trip result: 0 byte differences out of 143360.
Why this exists
The original cpm-videx investigation answered a specific question: why does Microsoft SoftCard CP/M 2.20 hang on a Videx Videoterm and 2.23 doesn’t? The answer turned out to be 21 bytes of code spread across two CPUs, sitting inside an 8 KB version-bump-driven rewrite that took CP/M from 2.0 to 2.2 and redesigned the BIOS into a runtime-generated architecture.
That answer is satisfying as far as it goes, but reverse-engineering produces a lot of artifacts along the way — annotated disassembly, sector maps, extraction scripts, devlogs documenting dead ends. Pulling those into a single reproducible build target makes the work useful: the disk image isn’t a sealed binary anymore; it’s source code that compiles into the disk image.
That matters because:
- Verification is mechanical. Anyone with the source can rebuild the
.DSKand check it byte-for-byte against the original. The build tool’s round-trip is the proof that the chunk map is correct. - Modification is local. Want to know what happens if you change the slot scanner’s signature table? Edit the
.asm, rebuild, run in an emulator. The Videx fix itself is a 21-byte patch sitting inside a known location; producing an alternate version of CP/M 2.20 with the Videx fix backported is now a small assembly diff plus a rebuild. - The architecture is documented in the form that matches it. Annotated assembly source is the natural representation of a 6502/Z-80 system. The cpm-videx article series narrates what’s there; this project is what it is, in the form a future reader can both read and rebuild.
What’s deferred and why
The packer reproduces CPMV233.DSK byte-for-byte from extracted binary chunks, so every byte on the disk is accounted for. What’s not yet at the same depth of annotation:
CCP + BDOS body — CPM223_SystemImage.asm and CPM220_SystemImage.asm cover the runtime layout of $8000-$96FF and identify the section boundaries (CCP entry, command table, BDOS body, banner) but do not annotate every instruction. CCP+BDOS is Digital Research’s code (CP/M 2.2 in 2.23, CP/M 2.0 in 2.20). Per-instruction breakdown matches public reference disassemblies of standard CP/M 2.x; reproducing that work in this project would be ~5,000 lines of restated reference material rather than novel investigation. The cpm-videx investigation isn’t about reverse-engineering Digital Research; it’s about what Microsoft built on top.
Filesystem content (tracks 3-34) — the user-visible programs that ship on the disk (MBASIC.COM, STAT.COM, DDT.COM, etc.) are documented as a directory listing in docs/CPM_Filesystem.md. The packer carries those bytes through unchanged from the reference image. They’re stock CP/M utilities that are not specific to the SoftCard; reverse-engineering each one is its own separate project.
Three runtime mechanisms that the cpm-videx investigation flagged as open (initial population of $FA00-$FAB7, runtime population of trap-marker pages, the 6502-side disk-service dispatch between yields) require dynamic execution to confirm — running the system in a Z-80 emulator and observing memory at specific points. Static analysis can identify the candidate paths and document the architectural skeleton (and Part 10 of the article series does both), but the precise byte-level mechanism for these three pieces is bounded by what static disassembly can know. The packer doesn’t depend on resolving them; the round-trip works regardless because the bytes-on-disk are what get packed, independent of what the bytes mean at runtime.
In short: every byte of CPMV233.DSK is sourced; the boot pipeline is fully annotated; CCP + BDOS are sectioned but defer instruction-level work to public CP/M references; the filesystem is documented as a directory listing; and three runtime mechanisms remain open because they need an emulator to settle, not more disassembly.
Repository
All the source listings, the compilable forms, the packer, the build script, the disk sector map, the filesystem directory listing, and the supporting extraction scripts live in the Orchard repository:
| Path | Contents |
|---|---|
docs/CPM*.asm | Directly compilable annotated source listings (11 files) |
docs/CPM_DiskSectorMap.md | Per-physical-sector reference for CPMV233.DSK |
docs/CPM_Filesystem.md | User-file inventory on tracks 3-34 |
cpm-investigation/pack_dsk.py | Disk-pack tool (verified round-trip) |
cpm-investigation/build_dsk.py | End-to-end build script (assemble + pack) |
cpm-investigation/gen_*.py | Per-region source generators |
nibbler/disasm.py, nibbler/z80.py | Disassemblers with --format asm for compilable output |
The cpm-videx article series at /projects/cpm-videx is the narrative half; this project is the artifact half. Together they’re as complete a picture of an early-1980s coprocessor operating system as static analysis allows.