Z-80 disassembler online; CONOUT loads HL with the Videx VRAM window
Detail for Part 3 — Apple Memory, Through Z-80 Eyes.
The 6502-side analysis is complete enough to write the Part 2 article. Continuing into the Z-80 side requires a Z-80 disassembler. nibbler is 6502-only, so wrote one.
First-pass coverage: all 256 unprefixed Z-80 opcodes, decoded into the standard mnemonic notation (LD, ADD, JP, CALL, IN, OUT, JR, etc.) with proper operand formatting (8-bit immediate, 16-bit immediate, signed displacement converted to absolute target address). The four prefix bytes (CB for bit ops, DD for IX, ED for extended, FD for IY) emit DB $xx raw-byte placeholders for now — covering the prefix tables is incremental work but the unprefixed core handles ~90% of typical BIOS code.
Wired into the nibbler CLI as a new z80disasm subcommand: python -m nibbler z80disasm <file> --base <addr> [--start <addr>] [--end <addr>].
Smoke-tested on the BIOS image extracted earlier (cpm-investigation/bios_223.bin, 2 KB starting at Z-80 $FAB8):
$FAB8: C3 D1 FE JP FED1 ; BOOT
$FABB: C3 B8 FA JP FAB8 ; WBOOT (jumps to BOOT)
$FABE: C3 10 FB JP FB10 ; CONST
$FAC1: C3 1A FB JP FB1A ; CONIN
$FAC4: C3 4D FB JP FB4D ; CONOUT
$FAC7: C3 70 FB JP FB70 ; LIST
... (15 entries total)
The jump table reads correctly. Then disassembled CONOUT at $FB4D:
$FB4D: 3D DEC A
$FB4E: 20 0B JR NZ,FB5B
$FB50: CD 83 FD CALL FD83
$FB53: 21 00 C8 LD HL,C800 ; <-- THE VIDEX VRAM WINDOW!
$FB56: CD 45 FB CALL FB45
$FB59: 18 0A JR FB65
$FB5B: FE 02 CP 02
$FB5D: 20 06 JR NZ,FB65
$FB5F: 21 D0 0D LD HL,0DD0
$FB62: CD B0 FD CALL FDB0
$FB65: 1D DEC E
$FB66: 20 D5 JR NZ,FB3D
$FB68: C9 RET
$C800-$CFFF is the Apple ][ expansion ROM shared window — a 2 KB region that any slot card with an expansion ROM gets paged into when its slot ROM at $Cn00-$CnFF is touched. The Videx Videoterm uses it for its 80-column VRAM (write a character byte to $CC00+offset and the character appears on screen). But it’s a shared window — Pascal-1.1 cards generally use it, ProDOS-era cards use it, lots of expansion cards use it. The Videx isn’t the only inhabitant.
So $FB4D-$FB68 is structured roughly: decrement A, branch on result (probably parsing a control character), fall through paths that load HL with either $C800 (the expansion-ROM shared window) or $0DD0 (probably some Apple text-page or per-device buffer address), then call into a write helper. The decrement loop on E at the end (DEC E / JR NZ,$FB3D) suggests this is a string-output loop where E is the count.
This is evidence that 2.23’s CONOUT targets the expansion-ROM area — meaning it knows how to drive cards that live there. To prove this is the Videx path specifically, I’d need to see preceding instructions that page in the SoftCard-detected slot (e.g., a LD A,(slot ROM) or analogous read of $Cn00 for the slot the scanner flagged with device code 6), or characteristic Videx hardware writes (the 6845 CRTC programming via $C0B0/$C0B1, the $CFFF ROM-release toggle). The LD HL,$C800 alone shows the BIOS is expansion-ROM-aware, not Videx-aware.
That’s still meaningful: it confirms 2.23 has a code path that goes through expansion-ROM cards. CP/M 2.20’s equivalent (which can’t be cleanly extracted at the moment for the same routine — see the BIOS-partial devlog) presumably lacks that path or routes through a different one.
The next question: what happens for the various device codes? The CALL FB45 callee that follows the LD HL,$C800 is where the expansion-ROM write actually happens — and tracing it should tell us whether it’s a Videx-specific protocol or a generic Pascal-1.1-firmware-call wrapper.
Status: Z-80 disassembler integrated into nibbler. BIOS jump table verified. CONOUT confirmed to target the expansion-ROM shared window at $C800, but Videx-specificity not yet proven from the disassembly alone. Next: trace CALL FB45 and CALL FD83 to see if they do Videx-pattern writes (CRTC programming, etc.) or generic Pascal-1.1 firmware calls.