Loader $191E: a second JSR $BBEB before the CPU switch

5 min read
6502cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

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

Tracing forward in the loader past LOAD_CPM and PREP_HANDOFF turned up the boot-finalization sequence at Apple $1900-$194B. This is the last 6502 code that runs before the SoftCard CPU switch. It’s worth a careful read because it shows several things that earlier devlogs hadn’t covered:

$1901: STA $1001                ; A = $00 (planted at $1001) — Z-80 reset target low byte
$1904: LDA #$FA
$1906: STA $1002                ; $1002 = $FA (high byte). Combined with $1000=$C3 → JP $FA00
$1909: LDA #$0A
$190B: STA $53                  ; dest pointer high = $0A
$190D: LDA #$BA
$190F: STA $51                  ; src pointer high = $BA
$1911: LDA #$00
$1913: STA $52                  ; dest pointer low = $00
$1915: STA $50                  ; src pointer low = $00
$1917: LDX #$06                 ; copy 6 pages
$1919: JSR $115C                ; PAGE COPY ROUTINE: copy $BA00-$BFFF → $0A00-$0FFF
$191C: LDA #$80                 ; sector/buffer param = $80
$191E: JSR $BBEB                ; ← SECOND CALL TO LOAD_CPM-EQUIVALENT
$1921: LDA #$0A
$1923: STA $BC08                ; store $0A at $BC08 (RWTS sector parameter)
$1926: LDA #$97
$1928: STA $53                  ; dest high = $97
$192A: LDA #$0A
$192C: STA $51                  ; src high = $0A
$192E: LDX #$06
$1930: JSR $115C                ; copy $0A00-$0FFF → $9700-$9CFF (6 pages)
$1933: LDA #$80
$1935: STA $53                  ; dest high = $80
$1937: LDA #$A3
$1939: STA $51                  ; src high = $A3
$193B: LDX #$17                 ; copy 23 pages
$193D: JSR $115C                ; copy $A300-$B9FF → $8000-$96FF (CCP+BDOS reverse-staging)
$1940: LDY #$06
$1942: LDA $116C,Y               ; from a small constants table
$1945: STA $FFF9,Y               ; patch Apple monitor reset vectors $FFF9-$FFFE
$1948: DEY
$1949: BNE $1942
$194B: JMP $03D2                 ; jump to warm-boot routine — SoftCard CPU switch follows

The discovery is the second JSR $BBEB at $191E. Earlier work traced the main LOAD_CPM call (29 sectors, into Apple $8000-$9CFF). This is a different call to the same routine, with A = $80, after the main load and after a $BA00 → $0A00 page copy. With different setup parameters, it almost certainly reads a different set of sectors into a different destination.

The setup before the call is suggestive:

  • The page copy puts the original 6502 disk routines (preserved at $BA00-$BFFF after PREP_HANDOFF’s first step) back at $0A00-$0FFF. So when LOAD_CPM runs the second time, the disk-callback bytes that were already at $0A00-$0FFF from earlier in PREP_HANDOFF are replaced with the original RWTS code.
  • LDA #$80 is the call’s parameter. It might be a destination high byte ($80$8000), a sector count, or a flag.

The plausible reading: the loader does two LOAD_CPM passes. First pass loads CCP+BDOS+callbacks+BIOS-first-1KB into staging (the 29-sector read I’d previously been treating as the only load). Second pass loads additional sectors — possibly BIOS handler templates, additional BIOS pages, or the per-device handler code that ends up populating the trap-marker pages at runtime.

Why this matters for the BIOS factory question. Earlier I noted that trk2:physA of the disk image contains real Z-80 code calling BIOS routines, but those bytes weren’t in staging_223.bin (the LOAD_CPM result). If the second JSR $BBEB reads them into a different memory area, that closes the gap — those bytes do reach the system, just by a path we hadn’t traced.

What we still don’t know:

  • Exactly which sectors the second LOAD_CPM call reads.
  • Where it puts them.
  • Whether the destination is somewhere the Z-80 will see it.

The post-call STA $BC08 and the page copies suggest the second load might be reusing the same staging buffer ($0A00-$0FFF) and then the loader is moving things around — $0A00 → $9700 and $A300 → $8000 are the page copies that follow. These could be the final-position relocation of CCP+BDOS to $8000-$96FF (where $9C06 would land if BDOS occupies the range $8000-$9C05 — not the upper-memory $D300+ slot I’d assumed).

Actually that calculation is interesting. If BDOS final position is $9C06 (per the cold-boot’s LD HL,$9C06; LD ($0006),HL plant), and BDOS is roughly 3.5 KB, then BDOS would occupy $8E06-$9C05 and CCP would be above $9C06. With CCP at ~2 KB ending around $A406. That’s just inside the $8000-$96FF range that the third copy puts there. So BDOS final position at $9C06 is consistent with this third page copy — which puts the CCP+BDOS image back at $8000, where $9C06 lands inside it.

That’s the CCP+BDOS relocation I’d been looking for. Resolved: the loader does the relocation, by copying the staged CCP+BDOS from $A300 (where PREP_HANDOFF #3 had put it) back down to $8000 (where the BDOS call vector $9C06 lands inside). It’s not done by the BDOS itself or by the cold-boot; it’s the last 6502 page copy before the CPU switch.

Implications for the resume-prompt’s “open” list:

Open questionStatus after this devlog
CCP+BDOS relocation from $A300 to $9C06 areaResolved: loader at $1933-$193D copies $A300-$B9FF back to $8000-$96FF. BDOS at $9C06 lands inside.
Where the trap-marker page handler bytes come fromLead identified: second JSR $BBEB at $191E reads additional sectors. Destination not yet pinned down — could be the trap-marker areas via subsequent page copies.
SoftCard CPU-switch triggerOpen. Code at $194B: JMP $03D2 enters the warm-boot routine, which is where the switch happens.
Initial population of $FA00-$FAB7Open. May be related to the second LOAD_CPM call.

Status: the boot-finalization sequence is fully disassembled. The CCP+BDOS relocation question is closed. The second JSR $BBEB is identified as a candidate mechanism for the still-open trap-marker-page population question. Next step: trace what sectors $BBEB reads with A=$80, by examining LOAD_CPM’s source-address parameterization.