0x05 Ensamblador ZX Spectrum Marciano – Interrupciones y disparo

En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a implementar el manejo de las interrupciones; si queréis saber más sobre ellas os recomiendo que leáis el capítulo dedicado a la mismas en el curso de Compiler Software, y la forma de implementarlas para el modelo 16K.

El ZX Spectrum genera un total de cincuenta interrupciones por segundo en sistemas PAL, y sesenta en sistemas NTSC.

Antes de continuar comprobamos cuánto ocupa nuestro programa, veremos que ronda los mil seiscientos bytes.

Interrupciones

La rutina que se ejecuta cuando se genera una interrupción la vamos a implementar en el archivo Int.asm, así que lo creamos.

Siguiendo lo explicado en el curso de Compiler Software, vamos a cargar $28 (40) en el registro I y nuestra rutina en la dirección $7e5c (32348), con lo que dejamos cuatrocientos diecinueve bytes para la rutina. Teniendo en cuenta que el programa lo cargamos en $5dad (23981), nos quedan ocho mil trescientos sesenta y siete bytes para nuestro juego.

Abrimos el archivo Main.asm y, justo antes de Main_loop, añadimos las líneas para preparar las interrupciones.

di
ld      a, $28
ld      i, a
im      2
ei

Deshabilitamos las interrupciones, DI, cargamos $28 (40) en el registro A, LD A, $28, y cargamos A en el registro I, LD I, A. Cambiamos al modo de interrupción a dos, IM 2, y activamos las interrupciones, EI.

La rutina la vamos a implementar en el archivo Int.asm, de manera que lo abrimos y añadimos las líneas siguientes:

org     $7e5c

Isr:
push    hl
push    de
push    bc
push    af

Cargamos la rutina Isr en la dirección $7E5C (32348), ORG $7E5C y preservamos el valor de HL, DE, BC y AF, PUSH HL, PUSH DE, PUSH BC, PUSH AF.

De momento no hacemos nada más y salimos.

Isr_end:
pop    	af
pop    	bc
pop    	de
pop    	hl
ei
reti

Recuperamos el valor de AF, BC, DE y HL, POP AF, POP BC, POP DE, POP HL, activamos las interrupciones, EI, y salimos, RETI.

Ahora vamos a Main.asm y, al final, justo antes de END Main, incluímos el archivo Int.asm.

Compilamos, cargamos en el emulador y probamos. Aparentemente no pasa nada, pero ¿qué pasa si comprobamos lo que ocupa ahora nuestro programa? Pues que ocupa la friolera de más de nueve mil bytes. ¿Cómo es esto posible? Al cargar la rutina en la dirección $7E5C, PASMO rellena con ceros todo el espacio desde dónde acababa el programa anteriormente hasta dónde acaba ahora, por eso ocupa tanto. Si cargamos en un emulador no suele importar, la carga la podemos hacer inmediata, pero en un ZX Spectrum real estamos añadiendo tiempo de carga innecesariamente.

Compilamos en múltiples ficheros

Para evitar que nuestro programa crezca innecesariamente, vamos a compilar por separado el archivo Int.asm y el resto, y también vamos a prescindir del cargador que nos genera PASMO y vamos a hacer el nuestro propio.

Creación del cargador

Desde el emulador vamos a generar un cargador BASIC personalizado, que vamos a grabar como el archivo Cargador.tap. El código del cargador es el siguiente:

10 CLEAR 23980
20 LOAD ""CODE 
30 LOAD ""CODE 32348
40 RANDOMIZE USR 23981

Grabamos nuestro cargador con la siguiente instrucción:

SAVE "BATALLAESP" LINE 10

De esta manera, al cargar el programa, se auto ejecuta en la línea diez.

Compilación en varios ficheros

Vamos a compilar por separado el archivo Int.asm, generando el archivo Int.tap, y el resto del programa generando el archivo Marciano.tap. Por último, vamos a concatenar los ficheros Cargador.tap, Marciano.tap e Int.tap en el fichero BatallaEspacial.tap.

Dado que realizar estas operaciones cada vez que compilemos y queramos ver los resultados es tedioso, vamos a crear un script; para los que uséis Windows, cread en la carpeta Paso05 el archivo make.bat, para los que usamos Linux, ejecutamos desde la línea de comandos:

