Skip to content

zx81sd: nueva arquitectura para ZX81 + SD81 Booster#1090

Merged
boriel merged 27 commits into
boriel-basic:mainfrom
wilco2009:zx81sd-arch
Jul 5, 2026
Merged

zx81sd: nueva arquitectura para ZX81 + SD81 Booster#1090
boriel merged 27 commits into
boriel-basic:mainfrom
wilco2009:zx81sd-arch

Conversation

@wilco2009

Copy link
Copy Markdown
Contributor

Puerto del compilador Boriel ZX BASIC a hardware ZX81 + SD81 Booster (arquitectura zx81sd).

Resumen

  • Nueva arquitectura zx81sd en src/lib/arch/zx81sd/ (runtime + stdlib), siguiendo la convencion de copiar en vez de modificar los ficheros de zx48k existentes.
  • Reimplementaciones propias de PLOT/DRAW/BORDER/VSYNC/teclado, ya que no hay ROM mapeada en tiempo de ejecucion del ZX81+SD81 Booster (el binario compilado ocupa el bloque 0 por completo).
  • Driver de teclado nativo (escaneo directo de la matriz $FEFE) con decodificacion a ASCII, ya que la ROM del ZX81 decodifica a su propio charset/tokens, no al charset Spectrum/ASCII que usa este runtime.
  • Beeper de clic de tecla reutilizando el puerto ULA emulado ($FBh) del SD81 Booster en modo Spectrum.
  • Fix de un bug de corrupcion de atributos INK/PAPER (faltaba el sysvar MASK_P).

Test plan

  • Compilacion de programas de prueba (graficos, INPUT/INKEY$, BORDER) sin errores
  • Probado en emulador/hardware real por el autor: PLOT/DRAW/BORDER, PAUSE/VSYNC, impresion de variables, INPUT()/teclado con clic
  • Revision por Boriel

wilco2009 and others added 27 commits June 30, 2026 16:07
New target architecture that compiles ZX BASIC programs for the ZX81
with SD81 Booster expansion (512KB RAM, SD card, AY sound, FPGA video).

Key design decisions:
- Inherits zx48k runtime; only overrides files specific to SD81 hardware
- Two-stage boot: stage 1 at $6000 (external BASIC loader), stage 2 at $0100
- Flat binary ORG $0000 with RST vector table ($0000-$00FF) and 8KB page layout
- Spectrum HiRes mode: framebuffer at $C000 (block 6), attrs at $D800
- Sysvars mapped to $8000+ (blocks 4-5) instead of Spectrum ROM addresses
- SCREEN_ADDR / SCREEN_ATTR_ADDR as RAM pointers (not EQU constants) to
  match the indirect addressing used by the inherited zx48k runtime
- CHARS pointer initialised to font_base - 256 (Spectrum convention)
- Bold/italic FLAGS2 check uses direct LD A,(FLAGS2) without PUSH/POP AF
  to avoid destroying the Z flag from BIT before CALL NZ

New files:
  src/arch/zx81sd/              backend (main.py, generic.py)
  src/lib/arch/zx81sd/runtime/  sysvars, bootstrap, charset (INCBIN specfont.bin),
                                 print, border, pause, vsync, paging, vectors, ...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sysvars.asm/bootstrap.asm: add missing MASK_P sysvar between ATTR_P
  and ATTR_T so COPY_ATTR no longer corrupts INK/PAPER attributes.
- pixel_addr.asm: rework to return bit position directly (matches the
  interface plot.asm/draw.asm expect) instead of a rotated bitmask.
- vsync.asm: fix port ($AF) and semantics (bits 1-6 = pulse counter
  since last read, auto-resets on IN) per SD81 Booster hardware.
- arch_config.asm: pull in pixel_addr.asm/po_gr_1.asm so ROM-call
  substitutes are available project-wide.
- Add native ZX81 keyboard driver (io/keyboard/keyscan.asm) since no
  ROM is mapped at runtime: scans the $FEFE matrix directly and
  decodes to ASCII (not the ZX81 ROM's own charset/token codes, since
  this runtime prints via the Spectrum/ASCII charset). Wire it into
  INKEY$ (io/keyboard/inkey.asm) and a new stdlib/input.bas that
  replaces the Spectrum-ROM-dependent LAST_K/BEEPER polling.
- beep.asm: key click using the SD81 Booster's emulated Spectrum ULA
  port ($FBh, bits 4-3), sharing a shadow byte with border.asm so
  border color and beeper bits don't clobber each other.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…DRAW arc mode

