Ensamblador ZX Spectrum – Utilidades
Esta es una recopilación de distintas rutinas que pueden ser útiles para tratar determinados aspectos del ZX Spectrum.
La entrada irá recibiendo actualizaciones.
Tabla de contenidos
Modelo de ZX Spectrum
En más de una ocasión necesitaremos saber en que modelo de ZX Spectrum se va a ejecutar nuestro programa, 16K, 48K o 128K, y en éste último caso si está en modo 48K o 128K.
La manera de obtener el modelo de ZX Spectrum que voy a mostrar se basa en lo expuesto por Sergio thEpOpE en el grupo de Curso Basic ZX de Telegram.
Según el valor que encontremos en la posición de memoria 0x4B (75), podemos saber si se trata de un modelo 128K o por el contrario si es un modelo de 16/48K. Si al leer el valor de esta posición de memoria obtenemos 0x6E (110), el programa se está ejecutando en un modelo de 128K, de lo contrario lo hace en un modelo 16/48K.
Si se está ejecutando en un modelo de ZX Spectrum de 16/48K, es interesante saber en cuál de ellos es, información que encontramos en las posiciones 0x5CB4 (23.732) y 0x5CB5 (23.733). En estas direcciones encontraremos la cantidad total de memoria: 0x7FFF (32.767) en modelos de 16K o 0xFFFF (65.535) en modelos de 48K. Como vemos, el segundo byte (el menos significativo) tiene el mismo valor, cambiando solo el más significativo, que se encuentra en la posición de memoria 0x5CB5 (23.733). Si leemos el valor de esta posición y obtenemos 0x7F (127) estamos ejecutando en un modelo de 16K, si obtenemos 0xFF (255) lo estamos haciendo en uno de 48K.
En los modelos de 128K el valor es el mismo que en los modelos de 48K.
Si se está ejecutando en un modelo de ZX Spectrum de 128K, es posible que se esté ejecutando en modo 48K o 128K, lo cual también podemos saber. Primero obtenemos el valor de la posición de memoria 0x5C3B (23.611), hacemos la división entre 16 y el resultado lo dividimos entre 2. Si el resultado de esta operación es igual a hacer la división entera del valor de la posición de memoria entre 16 y al resultado hacerle la división entera entre 2, se estaría ejecutando en modo 48K, de lo contrario en modo 128K.
SI INT(PEEK(23611)/16)/2 = INT(INT(PEEK(32611)/16/2)
modo 48K
SI NO
modo 128K
FIN SI
O lo que es lo mismo, si el resultado de la división entera entre el valor de la posición de memoria 0x5C3B (23.611) y 16 es par está en modo 48K, de lo contrario está en modo 128K.
En ensamblador para ZX Spectrum se nos permite, con rotaciones hacia la derecha, hacer divisiones entre potencias de 2 (16 = 2^4), por lo que tendríamos que hacer cuatro rotaciones para realizar la división entre 16 y luego comprobar si es resultado es par, el bit 0 está a cero.
ld a, $cc ; A = 1100 1100
; Usamos desplazamientos para realizar la división entre 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
; Comprueba si A es par
bit $00, a
jr z, EsPar ; Si el bit 0 es 0, es par, salta
EsImpar:
; Código de EsImpar
EsPar:
; Código de EsPar
Si os fijáis, hemos desplazado los bits 4 a 7 hasta los bits 0 a 3 y los 4 a 7 se han puesto a cero, comprobando finalmente si el bit 0, anteriormente bit 4, es 0 en el caso de ser par o 1 en el caso de ser impar. Nos podemos ahorrar desplazamientos y comprobar directamente el bit 4.
En base a lo que hemos visto, la rutina para averiguar en qué modelo de ZX Spectrum estamos ejecutando un programa podría ser la siguiente:
; ZXModel
;
; Obtiene el modelo de ZX
;
; Entrada: nada
;
; Salida: BC = modelo de ZX
; 0 = 16K
; 1 = 48K
; 2 = 128K
; 3 = 128K en modo 48K
; Altera el valor de los registros BC y AF.
ZXModel:
ld bc, $00 ; BC se utiliza para devolver el valor a BASIC
ld a, ($4b) ; A = dirección modelo
cp $6e
jr z, is128 ; $6E, 128, salta
is16or48:
ld a, ($5cb5) ; A = cantidad memoria, parte alta
rla ; Rota a la derecha para comprobar si es $BF o $FF
ret nc ; NC = 16K, sale ($BF)
inc c ; C+=1
ret ; 48K, sale
is128:
ld c, $02 ; C = 128K
ld a, ($5c3b) ; A = dirección de modo
; Sí INT(PEEK 23611/16)/2 = INT(INT(PEEK 23611/16)/2) = 48K.
; Dividir ente 16 es como desplazar 4 veces hacia de derecha,
; el bit 4 pasa al bit 0.
; Saber si es divisible entre dos es como saber si es par, el bit 0 = 0
; Todo se resume en evaluar el bit 4, si es 0 es modo 48K, si es 1 es modo 128K.
bit $04, a ; ¿Bit 4?
ret nz ; !0, modo 128K, sale
inc c ; C+=1
ret ; Modo 48K
Para probar esta rutina, deberéis compilarla y cargarla, por ejemplo, en la dirección de memoria 0x7D00 (32.000), para que corra en cualquier modelo o clon de ZX Spectrum. Una vez cargada la rutina, si tecleáis PRINT USR 32000 saldrá impreso en pantalla el valor correspondiente al modelo de ZX Spectrum en el que se está ejecutando.
El valor devuelto por un programa ensamblador al BASIC del ZX Spectrum se hace a través del registro BC.
PAL o NTSC
En ocasiones, nos puede interesar saber si el ZX Spectrum en el que se está ejecutando nuestro programa es sistema PAL o NTSC.
¿Qué diferencia hay?
En sistema PAL se producen 50 interrupciones por segundo, mientras que en sistema NTSC el número de interrupciones por segundo asciende a 60.
¿Cómo nos afecta?
Nos puede llegar a afectar y mucho. Si nuestro programa se apoya en las interrupciones para temporizar, por ejemplo ejecutando la rutina de movimiento del enemigo cada cinco interrupciones, en un sistema PAL el enemigo se moverá cinco veces por segundo, mientras que en NTSC se moverá seis veces por segundo, resultando el movimiento del enemigo más rápido en NTSC que en PAL, pudiendo influir en la jugabilidad.
¿Cómo averiguamos el sistema?
En este caso me he apoyado en el emulador Es.pectrum de Habisoft, que podéis descargar desde aquí.
Este maravilloso emulador emula una vasta variedad de modelos de ZX Spectrum y sus clones, por lo que es ideal para probar el programa que he implementado para detectar si es PAL o NTSC, y además es gratuito.
El programa es sencillo, activa la interrupciones del ZX Spectrum en modo 2 y la primera vez que se ejecuta la rutina de interrupciones pone swCount a uno para indicar que se vaya incrementando la variable counter en el bucle principal. La segunda vez que se ejecuta la rutina de interrupciones pone swCount a dos para indicar que se debe salir el programa. El valor de counter se carga en BC para que se pueda obtener desde Basic.
En resumen, el programa cuenta las veces que se ejecuta el bucle principal entre dos interrupciones. En base a los resultados arrojados por la pruebas, podríamos decir que NTSC arroja valores inferiores a 100, mientras que PAL los arroja superiores.
Todo esto hay que tratarlo con mucho cuidado, si observáis los resultados podéis ver valores que están al límite.
; Programa para averiguar si es NTSC o PAL
; Compilar: pasmo --name PAL_NTSC --tapbas PAL_NTSC.asm PAL_NTSC.tap
; Una vez cargado, ejecutar PRINT USR 32000 para ver el resultado
; No compatible con modelo de 16K
; Resultados de PRINT USR 32000 usando emulador Es.pectrum
;
; https://habisoft.com/espectrum/
;
; Sinclair:
; 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
; Entrada: prepara las interrupciones
main:
ld hl, isr ; HL = dirección rutina interrupciones
ld ($feff), hl ; Guarda en $feff la dirección
ld a, $fe ; de la rutina isr
ld i, a ; I = $fe
im 2 ; Pasa a modo 2 de interrupciones
; Bucle, espera a que swCount sea 1 para empezar a contar
; Cuando swCount es 2, sale del bucle y fin de programa
loop:
ld a, (swCount) ; A = indicador para saber si contar
or a ; ¿A = 0?
jr z, loop ; Z = sí, salta
cp $02 ; ¿A = 2?
jr z, exit ; Z = sí, salta
ld hl, counter ; HL = dirección contador
inc (hl) ; Contador+=1
jr loop ; Bucle
; Salida del programa
; Pone las interrupciones en modo 1
; Pone el contador en BC para que pueda ser recuperado
; desde BASIC
exit:
im 1 ; Pasa a modo 1 de interrupciones
ld b, $00
ld a, (counter)
ld c, a ; BC = contador, para devolver el resultado
ret ; Vuelta al BASIC
; Rutina de interrupciones
; En cada interrupción incrementa swCount
isr:
push hl ; Preserva HL
ld hl, swCount
inc (hl) ; Incrementa el indicador para saber si contar
pop hl ; Recupera HL
ei ; Habilita interrupciones
ret ; Sale
; Variables
swCount: ; Indicador para saber si contar
db $00
counter: ; Contador
db $00