touch make

Luego damos permisos de ejecución.

chmod +x make

Antes de seguir, abrimos el archivo Main.asm y borramos, casi al final, el include del archivo Int.asm.

Y ahora podemos editar el archivo make o make.bat. Las dos primeras líneas son comunes tanto en Windows como en Linux.

pasmo --name Marciano --tap Main.asm Marciano.tap --public
pasmo --name Int --tap Int.asm Int.tap

Primero compilamos el programa y luego compilamos el archivo Int.asm y generamos el arhivo Int.tap. Observad que en lugar de –tapbas hemos puesto –tap, pues el cargador BASIC lo hemos hecho a mano.

Por último, combinamos Cargador.tap, Marciano.tap e Int.tap en BatallaEspacial.tap.

Para los que usamos Linux, añadimos la línea siguiente al final del archivo make:

cat Cargador.tap Marciano.tap Int.tap > BatallaEspacial.tap

Para los que usáis Windows, la línea que tenéis que añadir es la siguiente:

copy Cargador.tap+Marciano.tap+Int.tap BatallaEspacial.tap

A partir de este momento, la manera de compilar será ejecutando make o make.bat, y en el emulador cargaremos el archivo BatallaEspacial.tap.

Compilamos, cargamos en el emulador y vemos que todo sigue igual, pero el tamaño de BatallaEspacial.tap esta muy por debajo de nueve mil bytes.

Ralentizamos la nave

Vimos en la entrega anterior que la nave se movía muy rápido, para solucionar esta cuestión vamos a usar las interrupciones para mover la nave un máximo de cincuenta veces por segundo (en sistemas PAL, sesenta en NTSC), es decir, vamos a mover la nave cuándo salte la interrupción.

Abrimos el archivo Var.asm y al inicio del mismo añadimos lo siguiente:

; ----------------------------------------------------------------------------
; Indicadores
;
; Bit 0 -> se debe mover la nave            0 = No, 1 = Si
; ----------------------------------------------------------------------------
flags:
db $00

Volvemos a Int.asm y tras PUSH AF añadimos las líneas siguientes:

ld      hl, flags
set     $00, (hl)

Cargamos en HL la dirección de memoria de flags, LD HL, flags, y ponemos el bit cero a uno, SET $00, (HL).

Ahora vamos al archivo Game.asm y justo debajo de la etiquetaMoveShip, añadimos las líneas siguientes:

ld      hl, flags		    ; Cargamos la dirección de memoria de flags en HL
bit     $00, (hl)          	; Comprueba si el bit 0 está activo
ret     z                  	; Si no es así, sale                
res     $00, (hl)          	; Desactiva el bit 0 de flags

Cargamos en HL la dirección de memoria de flags, LD HL, flags, comprobamos si hay que mover la nave, BIT $00, (HL), y si no es así salimos, RET Z. Si hay que mover la nave ponemos el bit cero a cero, SET $00, (HL), de esta manera no volveremos a mover la nave hasta que salte una interrupción y vuelva a poner a uno el bit cero de flags.

Esto que hemos implementado hará que nuestra nave se mueva cincuenta veces por segundo (o sesenta, según el sistema), así que compilamos y …

Efectivamente, tenemos errores de compilación.

ERROR on line 10 of file Int.asm

ERROR: Symbol ‘flags’ is undefined

Hasta ahora, en el archivo Main.asm incluíamos todos los archivos .asm que tenemos, pero hemos quitado el include del archivo Int.asm para compilarlo por separado, por lo que en Int.asm no se conoce la etiqueta flags. La solución es sencilla, en Int.asm hay que sustituir LD HL, flags por LD HL, direccionMemoria.

Echemos un vistazo a la línea que usamos para compilar.

pasmo --name Marciano --tap Main.asm Marciano.tap --public

El último parámetro, –public, genera un fichero con ese nombre donde podemos ver cada una de las etiquetas de nuestro programa, en que dirección de memoria se encuentran. En mi caso, flags está en la dirección de memoria $5dee, por lo que solo hay que ir a Int.asm y sustituir LD HL, flags por LD HL, $5DEE.

