Add zx81sd architecture: ZX81 + SD81 Booster support#1099
Open
wilco2009 wants to merge 10 commits into
Open
Conversation
Confirmed on real hardware: block 6 (screen RAM) survives a reset, so programs that never call CLS themselves (many quick debug tests just PRINT sequentially) inherited leftover pixels/attributes from whatever ran before. Unlike a real Spectrum ROM cold boot, which always starts from a cleared display file. SD81_INIT_SYSVARS now clears 6144 bytes of bitmap to 0 and 768 bytes of attributes to ATTR_P right after setting up SCREEN_ADDR/ SCREEN_ATTR_ADDR, inlined rather than calling CLS to avoid depending on cls.asm being linked into every binary. Runs automatically for every zx81sd program via the existing #init hook. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wraps the FPGA's present-blit double buffer (SD81DBufOn/SD81DBufOff/ SD81WaitVSync in stdlib/dbuf.bas), controlled via port $E7's pseudo-block-8 encoding since memory-mapped IO (the classic POKE 2057 path) is disabled by boot1.asm before any compiled program runs. examples/sd81/bounce_sd81.bas ports SD81-Booster/EXAMPLES/DBUF/ bounce.asm's flicker-comparison demo to zxbasic. This is the first test of the port $E7 dbuf path (only POKE 2057 had been validated on real hardware before). Verified in simulation: SD81DBufOn(5) emits the expected OUT $E7 (A=8, B=37), border changes correctly, main loop runs many frames without hanging -- whether it actually eliminates tearing needs real hardware. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move() widened ubyte(bally) + byte(dy) to integer for the range check. The generated code sign-extends the 8-bit sum's own bit 7 to fill the high byte, which is wrong when one operand is genuinely unsigned and can exceed 127 (bally=126, dy=2 -> 8-bit sum 128 gets read as -128). Fixed by keeping Move() entirely in ubyte/mod-256 arithmetic with an unsigned comparison, matching bounce.asm's own "add a,b / cp 177" approach instead of widening to a signed type at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Interrupts stay disabled for the whole program by design, so RST $38 "should never be reached" -- but the old di+halt handler locked the machine up permanently if something did reactivate them. Now waits for the next VSYNC pulse (port $AF) and rets, matching what a real Z80 HALT does (resume after the next interrupt). Found along the way: runtime/vectors.asm was dead code, never #include'd by anything -- the real RST vector table is emitted directly in backend/main.py::emit_prologue() (added in the same original commit, apparently never wired together). A first attempt editing vectors.asm compiled fine but had zero effect on the output binary. Fixed the actual source in main.py and deleted the misleading duplicate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed with the SD81 Booster's hardware author: there is no software-triggerable reset in the FPGA (nRESET is a plain input wire), so a genuine "exit to BASIC" isn't possible from a running zx81sd program regardless of the memory-mapping concerns already noted. RST 0 (restart the current program) remains the closest equivalent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The shared __ARRAY routine (zx48k/runtime/array/array.asm, used by every zxbasic program with 2D+ arrays) uses the Spectrum's MEMBOT system variable (23698) as scratch storage for its internal pointers. On zx81sd that address falls inside the program's own compiled code (block 2) instead of safe RAM, silently corrupting a few bytes on every array access -- a general zx81sd bug, not specific to any one program, that simply hadn't been exercised by earlier examples. Found while porting a third-party game (heavy 2D-array user) and traced live on real hardware: a write breakpoint on the stack pointer's own descent caught a PUSH corrupting FP_STKEND, which cascaded into the FP calculator operating on garbage addresses and eventually executing screen memory as code. Fix: new src/lib/arch/zx81sd/runtime/array/array.asm override (only change: LBOUND_PTR points at a new dedicated ARRAY_SCRATCH sysvar instead of the hardcoded Spectrum address). Verified by simulation: 1.2 billion T-states with no writes to the old address and no crashes, versus reliably crashing within a few hundred million before the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while continuing to debug ZXOilPanic on real hardware after the array.asm fix -- the code corruption just moved to a different address once the array.asm fix shifted the binary's layout. A project-wide grep for hardcoded Spectrum sysvar addresses (EQU 2[34]...) turned up about a dozen shared zx48k runtime files with this same pattern (scratch storage at a fixed "safe on real Spectrum" address, bypassing sysvars.asm entirely); these two are the ones actually observed corrupting a real, running program: - chr.asm: CHR$()'s TMP (23629, Spectrum's DEST) landed inside __DIVU32 in this build. - arith/divf.asm: division's TMP (23629, same as chr.asm) and ERR_SP (23613) run on every division, not just on error -- worth noting the ERR_SP-based recovery this saves for doesn't actually do anything on zx81sd anyway (__ERROR halts directly, never restores SP from it). Same fix pattern as array.asm: new overrides with only the scratch address changed (CHR_SCRATCH, DIVF_SCRATCH in sysvars.asm). Verified with a corrected VSYNC-pulse simulation mock, monitoring writes across the entire program's address space: 200M T-states with zero corruption, versus reliably corrupting within a few hundred thousand before. Not yet audited: arith/modf16.asm (same MEMBOT address, unused by this game) and break.asm's PPC -- a full audit of every zx48k runtime file for this pattern is worth doing separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… tools - backend/main.py: the $0038 (RST38) vector now preserves AF around the VSYNC wait. Ported Spectrum code that leaves interrupts enabled (a stray EI) makes the ZX81's continuous IM1 INTs land there mid-flight; without this, A/flags got clobbered and corrupted whatever computation was interrupted (root cause of a heisenbug in a ported game's sprite(N, col) array reads, see MAP.md). - doc/SYSVARS.md (new): full Spectrum ROM sysvar equivalence table, plus a quick-reference for screen/attribute base addresses, the ULA port (border/beeper, $FE -> $FB), and the AY sound chip ports/clock ($FFFD/$BFFD -> $CF/$0F, 1.7734MHz -> 1.625MHz). - doc/PORTING_GUIDE.md (new): step-by-step checklist for porting a Spectrum program, cross-referencing the existing docs. - doc/PRECAUTIONS.md, doc/USAGE.md, doc/MAP.md, README.md: updated to link the new docs and document the EI/RST38 finding. - tools/build_sd81.py (new): one-shot compile + SD81 page-split + .P loader generation, with an optional --copy-to to avoid testing against a stale package. - tools/gen_map.py (new): regenerate a .map via the explicit bas -> asm -> map pipeline, for when the .asm is being inspected or hand-edited between steps.
boot1.bin is required on the SD card alongside the page files and .P loader for the program to actually boot -- copying everything but that made --copy-to produce an incomplete package.
Programs whose compiled size exceeds the ~32KB code/sysvar/heap budget (sprite bitmaps, screen dumps, sample tables...) can now relocate big constant data out of the way via a manual ORG into block 7 ($E000- $FFFF, previously unused). Adds block 7 to the boot page-map, and teaches split_sd81.py to skip packaging/loading pages that come out as 100% assembler gap-filler (the unused span this creates between code and the relocated data), instead of wastefully shipping/loading them. build_sd81.py gains --asm to also emit the generated assembly listing (a second zxbc.py pass), useful for tracing bugs live against the .map. Documents a related gotcha found while doing this on Mario GW: relocating a sub's asm body with ORG doesn't move where @name resolves to, since the compiler's own entry label is emitted before the ORG takes effect (PRECAUTIONS.md section 9, cross-linked from PORTING_GUIDE.md step 6). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a new architecture,
zx81sd, targeting a real ZX81 with theSD81 Booster card (Spectrum-like screen, AY/beeper sound, paged
memory mapper, SD card access).
Isolation
All architecture-specific code lives exclusively under:
src/arch/zx81sd/(backend, docs, packaging tools)src/lib/arch/zx81sd/(ASM runtime + BASIC stdlib)No shared frontend/runtime/stdlib file is modified.
#include/#requireresolution already falls back fromsrc/lib/arch/zx81sd/to
src/lib/arch/zx48k/when a file isn't overridden, so this is apurely additive change with no effect on zx48k/zx128k/zxnext.
Status
Functionally complete since 2026-07-02: FP calculator, graphics
(PLOT/DRAW/arcs/CIRCLE), sound (BEEP/PLAY over AY ZonX + beeper),
keyboard, joystick, the full MCU library (files, RTC, memory mapper),
LOAD/SAVE/VERIFY...CODE against SD. A few shared stdlib screen
utilities are still pending audit (see
src/arch/zx81sd/README.md).Notable findings
Two runtime incompatibilities with zx81sd's memory map were found and
fixed via architecture-specific overrides (no shared code touched):
array.asm/chr.asm/arith/divf.asmused fixed Spectrum sysvaraddresses (MEMBOT/DEST/ERR_SP) as scratch storage — valid on a real
Spectrum, but landing inside the compiled program's own code on
zx81sd.
EI(harmless there) leaves interruptsenabled for the rest of execution on zx81sd, since it otherwise runs
fully interrupt-disabled; a stray IM1 INT landing mid-computation
corrupts registers at random. Documented in
doc/MAP.mdwith thefull live-debugging trace, in case it's useful for other
no-ROM/no-interrupt ports.
Docs and tooling
doc/PORTING_GUIDE.md— step-by-step checklist for porting aSpectrum program.
doc/SYSVARS.md— full Spectrum ↔ zx81sd sysvar and I/O portequivalence table.
tools/build_sd81.py/tools/gen_map.py— one-shot compile +package / regenerate the symbol map.