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.
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.

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.

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
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Ensamblador ZX Spectrum Pong
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF y EPUB
- Proyecto en itch.io
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.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.
Pingback: 0x0D Ensamblador ZX Spectrum Marciano – Música - Espamática