Port the Spectrum ROM floating-point calculator to the zx81sd
architecture as native runtime code (no Spectrum ROM available):

- fp_calc.asm: CALCULATE engine (RST 28h + literal bytecode), FP stack
  sysvars, arithmetic, comparison, trig/log/exp/sqrt (series-based, as
  in the ROM), and STK-TO-A/STK-TO-BC/CD-PRMS1 for arc drawing.
- stackf.asm: native __FPSTACK_PUSH/__FPSTACK_POP over FP_STKEND.
- fp_tostr.asm / printf.asm / str.asm: PRINT and STR$ of FLOAT values
  (sign + integer part + up to 5 decimals, trailing zeros trimmed).
- val.asm: VAL for FLOAT expressions.
- draw3.asm: DRAW x,y,angle (arc mode) override, replacing fixed ROM
  addresses with the routines ported in fp_calc.asm. Includes optional
  trace instrumentation behind -D DRAW3_DEBUG.
- pixel_addr.asm: fix register contract — PIXEL_ADDR now preserves DE
  like ROM PIXEL-ADD ($22AC). It used D as scratch, which corrupted
  the Bresenham Y coordinate saved in D' by draw.asm, breaking every
  non-horizontal DRAW line (found while debugging arc mode).
- sysvars.asm / bootstrap.asm: FP calculator sysvars (STKBOT/STKEND/
  BREG/MEM + stack and MEM area) and their initialization.
- backend main.py: include fp_calc.asm unconditionally in the
  prologue (RST 28h vector must always exist).

Verified on hardware/EightyOne against a real Spectrum reference:
PLOT 100,100: DRAW 20,0,3.14159: DRAW 10,10,1.5708: CIRCLE 60,30,20
renders identically on both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BEEP (sin ROM Spectrum):
- io/sound/beeper.asm: bucle BEEPER de la ROM ($03B5) portado al puerto
  ULA $FB del SD81 (bits como el $FE del Spectrum), con el borde tomado
  de la copia sombra y sin EI final (el runtime corre con las
  interrupciones deshabilitadas). La entrada __BEEPER del compilador
  corrige el periodo precalculado por el frontend (que usa la constante
  del Spectrum, 437500 = 3.5MHz/8) a los 3.25 MHz del ZX81:
  HL' = HL*13/14 - 2 (406250/437500 = 13/14 exacto).
- io/sound/beep.asm: comando BEEP de la ROM ($03F8) portado sobre el
  calculador FP propio: misma matematica (incluida la parte fraccionaria
  del semitono), tabla de semitonos de la ROM, y constante 406250
  (= 3.25MHz/8). FIND-INT1/2 sustituidos por __FPSTACK_POP+__FTOU32REG.

PLAY (chip A del ZonX, 3 canales):
- stdlib/play.bas: copia de zx48k/stdlib/play.bas con 4 cambios
  documentados en cabecera: puertos ZonX (latch $CF, dato $0F), tabla
  de divisores regenerada para reloj AY de 1.625 MHz (26MHz/16 en la
  FPGA), CpuCyclesPerSecond = 3250000 para el tempo, y sin EI final.

Verificado en EightyOne con analisis FFT de la salida de audio:
beeper y AY al unisono en 440 Hz (LA4), escala y acorde de 3 canales
correctos, duraciones exactas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mcu.bas — primitivas del protocolo del MCU (puertos $A7 datos /
$AF reloj) con las transferencias en ensamblador (McuSend/McuRecv/
McuSendBlock/McuRecvBlock: camino critico de LOAD/SAVE/F_READ/F_WRITE)
y wrappers para todos los comandos del manual:

- Sistema: NOP, VERSION, GETBYTE, SETBYTE
- Ficheros: PWD, CD, DEL, MKDIR, RMDIR, MOVE, COPY, LOAD, SAVE, TYPE,
  DIR, FREE (texto y binario), OPENDIR/GETROWLEN/GETROW
- Handles (32 bits): F_OPEN (ASCII), F_OPEN_ZX81, F_SEEK, F_READ,
  F_WRITE, F_CLOSE — verificado contra el firmware (COMMANDS.cpp):
  el MCU asigna el handle y lo devuelve
- Hardware: MC45, 128C/64C, FULLPAGING/HALFPAGING, RAM48, y el
  mapeador de memoria Map/MapGet (puerto $E7, valido en ambos modos
  de paginacion con una sola escritura)
- Voz: SAY, BINARY_SAY
- AY del MCU (chip 2): AY_SET_REG/AY_GET_REG/AY_PLAY (con conversion
  de octavas mayuscula<->video inverso como el interprete del MCU)
