Skip to content

Instantly share code, notes, and snippets.

@damieng
Created February 23, 2026 07:06
Show Gist options
  • Select an option

  • Save damieng/f276c7f03a49c2b030200e94f386f2ab to your computer and use it in GitHub Desktop.

Select an option

Save damieng/f276c7f03a49c2b030200e94f386f2ab to your computer and use it in GitHub Desktop.
Full 64KB banked memory test for the Spectrum +3
DEVICE ZXSPECTRUM128
ORG $4800 ; Bank 5 (display bank)
; ---------------------------------------------------------
; All-RAM mode 0 ($1FFD=01): Banks 0,1,2,3 at $0000/$4000/$8000/$C000.
;
; Strategy:
; • Bootstrap runs in Bank 5. Prints title/banks to display.
; ULA always reads Bank 5, so display is visible throughout the test.
; • Copies this code to Bank 1 (via $7FFD paging), then enables All-RAM.
; • Test runs in All-RAM. Every 256 addresses, briefly exits All-RAM,
; writes current address to Bank 5 display, then re-enters All-RAM.
; Registers (HL, DE) are preserved in the CPU across the bank switch;
; only stack must not straddle a switch (all PUSHes happen after exit,
; all POPs happen before re-entry).
; • Bank 0 ($0000-$3FFF): fully tested.
; • Bank 1 ($4000-$7FFF): skip first 6912 bytes ($4000-$5AFF, screen+code),
; test $5B00-$7FFF (above code and stack).
; • Bank 2 ($8000-$BFFF): fully tested.
; • Bank 3 ($C000-$FFFF): fully tested.
; • Each address: all 256 values written, read back and compared.
; • On PASS/FAIL: exit All-RAM (Bank 5 back at $4000), write result.
; ---------------------------------------------------------
BOOTSTRAP:
DI
LD SP, $57FF ; Bank 5, below attributes; same SP used in All-RAM
; Copy ROM charset before All-RAM removes ROM access
LD HL, $3D00
LD DE, FONT_BUF
LD BC, 768
LDIR
; Clear first third of screen pixels ($4000-$47FF)
LD HL, $4000
LD (HL), 0
LD DE, $4001
LD BC, $07FF
LDIR
; Set all attributes: white ink on black paper
LD HL, $5800
LD (HL), $07
LD DE, $5801
LD BC, $02FF
LDIR
; Print to Bank 5 display — stays visible during All-RAM (ULA reads Bank 5)
LD DE, $4000
LD HL, STR_TITLE
CALL PRINT_STRING
LD DE, $4020
LD HL, STR_BANKS
CALL PRINT_STRING
; Page Bank 1 to $C000, copy our code there
; In All-RAM mode 0, Bank 1 sits at $4000-$7FFF.
; Our code at $4800 will be at $C800 while Bank 1 is paged to $C000.
LD A, 1
LD BC, $7FFD
OUT (C), A
LD HL, $4800
LD DE, $C800
LD BC, CODE_SIZE
LDIR
; Restore default paging ($7FFD=0)
XOR A
LD BC, $7FFD
OUT (C), A
; Enable All-RAM mode 0 — execution continues from Bank 1 at $4800
LD A, %00000001
LD BC, $1FFD
OUT (C), A
; --- Bank 0: $0000-$3FFF ---
LD A, 1 ; blue border
OUT ($FE), A
LD HL, $0000
LD DE, $4000 ; 16384 bytes
CALL RUN_TEST_BLOCK
; --- Bank 1: $5B00-$7FFF (skip $4000-$5AFF: screen+code+stack) ---
LD A, 3 ; yellow border
OUT ($FE), A
LD HL, $5B00
LD DE, $2500 ; $8000 - $5B00 = 9472 bytes
CALL RUN_TEST_BLOCK
; --- Bank 2: $8000-$BFFF ---
LD A, 5 ; cyan border
OUT ($FE), A
LD HL, $8000
LD DE, $4000
CALL RUN_TEST_BLOCK
; --- Bank 3: $C000-$FFFF ---
LD A, 6 ; magenta border
OUT ($FE), A
LD HL, $C000
LD DE, $4000
CALL RUN_TEST_BLOCK
; SUCCESS — exit All-RAM, Bank 5 back at $4000 (display writable again)
XOR A
LD BC, $1FFD
OUT (C), A
LD A, 4 ; green border
OUT ($FE), A
LD DE, $4080
LD HL, STR_PASS
CALL PRINT_STRING
; ~2 second delay so user can see the result
LD B, 8
DELAY_OUTER:
LD HL, 0
DELAY_INNER:
DEC HL
LD A, H
OR L
JR NZ, DELAY_INNER
DJNZ DELAY_OUTER
; Return to BASIC
EI
JP $0000
; ---------------------------------------------------------
; BLOCK TESTER — HL = start address, DE = byte count
; For each address, writes every value 0-255 and reads back.
; Every 256 addresses (L=0), briefly exits All-RAM to update
; the Bank 5 display with the current address, then re-enters.
; Stack discipline: NO push/pop straddles a bank switch.
; All pushes after exit OUT, all pops before re-entry OUT.
; ---------------------------------------------------------
RUN_TEST_BLOCK:
BLOCK_LOOP:
LD A, L
OR A
JR NZ, NO_DISP
; --- display update: exit All-RAM, write address, re-enter ---
; HL = current address, DE = remaining count — both live in registers,
; safe across the bank switch with no push needed yet.
XOR A
LD BC, $1FFD
OUT (C), A ; exit All-RAM — Bank 5 now at $4000
; NOW on Bank 5 stack — safe to push
PUSH DE ; save remaining count
PUSH HL ; save test address (DISP_HEX will clobber HL)
CALL DISP_HEX ; show current address on line 4 (HL passed in)
POP HL ; restore test address
POP DE ; restore remaining count
; Re-enter All-RAM — no push/pop between here and the OUT
LD A, %00000001
LD BC, $1FFD
OUT (C), A ; Bank 1 back at $4000, same code, same address
NO_DISP:
; Test every value 0-255 at address HL
LD C, 0
VALUE_LOOP:
LD (HL), C ; write value
LD A, (HL) ; read back
CP C ; compare
JP NZ, FAIL ; HL = failing address, C = failing value
INC C
JR NZ, VALUE_LOOP ; loop until C wraps 255->0 (all 256 values done)
INC HL
DEC DE
LD A, D
OR E
JR NZ, BLOCK_LOOP
RET
; ---------------------------------------------------------
; DISP_HEX — show current address HL on line 4
; Called from Bank 5 (after exit All-RAM), so stack is Bank 5.
; HL is clobbered; caller saves/restores it.
; ---------------------------------------------------------
DISP_HEX:
LD DE, $4080 ; line 4, col 0
CALL PRINT_ADDR ; prints HL as 4 hex digits; DE advances to col 4
LD HL, STR_TESTING
JP PRINT_STRING ; tail-call; trailing spaces clear old content
; ---------------------------------------------------------
; PRINT_ADDR — print HL as 4 hex digits at DE
; ---------------------------------------------------------
PRINT_ADDR:
LD A, H
PUSH HL
CALL PRINT_BYTE
POP HL
LD A, L
JP PRINT_BYTE ; tail-call
; ---------------------------------------------------------
; FAILURE HANDLER
; HL = failing address, preserved in registers across the OUT
; Exit All-RAM before displaying so Bank 5 display is writable
; ---------------------------------------------------------
FAIL:
LD A, 2 ; red border
OUT ($FE), A
XOR A ; HL unaffected by XOR A / LD BC / OUT
LD BC, $1FFD
OUT (C), A ; exit All-RAM — Bank 5 at $4000, HL still = fail addr
LD DE, $4080
CALL PRINT_ADDR
LD HL, STR_FAIL
CALL PRINT_STRING
HALT
; ---------------------------------------------------------
; PRINT_BYTE — print A as two hex digits at DE
; ---------------------------------------------------------
PRINT_BYTE:
PUSH AF
RRCA : RRCA : RRCA : RRCA
CALL PRINT_NIBBLE
POP AF
; fall through
PRINT_NIBBLE:
AND $0F
CP 10
JR C, NIBBLE_DIG
ADD A, 'A' - 10
JR PRINT_CHAR
NIBBLE_DIG:
ADD A, '0'
; fall through
PRINT_CHAR:
SUB ' '
LD L, A
LD H, 0
ADD HL, HL
ADD HL, HL
ADD HL, HL ; HL = (char - 32) * 8, 16-bit
PUSH DE
LD BC, FONT_BUF
ADD HL, BC
REPT 8
LD A, (HL)
LD (DE), A
INC HL
INC D ; next scanline
ENDR
POP DE
INC E ; next column
LD D, $40 ; reset to top scanline
RET
PRINT_STRING:
LD A, (HL)
OR A
RET Z
PUSH HL
CALL PRINT_CHAR
POP HL
INC HL
JR PRINT_STRING
; ---------------------------------------------------------
; STRING DATA
; ---------------------------------------------------------
STR_TITLE: DB "DamienG 64kb RAM test", 0
STR_BANKS: DB "BANKS 0/1/2/3", 0
STR_TESTING: DB " Testing ", 0
STR_PASS: DB "PASS ", 0
STR_FAIL: DB " FAIL ", 0
; ---------------------------------------------------------
; FONT BUFFER — 96 chars (ASCII 32-127), 8 bytes each
; ---------------------------------------------------------
ALIGN 8
FONT_BUF:
DEFS 768, 0
CODE_END:
CODE_SIZE EQU CODE_END - $4800
SAVETAP "64kbramtest.tap", CODE, "RAMtest", BOOTSTRAP, CODE_SIZE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment