ZX Spectrum Assembly – Utilities
This is a compilation of different routines that can be useful to deal with certain aspects of the ZX Spectrum.
The entry will be updated in the future.
Translated with DeepL.
ZX Spectrum model
On more than one occasion we will need to know which model of ZX Spectrum our program is going to run on, 16K, 48K or 128K, and in the latter case if it is in 48K or 128K mode.
The way to get the ZX Spectrum model I’m going to show is based on what Sergio thEpOpE exposed in the Basic ZX Course group on Telegram.
Depending on the value that we find in the memory position 0x4B (75), we can know if it is a 128K model or on the contrary if it is a 16/48K model. If when reading the value of this memory position we get 0x6E (110), the program is running in a 128K model, otherwise it is running in a 16/48K model.
If you are running on a 16/48K ZX Spectrum model, it is interesting to know which one it is, information that can be found at locations 0x5CB4 (23,732) and 0x5CB5 (23,733). In these addresses we will find the total amount of memory: 0x7FFF (32,767) in 16K models or 0xFFFFFF (65,535) in 48K models. As we can see, the second byte (the least significant one) has the same value, changing only the most significant one, which is located at memory location 0x5CB5 (23,733).
If we read the value of this position and get 0x7F (127) we are executing in a 16K model, if we get 0xFF (255) we are executing in a 48K model. For 128K models the value is the same as for 48K models.
If it is running on a 128K ZX Spectrum model, it is possible that it is running in 48K or 128K mode, which we can also find out. First we get the value of the memory location 0x5C3B (23,611), we divide by 16 and the result is divided by 2. If the result of this operation is equal to the integer division of the value of the memory location by 16 and the result is divided by 2, it is running in 48K mode, otherwise in 128K mode.
IF INT(PEEK(23611)/16)/2 = INT(INT(PEEK(32611)/16/2)
48K model
ELSE
128K model
END IFIn other words, if the result of the integer division between the value of memory location 0x5C3B (23,611) and 16 is even, it is in 48K mode, otherwise it is in 128K mode.
In ZX Spectrum assembly we are allowed, with right-hand rotations, to make divisions by powers of 2 (16 = 2^4), so we would have to make four rotations to make the division by 16 and then check if the result is even, bit 0 is zero.
ld a, $cc ; A = 1100 1100
; We use displacements to perform the division by 16
sra a ; A = A/2 = 0110 0110
sra a ; A = A/4 = 0011 0011
sra a ; A = A/8 = 0001 1001
sra a ; A = A/16 = 0000 1100
; Check if A is even
bit $00, a
jr z, isEven ; If bit 0 is 0, it is even, it skips
isOdd:
; isOdd code
isEven:
; isEven codeIf you notice, we have shifted bits 4 to 7 to bits 0 to 3 and bits 4 to 7 have been set to zero, finally checking if bit 0, formerly bit 4, is 0 if it is even or 1 if it is odd. We can save displacements and check bit 4 directly.
Based on what we have seen, the routine to find out on which ZX Spectrum model we are running a program could be the following:
; ZXModel
;
; Gets the ZX model
;
; Entry: nothing
;
; Output: BC = ZX model
; 0 = 16K
; 1 = 48K
; 2 = 128K
; 3 = 128K in 48K mode
; Alters the value of the BC and AF registers.
ZXModel:
ld bc, $00 ; BC is used to return the value to BASIC
ld a, ($4b) ; A = model address
cp $6e
jr z, is128 ; $6E, 128, jump
is16or48:
ld a, ($5cb5) ; A = amount of memory, upper part
rla ; Rotate to the right to check if it is $BF or $FF
ret nc ; NC = 16K, jump ($BF)
inc c ; C+=1
ret ; 48K, jump
is128:
ld c, $02 ; C = 128K
ld a, ($5c3b) ; A = mode address
; IF INT(PEEK 23611/16)/2 = INT(INT(PEEK 23611/16)/2) = 48K.
; Dividing by 16 is like shifting 4 times to the right,
; bit 4 is set to bit 0
; Knowing if it is divisible by two is like knowing if it is even, bit 0 = 0
; It all boils down to evaluating bit 4, if it is 0 it is 48K mode, if it is 1 it is 128K mode
bit $04, a ; Bit 4?
ret nz ; !0, 128K mode, exit
inc c ; C+=1
ret ; 48K modeTo test this routine, you must compile it and load it, for example, at memory address 0x7D00 (32,000), so that it will run on any ZX Spectrum model or clone. Once the routine is loaded, if you type PRINT USR 32000, the value corresponding to the ZX Spectrum model on which it is running will be printed on the screen.
The value returned by an assembly program to the ZX Spectrum BASIC is done through the BC register.

PAL vs NTSC
Sometimes, we may be interested in knowing if the ZX Spectrum on which our program is running is PAL or NTSC system.
What is the difference?
In PAL system there are 50 interruptions per second, while in NTSC system the number of interruptions per second is 60.
How does it affect us?
It can affect us a lot. If our program relies on interrupts for timing, for example by executing the enemy movement routine every five interrupts, on a PAL system the enemy will move five times per second, while on NTSC it will move six times per second, resulting in faster enemy movement on NTSC than on PAL, which can affect gameplay.
How do we find out the system?
In this case I have relied on Habisoft’s Es.pectrum emulator, which you can download from here.
This wonderful emulator emulates a wide variety of ZX Spectrum models and their clones, so it’s ideal for testing the program I’ve implemented to detect if it’s PAL or NTSC, and it’s also free.
The program is simple, activate the ZX Spectrum interrupts in mode 2 and the first time the interrupt routine is executed it sets swCount to one to indicate that the counter variable in the main loop is being incremented. The second time the interrupt routine is executed it sets swCount to two to indicate that the program should exit. The value of counter is loaded into BC so that it can be obtained from Basic.
In short, the program counts the number of times the main loop is executed between two interrupts. Based on the test results, we could say that NTSC yields values below 100, while PAL yields values above 100.
All this has to be treated with great care, if you look at the results you can see values that are at the limit.
; Program to find out if it's NTSC or PAL
; Compile: pasmo --name PAL_NTSC --tapbas PAL_NTSC.asm PAL_NTSC.tap
; Once loaded, run PRINT USR 32000 to see the result
; Not compatible with 16K model
; Results of PRINT USR 32768 using emulator Es.pectrum
;
; https://habisoft.com/espectrum/
;
; Sinclair ZX Spectrum:
; 48/48+ 216
; 48 NTSC 64
; 128 230
; 48 ar ¿?
; 48 se 215
; Timex:
; TS 2068 64
; TC 2068 216
; TC 2048 216
; Komputer 2086 216
; Investrónica:
; 48+ es 215
; 128+ es 230
; Inves+ 230
; Amstrad:
; +2 230
; +2 es 230
; +2 fr 230
; +2 ar ¿?
; +2A 4.0 230
; +2A 4.1 230
; +2A 4.0 es 230
; +2A 4.1 es 230
; +2A ar ¿?
; +3 4.0 230
; +3 4.1 230
; +3 4.0 es 230
; +3 4.1 es 230
; Pentagon:
; Pentagon 128 241
; Pentagon 512 241
; Pentagon 1024 SL 227
; ZS Rechearch:
; Leningrad 184
; Scorpion ZS-256 151
; Scorpion ZS-256 Turbo+ 215
; Microdigital:
; Tk90x pt-BR 73
; Tk90x es-AR 233
; Tk95 pt-BR 72
; Tk95 es-AR 233
; ATM:
; ATM-Turbo 215
; ATM-Turbo 2+ 176
; Otros:
; Orel BK-08 ¿?
; Dubna 48K 15
; HC-91 230
; BK-001 105
; Foton-Ik03 ¿?
; Alf TV Game (Elf) ¿?
; Vesta IK-30 184
; Mods:
; +3e 230
; +3e es 230
; +2e 230
; +2e es 230
; Pentagon es 241
; Virtuales:
; Spec256 48K 216
; Spect256 128K 230
org $8000
; Input: prepare for interruptions
main:
ld hl, isr ; HL = interrupt routine address
ld ($feff), hl ; Store in $feff the address
ld a, $fe ; of routine isr
ld i, a ; I = $fe
im 2 ; Go to interrupt mode 2
; Loop, waits for swCount to be 1 to start counting
; When swCount is 2, exit loop and end of program
loop:
ld a, (swCount) ; A = indicator to know whether to count
or a ; A = 0?
jr z, loop ; Z = yes, jump
cp $02 ; A = 2?
jr z, exit ; Z = yes, jump
ld hl, counter ; HL = counter address
inc (hl) ; Counter+=1
jr loop ; Loop
; Exit from the programme
; Sets interrupts to mode 1
; Sets the counter to BC so that it can be retrieved
; from BASIC
exit:
im 1 ; Go to interrupt mode 1
ld b, $00
ld a, (counter)
ld c, a ; BC = counter, to return the result
ret ; Back to BASIC
; Interruptions routine
; At each interrupt it increments swCount
isr:
push hl ; Preserves HL
ld hl, swCount
inc (hl) ; Increases the indicator to know if counting
pop hl ; Retrieves HL
ei ; Enables interruptions
ret ; Exit
; Vars
swCount: ; Indicator to know whether to count
db $00
counter: ; Counter
db $00


Juanma, en el libro e la parte de la impresión de la linea central hay una errata ,donde pone compilar va ensamblar. Saludos
Muchas gracias por el aviso, ¿me puedes decir la página?