What does device code 4 mean? What does device code 6 mean?

5 min read
apple-ii6502z80cpmvidexsoftcardpascal-firmwarereverse-engineeringretrocomputingcpm-videx-series

Detail for Part 1 — Why Microsoft CP/M Didn’t Recognize an 80-Column Card.

The detection mechanism is settled (see previous entry). Both versions tag each slot 1-7 with a small-integer device code at apple2 $03B9-$03BF. 2.20 produces codes 1-5; 2.23 adds code 6 for Pascal-1.1-compliant cards. The interesting question is what those codes do — what behavior 2.23 enables for code 6 that 2.20 doesn’t enable for any code.

Three concrete observations from dumping the loader-installed bytes at $0300-$03FF (which the boot loader copies from $1300-$13FF in its memory image, then the slot scanner overwrites parts of):

Default device codes — 2.20 only

2.20 ships with a default device-code table baked into its loader image at $13B8-$13BF (which becomes $03B8-$03BF after install):

$03B8: 02 05 03 04 00 00 02 00
       ^   ^  ^  ^  ^  ^  ^  ^
   counter S1 S2 S3 S4 S5 S6 S7

Conventional Apple ][+ defaults: slot 1=printer (5), slot 2=serial card (3), slot 3=Pascal card (4), slot 6=disk controller (2). The slot scanner overrides defaults when it finds matching signatures; slots that fail to match keep their defaults. So a 2.20 system with no Videx in slot 3 would still get device code 4 there by default — which is fine, because there’s nothing in the slot to drive.

2.23 has all zeros at the same location. The defaults are gone. The table is built entirely by the scanner. That makes the scanner authoritative — if no card is detected in a slot, the slot is $00 (unused). Cleaner design.

Z-80 dispatch table at $0380-$0395

Both versions install a small table of 16-bit values at this address. In 2.23 the values are $FB14, $FB33, $FB33, $FCB5, $FCB5, $FE66, $FE66, $FE60, $FE60, $FE4C, $FE4C — eleven words, all pointing into the 2.23 BIOS range ($FAB8-$FFFF). 2.20 has a different set, all pointing into its own BIOS range ($DACC-$DEFF) with a few values in $F3xx (the Pascal firmware area). Five of the addresses appear as adjacent duplicates ($FB33, $FB33; $FCB5, $FCB5; $FE66, $FE66; $FE60, $FE60; $FE4C, $FE4C), suggesting the table is structured as (input_addr, output_addr) pairs where some device classes share a single routine for both directions.

This is the per-device-code I/O dispatch table — the thing that decides which BIOS routine handles characters going to or from a slot, indexed by the value the slot scanner stored at $03B9+$Cn. Reading the table’s structure clarifies how $06 differs from $04 in 2.23, and why 2.20 (lacking $06 entirely) routes through the wrong code.

Z-80 code at $0344-$0397 — 2.20 only

2.20’s loader image has Z-80 instructions at this address that 2.23 has zeroed out:

$0344: 3A BB F3       LD A,($F3BB)
$0347: FE 03          CP $03
$0349: C2 0C DB       JP NZ,$DB0C
$034C: 3A BE E0       LD A,($E0BE)
$034F: 1F             RRA
$0350: 9F             SBC A,A
$0351: C9             RET
... (similar pattern repeats with different constants)

$F3BB is the Z-80-side address of apple2 $03BB after the SoftCard’s address translation — that’s slot 3’s entry in the device table. $DB0C is in the 2.20 BIOS range, near the CONST/CONIN routines. $E0BE is in the TPA area, possibly Pascal firmware-related state.

So 2.20 has bespoke per-slot Z-80 fallback handlers stitched into low memory. It looks like 2.20’s device-code consumer is partly inlined this way, partly dispatched through the $0380 table. 2.23 has eliminated the inlined low-memory code entirely — presumably consolidating all dispatch through the table — which fits the 8 KB BIOS-base shift observed earlier (more BIOS code, less special-case clutter in low memory).

What’s still needed

Reading those tables and the BIOS routines they point at requires actual Z-80 disassembly, which the existing nibbler toolkit (6502-only) doesn’t provide. Options:

  • Add a Z-80 disassembler to nibbler (1-2 day project; the Z-80 ISA is well-documented but large)
  • Hand-decode the relevant routines (the routines are short — maybe 50-200 bytes each — so this is tractable for the half-dozen entries that matter)
  • Use an external tool (z80dasm, disz80, SkoolKit) on the extracted byte ranges

The hand-decode approach is fastest for the immediate question: pick the routine at $FE4C (2.23, the address shared by the last pair in the dispatch table — if my pairing is right, this is what device code 6 maps to) and read the Z-80 there. That should expose the actual Videx I/O sequence: 6845 CRTC programming at $C0B0/$C0B1, $CC00-$CDFF VRAM writes, $CFFF ROM-release. If those soft switches and addresses appear in $FE4C+, the case is closed.

Status: entry points to the consumer side identified. Three structural differences in low memory documented. Z-80 disassembly of the dispatch targets is the next concrete step. Also need to verify the dispatch-table indexing scheme — does it use device code as an index, or is there a more elaborate scan? (The 11-word, half-paired structure is suspicious of something like “skip slot 0, 5 pairs for codes 1-5, then add an extra entry for code 6” but I haven’t confirmed that.)