- VGM y PEG completos; RTC y BAT
- Extensiones no-MCU del BASIC del SD81: HexPoke (*HEX),
  StrInv/StrBold (*INV/*BOLD); *LDIR/*LDDR cubiertos por MemMove
  de la stdlib estandar

Conversion automatica ASCII<->codigos ZX81 en los comandos de texto.
joy.bas (comando 21) reescrito como capa fina sobre mcu.bas.

Verificado en EightyOne(SD81): version, get/setbyte, pwd, save+load
con verificacion byte a byte, del, free, rtc, bat, dir, AY2 por
registros, AyPlay, joystick, mapeador y HexPoke/MemMove/StrInv.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sustituyen a los load.asm/save.asm de zx48k (rutinas de cinta de la ROM
Spectrum, inutilizables sin ROM): ahora LOAD/SAVE/VERIFY ... CODE operan
sobre la tarjeta SD mediante los comandos 9/10 del MCU.

- io/sd81_mcu.asm: primitivas del protocolo del MCU para el runtime
  (send/recv con espera de reloj, bloques, RECV_VERIFY que compara sin
  escribir, RECV_SINK para descartar sobrante manteniendo el protocolo
  sincronizado, conversion ASCII->ZX81 de nombres).
- load.asm: LOAD carga el fichero completo (longitud 0) o hasta N bytes
  (descarta el exceso; fichero mas corto = error). VERIFY compara sin
  escribir. Errores blandos como en cinta: ERR_NR = 26 (Tape loading)
  o 14 (nombre invalido) y retorna.
- save.asm: SAVE guarda el bloque; longitud 0 = InvalidArg.

Interfaz del compilador identica a zx48k (incluida la liberacion del
string del nombre con MEM_FREE).

Verificado en EightyOne(SD81): ciclo SAVE+LOAD con verificacion byte a
byte, VERIFY correcto y con byte corrupto (ERR 26), y carga de fichero
inexistente (ERR 26).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La sysvar UDG apuntaba a __ZX81SD_CHARSET+896 asumiendo una fuente
completa de 256 caracteres, pero specfont.bin solo cubre CHR$(32)-
CHR$(127) (768 bytes): el puntero caia 128 bytes mas alla del final de
la fuente, sobre el codigo del runtime que tocara despues en el
enlazado. Cualquier programa que definiera UDGs (POKE USR CHR$(144+n))
machacaba ese codigo — p.ej. comecoquitos.bas corrompia CALC_TEST_ROOM
de fp_calc y colgaba el sistema al primer uso del calculador.

Fix: charset.asm reserva un area de 21 UDGs (CHR$(144)-CHR$(164), 168
bytes) tras la fuente, y SD81_INIT_SYSVARS apunta UDG a ella y la
inicializa con copias de las letras A-U, igual que la ROM del Spectrum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- keyscan.asm: las letras se decodifican ahora en minuscula, igual que
  el INKEY$ del Spectrum en modo L (el por defecto). Los programas de
  la epoca comparan INKEY$ con "o","p","q","a","y","n"... y con
  mayusculas no respondian (p.ej. examples/comecoquitos.bas).

- po_gr_1.asm: reescrito con el algoritmo literal de PO-GR-2 ($0B3E,
  ROM Spectrum). La version anterior tenia un OR aplicado al registro
  equivocado (machacaba los bits de cuadrante y dejaba vacia la mitad
  inferior de CHR$(143), entre otros) y el mapeo izquierda/derecha de
  los cuadrantes invertido respecto a la ROM (bit0 = superior DERECHO).

Verificado con comecoquitos.bas completo (jugable, identico al .tap de
Spectrum) y un test de los 16 graficos CHR$(128)-CHR$(143).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bloque 7 (WIP)

- backend/main.py: heap_size/heap_address se asignaban con ADD_IF_NOT_DEFINED,
  pero Z80Backend.init() ya los definia antes -> el heap se quedaba inline en
  vez de en $8100. Fix: asignacion directa tras super().init().
- keyscan.asm: rediseno completo. Pulsacion directa = minuscula, SHIFT+letra =
  mayuscula (antes solo devolvia minuscula), SHIFT+"2" = CAPS LOCK persistente,
  "." como tecla normal con los simbolos clasicos del ZX81 accesibles como
  tecla muerta desde INPUT() (ver input.bas).
- input.bas: composicion de simbolos ZX81 (tecla muerta "."+tecla) y doble
  punto = punto literal sin combo.
- print.asm: __SCROLL_SCR ya no cae en la ROM del Spectrum ($0DFE) - la
  implementacion por buffer es ahora la unica rama.
- stdlib/scroll.bas: nuevo override zx81sd (llamaba a PIXEL-ADD $22AC de la
  ROM, sustituido por PIXEL_ADDR propio).
- stdlib/cb/maskedsprites.bas: nuevo override que porta MSFS al mapeador de
  memoria (bloque 7) en vez de BANKM/$7FFD del Spectrum. Sigue en proceso,
  aun no funciona del todo bien.
- mcu.bas: _McuStreamPrint admite controlar el salto de linea final (para
  listados tipo DIR sin salto extra).
- fzx_fonts/: fuentes FZX de ejemplo para la libreria de fuentes.
- examples/sd81/filesystem.bas: demo de McuCd/McuDirPrint/McuLoad.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Añade docs/zx81sd/ (README, USO, PRECAUCIONES, CAMBIOS_BASIC, MAP) con
la guia de uso de la arquitectura zx81sd, las precauciones a tener en
cuenta al escribir/portar software (sysvars, contratos de registros,
teclado del ZX81, ausencia de interrupciones, namespaces ASM,
eliminacion de codigo muerto, metodologia de simulacion) y el catalogo
de cambios de fuente ya necesarios en ejemplos oficiales.

Añade a examples/sd81/ los ejemplos adaptados/probados hasta ahora:
snake_sd81.bas, flights_sd81.bas, maskedsprites_sd81.bas (MSFS sobre el
mapeador de memoria, aun en proceso), pong.bas y block7test.bas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SD81 Booster (hardware/firmware), EightyOne Cross-platform (emulador
usado en el desarrollo) y CPM_SD81.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
winscroll se cree portado/probado pero sin auditoria formal; putchars/
puttile sin auditar (sin ROM detectada a simple vista); screen.bas SI
depende de ROM (STK_END/S_SCRNS_BC/RECLAIM2); print42/print64 SI
dependen de sysvars Spectrum (UDG/ATTR_P) y son el siguiente objetivo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Overrides completos de la libreria compartida (Britlion): las sysvars
fijas de la ROM del Spectrum (UDG $5C7B, CHARS-256 15360, ATTR_P 23693)
se sustituyen por los sysvars propios de zx81sd, y las constantes de
base de pantalla/atributos (antes literales de $4000/$5800) se
parchean en tiempo de ejecucion via automodificacion de codigo,
leyendo el byte alto real de SCREEN_ADDR/SCREEN_ATTR_ADDR.

Bug real encontrado durante el port: un #include once <sysvars.asm>
dentro de estos ficheros, si resultaba ser la primera inclusion de todo
el programa, arrastraba bootstrap.asm/charset.asm (con su INCBIN del
font completo) justo en el punto del fichero fuente donde se puso el
include -- a nivel BASIC eso da "illegal preprocessor character", y
dentro de un bloque ASM se emite como codigo ejecutable real (HALT en
direccion basura). Fix: no incluir sysvars.asm desde estos ficheros,
solo referenciar los simbolos con el prefijo .core. (confiando en que
CLS/PRINT ya lo trajeron). Documentado en PRECAUCIONES.md (nueva
seccion 8) y CAMBIOS_BASIC.md (nueva seccion 9) como gotcha general
para cualquier libreria nueva que necesite sysvars propios.

Verificado por simulacion (examples/sd81/print4264.bas): ambas cadenas
de prueba se renderizan legibles pixel a pixel, sin corrupcion de
memoria, con HALT normal en __END_PROGRAM. Pendiente de confirmar en
emulador/hardware real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- docs/zx81sd/README.md -> src/arch/zx81sd/README.md
- docs/zx81sd/{USO,PRECAUCIONES,CAMBIOS_BASIC,MAP}.md -> src/arch/zx81sd/doc/
- Copia del repositorio complementario (solo para pruebas, no fuente
  canonica) las herramientas de empaquetado a src/arch/zx81sd/tools/:
  split_sd81.py, zx81_p_loader.py, boot1.asm, boot1.bin.

Enlaces relativos corregidos en toda la documentacion movida. Probado
end-to-end (compilar + split_sd81.py) desde la nueva ubicacion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix: the packaged program is loaded with LOAD FAST "<PREFIX>" (nothing
to select afterwards), not LOAD "" + select. Corrected in USO.md (now
USAGE.md) and split_sd81.py's docstring.

Translate all zx81sd documentation and tooling to English, including
file names:
- doc/USO.md -> doc/USAGE.md
- doc/PRECAUCIONES.md -> doc/PRECAUTIONS.md
- doc/CAMBIOS_BASIC.md -> doc/BASIC_CHANGES.md
- doc/MAP.md (kept name, content translated)
- README.md (content translated)
- tools/split_sd81.py, tools/zx81_p_loader.py, tools/boot1.asm
  (docstrings/comments translated)
- backend/main.py, backend/generic.py (comments translated)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…, F_*)

New examples exercising the SD81 Booster MCU's command surface
(stdlib/mcu.bas/joy.bas), all compile clean but not yet run on real
hardware:

- filesystem.bas (expanded): McuPwd/McuCd/McuDirPrint/McuMkdir/
  McuRmdir/McuSave/McuCopy/McuMove/McuDel/McuFree
- wavplayer.bas: McuLoad with a .WAV file (MCU plays it directly)
- vgmplayer.bas: McuPlayVgm/McuContVgm/McuPauseVgm/McuStopVgm
- pegdemo.bas: McuLoadPeg/McuPlayPeg/McuStopPeg (byte encoding
  unconfirmed on hardware, see BASIC_CHANGES.md)
- joystick.bas: Joy() programmable joystick mapping
- randomaccess.bas: McuFOpenZx/McuFSeek/McuFRead/McuFWrite/McuFClose

Found along the way: stdlib/input.bas defines input() as a function
(a$ = input(n)), not the native INPUT a$ statement; using the
statement form after #include <input.bas> fails to compile. Documented
in BASIC_CHANGES.md as a new pattern to watch for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All six (filesystem, wavplayer, vgmplayer, pegdemo, joystick,
randomaccess) tested OK, including pegdemo's PEG byte encoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- split_sd81.py: sort/organize the import block
- zx81_p_loader.py: drop redundant int() around math.floor() (RUF046)
- ruff format on the three files touched this session

ruff check/mypy now clean project-wide; the only remaining ruff
format finding (src/zxbpp/zxbpp.py) is pre-existing and unrelated to
zx81sd, left untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audited: WinScrollRight/Left/Up/Down have no Spectrum ROM dependency
(everything reads SCREEN_ADDR/SCREEN_ATTR_ADDR dynamically), and the
sysvars.asm dependency goes through #require (a link-time tag, not a
textual #include), so it doesn't risk the INCBIN-inline bug found
while porting print42.bas/print64.bas. Compiles clean and verified by
simulation (screen/attributes fully touched, clean HALT at
__END_PROGRAM). Pending confirmation on real hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
putchars.bas: audited, no ROM dependency (SCREEN_ADDR/SCREEN_ATTR_ADDR
read dynamically, #require "sysvars.asm" is a safe link-time tag) --
no override needed, used as-is.

puttile.bas: needed an override -- hardcodes the screen/attribute base
as immediate constants (add a,64 / add a,88, the Spectrum ROM's fixed
$4000/$5800 high bytes) instead of reading SCREEN_ADDR/
SCREEN_ATTR_ADDR. Same bug class as print42.bas/print64.bas; fixed the
same way (self-modifying code patches both constants once on entering
putTile(), reading the real high bytes at that point).

New example examples/sd81/putcharstile.bas exercises putChars/paint/
putTile. Verified by simulation with breakpoints right after each
function returns (a naive end-of-program check looked broken due to an
unrelated screen scroll triggered by the demo's own final PRINT AT
landing on the last screen row -- moved to row 20, false alarm).
Pending confirmation on real hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found on real hardware: activating Superfast HiRes Spectrum mode
(POKE 2045,172) does not turn on Chroma81 colour by itself -- screen
came up monochrome despite correct INK/PAPER/attributes. EightyOne
shows colour regardless, which is an emulator bug (needs reporting/
fixing there separately, not something to work around here).

Fix: boot1.asm now also writes to the Chroma81 port ($7FEF) right
after activating Spectrum mode:
  ld bc, $7FEF / ld a, 39 / out (c), a
(bit5=1 color on, bit4=0 char-code mode)

boot1.bin reassembled with zxbasm.py (34 bytes, was 27 -- exactly the
3 new instructions). Shared stage 1 loader used by every zx81sd
program: no program needs recompiling, just copy the updated
BOOT1.BIN to the SD card.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@boriel boriel self-requested a review July 5, 2026 21:21

@boriel boriel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

@boriel boriel merged commit 97cf3f4 into boriel-basic:main Jul 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants