The 6502 loader carries 270 bytes of embedded Z-80 code at $143A-$1547
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
$FEDxrange —$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
LDIRinstruction (ED B0) at$1515-$1516withBC = $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 Lshift sequence at$1500-$1502that propagates a carry into the L register — used for parameterizing the copy-source address.
What it probably does, structurally:
- Initialize state slots at
$FEDA-$FEDDwith starting values. - Read a slot-info byte (probably from
$F3xx). - Compute a target address from the slot info (the
RRA / RR Lshift produces a slot-keyed address). - Set source =
$F800(or computed offset of it), destination =($FEE1), count =$0080(128 bytes). LDIRto copy 128 bytes — this could be the per-device handler installation.- After the copy, jump to a BIOS handler (
JP $FECAorJP $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 hasJP $000Bsomewhere andLD HL,$0DD0references 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.