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.

Active April 2026 – present Apple ][ + Microsoft Z-80 SoftCard
apple-ii6502z80cpmsoftcardreverse-engineeringretrocomputingoperating-systemstool
↗ Repository

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-$03FF after the stage-2 install. Centerpiece: the 24-byte warm-boot routine at $03C0 with the JSR $0E36 CPU-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-$E1CB static-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-$0BFF after PREP_HANDOFF). BDOS-style thunks plus the bridge to the inter-CPU sync polling loop. (2.20’s callbacks are embedded in CPM220_InstallFragments.asm at $034A-$037B — different layout.)

  • CPM223_SystemImage.asm, CPM220_SystemImage.asm — Z-80 source for CCP+BDOS at $8000-$96FF after 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:

  1. Boot stub — 256 bytes at trk0:phys0. The first sector the Disk II PROM reads.
  2. 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.
  3. 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 .DSK file just stores those bytes at the file offsets DOS-3.3 would use to satisfy phys → logical interleave.)
  4. Additional BIOS handler bytes — loaded by the second JSR $BBEB call 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).
  5. 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 .DSK and 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 bodyCPM223_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:

PathContents
docs/CPM*.asmDirectly compilable annotated source listings (11 files)
docs/CPM_DiskSectorMap.mdPer-physical-sector reference for CPMV233.DSK
docs/CPM_Filesystem.mdUser-file inventory on tracks 3-34
cpm-investigation/pack_dsk.pyDisk-pack tool (verified round-trip)
cpm-investigation/build_dsk.pyEnd-to-end build script (assemble + pack)
cpm-investigation/gen_*.pyPer-region source generators
nibbler/disasm.py, nibbler/z80.pyDisassemblers 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.

← All Projects