Ahora sí, compilamos, cargamos en el emulador y vemos que la nave se mueve más lenta, pero seguimos teniendo un problema; poned una instrucción NOP al inicio de Main.asm, justo antes de la etiqueta Main.

Compilad y veréis que compila bien, cargad en el emulador y veréis que ha dejado de funcionar. Si ahora vais a –public, veréis que la etiqueta flags está en la dirección $5DEF, sin embargo en el archivo Int.asm el valor que se carga en HL es $5DEE. Cada vez que modifiquemos algo de código, es muy probable que la dirección de memoria de flags cambie, así que tenemos que asegurarnos de cambiarla también en Int.asm.

Para evitar que cambie la dirección de memoria de flags, vamos a abrir el archivo Var.asm, vamos a cortar la declaración de flags y la vamos a pegar al inicio del archivo Main.asm, justo debajo de ORG $5DAD, de esta manera nos aseguramos que flags siempre esté en la dirección de memoria $5DAD; no olvidéis sustituir $5DEE por $5DAD en el archivo Int.asm.

Compilamos, cargamos en el emulador y todo vuelve a funcionar, pero cuidado, funciona porque hemos inicializado flags a cero, que es el código de la instrucción NOP, que lo que hace es tardar cuatro ciclos de reloj en ejecutarse, nada más. Si la iniciáramos con otro valor, por ejemplo $C9, el programa nada más ejecutarse volvería al BASIC, ya que $C9 es RET, probad y veréis.

Ensamblador ZX Spectrum, interrupciones y disparo
Ensamblador ZX Spectrum, interrupciones y disparo

Esto tiene fácil solución, antes de la etiqueta flags, añadimos JR Main, aunque dado que solo vamos a necesitar la etiqueta flags y la iniciamos a cero, no tenemos problema, pero tened cuidado con esto.

No es necesario añadir JR Main, en caso de añadirlo, la dirección de flags cambia.

Implementamos el disparo

Igual que hicimos con la nave, vamos a incluir las constantes necesarias para el manejo del disparo en el archivo Const.asm.

; Código de carácter del disparo y tope
FIRE_GRAPH: EQU $91
FIRE_TOP_T: EQU COR_Y

De igual manera, en el archivo Var.asm vamos a añadir una etiqueta para guardar la posición actual del disparo.

; ----------------------------------------------------------------------------
; Disparo
; ----------------------------------------------------------------------------
firePos:
dw $0000

En Print.asm vamos a implementar la rutina que pinta el disparo.

PrintFire:
ld     	a, $02
call   	Ink

Cargamos en A el color de tinta rojo, LD A, $02, y llamamos a cambiar la tinta, CALL Ink.

ld      bc, (firePos) 
call    At

Cargamos en BC la posición actual del disparo, LD BC, (firePos), y llamamos a posicionar el cursor, CALL At.

ld     	a, FIRE_GRAPH 
rst    	$10 

ret

Cargamos en A el código de carácter del disparo, LD A, FIRE_GRAPH, lo pintamos, RST $10, y salimos, RET.

El aspecto final de la rutina es el siguiente:

; ----------------------------------------------------------------------------
; Pinta el disparo en la posición actual.
; Altera el valor de los registros AF y BC.
; ----------------------------------------------------------------------------
PrintFire:
ld      a, $02			; Carga en A la tinta roja
call    Ink            	; Llama al cambio de tinta

ld      bc, (firePos)  	; Carga en BC la posición actual del disparo
call    At             	; Llama a posicionar el cursor

ld      a, FIRE_GRAPH  	; Carga en A el carácter del fuego
rst     $10            	; Lo pinta

ret

Implementamos en Game.asm la rutina que mueve el disparo.

MoveFire:
ld      hl, flags
bit     $01, (hl)
jr      nz, moveFire_try
bit     $02, d
ret     z
set     $01, (hl)
ld      bc, (shipPos)
inc     h
jr      moveFire_print

