The 6502 loader carries 270 bytes of embedded Z-80 code at $143A-$1547

5 min read
z806502cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

Detail for Part 4 — The Handoff: 6502 to Z-80.

While disassembling around the loader’s $1500 region during Part 4 research, I noticed instruction patterns that don’t read as 6502. They read as Z-80. Disassembled with z80disasm they make sense as a coherent routine.

The Z-80 fragment runs from approximately Apple $143A to $1547 — about 270 bytes. Selected portions:

$143A: AF              XOR A
$143B: 32 DD FE        LD ($FEDD),A          ; clear state byte
$143E: 3E 02           LD A,$02
$1440: 21 DA FE        LD HL,$FEDA
$1443: 77              LD (HL),A
$1444: 23 77           INC HL; LD (HL),A     ; init FEDB
$1446: 23 77           INC HL; LD (HL),A     ; init FEDC
$1448: 18 48           JR $1492              ; forward jump
...
$1492: 21 4A AD        LD HL,$AD4A
$1495: 85              ADD A,L
$1496: 6F              LD L,A
$1497: 4E              LD C,(HL)
$1498: 21 D8 FE        LD HL,$FED8
$149B: 7E              LD A,(HL)
$149C: 36 01           LD (HL),$01
$149E: B7              OR A
$149F: 28 1B           JR Z,$14BC
...
$14F1: 3A DC FE        LD A,($FEDC)
$14F4: B7              OR A
$14F5: C4 2C AD        CALL NZ,$AD2C
$14F8: AF              XOR A
$14F9: 32 D9 FE        LD (FED9),A
$14FC: 7B              LD A,E
$14FD: 21 00 F8        LD HL,$F800
$1500: 1F              RRA
$1501: CB 1D           RR L
$1503: ED 5B E1 FE     LD DE,($FEE1)
$1507: 01 80 00        LD BC,$0080
$150A: 3A DA FE        LD A,($FEDA)
$150D: B7              OR A
$150E: 20 05           JR NZ,$1515
$1510: 3C              INC A
$1511: 32 D9 FE        LD ($FED9),A
$1514: EB              EX DE,HL
$1515: ED B0           LDIR                  ; ← BLOCK MOVE: copy BC=$80 bytes
$1517: 3A DB FE        LD A,($FEDB)
$151A: 1F              RRA
$151B: 3E 00           LD A,$00
$151D: 30 03           JR NC,$1522
$151F: CD 25 AD        CALL $AD25            ; BDOS-area call
$1522: C3 CA FE        JP $FECA              ; 2.23 BIOS handler
...
$1525: AF              XOR A
$1526: 32 D9 FE        LD (FED9),A
$1529: 3E 02           LD A,$02
$152B: 21 3E 01        LD HL,$013E
$152E: 32 EB F3        LD ($F3EB),A
$1531: 21 00 08        LD HL,$0800
$1534: 22 E8 F3        LD ($F3E8),HL
$1537: 21 03 0E        LD HL,$0E03
$153A: CD C3 FE        CALL $FEC3             ; 2.23 BIOS handler
$153D: 3A EA F3        LD A,($F3EA)
$1540: B7              OR A
$1541: C8              RET Z
$1542: D1              POP DE
$1543: FE 10           CP $10
$1545: 20 DB           JR NZ,$1522
$1547: C3 C6 FE        JP $FEC6              ; 2.23 BIOS handler

Markers that confirm this is real Z-80 code, not coincidence:

  • Six absolute calls/jumps to BIOS addresses ($FECA, $FEC6, $FEC3, $AD25, $AD2C, $AD4A) — all in 2.23’s actual address layout.
  • Multiple references to BIOS state slots in the $FEDx range — $FED1, $FED2, $FED3, $FED6, $FED7, $FED8, $FED9, $FEDA, $FEDB, $FEDC, $FEDD, $FEE1, $FEE3, $FE03, $FE07, $FE0E — exactly the slots the BIOS uses for per-device state and dispatch flags.
  • An LDIR instruction (ED B0) at $1515-$1516 with BC = $0080 (128 bytes) preset just before. So the routine moves 128 bytes from (HL) to (DE) — a generic block-copy, sized as one would expect for a per-device handler installation.
  • A RRA / RR L shift sequence at $1500-$1502 that propagates a carry into the L register — used for parameterizing the copy-source address.

What it probably does, structurally:

  1. Initialize state slots at $FEDA-$FEDD with starting values.
  2. Read a slot-info byte (probably from $F3xx).
  3. Compute a target address from the slot info (the RRA / RR L shift produces a slot-keyed address).
  4. Set source = $F800 (or computed offset of it), destination = ($FEE1), count = $0080 (128 bytes).
  5. LDIR to copy 128 bytes — this could be the per-device handler installation.
  6. After the copy, jump to a BIOS handler (JP $FECA or JP $FEC6).

So this fragment looks like a runtime per-device installer: read what device was detected, copy 128 bytes of handler code from a source area to a destination in the BIOS region (probably trap-marker slots), then jump to a BIOS entry point that uses the just-installed code.

Why this connects to the BIOS factory question. Earlier I’d been searching for the mechanism by which trap-marker pages get populated at runtime. The LDIR here is exactly that mechanism. It copies 128 bytes from a source area into a destination in BIOS — which is the “code-emit handler bytes” step the cold-boot generator’s per-device init routines presumably trigger.

Where the fragment runs. This is the open piece. The fragment is in the 6502 loader binary at Apple $143A-$1547. Z-80 mode sees Apple $1xxx as Z-80 $0xxx (under bit-12 XOR), so Apple $143A is Z-80 $043A — TPA territory, not normal BIOS. Either:

  • The fragment runs in place at Z-80 $043A-ish, called via some BIOS jump that points there. The cold-boot path has JP $000B somewhere and LD HL,$0DD0 references low Z-80 addresses, so this is plausible.
  • The fragment is copied elsewhere by another step in the boot pipeline. The loader’s page copies don’t visibly target a 270-byte range at any specific destination, but a smaller subset might be moved by code I haven’t fully traced.

What the fragment is NOT. It’s not the standard CP/M warm-boot routine (those are smaller and don’t reference SoftCard-specific addresses). It’s not the disk callback at Apple $0A00 (those are in newdisk and have a different shape). It’s something distinct — a runtime installer or a per-device init routine that’s part of the 2.23 BIOS factory’s plumbing.

2.20 has no equivalent fragment. Searching 2.20’s loader for ED B0 (LDIR) returns zero hits. Searching for LD A,nn / LD ($DExx),A (the analog of 2.23’s LD ($FExx),A BIOS-state writes, since 2.20 BIOS is at $DACC so its state slots would be in $DExx) also returns zero. The embedded Z-80 fragment is 2.23-specific.

That fits the architectural picture: 2.23 expanded its dispatch table to all-runtime-installed handlers (vs 2.20’s 4-static + 2-runtime mix per the dispatch-table comparison). To install handlers at runtime, 2.23 needs an LDIR-driven block-copy installer. 2.20’s mostly-static handler architecture doesn’t need one.

So this fragment is part of the architectural rewrite 2.23 did to support the runtime-handler model. It’s a concrete byte-level expression of “what 2.20 doesn’t have because it didn’t need it; what 2.23 adds because the new architecture requires it.”

Status: 270 bytes of Z-80 code located inside the 6502 loader. Confirmed-Z-80 by call targets and state references; structurally a per-device installer using LDIR for block copy. 2.23-specific — 2.20 has no analog. Where the fragment ultimately executes is the open question. Part 4 discusses the implications for the handoff mechanism.