2.20 and 2.23 BIOSes have identical device-scan loops

5 min read
z80cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

Detail for Part 6 — The BIOS Factory.

After correcting the BIOS jump-table reads, I have proper BOOT-vector targets: $DEA8 for 2.20, $FED1 for 2.23. Both land in runtime-generated regions, but real code resumes shortly after — at $DECC for 2.20 (after $E5 filler) and $FF0E for 2.23 (after a NOP slide).

Side-by-side, the post-runtime-generated code is structurally identical:

2.20 cold-boot scan loop ($DECC-$DEDB):

$DECC: B7              OR A
$DECD: 28 04           JR Z,DED3        ; entry empty → skip
$DECF: AB              XOR E
$DED0: B9              CP C
$DED1: 28 05           JR Z,DED8        ; match!
$DED3: 2B              DEC HL
$DED4: 10 F5           DJNZ DECB        ; loop, B = entries left
$DED6: 18 21           JR DEF9          ; no match
$DED8: 11 0B 00        LD DE,000B
$DEDB: 19              ADD HL,DE

2.23 cold-boot scan loop ($FF2E-$FF3E):

$FF2E: 7E              LD A,(HL)
$FF2F: B7              OR A
$FF30: 28 04           JR Z,FF36        ; entry empty → skip
$FF32: AB              XOR E
$FF33: B9              CP C
$FF34: 28 05           JR Z,FF3B        ; match!
$FF36: 2B              DEC HL
$FF37: 10 F5           DJNZ FF2E        ; loop, B = entries left
$FF39: 18 21           JR FF5C          ; no match
$FF3B: 11 0B 00        LD DE,000B
$FF3E: 19              ADD HL,DE

Same instructions, same encoding, same control flow. The base-table pointer is set up the same way too — LD HL,$F3A0 in 2.23, LD A,($F3A2) in 2.20 (different base byte but same slot-info window in TPA).

The difference is what surrounds the loop. 2.23 has preflight code 2.20 doesn’t:

$FF0E: LD B,A
$FF0F: LD HL,FECD       ; state-byte address (in runtime-gen zone)
$FF12: LD A,(HL)
$FF13: LD E,A
$FF14: OR A
$FF15: JR NZ,FF29       ; if state nonzero, skip preflight
$FF17: LD A,(F397)      ; read slot info
$FF1A: OR A
$FF1B: JR Z,FF23
$FF1D: CP C
$FF1E: JR NZ,FF23
$FF20: LD (HL),80       ; mark state
$FF22: RET
$FF23: LD A,1F
$FF25: CP C
$FF26: JP C,FCA4         ; jump to FCA4 if C >= 1F
$FF29: LD HL,F3A0        ; (fall-through to scan loop)
$FF2C: LD B,09

So 2.23 prefixes the device-scan with a state-byte check at $FECD (in the runtime-generated zone, so it’s a state slot the generator establishes). If the state byte matches some condition, the scan is skipped. 2.20 has no equivalent — it goes straight to the scan loop.

Two implications.

The device-scan architecture didn’t change between versions. 2.20 was already capable of handling N device codes from a 9-entry table. The fix between 2.20 and 2.23 didn’t add new device-handling capacity to the BIOS — that was already there.

The fix lives in the boot-time slot scanner, not the BIOS. Recall from Part 1: the only material difference between 2.20 and 2.23 in the 6502 boot code was 11 bytes that added a Pascal 1.1 detection branch. That fix changes which device code gets written into the table at $F3A0, but the table consumer (the BIOS device-scan) was already there in 2.20. Pre-2.23, the slot scanner just never wrote the right code for a Videx — so the BIOS scan never matched, and no Videx-aware handler was selected.

That’s a clean refactor pattern: the BIOS was forward-compatible, designed to dispatch on device codes from a configurable table; only the table-population logic needed to change. Microsoft shipped 2.23 by patching detection, not by patching the BIOS.

Status: device-scan loop structurally identical between versions. The 2.23 preflight at $FF0E-$FF28 is new and references a state byte at $FECD in the runtime-generated zone — worth understanding, since it’s the entry point to the scan and may carry the “is this Pascal 1.1?” flag the slot scanner now sets. Next: trace what writes to $FECD and what $FCA4 does (the early-exit target on C >= 1F).