Cargamos la dirección de memoria de flags en HL, LD HL, flags, comprobamos si el bit uno (disparo) está activo, BIT $01, (HL), y de ser así saltamos, JR NZ, moveFire_try. Si el bit uno no esta activo, comprobamos si se ha pulsado el control disparo, BIT $02, D, y de no ser así salimos, RET Z. En caso de haberse pulsado, activamos el bit de disparo en flags, SET $01, (HL), cargamos la posición actual de la nave en BC, LD BC, (shipPos), apuntamos a la línea superior, INC B, y saltamos a pintarlo, JR moveFire_print.

moveFire_try:
ld      bc, (firePos)
call    DeleteChar
inc     b
ld      a, FIRE_TOP_T
sub     b
jr      nz, moveFire_print
res     $01, (hl)

ret

Si el disparo estaba activo, cargamos la posición del disparo en BC, LD BC, (firePos), borramos el disparo, CALL DeleteChar, incrementamos B para apuntar a la línea superior, INC B, cargamos en A el tope superior del disparo, LD A, FIRE_TOP_T, y le restamos B, SUB B, si el resultado no es cero, todavía no hemos llegado al tope y saltamos, JR NZ, moveFire_print. Si hemos llegamos al tope desactivamos el disparo, RES $01, (HL), y salimos, RET.

moveFire_print:
ld      (firePos), bc
call    PrintFire

ret

Si no hemos llegado al tope o acabamos de activar el disparo, actualizamos la posición actual del disparo, LD (firePos), BC, llamamos a pintar el disparo, CALL PrintFire, y salimos, RET.

El aspecto final de la rutina es el siguiente:

; ----------------------------------------------------------------------------
; Mueve el disparo
;
; Entrada:  D -> Estado de los controles
; Altera el valor de los registros AF, BC y HL.
; ----------------------------------------------------------------------------
MoveFire:
ld      hl, flags           ; Carga en HL la dirección de memoria de flags
bit     $01, (hl)           ; Evalúa si el bit del disparo está activo
jr      nz, moveFire_try    ; Si está activo, salta
bit     $02, d              ; Evalúa si el control de disparo está activo
ret     z                   ; Si no está activo, sale
set     $01, (hl)           ; Activa el bit del disparo en flags
ld      bc, (shipPos)       ; Carga la posición actual de la nave en HL
inc     b                   ; Apunta a la línea superior
jr      moveFire_print      ; Salta a pintar el diparo

moveFire_try:
ld      bc, (firePos)       ; Carga en BC la posición actual del disparo
call    DeleteChar          ; Borra el disparo
inc     b                   ; Apunta B a la línea superior
ld      a, FIRE_TOP_T       ; Carga en A el tope superior del disparo
sub     b                   ; Le restamos coordenada Y del disparo
jr      nz, moveFire_print  ; Si son distintos, no ha llegado al tope, salta
res     $01, (hl)           ; Desactiva el disparo

ret

moveFire_print:
ld      (firePos), bc       ; Actualiza la posición del disparo
call    PrintFire           ; Pinta el disparo

ret

Es hora de probar el disparo, abrimos el achivo Main.asm y al inicio, en la declaración de flags, añadimos el comentario para el bit uno.

; Bit 1 -> el disparo está activo           0 = No, 1 = Sí

Localizamos la etiqueta Main_loop, y entre las líneas CALL CheckCtrl y CALL MoveShip añadimos la llamada al movimiento del disparo.

call    MoveFire

Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, interrupciones y disparo
Ensamblador ZX Spectrum, interrupciones y disparo

En la imagen no se aprecia, pero en el emulador podemos ver que parece que el disparo realiza ráfagas, y si dejamos el disparo pulsado es como si no parase de disparar, es un efecto óptico debido a que movemos el disparo más deprisa de lo que la ULA refresca la pantalla, lo vamos a dejar así para que parezca que disparamos varias veces a un mismo tiempo.

Ensamblador ZX Spectrum, conclusión

Hemos empezado a trabajar con interrupciones, temporizado el movimiento de la nave e implementado el disparo, además de compilar el programa en varios ficheros y personalizar el cargador.

En el próximo capítulo introduciremos los enemigos.

Podéis descargar desde aquí el código que hemos generado.

Enlaces de interés

Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

Un comentario en «0x05 Ensamblador ZX Spectrum Marciano – Interrupciones y disparo»

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies
A %d blogueros les gusta esto: