The inter-CPU sync polling loop at Z-80 $1E39

5 min read
z80cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

Detail for Part 8 — The Cooperative-CPU Round-Trip.

The cooperative-CPU model in SoftCard CP/M is described in earlier devlogs in conceptual terms — the Z-80 needs disk I/O, signals the 6502 via shared memory, the SoftCard switches CPUs, the 6502 services the request, signals back, the SoftCard switches the Z-80 back in. Today the actual sync code is concrete:

Z-80 $1E36: C3 39 FB        JP $FB39                ; (separate routine — entry into BIOS)
Z-80 $1E39: 3A 00 E0        LD A,($E000)             ; ← polling start
Z-80 $1E3C: 17              RLA                       ; high bit → carry
Z-80 $1E3D: 30 FA           JR NC,$1E39               ; loop until carry set
Z-80 $1E3F: 32 10 E0        LD ($E010),A              ; signal 6502
Z-80 $1E42: 3F              CCF                       ; complement carry
Z-80 $1E43: 1F              RRA                       ; restore A's high bit
Z-80 $1E44: C9              RET

Six instructions. That’s the entire Z-80 side of the inter-CPU sync.

The protocol:

  1. 6502 sets $E000 high bit ($80+ value at $E000) when it has finished servicing whatever the Z-80 needed.
  2. Z-80 reads $E000 repeatedly until the high bit is set. The RLA / JR NC pattern is a tight 2-instruction polling loop.
  3. Z-80 writes the read value to $E010 as an acknowledgment / signal that the Z-80 has seen the response. The 6502 is presumably watching $E010 and resets state when it sees the write.
  4. Z-80 returns to caller with the read byte cleaned up via CCF / RRA.

The full round-trip (conceptually):

[Z-80 BIOS routine needs disk]
  Z-80 calls disk-callback at $1Axx
  Disk-callback sets up parameters in shared state, ($FECB/$FED2/$FED4 etc.)
  Disk-callback calls $1E39 (polling)
    [SoftCard CPU switch fires here, somehow]
  6502 wakes: reads parameters, runs RWTS
  6502 deposits sector data at DMA address
  6502 sets $E000 high bit (signaling done)
    [SoftCard switches back to Z-80]
  Z-80 continues at $1E39:
    LD A,($E000) — reads the now-set value
    RLA — high bit to carry
    JR NC — carry IS set, fall through
    LD ($E010),A — acknowledge
    RET — back to disk-callback caller
  Disk-callback uses results, returns to BIOS routine
[Z-80 BIOS routine resumes with disk data available]

The piece still missing: what actually triggers the SoftCard CPU switch when the Z-80 reaches $1E39. Reading from $E000 is the first instruction Z-80 executes after the 6502 should have run. So the switch happens between Z-80 wrote the request parameters and Z-80 reads $E000. There must be either:

  • An explicit “kick the SoftCard” write somewhere in the disk-callback before the polling, or
  • A side-effect of the polling read (like the SoftCard hardware monitoring the address bus and triggering on $E000 reads).

The second option is plausible. SoftCard hardware monitoring could work like: when Z-80 reads $E000 and the high bit isn’t set yet, the SoftCard treats this as a “wait for 6502” signal and blocks the Z-80 until the 6502 has finished. The Z-80 sees the polling loop spin once and then succeed; the SoftCard internally has paused the Z-80 clock or alternated execution.

That would mean the polling loop is actually a very short busy-wait that the SoftCard hardware turns into a controlled CPU switch. Z-80 sees LD A,($E000); RLA; JR NC, $1E39; ... execute as if it always succeeded on the first iteration.

That’s hypothesis. What’s confirmed is the six instructions of the sync code itself.

The address $1E39 is interesting. In Z-80 view that’s TPA territory (just above zero page). But these bytes live in the BIOS first 1 KB region (Apple $0E39 post-PREP_HANDOFF). After bit-12 XOR, Apple $0E39 ↔ Z-80 $1E39. So the BIOS first 1 KB serves dual duty: it’s the disk-callback area the Z-80 sees in TPA, and it’s also visible at the BIOS address $FAB8+offset once the SoftCard maps LC RAM. Same bytes, two different Z-80 addresses.

That dual-mapping is a SoftCard architectural choice. It means the BIOS first 1 KB doesn’t have to be moved between “BIOS address space” and “callback address space” — both are the same physical bytes, accessed via different views of memory.

Status: sync polling loop located at Z-80 $1E39, fully decoded. Six instructions. The BIOS-disk-callback bridge is now concrete on the Z-80 side. The 6502-side counterpart — the code that sets $E000 and reads parameters from shared state — is the next piece.