¡Hola a todos!, hay problemas con la web que espero poder arreglar lo antes posible, ¡mil perdones!

Espamatica

Espamatica
Visitar página web/blogCastellanoEspaña38 entradas

Espamatica es una página web/blog de España en castellano que ha publicado 38 entradas, siendo la última del día 18/10/2021.

Programación en 8 bits y algo más.

Ver índice de webs/blogs

0x05 Ensamblador ZX Spectrum Pong – Movemos la bola por la pantalla [Espamatica] [Leer]


Llegamos a una nueva entrega de Ensamblador ZX Spectrum Pong, y en esta ocasión vamos a mover la bola por toda la pantalla, finalizando la primera parte del tutorial.

Ensamblador ZX Spectrum Pong – Movemos la bola por la pantalla

Creamos la carpeta Paso05, dentro de la misma creamos los archivos Main.asm y Game.asm, y copiamos los archivos Sprite.asm y Video.asm que tenemos en la carpeta Paso04.

Empezamos editando Sprite.asm para añadir dos nuevas constantes que vamos a necesitar para mover la bola por la pantalla.

MARGIN_LEFT: EQU $00 MARGIN_RIGHT: EQU $1e

Igual que tenemos los límites superior e inferior, necesitamos los límites derecho e izquierdo para que la bola se mantenga dentro de los mismos.

El siguiente paso es implementar la lógica del movimiento de la bola, lo que haremos en Game.asm.

MoveBall: ld a, (ballSetting) and $80 jr nz, moveBall_down

Primero cargamos en A la configuración actual de la bola, LD A, (ballSetting), y nos quedamos con el bit 7, AND $80, que indica si la bola se desplaza hacia arriba o hacia abajo. Si el bit no está a 0, la bola se desplaza hacia abajo y salta, JR NZ, moveBall_down.

Si el bit está a 0, la bola se desplaza hacia arriba.

moveBall_up: ld hl, (ballPos) ld a, BALL_TOP call CheckTop jr z, moveBall_upChg call PreviousScan ld (ballPos), hl jr moveBall_x

Cargamos la posición actual de la bola en HL, LD HL, (ballPos), el límite vertical en A, LD A, BALL_TOP, y comprobamos si se ha alcanzado dicho límite, CALL CheckTop. Si se activa el flag Z, se ha alcanzado el límite y saltamos para cambiar la dirección vertical de la bola, JR Z, moveBall_upChg.

Si la bola no ha llegado al límite vertical, calculamos la nueva posición, CALL PreviousScan, la cargamos en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.

En el caso de haber alcanzado el límite superior, hay que cambiar la dirección vertical de la bola.

moveBall_upChg: ld a, (ballSetting) or $80 ld (ballSetting), a call NextScan ld (ballPos), hl jr moveBall_x

Primero cargamos la configuración de la bola en A, LD A, (ballSetting), luego activamos el bit 7, OR $80, para indicar que ahora la bola debe ir hacia abajo, y cargamos el valor en memoria, LD (ballSetting), A. Calculamos la nueva posición vertical de la bola, CALL NextScan, cargamos el valor en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.

Para activar el bit 7 hemos hecho un OR con $80 (10000000). Es conveniente recordar el resultado de la operación OR, dependiendo del valor de los bits.

Bit 1Bit 2Resultado
000
101
011
111
Ensamblador ZX Spectrum, resultado de OR

Según se ve en la tabla, al aplicar OR $80, pone el bit 7 a 1 y el resto los deja como estaban.

Si al iniciar la rutina la bola iba hacia abajo, hay que hacer algo parecido a lo visto anteriormente.

moveBall_down: ld hl, (ballPos) ld a, BALL_BOTTOM call CheckBottom jr z, moveBall_downChg call NextScan ld (ballPos), hl jr moveBall_x

Primero cargamos la posición de la bola en HL, LD HL, (ballPos), el límite inferior en A, LD A, BALL_BOTTOM, y comprobamos si se ha alcanzado, CALL CheckBottom, en cuyo caso saltamos para cambiar la dirección de la bola, JR Z, moveBall_downChg.

Si no se ha alcanzado el límite inferior, calculamos la nueva posición de la bola, CALL NextScan, la cargamos en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.

En el caso de haber alcanzado el límite inferior, hay que cambiar la dirección vertical de la bola.

moveBall_downChg: ld a, (ballSetting) and $7f ld (ballSetting), a call PreviousScan ld (ballPos), hl

Primero cargamos la configuración de la bola en A, LD A, (ballSetting), luego desactivamos el bit 7, AND $7F, para indicar que ahora la bola debe ir hacia arriba, y cargamos el valor en memoria, LD (ballSetting), A. Calculamos la nueva posición vertical de la bola, CALL PreviousScan, y cargamos el valor en memoria, LD (ballPos), HL.

Para desactivar el bit 7 hemos hecho un AND con $7F (01111111). Es conveniente recordar el resultado de la operación AND, dependiendo del valor de los bits.

Bit 1Bit 2Resultado
000
100
010
111
Ensamblador ZX Spectrum, resultado de AND

Según se ve en la tabla, al aplicar AND $7F, pone el bit 7 a 0 y el resto los deja como estaban.

Empezamos a calcular el desplazamiento horizontal.

moveBall_x: ld a, (ballSetting) and $40 jr nz, moveBall_left

Cargamos la configuración de la bola en A, LD A, (ballSetting), comprobamos el estado del bit 6, AND $40, y si no está a 0, la bola va hacia la izquierda y salta, JR NZ, moveBall_left.

Si el bit 6 está a 0, la bola va hacia la derecha.

moveBall_right: ld a, (ballRotation) cp $08 jr z, moveBall_rightLast inc a ld (ballRotation), a jr moveBall_end

Cargamos la rotación en A, LD A, (ballRotation), y comprobamos si está en la última, CP $08, en cuyo caso saltamos, JR Z, moveBall_rightLast.

Si no está en la última rotación, incrementamos la misma, INC A, la cargamos en memoria, LD (ballRotation), A, y saltamos al final de la rutina, JR moveBall_end.

Si, por el contrario, ha llegado a la última rotación y no está en el límite derecho, desplazamos la bola a la siguiente columna.

moveBall_rightLast: ld a, (ballPos) and $1f cp MARGIN_RIGHT jr z, moveBall_rightChg ld hl, ballPos inc (hl) ld a, $01 ld (ballRotation), a jr moveBall_end

Cargamos la línea y columna en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y evaluamos si ha llegado al límite derecho, CP MARGIN_RIGHT, en cuyo caso saltamos para cambiar la dirección de la bola, JR Z, moveBall_rightChg.

Si no se ha llegado al límite derecho, desplazamos la bola a la siguiente columna. Cargamos la dirección donde se encuentra la posición de la bola en HL, LD HL, ballPos, e incrementamos la columna, INC (HL).

Ponemos la rotación de la bola a 1, LD A, $01, lo cargamos en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.

Como se puede ver, para cargar la columna en A, la instrucción usada ha sido LD A, (ballPos), y para incrementar la columna LD HL, ballPos y INC (HL).

Teniendo en cuenta que las posiciones de memoria de la VideRAM se codifican 010TTSS LLLCCCCC, ¿no estaríamos cargando y alterando el scanline? No, y ello se debe a que el Z80 es un micro de tipo Little Endian.

Un micro Little Endian, cuando carga valores de 16 bits en memoria, carga en la primera posición de memoria el byte menos significativo, y en la siguiente el más significativo, de tal manera que si en la posición de memoria $C000 se carga el valor $4000, en la posición $C000 se carga $00 y en la $C001 se carga $40. Es por eso que cuando se carga en A el valor desde (ballPos), lo que carga es el byte menos significativo que es donde están la línea y columna. De igual modo al incrementar (HL), incrementa la columna.

Si la carga se hace sobre un registro de 16 bits, carga el byte menos significativo en la parte baja del registro, y el más significativo en la parte alta. Es por eso que al cargar ballPos en HL, carga en H el byte más significativo de la dirección de memoria y en L el menos significativo.

Seguimos con la rutina…

Si ha llegado al límite derecho, hay que cambiar la dirección de la bola.

moveBall_rightChg: ld a, (ballSetting) or $40 ld (ballSetting), a ld a, $ff ld (ballRotation), a jr moveBall_end

Cargamos la configuración de la bola en A, LD A, (ballSetting), activamos el bit 6 para cambiar la dirección hacia la izquierda, OR $40, y cargamos el valor en memoria, LD (ballSetting), A.

Ponemos la rotación de la bola a -1, LD A, $FF, la cargamos en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.

Si la bola va hacia la izquierda, hay que hacer algo parecido a lo visto anteriormente.

moveBall_left: ld a, (ballRotation) cp $f8 jr z, moveBall_leftLast dec a ld (ballRotation), a jr moveBall_end

Cargamos la rotación en A, LD A, (ballRotation), comprobamos si está en la última, CP $F8, y de ser así saltamos, JR Z, moveBall_leftLast.

Si no está en la última rotación la decrementamos, DEC A, cargamos el valor en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.

Si ha llegado a la última rotación y no ha alcanzado el límite izquierdo, desplazamos la bola a la columna anterior.

moveBall_leftLast: ld a, (ballPos) and $1f cp MARGIN_LEFT jr z, moveBall_leftChg ld hl, ballPos dec (hl) ld a, $ff ld (ballRotation), a jr moveBall_end

Cargamos la línea y columna en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y comprobamos si ha llegado al límite izquierdo, CP MARGIN_LEFT, en cuyo caso saltamos, JR Z, moveBall_leftChg.

Si no ha llegado al límite izquierdo, cargamos la dirección donde está la posición de la bola en HL, LD HL, ballPos, y decrementamos la columna, DEC (HL).

Ponemos la rotación de la bola a -1, LD A, $FF, cargamos el valor en memoria, LD (ballRotation), A, y saltamos al fin de la rutina.

Terminamos la rutina con el cambio de dirección, si se ha alcanzado el límite izquierdo.

moveBall_leftChg: ld a, $01 ld (ballRotation), a ld a, (ballSetting) and $bf ld (ballSetting), a moveBall_end: ret

Ponemos la rotación de la bola a 1, LD A, $01, y la cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), desactivamos el bit 6 para que la dirección sea hacia la derecha, AND $BF, y cargamos el valor en memoria, LD (ballSetting), A.

Podemos ahorrar 2 ciclos de reloj y 5 bytes haciendo una pequeña modificación. Lo dejamos en vuestras manos y veremos la forma de hacerlo al final del tutorial.

El aspecto final de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Calcula la posición, rotación y dirección de la bola para pintarla. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- MoveBall: ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $80 ; Comprueba la dirección vertical jr nz, moveBall_down ; Si el bit 7 está a uno, va hacia abajo moveBall_up: ; La bola va hacia arriba ld hl, (ballPos) ; Carga la posición de la bola en HL ld a, BALL_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, moveBall_upChg ; Si se ha alcanzado salta call PreviousScan ; Obtiene el scanline anterior a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola jr moveBall_x ; Salta moveBall_upChg: ; La bola va hacia arriba, pero ha llegado al tope y cambia de dirección ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola or $80 ; Pone la dirección vertical hacia abajo ld (ballSetting), a ; Carga en memoria la nueva dirección de la bola call NextScan ; Obtiene el scanline siguiente a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola jr moveBall_x ; Salta moveBall_down: ; La bola va hacia abajo ld hl, (ballPos) ; Carga la posición de la bola en HL ld a, BALL_BOTTOM ; Carga en A el margen superior call CheckBottom ; Evalúa si se ha alcanzado el margen superior jr z, moveBall_downChg ; Si se ha alcanzado salta call NextScan ; Obtiene el scanline siguiente a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola jr moveBall_x ; Salta moveBall_downChg: ; La bola va hacia abajo, pero ha llegado al tope y cambia de dirección ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $7f ; Pone la dirección vertical hacia arriba ld (ballSetting), a ; Carga en memoria la nueva dirección de la bola call PreviousScan ; Obtiene el scanline anterior a la posición de la bola ld (ballPos), hl ; Carga en memoria la nueva posición de la bola moveBall_x: ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $40 ; Comprueba la dirección horizontal jr nz, moveBall_left ; Si el bit 6 está a uno, va hacia la izquierda moveBall_right: ; La bola va hacia la derecha ld a, (ballRotation) ; Carga la rotación actual de la bola cp $08 ; Comprueba si ya está en la última rotación jr z, moveBall_rightLast ; Si está en la última rotación salta inc a ; Incrementa la rotación ld (ballRotation), a ; La carga en memoria jr moveBall_end ; Fin de la rutina moveBall_rightLast: ; Está en la última rotación ; Si no ha llegado al límite derecho pone la rotación a 1 ; y pone la bola en la siguiente columna ld a, (ballPos) ; Carga la línea y columna de la bola en A and $1f ; Se queda solo con la columna cp MARGIN_RIGHT ; Lo comprara con el límite derecho jr z, moveBall_rightChg ; Si lo ha alcanzado salta ld hl, ballPos ; Carga la dirección de la posición de la bola en HL inc (hl) ; Incrementa la columna ld a, $01 ; Pone la rotación a 1 ld (ballRotation), a ; Carga el valor en memoria jr moveBall_end ; Fin de la rutina moveBall_rightChg: ; Ha llegado al límite derecho ; Pone la rotación a -1 y cambia la dirección horizontal de la bola ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola or $40 ; Pone la dirección horizontal hacia la izquierda ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $ff ; Carga -1 en A ld (ballRotation), a ; Lo carga en memoria Rotación = -1 jr moveBall_end ; Fin de la rutina moveBall_left: ; La bola va hacia la izquierda ld a, (ballRotation) ; Carga la rotación actual de la bola cp $f8 ; Comprueba si ya está en la última rotación jr z, moveBall_leftLast ; Si está en la última rotación salta dec a ; Decrementa la rotación ld (ballRotation), a ; La carga en memoria jr moveBall_end ; Fin de la rutina moveBall_leftLast: ; Esta en la última rotación ; Si no ha llegado al límite izquierdo pone la rotación a -1 ; y pone la bola en la columna anterior ld a, (ballPos) ; Carga la línea y columna en A and $1f ; Se queda solo con la columna cp MARGIN_LEFT ; Lo comprara con el límite izquierdo jr z, moveBall_leftChg ; Si lo ha alcanzado salta ld hl, ballPos ; Carga la dirección de la posición de la bola en HL dec (hl) ; Pasa a la columna anterior ld a, $ff ; Pone la rotación a -1 ld (ballRotation), a ; Carga el valor en memoria jr moveBall_end ; Fin de la rutina moveBall_leftChg: ; Ha llegado al límite izquierdo ; Pone la rotación a 1 y cambia la dirección ld a, $01 ; Carga la posición de la bola en HL ld (ballRotation), a ; Carga el valor en memoria Rotación = 1 ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola and $bf ; Pone la dirección horizontal hacia la derecha ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria moveBall_end: ret

Ha llegado el momento de probar todo lo implementado, vamos a editar el archivo Main.asm. En este caso la implementación es muy sencilla.

org $8000 ld a, $02 out ($fe), a call PrintBall

Indicamos la dirección dónde cargar el programa, ponemos el borde en rojo y pintamos la bola en la posición inicial.

Loop: call MoveBall call PrintBall halt jr Loop

Implementamos un bucle infinito en el que movemos la bola, la pintamos, esperamos al refresco de la pantalla y volvemos a realizar estas tres operaciones indefinidamente.

Include "Game.asm" Include "Sprite.asm" Include "Video.asm" end $8000

Por último, incluimos los archivos necesarios e indicamos a PASMO dónde tiene que llamar al cargar el programa.

El aspecto final de Main.asm es el siguiente.

; Mueve la bola por la pantalla trazando diagonales org $8000 ld a, $02 ; A = 2 out ($fe), a ; Pone el borde en rojo call PrintBall ; Imprime la bola Loop: call MoveBall ; Mueve la bola call PrintBall ; Pinta la bola halt ; Espera al refresco de pantalla jr Loop ; Bucle infinito include "Game.asm" include "Sprite.asm" include "Video.asm" end $8000

Llega el gran momento… compilamos y vemos el resultado en el emulador.

Ensamblador ZX Spectrum Pong - Movemos la bola por la pantallaEnsamblador ZX Spectrum, movemos la bola por la pantalla

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, movemos la bola por la pantalla

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

0x04 Ensamblador ZX Spectrum Pong – Empezamos a mover la bola [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a empezar a mover la bola, en este caso solo de izquierda a derecha, pero nos va a servir para lo que abordaremos más adelante.

Ensamblador ZX Spectrum – Empezamos a mover la bola

Creamos la carpeta Paso04, dentro de la misma creamos el archivo Main.asm y copiamos los archivos Sprite.asm y Video.asm que tenemos en la carpeta Paso03.

Empezamos editando el archivo Sprite.asm para definir los datos necesarios relativos a la bola.

BALL_BOTTOM: EQU $ba BALL_TOP: EQU $00

Como hicimos con las palas, definimos los límites inferior y superior para la bola, en formato TTLLLSSS.

ballPos: dw $4870 ballSetting: db $00 ballRotation: db $f8

Al igual que con las palas, vamos a usar una variable donde vamos a tener la posición de la bola en cada momento, ballPos.

En ballSetting vamos a guardar en los bits 0 a 3 la velocidad X, en los bits 4 y 5 la velocidad Y, en el bit 6 la dirección X (0 derecha / 1 izquierda) y en el bit 7 la dirección Y (0 arriba / 1 abajo).

Por último, en ballRotation vamos a guardar la rotación de la bola, indicando con los valores positivos la rotación hacia la derecha y con los negativos hacia la izquierda. La rotación es necesaria debido a la forma en la que vamos a realizar el movimiento horizontal.

La bola va a constar de un scanline en blanco, cuatro scanlines con la parte visible y otro scanline en blanco. Los scanlines en blanco hacen que la bola no deje rastro al moverse.

Vamos a definir 2 bytes para pintar la bola, y a definir cada movimiento píxel a píxel.

; Sprite de la bola. 1 línea a 0, 4 líneas visibles, 1 línea a 0 ballRight: ; Derecha Sprite Izquierda db $3c, $00 ; +0/$00 00111100 00000000 -8/$f8 db $1e, $00 ; +1/$01 00011110 00000000 -7/$f9 db $0f, $00 ; +2/$02 00001111 00000000 -6/$fa db $07, $80 ; +3/$03 00000111 10000000 -5/$fb db $03, $c0 ; +4/$04 00000011 11000000 -4/$fc db $01, $e0 ; +5/$05 00000001 11100000 -3/$fd db $00, $f0 ; +6/$06 00000000 11110000 -2/$fe db $00, $78 ; +7/$07 00000000 01111000 -1/$ff ballLeft: db $00, $3c ; +8/$08 00000000 00111100 +0/$00

Cada línea define la parte visible de la bola, dependiendo de como estén los píxeles. Definimos 2 bytes por cada posición. En el comentario vemos la rotación cuando la bola va hacia la derecha, los bits que vamos a pintar, y la rotación cuando la bola va hacia la izquierda.

La bola inicialmente se pinta tal y como muestra el primer sprite.

00111100 00000000

Si se mueve un píxel a la derecha, no cambiamos la posición de la bola, cambiamos la rotación y pintamos el segundo sprite.

000111100 00000000

Al llegar a la última rotación, es cuando cambiamos la posición de la bola, más concretamente la columna. El aspecto final del código es:

; Limites de los objetos en pantalla BALL_BOTTOM: EQU $ba ; TTLLLSSS BALL_TOP: EQU $00 ; TTLLLSSS ; Sprite de la bola. 1 línea a 0, 4 líneas 3c, 1 línea a 0 ballRight: ; Derecha Sprite Izquierda db $3c, $00 ; +0/$00 00111100 00000000 -8/$f8 db $1e, $00 ; +1/$01 00011110 00000000 -7/$f9 db $0f, $00 ; +2/$02 00001111 00000000 -6/$fa db $07, $80 ; +3/$03 00000111 10000000 -5/$fb db $03, $c0 ; +4/$04 00000011 11000000 -4/$fc db $01, $e0 ; +5/$05 00000001 11100000 -3/$fd db $00, $f0 ; +6/$06 00000000 11110000 -2/$fe db $00, $78 ; +7/$07 00000000 01111000 -1/$ff ballLeft: db $00, $3c ; +8/$08 00000000 00111100 +0/$00 ; Posición de la bola ballPos: dw $4870 ; 010T TSSS LLLC CCCC ; Velocidad y dirección de la bola. ; bits 0 a 3: velocidad X: 1 a 4 ; bits 4 a 5: velocidad Y: 0 a 3 ; bit 6: dirección X: 0 derecha / 1 izquierda ; bit 7: dirección Y: 0 arriba / 1 abajo ballSetting: db $00 ; Rotación de la bola ; Valores positivos derecha, negativos izquierda ballRotation: db $f8

Ahora vamos a implementar, en el archivo Video.asm, la rutina que pinta la bola, que vamos a poner después de la rutina PreviousScan.

PrintBall: ld b, $00 ld a, (ballRotation) ld c, a cp $00 ld a, $00 jp p, printBall_right

Lo primero es averiguar hacia dónde va la bola, izquierda o derecha. Una vez averiguado, al sprite base de la bola hay que sumarle o restarle la rotación, para obtener el sprite correcto. La dirección del sprite base la vamos a guardar en HL y restaremos o sumaremos la rotación que tendremos en BC, por eso lo primero es poner B a 0, LD B, $00.

El siguiente paso es cargar la rotación de la bola en A, LD A, (ballRotation), y de ahí cargarlo en C, LD C, A. Podríamos cargar directamente en C, previo paso por HL, pero dependiendo del valor obtendremos si va a derecha o izquierda. Para obtener este valor, comparamos el valor con 0, y como las comparaciones siempre se hacen contra el registro A, de ahí que sea necesario cargar la rotación en este registro.

Comparamos el valor de A con 0, CP A, $00, y si el resultado es positivo la bola se mueve hacia la derecha y salta, JP P, printBall_right. Antes de eso hemos cargado 0 en A para los siguientes cálculos, LD A, $00.

Continuamos implementando el movimiento hacia la izquierda.

printBall_left: ld hl, ballLeft sub c add a, a ld c, a sbc hl, bc jr printBall_continue

Si la bola se mueve hacia la izquierda, lo primero es cargar en HL la dirección del sprite base izquierda, LD HL, ballLeft.

En este punto A vale 0, por lo que se le resta la rotación que tenemos en C, de esta forma conseguimos el valor a restar para situarnos en el sprite correcto.

Ejemplo: C = $FF, A = $00 A – C = $01

Debido a que cada sprite ocupa 2 bytes, hay que duplicar el valor que se va a restar a HL, ADD A, A, y posteriormente cargarlo en C, LD C, A.

Ahora ya podemos calcular la dirección de memoria donde se encuentra el sprite a imprimir, SBC HL, BC, y saltar a imprimir la bola, JR printBall_continue.

Implementamos ahora el movimiento hacia la derecha.

printBall_right: ld hl, ballRight add a, c add a, a ld c, a add hl, bc

Si la bola se mueve hacia la derecha, la rutina es ligeramente distinta a la anterior. Volvemos a cargar en HL la dirección del sprite base, LD HL, ballRight, en este caso hacia la derecha, sumamos la rotación en A, ADD A, C, multiplicamos por dos, ADD A, A, y cargamos el resultado en C, LD C, A, para luego sumárselo a HL, ADD HL, BC, y así obtenemos la dirección del sprite a imprimir.

Y ahora imprimimos la bola.

printBall_continue: ex de, hl ld hl, (ballPos)

Como la rutina NextScan recibe en HL la dirección actual y devuelve, también en HL, la nueva dirección, lo primero es cargar el valor de HL en DE, EX DE, HL. Con EX intercambiamos el valor de los registros y ahorramos 4 ciclos de reloj y un byte con respecto de hacerlo con LD (LD D, H y LD E, L).

Después cargamos la posición de la bola en HL, LD HL, (ballPos).

ld (hl), ZERO inc l ld (hl), ZERO dec l call NextScan

Pintamos a 0 el primer byte del primer scanline, LD (HL), ZERO, pasamos al siguiente byte incrementando la columna, INC L, pintamos el segundo byte, LD (HL), ZERO, volvemos a dejar la columna como estaba, DEC L, y calculamos la dirección del siguiente scanline, CALL NextScan.

El siguiente paso es pintar los 4 scanlines que realmente se ven de la bola.

ld b, $04 printBall_loop: ld a, (de) ld (hl), a inc de inc l ld a, (de) ld (hl), a dec de dec l call NextScan djnz printBall_loop

Carga en B el número de scanlines que vamos a pintar, LD B, $04, carga el primer byte del sprite en A, LD A, (DE), y lo pinta en pantalla, LD (HL), A.

Apunta DE al siguiente byte del sprite, INC DE, apunta HL a la siguiente columna, INC L, carga el sprite en A, LD A, (DE), y lo pinta en pantalla, LD (HL), A.

Vuelve a apuntar DE al primer byte del sprite, DEC DE, vuelve a apuntar HL a la columna anterior, DEC L, y calcula la dirección del scanline siguiente, CALL NextScan.

Repite estas operaciones hasta que B valga 0, DJNZ printBall_loop.

ld (hl), ZERO inc l ld (hl), ZERO ret

Pinta el último scanline de la bola en blanco, primero el primer byte, LD (HL), ZERO, y tras apuntar HL a la siguiente columna, INC L, el segundo, LD (HL), ZERO.

El código final de la rutina queda de la siguiente manera.

; ----------------------------------------------------------------------------- ; Pinta la bola. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- PrintBall: ld b, $00 ; Pone B a 0 ld a, (ballRotation) ; Obtiene la rotación de la bola, para averiguar qué pintar ld c, a ; Carga el valor en C cp $00 ; Compara el valor de la rotación con 0 para ver ; si rota a derecha o izquierda ld a, $00 ; Pone A = 0 jp p, printBall_right ; Si es positivo salta, rota a derecha printBall_left: ; La rotación de la bola es a izquierda ld hl, ballLeft ; Carga la dirección donde están los bytes de la bola sub c ; Resta de A el valor de C, rotación de la bola add a, a ; Suma A + A. Cada definición de la bola son dos bytes ld c, a ; Carga el valor en C sbc hl, bc ; Resta a HL (dirección de los bytes de la bola) ; el desplazamiento para posicionarse en los correctos jr printBall_continue printBall_right: ; La rotación de la bola es a derecha ld hl, ballRight ; Carga la dirección donde están los bytes de la bola add a, c ; Suma en A el valor de C, rotación de la bola add a, a ; Suma A + A. Cada definición de la bola son dos bytes ld c, a ; Carga el valor en C add hl, bc ; Suma a HL (dirección de los bytes de la bola) ; el desplazamiento para posicionarse en los correctos printBall_continue: ; Se carga en DE la dirección dónde está la definición de la bola ex de, hl ld hl, (ballPos) ; Carga en HL la posición de la bola ; Pinta la primera línea en blanco ld (hl), ZERO ; Mueve blanco a la posición de pantalla inc l ; Pasa a la siguiente columna ld (hl), ZERO ; Mueve blanco a la posición de pantalla dec l ; Vuelve a la columna anterior call NextScan ; Pasa al siguiente scanline ld b, $04 ; Pinta la definición de la bola en las siguientes 4 líneas printBall_loop: ld a, (de) ; Carga en A la definición de la bola ld (hl), a ; Carga la definición de la bola a la posición de pantalla inc de ; Pasa al siguiente byte de la definición de la bola inc l ; Pasa a la siguiente columna ld a, (de) ; Carga en A la definición de la bola ld (hl), a ; Carga la definición de la bola a la posición de pantalla dec de ; Vuelve al primer byte de la definición de la bola dec l ; Vuelve a la columna anterior call NextScan ; Pasa al siguiente scanline djnz printBall_loop ; Hasta que B = 0 ; Pinta la última línea en blanco ld (hl), ZERO ; Mueve blanco a la posición de pantalla inc l ; Pasa a la siguiente columna ld (hl), ZERO ; Mueve blanco a la posición de pantalla ret

Y ahora ya sólo queda ver si todo lo que hemos implementado funciona, para lo cual vamos a editar el archivo Main.asm.

org $8000 ld a, $02 out ($fe), a ld a, $00 ld (ballRotation), a

Indicamos la dirección donde cargar el programa, ORG $8000, ponemos A = 2, LD A, $02, para poner el borde en rojo, OUT ($FE), A, y luego ponemos A = 0, LD A, $00, para inicializar la rotación de la bola, LD (ballRotation), A.

Vamos a implementar un bucle infinito para que la bola se mueva indefinidamente.

Loop: call PrintBall

Lo primero es imprimir la bola, CALL PrintBall, en la posición inicial.

loop_cont: ld b, $08 loopRight: exx ld a, (ballRotation) inc a ld (ballRotation), a call PrintBall exx halt djnz loopRight

En esta primera parte vamos a desplazar, rotar, la bola 8 píxeles hacia la derecha, LD B, $08, haciendo un intercambio de valores con los registros para preservar el valor de B, EXX.

EXX intercambia el valor de los registros de propósito común, con el de los registros alternativos.

BC <= => ‘BC
DE <= => ‘DE
HL <= => ‘HL

Hemos optado en este caso por EXX porque tarda 4 ciclos de reloj y ocupa 1 byte, mientras que PUSH BC tarda 11 ciclos de reloj, y el valor de los registros, exceptuando el de B, no es crítico para ninguna operación que debamos realizar en el bucle, y de paso vemos esta instrucción.

Cargamos en A la rotación actual de la bola, LD A, (ballRotation), incrementamos la rotación, INC A, y cargamos el valor resultante en memoria, LD (ballRotation), A.

Pintamos la bola, CALL PrintBall, volvemos a intercambiar el valor de los registros, EXX, para recuperar el valor de B y hacemos una pausa para poder ver cómo se mueve la bola, HALT.

Repetimos hasta que B valga 0, DJNZ loopRight.

Volvemos a poner a 0 la rotación de la bola, pero esta vez sin pintarla, para empezar a rotar los píxeles hacia la izquierda (ver definición del sprite de la bola).

ld a, $00 ld (ballRotation), a

Ahora vamos a desplazar, rotar, la bola 8 píxeles hacia la izquierda. Solo cambian una instrucción y una etiqueta respecto al desplazamiento hacia la derecha, por lo que no se explica la rutina, simplemente se marca la instrucción que cambia, para que se vea la diferencia.

ld b, $08 loopLeft: ; ¡CAMBIO! exx ld a, (ballRotation) dec a ; ¡CAMBIO! ld (ballRotation), a call PrintBall exx halt djnz loopLeft ; ¡CAMBIO!

Para terminar, volvemos a poner la rotación a 0, cargamos el valor en memoria y volvemos a repetir el bucle.

ld a, $00 ld (ballRotation), a jr loop_cont

Sin olvidarnos de incluir los ficheros Sprite.asm y Video.asm, e indicarle a PASMO dónde tiene que llamar al cargar el programa.

include "Sprite.asm" include "Video.asm" end $8000

En realidad, la bola no se mueve, muy al contrario, lo que hacemos es pintarla siempre en las mismas dos columnas, desplazando los píxeles ocho veces hacia la derecha y luego ocho veces hacia la izquierda, para volver a empezar una y otra vez.

El aspecto final del archivo Main.asm es el siguiente.

; Mueve la bola de izquierda a derecha entre dos columnas. org $8000 ld a, $02 ; A = 2 out ($fe), a ; Pone el borde en rojo ld a, $00 ; A = 0 ld (ballRotation), a ; Pone la rotación de la bola a 0 Loop: call PrintBall ; Imprime la bola loop_cont: ld b, $08 ; Mueve la bola 8 píxeles a la derecha loopRight: exx ; Intercambia el valor de los registros para preservar B ld a, (ballRotation) ; Recupera la rotación de la bola inc a ; Incrementa la rotación ld (ballRotation), a ; Guarda el valor de la rotación call PrintBall ; Imprime la bola exx ; Intercambia el valor de los registros para recuperar B halt ; Se sincroniza con el refresco de la pantalla djnz loopRight ; Hasta que B = 0 ld a, $00 ; A = 0 ld (ballRotation), a ; Pone la rotación de la bola a 0 ld b, $08 ; Mueve la bola 8 píxeles a la derecha loopLeft: exx ; Intercambia el valor de los registros para preservar B ld a, (ballRotation) ; Recupera la rotación de la bola dec a ; Decrementa la rotación ld (ballRotation), a ; Guarda el valor de la rotación call PrintBall ; Imprime la bola exx ; Intercambia el valor de los registros para recuperar B halt ; Se sincroniza con el refresco de la pantalla djnz loopLeft ; Hasta que B = 0 ld a, $00 ; A = 0 ld (ballRotation), a ; Pone la rotación de la bola a 0 jr loop_cont ; Bucle infinito include "Sprite.asm" include "Video.asm" end $8000

Ya solo queda compilar y ver los resultados en el emulador.

Ensamblador ZX Spectrum Pong - Empezando a mover la bolaEnsamblador ZX Spectrum, empezamos a mover la bola

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, empezamos a mover la bola

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

0x03 Ensamblador ZX Spectrum Pong – Palas y línea central [Espamatica] [Leer]


Llegados a esta nueva entrega de Ensamblador ZX Spectrum Pong, ya hemos adquirido los conocimientos suficientes para empezar con el desarrollo de nuestro PorompomPong, hemos implementado una buena parte de la base del programa.

Ensamblador ZX Spectrum Pong – Palas y línea central

En este paso vamos a:

  • Cambiar el color del borde.
  • Asignar atributos de color a la pantalla.
  • Dibujar la línea central del campo.
  • Dibujar las palas de ambos jugadores.
  • Mover las palas hacia arriba y hacia abajo.

Como siempre, creamos una carpeta a la que vamos a llamar Paso03, y dentro de la misma creamos los archivos Main.asm y Sprite.asm.

Esta vez no empezamos desde cero, ya que hemos desarrollado en los pasos anteriores código, en los ficheros Controls.asm y Video.asm, que vamos a usar en este paso, por lo que copiamos los dos ficheros en el nuevo directorio.

Cambiar el color del borde

Es el primer paso que vamos a realizar. Aunque el color del borde final será igual al de resto de la pantalla, en los primeros pasos lo vamos a poner en rojo para visualizar los límites de la misma.

Vamos a editar el fichero Main.asm, y lo primero, como siempre, es indicar la dirección de memoria dónde vamos a cargar el programa.

org $8000

Lo siguiente es poner el borde en rojo.

ld a, $02 out ($fe), a

Con LD, A, $02 cargamos el valor del color rojo en A y luego escribimos este valor en el puerto $FE (256), OUT ($FE), A. Este puerto ya lo conocemos, pues es el puerto desde dónde leemos el estado del teclado.

Por último, salimos del programa e indicamos a PASMO dónde llamar cuando lo cargue.

ret end $8000

Compilamos con PASMO y vemos el resultado final.

Ensamblador ZX Spectrum Pong - BordeEnsamblador ZX Spectrum, color del borde en rojo

El código de Main.asm queda así:

org $8000 ld a, $02 ; A = 2 out ($fe), a ; Pone el borde en rojo ret end $8000
Asignar los atributos de color a la pantalla

En nuestro caso, los atributos son blanco para la tinta y negro para el fondo. Vamos a implementar una rutina, Cls, que limpia la pantalla y pone el fondo en negro y la tinta en blanco.

Los atributos de la pantalla se encuentran a continuación del área donde se dibuja, empieza en la dirección $5800 y tiene una longitud $300 (768) bytes, 32 columnas por 24 líneas. En el ZX Spectrum los atributos de color van a nivel de carácter, cada atributo afecta a un área de 8×8 píxeles, siendo éste el motivo del famoso Attribute Clash.

Los atributos de un carácter están definidos en un byte.

Bit 7Bit 6Bit 5 – Bit 4 – Bit 3Bit 2 – Bit 1 – Bit 0
Parpadeo (0/1)Brillo (0/1)Fondo (0 a7)Tinta (0 a 7)
Ensamblador ZX Spectrum, definición de los atributos de color

La rutina Cls consta de dos partes:

  • Limpia la pantalla.
  • Asigna el color de tinta y fondo.

Vamos a editar el archivo Video.asm y vamos a implementar la rutina.

Cls: ld hl, $4000 ld (hl), $00 ld de, $4001 ld bc, $17ff ldir ret

Lo primero que hace nuestra rutina es apuntar HL al inicio de la VideoRAM, LD HL, $4000, y limpia ese byte de la pantalla, LD (HL), $00.

El siguiente paso es apuntar DE a la posición siguiente a HL, LD DE, $4001, y cargar en BC el número de bytes a limpiar, LD BC, $17FF, que es toda el área de la VideoRAM ($1800) menos uno, que es la posición donde apunta HL, y ya está limpia.

LDIR, LoadData, Increment and Repeat, carga el valor que hay en la posición de memoria a la que apunta HL, a la posición de memoria a la que apunta DE. Una vez realizado esto, incrementa HL y DE. Repite en bucle hasta que BC llegue a 0. Por último, salimos de la rutina.

Abrimos el archivo Main.asm y antes de RET añadimos la llamada a Cls.

call Cls

Antes de END $8000, añadimos la línea para incluir el archivo Video.asm.

include "Video.asm"

Compilamos con PASMO y cargamos en el emulador.

Ensamblador ZX Spectrum Pong - ClsEnsamblador ZX Spectrum, Cls

Como se aprecia en la imagen, ya no sale la línea «Bytes: PoromPong«, lo cual demuestra que hemos limpiado la pantalla.

Para implementar la segunda parte de la rutina, la asignación de los atributos de color, vamos a escribir las siguientes líneas justo antes de la instrucción RET de la rutina Cls.

ld hl, $5800 ld (hl), $07 ld de, $5801 ld bc, $2ff ldir

Lo primero que hace esta parte de la rutina es apuntar HL al inicio del área de atributos, LD HL, $5800, y pone esa zona sin parpadeo, sin brillo, con el fondo en negro y la tinta en blanco, LD (HL), $07.

$07 = 0000 0111 = 0 (parpadeo) 0 (brillo) 000 (fondo) 111 (tinta)

El siguiente paso es apuntar DE a la posición siguiente a HL, LD DE, $5801, y cargar en BC el número de bytes a cargar, LD BC, $2FF, que es toda el área de atributos ($300) menos uno, que es la posición donde apunta HL, y ya tiene los atributos. Se ejecuta LDIR, y se asigna el color a toda la pantalla.

El código completo de la rutina es.

; ----------------------------------------------------------------------------- ; Limpia la pantalla, tinta 7, fondo 0. ; Altera el valor de los registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- Cls: ; Limpia los píxeles de la pantalla ld hl, $4000 ; Carga en HL el inicio de la VideoRAM ld (hl), $00 ; Limpia los píxeles de esa dirección ld de, $4001 ; Carga en DE la siguiente posición de la VideoRAM ld bc, $17ff ; 6143 repeticiones ldir ; Limpia todos los píxeles de la VideoRAM ; Pone la tinta en blanco y el fondo en negro ld hl, $5800 ; Carga en HL el inicio del área de atributos ld (hl), $07 ; Lo pone con la tinta en blanco y el fondo en negro ld de, $5801 ; Carga en DE la siguiente posición del área de atributos ld bc, $2ff ; 767 repeticiones ldir ; Asigna el valor a toda el área de atributos ret

Llegados a este punto, compilamos y vemos el resultado.

Ensamblador ZX Spectrum Pong - Cls completoEnsamblador ZX Spectrum, Cls completo

Como se puede observar, además de limpiar la pantalla, ha puesto el fondo en negro y la tinta en blanco, aunque, al no haber pintado nada en la pantalla, no se ve si la tinta está realmente en blanco.

Para ver distintos efectos, cambiad los valores que cargáis en (HL).

Esta rutina se puede cambiar haciéndonos ahorrar 8 ciclos de reloj y 4 bytes. Dejamos en vuestras manos averiguar la manera de hacerlo y daremos la solución al final del tutorial. No os preocupéis, no es una rutina crítica, así que no va a afectar al desarrollo de nuestro videojuego.

Dibujar la línea central

La línea central del campo está compuesta por un primer scanline en blanco, otros seis con el bit 7 a 1 y un último scanline en blanco:

00000000
10000000
10000000
10000000
10000000
10000000
10000000
00000000

En este caso solo vamos a definir la parte en blanco y la parte que pinta la línea. Abrimos el fichero Sprite.asm y añadimos las siguientes líneas.

ZERO: EQU $00 LINE: EQU $80

Con la directiva EQU se definen valores constantes que no se compilan, al contrario, lo que hace el compilador es sustituir todas las referencias que haya en el código a estas etiquetas por el valor que se ha asignado a las mismas.

Ejemplo: ld a, ZERO – Compilador – ld a, $00

Una vez que tenemos el «sprite» de la línea, vamos a implementar la rutina para pintarla. Volvemos al archivo Video.asm.

PrintLine: ld b, $18 ld hl, $4010

Vamos a pintar el «sprite» de nuestra línea en las 24 líneas de la pantalla, LD B, $18, y vamos a empezar en el primer scanline, de la primera línea, del primer tercio, columna 16, LD HL, $4010.

printLine_loop: ld (hl), ZERO inc h push bc

Pintamos el primer scanline en blanco, LD (HL), ZERO, luego pasamos al siguiente scanline, INC H, y por último preservamos el valor de BC en la pila, ya que vamos a usar B para hacer un bucle que pinte la parte que se ve de la línea.

Para cambiar de scanline directamente incrementamos H en lugar de llamar a NextScan. ¿Por qué?. Sencillo. Dado que vamos a pintar los 8 scanlines de un mismo carácter, ni cambiamos de línea, ni de tercio, por lo que con aumentar el scanline es suficiente, y ahorramos tiempo de proceso y bytes.

Otra cosa que hacemos es subir un valor a la pila, concretamente BC. Es muy importante recordar que cada PUSH debe tener un POP, y además si hay varios PUSH, tiene que haber el mismo número de POP, pero en orden inverso.

push af
push bc
pop bc
pop af

Ahora vamos a hacer el bucle que pinte la parte que se ve de la línea.

ld b, $06 printLine_loop2: ld (hl), LINE inc h djnz printLine_loop2 pop bc

Lo primero es indicar el número de iteraciones del nuevo bucle, LD B, $06, pintamos el scanline con la parte visible de la línea, LD (HL), LINE, pasamos al siguiente scanline, INC H, y repetimos hasta que B valga 0, DJNZ printLine_loop2. Cuando B valga 0, recuperamos el valor de BC de la pila para continuar con el bucle de las 24 líneas de la pantalla, POP BC.

Y llegamos así a la parte final de la rutina.

ld (hl), ZERO call NextScan djnz printLine_loop ret

Pintamos el último scanline del carácter, LD (HL), ZERO, recuperamos el siguiente scanline, CALL NextScan, y repetimos hasta que B valga 0 y se hayan pintado las 24 líneas de la pantalla, DJNZ printLine_loop. Esta vez sí llamamos a NextScan, ya que cambiamos de línea.

El aspecto final de la rutina es el siguiente:

; ----------------------------------------------------------------------------- ; Imprime la línea central. ; Altera el valor de los registros AF, B y HL. ; ----------------------------------------------------------------------------- PrintLine: ld b, $18 ; Se imprime en las 24 líneas de pantalla ld hl, $4010 ; Se empieza en la línea 0, columna 16 printLine_loop: ld (hl), ZERO ; En el primer scanline se imprime el byte en blanco inc h ; Pasa al siguiente scanline push bc ; Preserva el valor de BC para realizar el segundo bucle ld b, $06 ; Se imprime seis veces printLine_loop2: ld (hl), LINE ; Imprime el byte de la línea, $10, b00010000 inc h ; Pasa el siguiente scanline djnz printLine_loop2 ; Hasta que B = 0 pop bc ; Recupera el valor de BC ld (hl), ZERO ; Imprime el último byte de la línea a 0 call NextScan ; Pasa al siguiente scanline djnz printLine_loop ; Hasta que B = 0 = 24 líneas ret

Y ahora ya sólo queda probarlo, para lo cual abrimos el fichero Main.asm y añadimos tras la llamada a Cls, la llamada a PrintLine e incluimos el fichero Sprite.asm, igual que hicimos con el fichero Video.asm.

call PrintLine include "Sprite.asm"

Compilamos y vemos el resultado en el emulador.

Ensamblador ZX Spectrum Pong - Lnea centralEnsamblador ZX Spectrum, línea central

Ahora sí se puede observar que habíamos puesto la tinta en blanco.

Dibujar las palas de ambos jugadores

En este paso vamos a dibujar las palas de ambos jugadores, que van a ocupar 1×3 caracteres, 1 byte (8 píxeles) y 24 scanlines.

Vamos a usar el mismo tipo de definición que usamos para definir la línea horizontal, y lo vamos a hacer en el archivo Sprite.asm.

PADDLE: EQU $3c

Esta sería la parte visible de la pala, 00111100, ya que vamos a pintar el primer scanline en blanco, 22 scanlines con esta definición y el último scanline en blanco.

Las palas van a ser elementos móviles, por lo que además de su «sprite», necesitamos saber en qué posición se encuentran y cuáles son los márgenes superior e inferior a los que las podemos mover.

Seguimos en el fichero Sprite.asm.

PADDLE_BOTTOM: EQU $a8 ; TTLLLSSS PADDLE_TOP: EQU $00 ; TTLLLSSS paddle1pos: dw $4861 ; 010T TSSS LLLC CCCC paddle2pos: dw $487e ; 010T TSSS LLLC CCCC

En las dos primeras constantes, que son los límites hasta donde podemos mover las palas, vamos a especificar la coordenada Y expresada en tercio, línea y scanline. Mientras que PADDLE_TOP sí apunta al límite superior de la pantalla (tercio 0, línea 0, scanline 0), PADDLE_BOTTOM no apunta al límite inferior de la pantalla (tercio 2, línea 7, scanline 7), por el contrario, apunta al tercio 2, línea 5, scanline 0, que es el resultado de restarle al límite inferior ($BF), 23 scanlines para que podamos pintar los 24 scanlines del sprite de la pala, sin invadir el área de atributos de la pantalla.

Por otro lado, paddle1pos y paddle2pos no son constantantes, pues estos valores van a cambiar respondiendo a las pulsaciones de las teclas de control.

La posición inicio de las palas es:

Pala 1Pala 2
Tercio11
Línea33
Scanline00
Columna130
Ensamblador ZX Spectrum, posición inicial de las palas

Una vez definido esto, vamos al archivo Video.asm e implementamos las rutina que dibuja las palas. Esta rutina tiene como parámetro de entrada la posición de la pala, que se recibe en HL. Es necesario porque tenemos dos palas que pintar, y la otra alternativa sería duplicar la rutina y que cada una pintara una pala.

PrintPaddle: ld (hl), ZERO call NextScan

Lo primero que hace es pintar en blanco el primer scanline de la pala, LD (HL), ZERO, y luego obtiene el siguiente scanline.

Al contrario de lo que pasaba al pintar la línea central, en esta rutina si son necesarias las llamadas a NextScan. Nuestro movimiento de la pala va a ser pixel a pixel, esto en vertical es scanline a scanline, lo que hace que no sepamos de antemano cuando cambiamos de línea (en realidad si podríamos saberlo).

Lo siguiente es pintar la parte visible de la pala.

ld b, $16 printPaddle_loop: ld (hl), PADDLE call NextScan djnz printPaddle_loop

La parte visible de la pala la vamos a pintar en 22 scanlines, LD B, $16, cargando en la posición apuntada por HL el sprite de la pala, LD (HL), PADDLE, y obteniendo el siguiente scanline, CALL NextScan, hasta que B valga 0, DJNZ printPaddle_loop.

Por último, pinta en blanco el último scanline de la pala.

ld (hl), ZERO ret

Pintar en blanco el primer y el último scanline sirve para que, al mover la pala, se vaya auto borrando y no deje rastro.

El aspecto fina de la rutina es el siguiente.

; ----------------------------------------------------------------------------- ; Imprime la pala. ; Entrada: HL = Posición de la pala ; Altera el valor de los registros B y HL. ; ----------------------------------------------------------------------------- PrintPaddle: ld (hl), ZERO ; Imprime el primer byte de la pala en blanco call NextScan ; Pasa al siguiente scanline ld b, $16 ; Pinta el byte visible de la pala 22 veces printPaddle_loop: ld (hl), PADDLE ; Imprime el byte de la pala call NextScan ; Pasa al siguiente scanline djnz printPaddle_loop ; Hasta que B = 0 ld (hl), ZERO ; Imprime el último byte de la pala en blanco ret

Por último, tenemos que probar si nuestra rutina funciona. Abrimos el archivo Main.asm y añadimos después de la llamada a PrintLine.

ld hl, (paddle1pos) call PrintPaddle ld hl, (paddle2pos) call PrintPaddle

Cargamos en HL la posición de la pala 1, LD HL, (paddle1pos), y la pintamos, CALL PrintPaddle. Hacemos lo mismo con la pala 2.

Compilamos y vemos los resultados.

Ensamblador ZX Spectrum Pong - PalasEnsamblador ZX Spectrum, palas
Mover las palas hacia arriba y hacia abajo

Abordamos la última parte del paso 3.

Anteriormente declaramos unas constantes con los límites inferior y superior. Ahora vamos a implementar las rutinas que comprueban si una posición de memoria, de la VideoRAM, ha llegado o está fuera de un límite especificado.

El conjunto de rutinas que vamos a implementar, recibe en el registro A el límite en formato TTLLLSSS, y la posición actual en HL en formato 010TTSSS LLLCCCCC. Estas rutinas devuelven Z si se ha alcanzado el límite y NZ en el caso contrario.

CheckBottom: call checkVerticalLimit ret c

Lo primero que hace es llamar a la rutina checkVerticalLimit, CALL checkVerticalLimit, y en el caso de que haya acarreo sale, RET C, con NZ. Si hay acarreo, la posición de memoria está por encima del límite inferior.

checkBottom_bottom: xor a ret

Si llega hasta aquí es porque ha llegado al límite inferior, activa el flag Z, XOR A, y sale, RET.

Esta rutina no hace gran cosa, por lo que se puede suponer que el grueso de la lógica estará en checkVerticalLimit.

Vamos a implementar la rutina para el límite superior.

CheckTop: call checkVerticalLimit jr c, checkTop_top ret nz

Igual que en la rutina anterior, se llama a checkVerticalLimit. En este caso, no se ha llegado al límite si no hay acarreo y el resultado de checkVerticalLimit no es 0, o lo que es lo mismo, es mayor de 0, de ahí la doble condición, JR C, checkTop_top y RET NZ.

checkTop_top: xor a ret

Llega aquí si el resultado de checkVerticalLimit es <= 0 (hay acarreo o el resultado es 0), en cuyo caso activa el flag Z, XOR A, y sale, RET.

El grueso de la detección de los límites, inferior y superior, lo realiza la rutina checkVerticalLimit, que recibe en A el límite vertical (TTLLLSSS) y en HL la posición actual (010TTSSS LLLCCCCC), o posición con la qué comparar.

Debido al distinto formato que tenemos en HL y en A, el primer paso es pasar el contenido que tiene HL al mismo formato que tiene el contenido de A.

checkVerticalLimit: ld b, a ld a, h and $18 rlca rlca rlca ld c, a

Lo primero que hacemos es preservar el valor de A, LD B, A, y acto seguido cargamos el valor de H en A, LD A, H, y nos quedamos con el tercio, AND $18. Rotamos circularmente tres veces el registro A hacia la izquierda, RLCA, para poner el tercio en los bits 6 y 7, y cargamos el valor en C, LD C, A. Ahora C tiene el tercio de la posición que hemos recibido en HL.

ld a, h and $07 or c ld c, a

Volvemos a cargar el valor de H en A, LD A, H, pero esta vez nos quedamos con el scanline, AND $07. Ahora tenemos en A el scanline que viene en HL, y le añadimos el tercio que hemos guardado en C, OR C, y cargamos el resultado en C, LD C, A. Ahora C tiene el tercio y el scanline que hemos recibido en HL, pero con el mismo formato que el valor que hemos recibido en A (TT000SSS).

ld a, l and $e0 rrca rrca or c

Ahora vamos a poner el valor de la línea donde le corresponde, cargando el valor de L en A, LD A, L, quedándonos con los bits donde viene la línea, AND $E0, y rotando circularmente dos veces los bits resultantes para poner la línea en los bits 3, 4 y 5, RRCA. Por último, agregamos el tercio y el scanline que hemos guardado en C, OR C, de tal manera que en A tenemos ahora el tercio, la línea y el scanline que venían en HL, pero con el formato que necesitamos (TTLLLSSS).

cp b ret

El último paso es comparar lo que ahora tenemos en A con lo que tenemos en B, que es el valor original de A (límite vertical), CP B.

Esta última operación va a alterar, entre otros, los flags de acarreo y cero.

ResultadoZC
A = B1 – Z0 – NC
A < B0 – NZ1 – C
A > B0 – NZ0 – NC
Ensamblador ZX Spectrum, resultados de CP B

Dependiendo de estos flags, y si se está evaluando el límite inferior o el superior, sabremos si se ha llegado o traspasado dicho límite.

El código completo de este conjunto de rutinas es el siguiente.

; ----------------------------------------------------------------------------- ; Evalúa si se ha alcanzado el límite inferior. ; Entrada: A = Límite superior (TTLLLSSS). ; HL = Posición actual (010TTSSS LLLCCCCC). ; Salida: Z = Se ha alcanzado. ; NZ = No se ha alcanzado. ; Altera el valor de los registros AF y BC. ; ----------------------------------------------------------------------------- CheckBottom: call checkVerticalLimit ; Compara la posición actual con el límite ; Si Z o NC, ha llegado al tope, se pone Z, de lo contrario NZ ret c checkBottom_bottom: xor a ; Activa Z ret ; ----------------------------------------------------------------------------- ; Evalúa si se ha alcanzado el límite superior. ; Entrada: A = Margen superior (TTLLLSSS). ; HL = Posición actual (010TTSSS LLLCCCCC). ; Salida: Z = Se ha alcanzado. ; NZ = No se ha alcanzado. ; Altera el valor de los registros AF y BC. ; ----------------------------------------------------------------------------- CheckTop: call checkVerticalLimit ; Compara la posición actual con el límite ; Si Z o C, ha llegado al tope, se pone Z, de lo contrario NZ jr c, checkTop_top ; Ha llegado al límite superior y salta ret nz ; No ha llegado al límite superior y sale checkTop_top: xor a ; Activa Z ret ; ----------------------------------------------------------------------------- ; Evalúa si se ha alcanzado el límite vertical. ; Entrada: A = Límite vertical (TTLLLSSS). ; HL = Posición actual (010TTSSS LLLCCCCC). ; Altera el valor de los registros AF y BC. ; ----------------------------------------------------------------------------- checkVerticalLimit: ld b, a ; Guarda el valor de A en B ld a, h ; Carga en A el valor de H (010TTSSSS) and $18 ; Se queda con el tercio rlca rlca rlca ; Pone el valor del tercio en los bits 6 y 7 ld c, a ; Carga el valor en C ld a, h ; Vuelve a cargar en A el valor de H (010TTSSSS) and $07 ; Se queda con el scanline or c ; Añade el tercio ld c, a ; Carga el valor en C ld a, l ; Carga en A el valor de L (LLLCCCCC) and $e0 ; Se queda con la línea rrca rrca ; Pone el valor de la línea en los bits 3, 4 y 5 or c ; Añade el tercio y el scanline. A = TTLLLSSS cp b ; Lo compara con B. B = valor original de A = Límite vertical ret

Usando estas rutinas, ya podemos implementar el movimiento de las palas y evitar que se salgan de la pantalla.

Editamos el fichero Main.asm e incluimos el fichero Controls.asm.

include "Controls.asm"

Vamos a implementar un bucle infinito en el que se evalúa si se ha pulsado alguna tecla de control, en cuyo caso movemos la pala que corresponda. El bucle lo vamos a implementar justo después de la llamada a PrintLine.

loop: call ScanKeys

Lo primero que hace el bucle es evaluar si se ha pulsado alguna de las teclas de control, CALL ScanKeys.

MovePaddle1Up: bit $00, d jr z, MovePaddle1Down ld hl, (paddle1pos) ld a, PADDLE_TOP call CheckTop jr z, MovePaddle2Up call PreviousScan ld (paddle1pos), hl jr MovePaddle2Up

Después de evaluar los controles, evalúa si se ha pulsado la tecla de control para mover la pala 1 hacia arriba, BIT $00, D, y si no es así salta a la siguiente comprobación, JR Z, MovePaddle1Down.

Para mover la pala hacia arriba tenemos que ver si al moverla se sale del límite superior, para lo cual necesitamos saber la posición actual de la pala, LD HL, (paddle1pos), obtener el límite superior, LD A, PADDLE_TOP, y verificar si se ha alcanzado, CALL CheckTop.

Si CheckTop activa el flag Z significa que hemos alcanzado el límite, por lo que saltamos a comprobar el movimiento de la pala 2, JR Z, MovePaddle2Up.

Si no se activa el flag Z, obtenemos la posición en la que se debe pintar la pala, CALL PreviousScan, y la cargamos en memoria, LD (paddle1pos), HL. Por último, saltamos a comprobar el movimiento de la pala 2, JR MovePaddle2Up.

Si no se ha pulsado la tecla de control arriba de la pala 1, se verifica si se ha pulsado la de abajo.

MovePaddle1Down: bit $01, d jr z, MovePaddle2Up ld hl, (paddle1pos) ld a, PADDLE_BOTTOM call CheckBottom jr z, MovePaddle2Up call NextScan ld (paddle1pos), hl

Evalúa si se ha pulsado la tecla de control para mover la pala 1 hacia abajo, BIT $01, D, y si no es así salta a la siguiente comprobación, JR Z, MovePaddle2Up.

Para mover la pala hacia abajo tenemos que comprobar si, al moverla, se sale del límite inferior, para lo cual necesitamos saber la posición actual de la pala, LD HL, (paddle1pos), obtener el límite inferior, LD A, PADDLE_BOTTOM, y verificar si se ha alcanzado, CALL CheckBottom.

Si CheckBottom activa el flag Z significa que hemos alcanzado el límite, por lo que saltamos a comprobar el movimiento de la pala 2, JR Z, MovePaddle2Up.

Si no se activa el flag Z, obtenemos la posición en la que se debe pintar la pala, CALL NextScan, y la cargamos en memoria, LD (paddle1Pos), HL. En esta ocasión no saltamos, ya que en la siguiente instrucción se empieza a comprobar el movimiento de la pala 2.

Debido a que la comprobación del movimiento de la pala 2 es muy parecido al de la pala 1, cambian las posiciones de memoria para obtener la posición de la pala 2 y las de salto, no vamos a entrar a explicarlo en detalle.

MovePaddle2Up: bit 2, d jr z, MovePaddle2Down ld hl, (paddle2pos) ld a, PADDLE_TOP call CheckTop jr z, MovePaddleEnd call PreviousScan ld (paddle2pos), hl jr MovePaddleEnd MovePaddle2Down: bit 3, d jr z, MovePaddleEnd ld hl, (paddle2pos) ld a, PADDLE_BOTTOM call CheckBottom jr z, MovePaddleEnd call NextScan ld (paddle2pos), hl MovePaddleEnd:

La última línea, MovePaddleEnd, es una etiqueta que hemos usado para poder saltar a la zona donde se pintan las palas.

Por último, después de pintar las palas vamos a sustituir RET por JR loop, para quedarnos en un bucle infinito.

El código final del archivo Main.asm queda como sigue:

; Dibuja las dos palas y la línea central. ; Mueve las palas arriba y abajo como respuesta a la pulsación de las teclas de control. org $8000 ld a, $02 ; A = 2 out ($fe), a ; Pone el borde en rojo call Cls ; Limpia la pantalla call PrintLine ; Imprime la línea central loop: call ScanKeys ; Escanea las teclas pulsadas MovePaddle1Up: bit $00, d ; Evalúa si se ha pulsado la A jr z, MovePaddle1Down ; Si no se ha pulsado salta ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 ld a, PADDLE_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, MovePaddle2Up ; Si se ha alcanzado, salta call PreviousScan ; Obtiene el scanline anterior a la posición de la pala 1 ld (paddle1pos), hl ; Carga en memoria la nueva posición de la pala 1 jr MovePaddle2Up ; Salta MovePaddle1Down: bit $01, d ; Evalúa si se ha pulsado la Z jr z, MovePaddle2Up ; Si no se ha pulsado salta ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 ld a, PADDLE_BOTTOM ; Carga en A el margen inferior call CheckBottom ; Evalúa si se ha alcanzado el margen inferior jr z, MovePaddle2Up ; Si se ha alcanzado, salta call NextScan ; Obtiene el scanline siguiente a la posición de la pala 1 ld (paddle1pos), hl ; Carga en memoria la nueva posición de la pala 1 MovePaddle2Up: bit $02, d ; Evalúa si se ha pulsado el 0 jr z, MovePaddle2Down ; Si no se ha pulsado salta ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 ld a, PADDLE_TOP ; Carga en A el margen superior call CheckTop ; Evalúa si se ha alcanzado el margen superior jr z, MovePaddleEnd ; Si se ha alcanzado, salta call PreviousScan ; Obtiene el scanline anterior a la posición de la pala 2 ld (paddle2pos), hl ; Carga en memoria la nueva posición de la pala 2 jr MovePaddleEnd ; Salta MovePaddle2Down: bit $03, d ; Evalúa si se ha pulsado la O jr z, MovePaddleEnd ; Si no se ha pulsado salta ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 ld a, PADDLE_BOTTOM ; Carga en A el margen inferior call CheckBottom ; Evalúa si se ha alcanzado el margen inferior jr z, MovePaddleEnd ; Si se ha alcanzado, salta call NextScan ; Obtiene el scanline siguiente a la posición de la pala 2 ld (paddle2pos), hl ; Carga en memoria la nueva posición de la pala 2 MovePaddleEnd: ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 call PrintPaddle ; Pinta la pala 1 ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 call PrintPaddle ; Pinta la pala 2 jr loop ; Bucle infinito include "Controls.asm" include "Sprite.asm" include "Video.asm" end $8000

Compilamos y vemos los resultados en el emulador.

Ensamblador ZX Spectrum Pong - Palas en movimientoEnsamblador ZX Spectrum, palas en movimiento

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, palas y línea central

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

0x02 Ensamblador ZX Spectrum Pong – Teclas de control [Espamatica] [Leer]


En esta entrega de Ensamblador ZX Spectrum Pong vamos a desarrollar la rutina que comprueba si se han pulsado las teclas de control de nuestro juego, y devuelve cuales son las teclas pulsadas.

Ensamblador ZX Spectrum Pong – Teclas de control

El teclado del ZX Spectrum está divido en ocho semi filas, cada una de las cuales contiene cinco teclas.

Cuando se evalúa si se ha pulsado alguna tecla de una semi fila, los valores vienen en un byte, en los bits 0 a 4, cuyos valores son 1 si no se ha pulsado, y 0 si se ha pulsado. El bit 0 hace referencia a la tecla más alejada del centro del teclado (Caps Shift, A, Q, 1, 0, P, Enter y Space) y el 4 a la tecla más cercana al centro (V, G, T, 5, 6, Y, H y B).

Cada semi fila está identificada por un número:

Semi filaValor HexadecimalValor Binario
Caps Shift-V$FE1111 1110
A-G$FD1111 1101
Q-T$FB1111 1011
1-5$F71111 0111
0-6$EF1110 1111
P-Y$DF1101 1111
Enter-H$BF1011 1111
Space-B$7F0111 1111
Ensamblador ZX Spectrum, valor de cada semi fila

Como se puede observar, para calcular el valor de la semi fila anterior o posterior, solo hay que hacer rotaciones circulares de bits (RLC, RRC).

Dentro de la carpeta Pong, creamos una carpeta llamada Paso02, y dentro de la misma los ficheros Main.asm y Controls.asm.

La rutina que vamos a usar para verificar los controles está sacada del Curso de Ensamblador para Z80 de Compiler Software de Santiago Romero. Podéis encontrar dicho curso en El wiki de speccy.org.

ScanKeys

Los controles que vamos a usar son: A-Z para el jugador 1, y 0-O para el jugador 2.

La rutina que vamos que vamos a implementar para comprobar si se ha pulsado alguna de las teclas expuestas, devuelve en el registro D las teclas que se han pulsado, usando el bit 0 para la tecla A, el bit 1 para la tecla Z, el bit 2 para la tecla 0 y el bit 3 para la tecla O. Los valores que toman estos bits son 1 si se ha pulsado la tecla y 0 en el caso contrario.

Lo primero que va a hacer la rutina es poner a 0 el registro D.

ScanKeys: ld d, $00

Comprobación de la tecla A

A continuación, comprueba si se ha pulsado la tecla A.

scanKeys_A: ld a, $fd in a, ($fe) bit $00, a jr nz, scanKeys_Z set $00, d

Con LD A, $FD cargamos el identificador de la semi fila A-G ($FD = 11111101) en A.

A continuación, con IN A, ($FE), leemos el puerto de entrada $FE (254) y dejamos el valor en A. El puerto de entrada $FE es el puerto desde el que leemos el estado del teclado.

Lo siguiente es comprobar si se ha pulsado la tecla A; para ello usamos la sentencia BIT $00, A, que evalúa el estado del bit 0 del registro A. Si el bit está a 0 se activa el flag Z, de lo contrario se desactiva.

Con la siguientes instrucción, JR NZ, scanKeys_Z, si el bit viene a 1 salta a evaluar la pulsación de la tecla Z.

Si el bit viene a 0, activamos el bit 0 del registro D, SET $00, D, para devolver que se ha pulsado la tecla A.

Comprobación de la tecla Z

El siguiente paso es comprobar si se ha pulsado la tecla Z.

scanKeys_Z: ld a, $fe in a, ($fe) bit $01, a jr nz, scanKeys_0 set $01, d

La diferencia con la comprobación de la tecla A radica en que cargamos en A la semi fila Caps Shift-V, LD A, $FE, comprobamos el estado del bit 1 correspondiente a la tecla Z, BIT $01, A, si no se ha pulsado saltamos a comprobar la pulsación de la tecla 0, JR NZ, scanKeys_0, y, por último, activamos el bit 1 de D, SET $01, D, si se ha pulsado la tecla Z.

Comprobación de pulsación simultánea de A y Z

Se puede dar el caso de que se pulsen a la vez las teclas A y Z. Si se diera, vamos a desactivar los indicadores para asimilar que no se ha pulsado ninguna. La otra opción sería dejar los indicadores de las dos teclas pulsadas y mover el personaje primero hacia arriba y luego hacia abajo, quedándose donde estaba.

Vamos comprobar si se han pulsado las dos teclas, y si es así desactivamos los bits correspondientes.

ld a, d cp $03 jr nz, scanKeys_0 xor a ld d, a

Lo primero es cargar el valor de D en A, LD A, D, y verificar si el valor es $03, CP $03, en cuyo caso se habrían pulsado las dos teclas. Si el valor de la comprobación no es 0, no se han pulsado las dos teclas y saltamos a comprobar las pulsación de la tecla 0, JR NZ, scanKeys_0.

Si el resultado es 0, ponemos A = 0, XOR A, y cargamos el valor en D, LD D, A.

La instrucción CP evalúa el valor del registro A con el valor de otro registro, un número o el valor de una dirección de memoria apuntada por (HL), (IX+n) o (IY+n). CP resta cualquiera de estos valores al valor del registro A. CP no altera el valor de A, pero sí altera los indicadores (registro F), de la siguiente manera:

Valor del flagSignificado
ZA = Valor
NZA <> Valor
CA < Valor
NCA >= Valor
Ensamblador ZX Spectrum, resultados de CP

Para cargar 0 en A, en lugar de LD A, $00 hemos utilizado XOR A.

Las instrucciones AND, OR y XOR, tienen como destino, siempre, el registro A y el resultado que dan a nivel de bits es el siguiente:

OperaciónBit 1Bit 2Resultado
AND111
100
010
000
OR111
101
011
000
XOR110
101
011
000
Ensamblador ZX Spectrum, resultado a nivel de bit de la operaciones lógicas

Como se puede ver en la tabla, XOR A siempre da como resultado 0, una operación que tiene 1 byte y consume 4 ciclos de reloj. Por el contrario, LD A, $00 tiene 2 bytes y consume 7 ciclos de reloj, por lo que ganamos 1 byte y 3 ciclos. Pero no todo son ventajas, ya que XOR afecta a los flags mientras que LD no.

También podríamos haber puesto D a 0, LD D, $00, pero no habríamos visto la instrucción XOR, aunque habríamos ahorrado un ciclo de reloj.

Hay otra forma más óptima de hacerlo; sustituimos CP $03 por SUB $03, y luego cargamos A en D, LD D, A.

ld a, d sub $03 jr nz, scanKeys_0 ld d, a

Estaríamos consumiendo 7 ciclos y 2 bytes con SUB $03, y 4 ciclos y 1 byte con LD D, A, ahorrándonos 3 o 4 ciclos, y 1 byte.

Comprobación de las teclas 0 y O

Por último, hay que comprobar si han pulsado las teclas 0 y O, y si se han pulsado las dos a la vez. El código es casi igual a lo que hemos visto hasta ahora, por lo que vamos a ver el código completo de la rutina.

; ----------------------------------------------------------------------------- ; ScanKeys ; Escanea las teclas de control y devuelve las pulsadas. ; Salida: D = Teclas pulsadas. ; Bit 0 = A pulsada 0/1. ; Bit 1 = Z pulsada 0/1. ; Bit 2 = 0 pulsada 0/1. ; Bit 3 = O pulsada 0/1. ; Altera el valor de los registros AF y D. ; ----------------------------------------------------------------------------- ScanKeys: ld d, $00 ; Pone el registro D a 0. scanKeys_A: ld a, $fd ; Carga en A la semi fila A-G in a, ($fe) ; Lee el estado de la semi fila bit $00, a ; Comprueba si se ha pulsado la A jr nz, scanKeys_Z ; Si no se ha pulsado, salta set $00, d ; Pone a 1 el bit correspondiente a la A scanKeys_Z: ld a, $fe ; Carga en A la semi fila CS-V in a, ($fe) ; Lee el estado de la semi fila bit $01, a ; Comprueba si se ha pulsado la Z jr nz, scanKeys_0 ; Si no se ha pulsado, salta set $01, d ; Pone a 1 el bit correspondiente a la Z ; Comprueba que no se hayan pulsado las dos teclas de dirección ld a, d ; Carga el valor de D en A sub $03 ; Comprueba si se han pulsado la A y la Z a la vez jr nz, scanKeys_0 ; Si no se han pulsado, salta ld d, a ; Pone D a 0 scanKeys_0: ld a, $ef ; Carga la semi fila 0-6 in a, ($fe) ; Lee el estado de la semi fila bit $00, a ; Comprueba si se ha pulsado el 0 jr nz, scanKeys_O ; Si no se ha pulsado, salta set $02, d ; Pone a 1 el bit correspondiente al 0 scanKeys_O: ld a, $cf ; Carga la semi fila P-Y in a, ($fe) ; Lee el estado de la semi fila bit $01, a ; Comprueba si se ha pulsado la O ret nz ; Si no se ha pulsado, salta set $03, d ; Pone a 1 el bit correspondiente a la O ; Comprueba que no se hayan pulsado las dos teclas de dirección ld a, d ; Carga el valor de D en A and $0c ; Se queda con los bits correspondientes a 0 y O cp $0c ; Comprueba si se han pulsado las dos teclas ret nz ; Si no se han pulsado, sale ld a, d ; Se han pulsado, carga el valor de D en A and $03 ; Se queda con los bits correspondientes a la A y Z ld d, a ; Carga el valor en D ret

Las diferencias más importantes con respecto a la comprobación de la pulsación de A-Z, están en la comprobación de si se han pulsado a la vez las dos teclas.

Antes de comprobar si están activos los bits del registro D, que se corresponden con 0 y O ($0C = 0000 1100), hay que quedarse sólo con estos bits, de los contrario, si se hubieran pulsado la A o la Z, CP $0C nunca daría 0, es por eso que antes de esta instrucción se ha incluido AND $0C, para quedarnos el valor de los bits 2 y 3.

La segunda diferencia es la forma en la que ponemos a 0 los bits 2 y 3, en el caso de que se hayan pulsado a la vez 0 y O.

Anteriormente hicimos XOR A o SUB $03 y LD D, A, pues lo único que teníamos en A era si se habían pulsado a la vez A y Z, pero esta vez, además de si se han pulsado 0 y O, tenemos las pulsaciones de A y Z, y si hiciéramos XOR A o SUB $03 y LD D, A, estaríamos destruyendo esta información.

Para evitar destruir esta información, cargamos en A el valor del registro D, LD A, D, luego nos quedamos solo con el valor de los bits 0 y 1, AND $03, y volvemos a cargar el valor en D, LD D, A. De esta manera hemos puesto a 0 el valor de los bits 2 y 3 sin destruir el valor de los bits 0 y 1.

Podemos optimizar sustituyendo LD A, D y AND $03 por XOR D. XOR D tendría el mismo efecto que las otras dos líneas, y solo consumiríamos 4 ciclos de reloj y un byte.

Si el valor de A es 00001100 y el valor de D es 00001101, después de XOR D, el valor de A es 00000001

Test de ScanKeys

Ya solo queda probar la rutina. Para ello vamos a pintar en la esquina superior izquierda el valor de D, una vez que vuelve de la rutina de chequeo de las pulsaciones de las teclas. El código lo vamos a escribir en el archivo Main.asm.

El primer paso es especificar la dirección donde se carga el programa.

org $8000

Apuntamos HL a la esquina superior izquierda de la pantalla.

ld hl, $4000

Y hacemos un bucle infinito que llame a la rutina ScanKeys y cargue en la esquina superior derecha de la ventana el valor del registro D.

Bucle: call ScanKeys ld (hl), d jr Bucle

Por último, incluimos el archivo Controls.asm y le indicamos a PASMO la dirección donde llamar cuando cargue el programa.

include "Controls.asm" end $8000

Llegados a este punto, compilamos el programa y cargamos en el emulador para ver el resultado:

pasmo --name PoromPong --tapbas Main.asm PorompomPong.tap --public

El resultado del programa sería algo así:

Ensamblador ZX Spectrum - Teclas de controlEnsamblador ZX Spectrum, teclas de control

El código final del archivo Main.asm quedará como sigue.

; Comprueba el funcionamiento de los controles A-Z y 0-O ; Pinta la representación de las teclas pulsadas. org $8000 ld hl, $4000 ; Posiciona HL en la primera posición de la pantalla Bucle: call ScanKeys ; Escanea las teclas pulsadas ld (hl), d ; Pinta la representación de las teclas pulsadas jr Bucle ; Bucle infinito include "Controls.asm" end $8000

Hemos dejado una optimización pendiente, que veremos en la última entrega del tutorial, con la que ahorraremos un ciclo de reloj en la comprobación de cada tecla pulsada, lo que hará un total de 4 ciclos de reloj de ahorro en la rutina ScanKeys.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, teclas de control

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

0x01 Ensamblador ZX Spectrum Pong – Dibujando por la pantalla [Espamatica] [Leer]


En esta nueva entrega de Ensamblador ZX Spectrum Pong, vamos a empezar a dibujar por la pantalla, veremos la estructura de la misma y como calcular las coordenadas.

Ensamblador ZX Spectrum Pong – Dibujando por la pantalla

La pantalla del ZX Spectrum está situada, el área de píxeles, desde la dirección de memoria $4000 a la $57FF, ambas inclusive, lo que hace un total de 6144 bytes, o lo que es lo mismo 256×192 píxeles, 32 columnas y 24 líneas.

El ZX Spectrum divide la pantalla en tres tercios, de ocho líneas cada uno, con ocho scanlines (línea horizontal de pantalla de un píxel de alto) por línea. Las direcciones de memoria que referencian a cada byte de la pantalla (área de píxeles), se codifican de la siguiente manera:

010T TSSS LLLC CCCC

Donde TT es el tercio (de 0 a 2), SSS es el scanline (de 0 a 7), LLL es la línea (de 0 a 7) y CCCCC es la columna (de 0 a 31).

En este primer paso vamos a aprender como dibujar por la pantalla, y vamos a ver dos rutinas que usaremos en nuestro Pong, y muy posiblemente en nuestros próximos desarrollos.

Lo primero que vamos a hacer es crear una carpeta llamada Pong, y dentro de la misma vamos a añadir otra carpeta a la que vamos a llamar Paso01. Dentro de esta última carpeta vamos a crear los archivos Main.asm y Video.asm.

Las dos rutinas que vamos a añadir al archivo Video.asm, NextScan y PreviousScan, han sido tomadas del Curso de ensamblador Z80 de Compiler Software de Santiago Romero, que podemos encontrar en El wiki de speecy.org, y calculan el scanline siguiente y anterior a una posición dada.

Ambas rutinas reciben en HL la posición de la VideoRAM desde la que se quiere calcular el siguiente o anterior scanline, y devuelve dicha posición en el mismo registro. También alteran el valor de AF.

NextScan

Pasamos a ver la rutina NextScan.

NextScan: inc h ld a, h and $07 ret nz

En la primera instrucción incrementamos el scanline, INC HL, que se encuentra en los bits 0 a 2 de H. Acto seguido cargamos el valor de H en A, LD A, H, y nos quedamos solo con el valor de los bits del scanline, AND $07.

Si el valor de la operación anterior no es 0, el scanline tiene un valor entre 1 y 7, no es necesario hacer ningún cálculo más y salimos de la runtina, RET NZ.

Si el valor es 0, el scanline antes de incrementar H era 7.

0100 0111

Al sumarle uno, deja los bits del scanline a 0 e incrementa en 1 los bits del tercio.

0100 1000

Lo siguiente que hace la rutina es.

ld a, l add a, $20 ld l, a ret c

Cargamos en A el valor de L, LD A, L, que contiene la línea dentro del tercio y la columna. Le sumamos 1 a la línea, ADD A, $20.

$20 = 0010 0000 = LLLC CCCC

Luego cargamos el resultado en L, LD L, A, y si hay acarreo salimos, RET C.

Si hay acarreo, la línea antes de añadirle $20 era 7. Al añadirle 1, la línea pasa a 0 y hay que incrementar el tercio, que ya se incrementó al incrementar el scanline.

Por último, si seguimos adelante es porque seguimos dentro del mismo tercio, por lo que hay que decrementarlo para dejarlo como estaba. Al llegar a este punto, al incrementar el scanline hemos cambiado de línea, y al incrementar la línea no hemos cambiado de tercio.

ld a, h sub $08 ld h, a ret

Cargamos el valor de H en A, LD A, H, tercio y scanline. A este valor le restamos $08 para decrementar en uno el tercio, SUB $08, y dejarlo como estaba.

$08 = 0000 1000 = 010T TSSS

Cargamos el resultado de la operación en H, LD H, A, y salimos de la rutina, RET.

El código completo de la rutina es:

; ----------------------------------------------------------------------------- ; NextScan. https://wiki.speccy.org/cursos/ensamblador/gfx2_direccionamiento ; Obtiene la posición de memoria correspondiente al scanline siguiente al indicado. ; 010T TSSS LLLC CCCC ; Entrada: HL = scanline actual. ; Salida: HL = scanline siguiente. ; Altera el valor de los registros AF y HL. ; ----------------------------------------------------------------------------- NextScan: inc h ; Incrementa H para incrementar el scanline ld a, h ; Carga el valor en A and $07 ; Se queda con los bits del scanline ret nz ; Si el valor no es 0, fin de la rutina ; Calcula la siguiente línea ld a, l ; Carga el valor en A add a, $20 ; Añade 1 a la línea (%0010 0000) ld l, a ; Carga el valor en L ret c ; Si hay acarreo, ha cambiado de tercio, ; que ya viene ajustado de arriba. Fin de la rutina ; Si llega aquí, no ha cambiado de tercio y hay que ajustar ; ya que el primer inc h incrementó el tercio ld a, h ; Carga el valor en A sub $08 ; Resta un tercio (%0000 1000) ld h, a ; Carga el valor en H ret
Test de NextScan

En este punto vamos a editar el archivo Main.asm para probar la rutina NextScan.

El primer paso es indicar donde se va a cargar el programa, en nuestro caso en la dirección $8000 (32768).

org $8000

Lo siguiente es apuntar HL en la dirección de memoria de la VideoRAM en donde vamos a empezar a dibujar, en nuestro caso en la esquina superior izquierda.

ld hl, $4000

Si recordamos cómo se codifica una dirección de memoria de la VideoRAM:

010T TSSS LLLC CCCC

Y ponemos $4000 en binario:

0100 0000 0000 0000

Vemos que $4000 hace referencia al tercio 0, línea 0, scanline 0 y columna 0.

Vamos a pintar una columna vertical, desde arriba hacia abajo, que ocupe toda la pantalla, por lo que tenemos que hacer un bucle de 192 iteraciones, número de scanlines que tiene la pantalla, y vamos a cargar este valor en B.

ld b, $c0

Una vez llegados a este punto, ya podemos hacer el bucle. Para ello vamos a poner una etiqueta para poder hacer referencia a ella. Cargamos el patrón 00111100 ($3c) en la dirección de la VideoRAM apuntada por HL, obtenemos la posición de memoria del scanline siguiente, y volvemos al principio del bucle hasta que B sea igual a 0.

loop: ld (hl), $3c call NextScan djnz loop

Como se puede ver en esta ocasión, HL va entre paréntesis, pero anteriormente cuando cargamos $4000 en HL no iba entre paréntesis. ¿Cuál es la diferencia?

Cuando escribimos LD HL, $4000, lo que hacemos es cargar $4000 en HL, es decir, HL = $4000. Por el contrario, al escribir LD (HL), $3C, lo que hacemos es cargar $3C en la posición de memoria apuntada por HL, es decir, ($4000) = $3C.

Después de cargar $3C en la posición de memoria apuntada por HL, obtenemos la dirección de memoria del siguiente scanline, lo que logramos llamando a la rutina NextScan, CALL NextScan.

La última instrucción, DJNZ loop, es el motivo de haber elegido el registro B para controlar las iteraciones del bucle. Si hubiéramos elegido otro registro de 8 bits, al llegar a este punto tendríamos que haberlo decrementado y luego comprobar que no ha llegado a 0, en cuyo caso saltaríamos a loop.

dec a
jr nz, loop

DJNZ hace todo esto, en una sola instrucción, usando el registro B, consumiendo 1 byte y 8 o 13 ciclos de reloj dependiendo de si no se cumple, o sí se cumple la condición. Usando DEC y JR se emplean 3 bytes y 11 o 17 ciclos de reloj.

Ya solo queda indicar al programa donde debe salir, incluir el fichero donde se encuentra la rutina NextScan e indicarle a PASMO la dirección a la que tiene que llamar cuando cargue el programa.

ret include "Video.asm" end $8000

Ahora vamos a compilar el programa, para lo cual vamos a utilizar PASMO. Desde la línea de comandos vamos al directorio donde tenemos los ficheros .asm, y tecleamos lo siguiente:

pasmo --name PoromPong --tapbas Main.asm PorompomPong.tap --public

Ahora podemos cargar nuestro programa en el emulador de ZX Spectrum y veremos algo así:

Ensamblador ZX Spectrum - Dibujar por la pantalla - Next ScanEnsamblador ZX Spectrum – Next Scan

Como se ve, ha dibujado una columna vertical, pero es lo suficientemente rápido como para no ver como se dibuja. Para poder verlo vamos a añadir la instrucción HALT antes de DJNZ. La instrucción HALT espera hasta que se produce una interrupción, que en el caso de ZX Spectrum es provocada por la ULA.

El código resultante es:

org $8000 ld hl, $4000 ; Apunta HL al primer scanline de la primera línea ; del primer tercio y columna 1 de la pantalla (Columna de 0 a 31) ld b, $c0 ; B = 192. Número de scanlines que tiene la pantalla loop: ld (hl), $3c ; Pinta en la pantalla 001111000 call NextScan ; Pasa al siguiente scanline halt ; Descomentar línea si se quiere ver el proceso de pintado djnz loop ; Hasta que B = 0 ret include "Video.asm" end $8000

Volvemos a compilar y ahora sí se ve como pinta scanline a scanline. Si quieremos que vuelva a ir rápido, comentamos la instrucción HALT.

PreviousScan

Ahora vamos a implementar, en Video.asm, la rutina que recupera la dirección de memoria del scanline anterior.

PreviousScan: ld a, h dec h and $07 ret nz

Lo primero que hacemos es cargar el valor de H, LD A, H, tercio y scanline en A, y a continuación, decrementamos H, DEC H. Luego nos quedamos con los bits del scanline original, AND $07, que tenemos en A, y si no estaba en el scanline 0 salimos de la rutina, RET NZ. A contiene el valor original de H.

Si estaba en el scanline 0, al decrementar H ha pasado al scanline 7 de la línea anterior y a decrementado el tercio.

Ahora hay que calcular la línea.

ld a, l sub $20 ld l, a ret c

Cargamos el valor de L, LD A, L, línea y columna en A, y le restamos $20, SUB $20, para decrementar la línea, volviendo a cargar el valor en L, LD L, A. Salimos si hay acarreo, RET C, ya que hay cambio de tercio, que se produjo al decrementar el scanline.

En el caso de no haber acarreo, es necesario dejar el tercio como estaba originalmente.

ld a, h add a, $08 ld h, a ret

Cargamos el valor de H, tercio y scanline, en A, LD A, H, y le sumamos $08 para incrementar el tercio, ADD A, $08, volviendo a cargar el valor en H, y salimos de la rutina, RET.

El código final de la rutina es el siguiente:

; ----------------------------------------------------------------------------- ; PreviousScan. https://wiki.speccy.org/cursos/ensamblador/gfx2_direccionamiento ; Obtiene la posición de memoria correspondiente al scanline anterior al indicado. ; 010T TSSS LLLC CCCC ; Entrada: HL = scanline actual. ; Salida: HL = scanline anterior. ; Altera el valor de los registros AF, BC y HL. ; ----------------------------------------------------------------------------- PreviousScan: ld a, h ; Carga el valor en A dec h ; Decrementa H para decrementar el scanline and $07 ; Se queda con los bits del scanline original ret nz ; Si no estaba en el 0, fin de la rutina ; Calcula la línea anterior ld a, l ; Carga el valor de L en A sub $20 ; Resta una línea ld l, a ; Carga el valor en L ret c ; Si hay acarreo, fin de la rutina ; Si llega aquí, ha pasado al scanline 7 de la línea anterior ; y ha restado un tercio, que volvemos a sumar ld a, h ; Carga el valor de H en A add a, $08 ; Vuelve a dejar el tercio como estaba ld h, a ; Carga el valor en h ret
Test PreviousScan

Por último, volvemos a Main.asm para implementar la prueba de PreviousScan. Vamos a añadir el nuevo código después de la instrucción DJNZ loop.

Lo primero es cargar en HL la dirección de la VideoRAM donde vamos a pintar, en este caso la esquina inferior derecha.

ld hl, $57ff

Si ponemos $57FF en binario:

0101 0111 1111 1111

Vemos que hace referencia al tercio 2, línea 7, scanline 7 y columna 31.

El bucle vuelve a ser de 192 iteraciones, para dibujar hasta la esquina superior derecha. Cargamos el valor en B.

ld b, $c0

Y luego hacemos el bucle.

loopUp: ld (hl), $3c call PreviousScan halt djnz loopUp

La única diferencia con el bucle loop radica en el CALL, que en esta ocasión se hace a PreviousScan en lugar de a NextScan. HALT está sin comentar para que se pueda apreciar como pinta.

Volvemos a compilar y vemos el resultado cargando el programa generado en el emulador de ZX Spectrum:

pasmo --name PoromPong --tapbas Main.asm PorompomPong.tap --public
Ensamblador ZX Spectrum - Dibujar por la pantalla - PreviousScanEnsamblador ZX Spectrum – PreviousScan

El código completo de Main.asm es:

; Dibuja dos líneas verticales, una de abajo a arriba y otra de arriba a abajo ; para probar las rutinas NextScan y PreviousScan. org $8000 ld hl, $4000 ; Apunta HL al primer scanline, primera línea, primer tercio ; y columna 0 de la pantalla (Columna de 0 a 31) ld b, $c0 ; B = 192. Número de scanlines que tiene la pantalla loop: ld (hl), $3c ; Pinta en la pantalla 001111000 call NextScan ; Pasa al siguiente scanline ; halt ; Descomentar línea si se quiere ver el proceso de pintado djnz loop ; Hasta que B = 0 ld hl, $57ff ; Apunta HL al último scanline, última línea, último tercio ; y columna 31 de la pantalla (Columna de 0 a 31) ld b, $c0 ; B = 192. Número de scanlines que tiene la pantalla loopUp: ld (hl), $3c ; Pinta en la pantalla 001111000 call PreviousScan ; Pasa al scanline anterior ; halt ; Descomentar línea si se quiere ver el proceso de pintado djnz loopUp ; Hasta que B = 0 ret include "Video.asm" end $8000

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, dibujando por la pantalla

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

0x00 Ensamblador ZX Spectrum Pong – Hola Mundo [Espamatica] [Leer]


Antes de entrar de lleno con el desarrollo de nuestro Pong en Ensamblador ZX Spectrum Pong, PorompomPong a partir de ahora, vamos implementar Hola Mundo, o lo que es lo mismo, hacer lo que se hace casi cada vez que se inicia el aprendizaje de un lenguaje de programación.

Ensamblador ZX Spectrum Pong – Hola Mundo

La implementación de nuestro «Hola Mundo» nos va a servir para adquirir los conocimientos necesarios, para el posterior desarrollo de nuestro PorompomPong.

Con «Hola Mundo» vamos a descubrir:

  • Características del microprocesador Zilog Z80 y de sus registros.
  • La distribución de la memoria del ZX Spectrum.
  • Números en distintas notaciones.
  • Etiquetas, variables y constantes en ensamblador.
  • Directivas ORG y END.
  • Instrucciones de carga.
  • Instrucciones RST.
  • Incrementos y decrementos.
  • Operaciones lógicas.
  • Cambios de flujo de programa.
  • Subrutinas.
  • Puertos de entrada y salida.
¿Qué es el Z80?

El Z80 es un microprocesador que salió al mercado en 1976 de la mano de Zilog. Es el microprocesador que lleva el ZX Spectrum, en todos sus modelos.

El Z80 es una CPU de tipo «Little Endian». Una CPU de este tipo, cuando almacena en memoria valores de 16 bits, almacena en la primera posición el byte menos significativo, y en la siguiente el más significativo; al cargar el valor $CCFF en la posición $8000, almacena en la posición $8000 el valor $FF y en la $8001 el valor $CC.

Otra característica del Z80 es que no es un microprocesador ortogonal, lo que hace que no todas las operaciones entre registros estén permitidas.

Registros del Z80

Los registros son memoria de alta velocidad y baja capacidad, y están integrados en el microprocesador.

El Z80 dispone de registros de 8 y 16 bits.

Registros de 8 bits

  • A: acumulador. Es el destino de las operaciones aritméticas, lógicas y de comparación de 8 bits. Es el byte más significativo del registro de 16 bits AF.
  • F: flags (indicadores). Conjunto de indicadores que dan información de las operaciones que se están realizando. Es el byte menos significativo del registro de 16 bits AF.
  • B: registro de propósito general que se suele usar en bucles; la instrucción DJNZ lo usa como contador. Es el byte más significativo del registro de 16 bits BC.
  • C: registro de propósito general. Es el byte menos significativo del registro de 16 bits BC.
  • D: registro de propósito general. Es el byte más significativo del registro de 16 bits DE.
  • E: registro de propósito general. Es el byte menos significativo del registro de 16 bits DE.
  • H: registro de propósito general. Es el byte más significativo del registro de 16 bits HL.
  • L: registro de propósito general. Es el byte menos significativo del registro de 16 bits HL.
  • I: registro de interrupción. Permite manejar 128 interrupciones distintas.
  • R: registro de refresco de memoria. Manejado por el Z80, cambia los bits del 0 al 6. Se puede usar para generar números pseudo aleatorios entre 0 y 127.

Registros alternativos

Los registros alternativos sirven para hacer una copia temporal de los registros de 8 bits:

  • A’: registro alternativo de A.
  • F’: registro alternativo de F.
  • B’: registro alternativo de B.
  • C’: registro alternativo de C.
  • D’: registro alternativo de D.
  • E’: registro alternativo de E.
  • H’: registro alternativo de H.
  • L’: registro alternativo de L.

Registros de 16 bits

  • AF: formado por el registro A como byte más significativo y el F como byte menos significativo.
  • BC: formado por el registro B como byte más significativo y el C como byte menos significativo. Se usa como contador en operaciones como LDIR, LDDR, etcétera.
  • DE: formado por el registro D como byte más significativo y el E como byte menos significativo. Se usa, generalmente, para leer y escribir en una operación única, así como registro de destino en operaciones como LDIR, LDDR, etcétera.
  • HL: formado por el registro H como byte más significativo y el L como byte menos significativo. Se usa, generalmente, para leer y escribir en una operación única, así como registro de origen en operaciones como LDIR, LDDR, etcétera. El registro HL es el registro acumulador en operaciones de 16 bits.
  • IX: acceso a memoria de forma indexada, LD (IX + desplazamiento), pudiendo ser el desplazamiento un valor entre -128 y 127.
  • IY: acceso a memoria de forma indexada, LD (IY + desplazamiento), pudiendo ser el desplazamiento un valor entre -128 y 127.
  • SP: puntero de pila. Apunta a la posición actual de la cabeza de la pila.
  • PC: contador de programa. Contiene la dirección de la instrucción actual a ejecutar.

Códigos de operación de los registros (opcodes)

  • 0: B
  • 1: C
  • 2: D
  • 3: E
  • 4: H
  • 5: L
  • 6: (HL)
  • 7: A

Estos códigos de operación se utilizan para calcular el código de operación de las instrucciones en las que el parámetro es un registro:

  • LD A, r: 0x78 + rb
  • LD C, r: 0x48 + rb

Siendo rb el código de operación de los registros que se cargan, en este caso en A o en B.

Registro F

Cada bit del registro F, indicadores, tiene un significado propio que cambia automáticamente según el resultado de las operaciones que se realizan:

  • Bit 0: flag C (acarreo). Se pone a 1 si el resultado de la operación anterior necesita un bit extra para representarse (me llevo una). Ese bit, flag de acarreo, es el bit extra que se necesita.
  • Bit 1: flag N (resta). Se pone a 1 si la última operación fue una resta.
  • Bit 2: flag P/V (paridad/desbordamiento). En operaciones que modifican el bit de paridad, se pone a 1 cuando el número de bits a 1 es par. En operaciones que modifican el bit de desbordamiento, se pone a 1 cuando el resultado de la operación necesita más de 8 bits para representarse.
  • Bit 3: no se usa.
  • Bit 4: flag H (acarreo BCD). Se pone a 1 cuando en operaciones BCD existe un acarreo del bit 3 al 4.
  • Bit 5: no se usa.
  • Bit 6: flag Z (cero). Se pone a 1 si el resultado de la operación anterior es 0. Muy útil en bucles.
  • Bit 7: flag S (signo). Se pone a 1 si el resultado de la operación en complemento a dos es negativo.

No se puede acceder directamente al registro F, y no todas las operaciones le afectan.

Registro F – indicadores de flags

Bit76543210
SZF5HF3P/VNC
Ensamblador ZX Spectrum, registro F Memoria del ZX Spectrum

La memoria está divida en dos, en los modelos 16K, o cuatro, en los modelos 48K, bloques de 16 KiB cada uno (16384 bytes):

  • Primer bloque: de la posición $0000 a la $3FFF (0 a 16383). Se corresponde con la ROM y es de solo lectura.
  • Segundo bloque: de la posición $4000 a la $7FFF (16384 a 32767). En este bloque se encuentran el área de la pantalla, el buffer de impresora, las variables de sistema, etcétera, dejando aproximadamente 9 KiB para los programas, en los casos de los modelos 16K.

Los siguientes bloques de memoria solo se encuentran en los modelos 48K:

  • Tercer bloque: de la posición $8000 a la $BFFF (32768 a 49151). Es memoria RAM de propósito general.
  • Cuarto bloque: de la posición $C000 a la $FFFF (49152 a 65535). En memoria RAM de propósito general.

La distribución del segundo bloque, muy por encima, es la siguiente:

  • $4000 – $57FF (16384 a 22527): área de los píxeles de la pantalla. La pantalla del ZX Spectrum tiene una resolución de 256*192 píxeles. Cada byte de este rango de memoria representa ocho píxeles (256*192/8 = 6144 bytes).
  • $5800 $5AFF (22528 a 23295): área de los atributos de color de la pantalla. La resolución en este caso es de 32*24 caracteres. Cada byte especifica el color de una zona de 8*8 píxeles, definiendo en los bits del 0 al 2 el color de tinta (de 0 a 7), en los bits del 3 al 5 el color del fondo (de 0 a 7), en el bit 6 el brillo (de 0 a 1) y en el bit 7 el parpadeo (de 0 a 1). El área ocupa un total de 768 bytes (32*24).
  • $5B00 a $5BFF (23296 a 23551): búfer de impresora. 256 bytes que se pueden usar si no tenemos impresora, o si no lo usa el programa.
  • $5C00 a $5CB5 (23552 a 23733): variables de sistema.
  • $7FFF: puntero de pila. Suele apuntar a esta dirección y decrece según se ponen cosas en ella.
Decimal, binario, hexadecimal

La representación decimal es en la que estamos acostumbrados a ver los números, en una secuencia de dígitos en los que cada uno puede contener un valor entre 0 y 9. Esta notación también se conoce como decimal o en base 10.

En informática es distinto ya que los ordenadores trabajan con dos valores: 0 y 1; estos números se conocen como binarios o en base 2.

En ensamblador, la forma más común de representar los números es en base 16 (notación hexadecimal). En hexadecimal, cada dígito puede representar un valor del 0 al 15; a partir del 9 se usan letras:

  • A: 10
  • B: 11
  • C: 12
  • D: 13
  • E: 14
  • F: 14

Un dígito hexadecimal representa 4 bits, por lo que a simple vista sabemos de cuántos bits se compone, siendo lo normal hablar de múltiplos de 8 (8, 16, 32, 64…).

Sin una calculadora a mano, la conversión de números entre distintas bases puede llegar a ser muy tediosa. Resulta de gran ayuda saber el valor de cada bit; en el caso del Z80, números de 8 y 16 bits.

Vamos a usar la siguiente tabla, en la que se muestran los valores de cada bit, para guiarnos en las conversiones:

1514131211109876543210
327681638481924096204810245122561286432168421
Ensamblador ZX Spectrum, valores de cada bit

Según se ve en esta tabla, solo tenemos que sumar para convertir números de una base a otra, como se puede observar en el ejemplo siguiente:

5FA0h = 0101 1111 1010 0000 = 32 + 128 + 256 + 512 + 1024 + 2048 + 4096 + 16384 = 24480

Como se puede ver, la conversión hexadecimal/binario es directa, de cuatro en cuatro bits.

F0h = 1111 0000 3Ah = 0011 1010 CCh = 1100 1100 78h = 0111 1000 0001 0000 = 10h 0100 0101 = 45h 1010 1010 = AAh 0010 0011 = 23h Etiquetas, variables y constantes

Las etiquetas nos permiten hacer referencia a posiciones de memoria a través de ellas, en lugar de tener que calcular y memorizar las direcciones. El programa ensamblador se encarga de sustituir las etiquetas por las direcciones de memoria correctas; este proceso lo realiza al crear el código objeto.

Si no pudiéramos utilizar etiquetas, al modificar alguna parte del código, habría que recalcular las direcciones de memoria para todos los JR, JP o CALL. El ensamblador sustituye las etiquetas por las direcciones de memoria de las instrucciones que siguen a las mismas.

Las etiquetas sirven para definir rutinas y datos; en el caso de los datos, pueden ser numéricos o texto, y constantes o variables.

Los datos se definen usando las siguientes directivas:

  • EQU: define constantes.
  • DB/DEFB: define bytes.
  • DM/DEFM: define message.
  • DW/DEFW: define word.
  • DS/DEFS: define space.
nombre EQU valor nombre DB 1, $FF, %10101010 nombre DEFM "Hola Mundo" nombre DW $0040 nombre DEFS $08

DB, DEFB, DM, DEFM, DW, DEFW, DS o DEFS no se ensamblan, por lo que es recomendable ponerlas al final del código, ya que se ejecutarán como si fueran instrucciones del Z80. Si el código empezara con:

DB $CD, $00, $00

Al no ensamblarse la directiva DB, esta línea haría un reset ya que DB $CD, $00, $00 es CALL $0000.

ORG y END

ORG y END son dos de las directivas más importantes de las que vamos a usar. Con ORG especificamos las dirección de memoria donde cargar el código, pudiéndose poner varios ORG para cargar partes del código en distintas direcciones de memoria.

END sirve para indicar dónde finaliza el programa, y una dirección de autoinicio para PASMO.

Con lo que hemos visto hasta ahora, podemos desarrollar nuestro primer programa; no olvidéis abrir el editor de texto para escribir estas líneas.

org $8000 ret end $8000

Grabamos el archivo como «holamundo.asm» y compilamos con PASMO:

pasmo --name HolaMundo --tapbas holamundo.asm holamundo.tap --public

Este comando (pasmo…) lo vamos a usar siempre para compilar nuestros programas.

Ahora podemos abrir el archivo holamundo.tap con un emulador de ZX Spectrum y vemos que se ejecuta, aunque lo único que hace es salir, pero al menos no hemos roto nada.

ZX Spectrum Pong, Hola Mundo, el primer programa Ensamblador ZX Spectrum, el primer programa
Instrucciones de carga

Estas instrucciones se utilizan para cargar un valor en un registro, copiar el valor de un registro en otro, cargar un valor en memoria, cargar un registro en memoria y cargar un valor de memoria en un registro.

La sintaxis de las instrucciones de carga es la siguiente:

LD destino, origen

Destino puede ser un registro o una posición de memoria, mientras que el origen puede ser un registro, una posición de memoria o un valor de 8 o 16 bits.

Estas instrucciones no afectan al registro F, a excepción de LD A, I y LD A, R.

Este es el momento de volver a nuestro primer programa donde, justo debajo de ORG, vamos a agregar las siguientes líneas:

ld hl, $4000 ld (hl), $ff

Con estas líneas activamos los 8 bits de la primera dirección de memoria de la pantalla, en adelante VideoRAM. Compilamos con PASMO y cargamos en el emulador:

pasmo --name HolaMundo --tapbas holamundo.asm holamundo.tap --public
Hola Mundo, primeros pxeles Ensamblador ZX Spectrum, primeros píxeles
Instrucciones RST

Estas instrucciones son utilizadas para saltar a una dirección concreta a través de una instrucción de un solo código de operación (opcode).

Existen varias instrucciones RST, aunque solo vamos a usar RST $10 (RST 16), que imprime el ASCII correspondiente al valor que tiene el registro A.

Recuperamos el archivo holamundo.asm, quitamos las dos líneas que habíamos añadido y escribimos las siguientes:

ld a, 'H' rst $10

Compilamos y cargamos en el emulador. La letra H se debe imprimir en la pantalla.

Hola Mundo, imprime la H Ensamblador ZX Spectrum, imprime la H
Incrementos y decrementos

Sirven para incrementar (INC), o decrementar (DEC), en una unidad el contenido de determinados registros o posiciones de memoria (apuntadas por los registros HL, IX o IY).

Las operaciones permitidas son:

INC r DEC r INC rr DEC rr INC (HL) DEC (HL) INC (IX + n) DEC (IX + n) INC (IY + n) DEC (IY + n)

Estas operaciones, cuando se realizan sobre registros de 16 bits no afectan al registro F, mientras que, si se realizan sobre registros de 8 bits afectan de distintas maneras:

Flags
InstrucciónSZHPNC
INC r***V0
INC (HL)***V0
INC (ri + n)***V0
INC rr
DEC r***V1
DEC (HL)***V1
DEC (ri + n)***V1
DEC rr
Ensamblador ZX Spectrum, – = no afecta, * = afecta, 0 = se pone a 0, 1 = se pone a 1, V = overflow

Recuperamos el archivo holamundo.asm, y lo dejamos tal y como sigue:

org $800 ; Dirección donde carga el programa ld hl, msg ; Carga en HL la dirección de memoria del mensaje ld a, (hl) ; Carga en A el primer carácter rst $10 ; Imprime el carácter inc hl ; Apunta HL al carácter siguiente ld a, (hl) ; Carga el carácter en A rst $10 ; Imprime el carácter ret msg: defm 'Hola ensamblador ZX Spectrum' end $8000

Compilamos y cargamos en el emulador. Ahora veremos «Ho» impreso en pantalla.

Hola Mundo, imprime Ho Ensamblador ZX Spectrum, imprime Ho
Operaciones lógicas

Las operaciones lógicas se realizan a nivel de bit, comparando dos bits. Hay tres tipos de operaciones lógicas:

  • AND: multiplicación lógica. El resultado solo es 1 si los dos bits están a 1.
  • OR: suma lógica. Si alguno de los dos bits está a 1, el resultado es 1, de lo contrario el resultado es 0.
  • XOR: or exclusivo. Si los dos bits son iguales, el resultado es 0, de lo contrario el resultado es 1.

En la siguiente tabla se muestran los posibles resultados de la operaciones lógicas:

Bit 1Bit 2ANDORXOR
11110
10011
01011
00000
Ensamblador ZX Spectrum, resultados de las operaciones lógicas

El formato de las operaciones lógicas es el siguiente:

AND origen OR origen XOR origen

En las operaciones lógicas, el origen puede ser cualquiera de los registros de 8 bits (a excepción del F), un valor, una posición de memoria apuntada por (HL) o por los registros índice, (IX + n) o (IY + n). El destino siempre es el registro A; la operaciones lógicas se hacen sobre el valor que contiene el registro A, y el resultado se deja en este mismo registro.

La operaciones lógicas afectan al registro F de la siguiente manera:

Flags
InstrucciónSZHPNC
AND s***P00
OR s***P00
XOR s***P00
Ensamblador ZX Spectrum, – = no afecta, * = afecta, 0 = se pone a 0, 1 = se pone a 1, P = paridad Cambios de flujo de programa

Cambian el flujo del programa (salta), con o sin condiciones, de manera absoluta (JP) o relativa (JR). Estas instrucciones no afectan al registro F.

Los saltos absolutos pueden ser:

  • JP nn: salta a la dirección de memoria nn, que puede ser una etiqueta (en los siguientes casos también).
  • JP (HL): salta a la dirección de memoria del valor que tiene HL; al valor de HL (16 bits), no al valor de la dirección apuntada por HL (8 bits).
  • JP (registro índice): salta a la dirección de memoria del valor que tiene IX o IY.
  • JP NZ, nn: salta a la dirección nn si el falg Z está a cero; el resultado de la última operación no es cero.
  • JP Z, nn: salta a la dirección de memoria nn si el flag Z está a uno; el resultado de la última operación es cero.
  • JP NC, nn: salta a la dirección de memoria nn si el flag C está a cero; no hay acarreo.
  • JP C, nn: salta a la dirección de memoria nn si el flag C está a uno; hay acarreo.
  • JP PO, nn: salta a la dirección de memoria nn si el flag P/V está a cero; no hay paridad/desbordamiento.
  • JP PE, nn: salta a la dirección de memoria nn si el flag P/V está a uno; hay paridad/desbordamiento.
  • JP P, nn: salta a la dirección de memoria nn si el flag S está a cero; el resultado de la última operación es positivo.
  • JP M, nn: salta a la dirección de memoria nn si el flag S está a uno; el resultado de la última operación es negativo.

Los saltos relativos, son saltos relativos a la instrucción actual y saltan un número de bytes que van desde -128 a 127. Las rutinas con saltos relativos son reubicables, pues no afecta la posición de memoria en la que se cargan. Los saltos relativos pueden ser:

  • JR n: salta a la dirección de memoria que está n bytes; n puede ser una etiqueta (en los siguientes casos también).
  • JR NZ, n: salta a la dirección de memoria que está n bytes si el flag Z está a cero; el resultado de la última operación no es cero.
  • JR Z, n: salta a la dirección de memoria que está n bytes si el flag Z está a uno; el resultado de la última operación es cero.
  • JR NC, n: salta a la dirección de memoria que está n bytes si el flag C está a cero; no hay acarreo.
  • JR C, n: salta a la dirección de memoria que está n bytes si el flag C está a uno; hay acarreo.

Recuperamos el fichero holamundo.asm y vamos a utilizar las operaciones lógicas, y los cambios de flujo, para imprimir todo el mensaje.

org $8000 ; Dirección donde se carga el programa ld hl, msg ; Carga en HL la dirección de memoria del mensaje Bucle: ld a, (hl) ; Carga un carácter de la cadena or a ; Comprueba si A es 0. A or A = 0 solo si A = 0 jr z, Fin ; Si A = 0, salta a la etiqueta Fin rst $10 ; Imprime el carácter inc hl ; Apunta HL al siguiente carácter jr Bucle ; Vuelve al principio del bucle Fin: ret ; Sale del programa msg: defm 'Hola ensamblador ZX Spectrum', $00 ; Cadena terminada en 0 = null end $8000

Compilamos con PASMO, cargamos en el emulador y vemos el los resultados.

Hola Mundo, Hola ensamblador ZX SpectrumHola ensamblador ZX Spectrum
Subrutinas

Las subrutinas son bloques de código, que hacen una acción concreta, y al que se puede llamar en ocasiones múltiples; se usa CALL para saltar a una subrutina y RET para salir y volver al lugar desde el que se ha llamado.

CALL es parecido a JP, pero antes de saltar hace un PUSH de PC para guardar por dónde va el programa. Al hacer RET, se hace POP de PC y el programa vuelve por donde iba.

Se pueden realizar CALL y RET condicionales, al igual que se ha visto con JP y JR.

CALL nn RET CALL NZ, nn RET NZ CALL Z, nn RET Z CALL NC, nn RET NC CALL C, nn RET C CALL PO, nn RET PO CALL PE, nn RET PE CALL P, nn RET P CALL M, nn RET M

Recuperamos holamundo.asm, y gracias a CALL vamos a llamar a alguna rutina de la ROM, para hacer que los resultados sean algo más vistosos.

org $8000 ; Dirección donde se carga el programa ; Variable de sistema donde están los atributos permanentes ; de la pantalla 1. La pantalla 1 es la principal. ; El formato es Flash, Bright, Paper, Ink (FBPPPIII). ATTR_S: equ $5c8d ; Variable de sistema donde está el atributo actual (FBPPPIII). ATTR_T: equ $5c8f ; ------------------------------------------------------------ ; Rutina de la ROM similar al AT de Basic ; Posiciona el cursor en las coordenadas especificadas. ; Entrada: B = Coordenada Y. ; C = Coordenada X. ; Para esta rutina, la esquina superior izquierda de la pantalla ; es (24, 33). ; Altera el valor de los registros A, DE y HL. ; ------------------------------------------------------------ LOCATE: equ $0dd9 ; ------------------------------------------------------------ ; Rutina de la ROM semejante al CLS de Basic. ; Borra la pantalla usando los atributos cargados en la ; variable de sistema ATTR_S. ; Altera el valor de los registros AF, BC, DE y HL. ; ------------------------------------------------------------ CLS: equ $0daf Inicio: ld a, $0e ; Carga en A los atributos de color ld hl, ATTR_T ; Carga en HL la dirección de memoria donde se ; encuentran los atributos actuales ld (hl), a ; Carga en memoria los atributos actuales ld hl, ATTR_S ; Carga en HL la dirección de memoria donde ; se encuentran los atributos permanentes ld (hl), a ; Carga en memoria los atributos permanentes call CLS ; Limpia la pantalla usando los atributos de ATTR_S ld b, $18-$0a ; Carga la coordenada Y en B ld c, $21-$02 ; Carga la coordenada X en C call LOCATE ; Posiciona el cursor ld hl, msg ; Carga en HL la dirección de memoria del mensaje Bucle: ld a, (hl) ; Carga un carácter de la cadena or a ; Comprueba si A es 0. jr z, Fin ; Salta a la etiqueta fin si A = 0 rst $10 ; Imprime el carácter inc hl ; Apunta HL al siguiente carácter jr Bucle ; Bucle hasta que A = 0 Fin: jr Fin ; Bucle infinito msg: defm 'Hola ensamblador ZX Spectrum', $00 ; Cadena terminada en 0 = null end $8000

Compilamos con PASMO, cargamos en el emulador y vemos los resultados.

Hola Mundo, fondo y tinta Ensamblador ZX Spectrum, fondo azul y tinta amarilla
Puertos de entrada y salida

Los puertos de entrada y salida se usan, entre otras cosas, para leer el teclado, el joystick, etcétera.

En nuestro caso, por ahora, solo lo vamos a usar para cambiar el color del borde de la pantalla, usando la instrucción OUT y el puerto $FE.

Vamos a realizar un pequeño programa para ver cómo se cambia el borde.

org $8000 ; Dirección donde se carga el programa ld a, $01 ; Carga el color del borde en A out ($fe), a ; Cambia el color del borde ret end $8000

Compilamos con PASMO, cargamos en el emulador y vemos el resultado.

Hola Mundo, borde Ensamblador ZX Spectrum, borde azul

Con esto ya podemos finalizar nuestro primer programa en ensamblador para ZX Spectrum. Recuperamos el archivo holamundo.asm y añadimos las líneas para cambiar el color del borde, justo antes de la llamada a CLS, quedando el código de la siguiente manera:

org $8000 ; Dirección donde se carga el programa ; Variable de sistema donde están los atributos permanentes ; de la pantalla 1. La pantalla 1 es la principal. ; El formato es Flash, Bright, Paper, Ink (FBPPPIII). ATTR_S: equ $5c8d ; Variable de sistema donde está el atributo actual (FBPPPIII). ATTR_T: equ $5c8f ; ------------------------------------------------------------ ; Rutina de la ROM similar al AT de Basic ; Posiciona el cursor en las coordenadas especificadas. ; Entrada: B = Coordenada Y. ; C = Coordenada X. ; Para esta rutina, la esquina superior izquierda de la pantalla ; es (24, 33). ; Altera el valor de los registros A, DE y HL. ; ------------------------------------------------------------ LOCATE: equ $0dd9 ; ------------------------------------------------------------ ; Rutina de la ROM semejante al CLS de Basic. ; Borra la pantalla usando los atributos cargados en la ; variable de sistema ATTR_S. ; Altera el valor de los registros AF, BC, DE y HL. ; ------------------------------------------------------------ CLS: equ $0daf Inicio: ld a, $0e ; Carga en A los atributos de color ld hl, ATTR_T ; Carga en HL la dirección de memoria donde se ; encuentran los atributos actuales ld (hl), a ; Carga en memoria los atributos actuales ld hl, ATTR_S ; Carga en HL la dirección de memoria donde ; se encuentran los atributos permanentes ld (hl), a ; Carga en memoria los atributos permanentes ld a, $01 ; Carga en A el color del borde out ($fe), a ; Cambia el color del borde call CLS ; Limpia la pantalla usando los atributos de ATTR_S ld b, $18-$0a ; Carga la coordenada Y en B ld c, $21-$02 ; Carga la coordenada X en C call LOCATE ; Posiciona el cursor ld hl, msg ; Carga en HL la dirección de memoria del mensaje Bucle: ld a, (hl) ; Carga un carácter de la cadena or a ; Comprueba si A es 0. jr z, Fin ; Salta a la etiqueta fin si A = 0 rst $10 ; Imprime el carácter inc hl ; Apunta HL al siguiente carácter jr Bucle ; Bucle hasta que A = 0 Fin: jr Fin ; Bucle infinito msg: defm 'Hola ensamblador ZX Spectrum', $00 ; Cadena terminada en 0 = null end $8000

Compilamos con PASMO, cargamos en el emulador y vemos los resultados.

Hola Mundo, aspecto finalHola ensamblador ZX Spectrum, aspecto final

Ya hemos realizado nuestro primer programa en ensamblador para ZX Spectrum. A partir de aquí empezamos con el desarrollo de nuestro PorompomPong.

Podéis descargar todo el código que hemos generado.

Enlaces de interés Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, Hola Mundo

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

0xFF Ensamblador ZX Spectrum Pong – Introducción [Espamatica] [Leer]


El objetivo del presente tutorial es adquirir las nociones básicas que nos permitirán, más adelante, realizar nuestros propios desarrollos en Ensamblador ZX Spectrum.

Para hacer más ameno el proceso, vamos a desarrollar, paso a paso, una versión de uno de los videojuegos más famosos de todos los tiempos, Pong.

Ensamblador ZX Spectrum Pong

Nuestra versión va a ocupar poco más de 1.5 Kb y es compatible con los modelos de 16, 48 y 128 Kb de ZX Spectrum.

A cada paso veremos algo nuevo, y el resultado será algo funcional, haremos cosas que luego cambiaremos, hasta concluir el desarrollo de nuestro Pong en ensamblador ZX Spectrum.

No se pretende hacer un código optimizado, más bien mostrar, paso a paso, cómo hacer las cosas de distintas maneras.

Ensamblador ZX Spectrum PongEnsamblador ZX Spectrum, Pong
Herramientas que vamos a usar

A continuación, se detallan las herramientas que vamos a usar para el desarrollo de nuestro Pong.

Editor de texto

Cualquier editor de texto vale, por sencillo que sea, como es el caso del Bloc de notas de Windows.

Otros editores gratuitos, más potentes y con resaltado de sintaxis son:

  • Notepad++
  • Visual Studio Code, instalando la extensión Z80 Assembly (imanolea.z80-asm)
  • Sublime Text, instalando el paquete z80asm-ti

Emulador de ZX Spectrum

Son muchos los emuladores de ZX Spectrum, siendo quizá los gratuitos los mejores.

Para el tutorial vamos a utilizar ZEsarUX, un emulador de desarrollo español y disponible para Windows, Mac y Linux.

Compilador de ensamblador

Para este tutorial vamos a utilizar PASMO, que es un ensamblador cruzado con versiones para Windows, Mac y Linux, y que genera código ejecutable, para entre otros, el ZX Spectrum.

PASMO funciona por línea de comandos, por lo que, si usas Windows, es recomendable incluirlo en la variable Path para que se pueda ejecutar desde cualquier directorio.

Control de código fuente

Es una buena práctica tener algún tipo de control de código fuente, para que en caso de que algo deje de funcionar poder ver como estaba en una versión anterior.

Es nuestro caso he optado por Git, creando un repositorio local e instalando en Visual Studio Code la extensión Git Graph (mhutchie.git-graph).

Este no es un requisito obligatorio, aunque es muy recomendable.

Enlaces de interés

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.

No olvides visitar las webs amigas.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

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

Dedicatorias de Jesús Martínez del Vals [Espamatica] [Leer]


Revisando todas las dedicatorias que tengo de Jesús Martínez del Vals (JMV), me he dado cuenta de que es algo de lo que puedo presumir, además de que debo compartir con el resto de los mortales. Así que dicho y hecho, me he puesto a escanearlas y las subo para disfrute propio y de todo aquel que aprecie el trabajo de Jesús.

Ya solo me queda por dedicar «Queremos su dinero», espero que tengamos ocasión de solucionar esta situación.

¡Muchas gracias Maestro!

El arte de Jesús Martínez del Vals
  • Jesús Martnez del Vals - Load JMV - The Prayer Of The WarriorThe Prayer of the Warrior – Realizada sobre la postal 15 de Load «JMV» en Retro Parla 2019
  • Load JMV - Panama JoePanama Joe – Realizada sobre la postal 15 de Load «JMV»
  • Run JMV - Sir FredSir Fred – Realizada sobre la postal 15 de Run «JMV» en Retro Parla 2019
  • Run JMV - Manic MinerManic Miner – Realizada sobre la postal 15 de Run «JMV»
  • Print JMV - Plaga GalacticaPlaga Galáctica – Realizada sobre la postal 15 de Print «JMV»
  • Print JMV - PssstPssst – Realizada sobre la postal 15 de Print «JMV»
  • El Mundo Del SpectrumRealizada sobre El Mundo del Spectrum en Retro Parla 2019
  • El Mundo Del Spectrum +Realizada sobre El Mundo del Spectrum + en Retro Parla 2019
  • La Aventura ColosalRealizada sobre La Aventura Colosal en Retro Parla 2019
  • PentragramPentragram – Regalo que me hizo JMV en Retro Parla 2020
  • Subasta solidariaSubasta solidaria en apoyo a joven jugador de baloncesto
  • BokRealizada sobre Bok Redux
Dedicatorias que me ha realizado Jesús Martínez del Vals


Puedes ver el resto de mi colección aquí.

Play Again 10 [Espamatica] [Leer]


No ha esperado mucho tiempo desde la reciente publicación de su libro 1990-1990 La década dorada de los videojuegos retro, y Enrique Segura Alcalde nos vuelve a obsequiar con una nueva entrega de Play Again, la número diez.

Esta nueva entrega consta de sesenta páginas repletas de contenido y, como es costumbre en Enrique, te puedes hacer con ella de manera totalmente gratuita.

La anterior entrega data de marzo del presente año, y es que desde entonces han pasado muchas cosas que nos han tenido bastante entretenidos, y Enrique, además, ha estado sacando adelante el proyecto que he mencionado anteriormente, cuyos derechos de autor dona, íntegros, a la Asociación Española Contra el Cáncer (AECC).

Play Again nº 10

Tras el editorial, en el que Enrique echa la vista atrás para recordar que hace dos años y medio que nació Play Again y anunciar que las entregas seguramente no sean trimestrales, nos sumerge por una variedad de artículos entre los que está muy presente su amada SEGA.

Entre los artículos que nos ofrece podemos encontrar un especial por el treinta cumpleaños de SEGA GAME GEAR, otro sobre películas basadas en videojuegos, leyendas urbanas del retro o emuladores para SEGA Dreamcast (sobre la que también ha escrito otro libro), entre otras cosas.

Llama la atención un coleccionable, Platinum Collection, con ocho páginas dedicadas en este caso a Resident Evil 2. Y llama la atención porque en el índice señala que es la primera entrega de diez, por lo que se puede deducir de Play Again va a llegar, como mínimo, hasta los veinte números, lo cual es una excelente noticia.

Espero que disfrutéis esta nueva publicación que podéis descargar desde aquí, y recordaros que también podéis descargar todas la publicaciones de libre distribución de Enrique desde aquí.

Solo queda dar las gracias a Enrique y desear que siga proporcionándonos buenos momentos de lectura y entretenimiento.

AUA – 8BP: Tabla de secuencias de animación [Espamatica] [Leer]


Sin apenas darnos tiempo a asimilar la entrada de ayer, AUA nos vuelve a regalar otro interesante artículo.

En esta ocasión nos guían a través del proceso de crear secuencias de animación con 8BP.

Esta nueva entrada forma parte del curso que están publicando sobre el uso de 8BP para crear juegos para Amstrad CPC, a razón de una entrega semanal. No dejéis de leer la primera entrega y, por supuesto, las siguientes.

Esperamos ver pronto los juegos que vayáis desarrollando.

https://auamstrad.es/cursos-tutoriales/8bp-secuencias-de-animacion/

AUA – La pantalla gris de la muerte [Espamatica] [Leer]


Abrimos esta sección con un más que interesante artículo publicado por AUA (Amigos y usuarios de Amstrad), en la que nos enseñan como arreglar «La pantalla gris de la muerte».

En este artículo, redactado por Pablo Forcén Soler, nos muestran paso a paso como arreglar nuestro CPC cuándo, al encenderlo, no vemos más que una pantalla gris con un borde negro.

No te pierdas este artículo, uno de tantos, en los que se nos enseñan a reparar o ampliar nuestro querido CPC.

Seguiremos atentos a todo lo que publiquen nuestros amigos de AUA.

https://auamstrad.es/taller/reparaciones/pantalla-gris-de-la-muerte/

Play Again 9 [Espamatica] [Leer]


Volvemos a tener buenas noticias de la mano de Enrique Segura, y es que acaba de editar un nuevo número de Play Again, el noveno.

Este esta ocasión abre con una propuesta de 10 grandes juegos para PSP que no debes perderte, para continuar con 20 cartuchos inolvidables para NES.

Tras estos dos reportajes nos trae una nueva sección, Leyendas Urbanas Retro. Esta primera entrega está dedicada a Bleemcast, «producto oficial destinado a venderse de forma legal para todos aquellos afortunados poseedores de una Dreamcast que quisieran rescatar los mejores juegos de PlayStation con mejor calidad, un considerable lavado de cara y una espectacular mejora en las texturas«. Y hasta aquí el spoiler.

El siguiente reportaje trata sobre videojuegos tramposos, 10 en total, que jugaban sucio para ganarnos.

Para continuar, Enrique pone en duda ese dicho de que segundas partes nunca fueron buenas, y para demostrar que si que lo son nos presenta unos cuantos ejemplos de varias plataformas, cargándose de argumentos.

En regreso al Video Club, Enrique nos trae una serie que, seguramente, muchas de las personas que lean estas líneas recuerdan, El Gran Héroe Americano.

De ahí pasa a Retro Remake, Casi Retro y Basurero Retro. Mejor es que lo leáis.

Finaliza el presente número con las cartas a los lectores y un avance de lo que vendrá en el próximo número.

Os dejo el enlace para que podáis descargar el numero 9 de Play Again y no olvidéis que tenéis todas las descargas disponibles aquí.

Play Again 9Play Again 9

Taller de ensamblador para ZX Spectrum 16K – Pong [Espamatica] [Leer]


Como segunda parte del taller de iniciación al ensamblador del ZX Spectrum que realicé con la ayuda de Retro Parla, llega este nuevo taller de ensamblador en el que os propongo desarrollar un Pong, paso a paso y desde cero.

El taller vuelve a contar con su inestimable colaboración y visto que la cosa se alarga, el taller se realizará en modalidad online.

Si no te pudiste inscribir, no te preocupes, tienes el tutorial aquí y se han grabado todas las sesiones y subido los vídeos.

¡Cerrado el plazo de inscripción!

A una semana del comienzo del taller, previsto para el 15 de enero de 2021, se cierra el plazo de inscripción con treinta personas apuntadas (en principio se preveía un máximo de veinticinco).

El taller constará de varias sesiones, aún por determinar, de una duración de unas dos horas por sesión, la cuales se realizarán los viernes por la tarde o los sábados por mañana (se podrá ir cambiando el día si la mayoría de los participantes están de acuerdo). El número de sesiones dependerá de la evolución del taller.

El taller se realizará en horario de España, por lo que si eres de otra parte no te preocupes pues todas las sesiones serán grabadas y publicadas.

Según se vayan confirmando fechas (esperamos empezar en enero) se irán anunciando, esta vez sí, a través de los canales oficiales de la asociación. También se dará más información a través del canal Ensamblador para Z80 de Telegram.

¿Cómo realizar el taller de ensamblador?

En esta ocasión la gestión de las inscripciones al taller de ensamblador la voy a realizar yo pues, como ya todos sabréis, cuando la idea surgió estaban inmersos en la organización de su segundo encuentro anual, Retro Parla 2020: algunos nos vimos por allí y lo pasamos bien, a que sí.

Para poder formar parte del taller es necesaria inscripción, ya que estimamos que un grupo formado por veinticinco personas es suficiente y más podría ser demasiado.

Debéis rellenar el siguiente formulario y me pondré en contacto con cada una de las personas interesadas.

A través de este link, podéis descargar el tutorial completo, el código fuente y el resultado final, por si le queréis ir echando un vistazo o por si no podéis acudir al taller.

También podéis seguir el tutorial completo que he publicado, en esta ocasión, en colaboración con AUA.

¡La colaboración hace la fuerza!

Los controles en nuestro PorompomPong son:

  • 5: Iniciar partida.
  • A: Arriba jugador 1.
  • Z: Abajo jugador 1.
  • 0: Arriba jugador 2.
  • O: Abajo jugador 2.
Taller de ensamblador para ZX Spectrum 16K, PongTaller de ensamblador para ZX Spectrum 16K – Pong

El mosquetero de la reina [Espamatica] [Leer]


De la mano de Fran Kapilla llega El mosquetero de la reina.

El mosquetero de la reina es una aventura conversacional en la que, encarnando a Dartagnan, tendrás que hacer frente a duelos con espadas e intrigas de palacio.

Déjate llevar a la historia escrita por Alejandro Dumas y disfruta de esta aventura clásica.

Ya disponible en edición digital y física.

El mosquetero de la reina

Vuelve, al retro, vuelve…por Navidad [Espamatica] [Leer]


Una vez más estamos de enhorabuena de la mano de Enrique Segura Alcalde.

Como regalo de Navidad, Enrique nos trae el número 8 de Play Again y el primer número de su nuevo proyecto ¡Spectrum A Toda Máquina!

¡Spectrumeros del mundo! ¡Estamos de enhorabuena! En realidad estamos de enhorabuena todos los aficionados a los 8 bits.

Enrique nos vuelve a hacer llegar su trabajo de manera gratuita, sin más pretensión que hacernos disfrutar, viajando hacia aquellos lejanos recuerdos de nuestra juventud.

Esperamos poder coincidir con Enrique en algún evento de los que se van anunciando para el año que viene, y poder disfrutar de las cosas que nos pueda contar, que seguro que son muchas e interesantes.

Desde aquí podéis descargar todas las publicaciones gratuitas que hasta ahora nos ha hecho llegar Enrique.

Y recuerda, la colaboración hace la fuerza.

Vuelveeeeeeeeeeeee, al retro vuelveeeeeeeeee,
vuelve a tu hogaaaaar.

  • ¡Spectrum A Toda Máquina 1!
  • Play Again 8

Extra, extra, Arcade Classics Magazine nº 2 [Espamatica] [Leer]


Y volvemos a estar de enhorabuena, y una vez más gracias a Enrique Segura Alcalde, que nos vuelve a regalar una nueva entrega de Arcade Classics Magazine, la revista que edita además de Play Again.

En esta caso nos trae el número 2 de esta publicación, y como viene siendo costumbre en él, totalmente gratuito. Y es que Enrique está dispuesto a darnos entretenimiento tan solo a cambio de nuestro tiempo.

Pero Enrique no solo dedica su tiempo a sus proyectos, también saca tiempo para colaborar con otros proyectos como es el caso de Mentero, magazine digital en el que se encarga de la sección de videojuegos retro.

Aquí os dejo los enlaces a dos de sus artículos:

Y hablando de Dreamcast. Como ya anunciamos, Enrique ha publicado el libro Dreamcast, el sueño eterno. Si no lo tenéis todavía, ya estáis tardando. En serio, merece la pena.

Si quereis saber algo más sobre el libro, aquí os dejo una reña de TuberViejuner.

TuberViejuner

Y otra más de El Spectrumero Javi Ortiz.

Play Again 7, fiel a su cita [Espamatica] [Leer]


Ha tardado algo más de lo previsto, pero al fin llega el número 7 de Play Again.

Pese a que Enrique ya juega en otra liga, Dolmen Editorial acaba de publicar su libro Deamcast, el sueño eterno, esto no ha impedido que nos vuelva a regalar otro número de la revista que el mismo edita, y que nos hace llegar de manera gratuita.

Es una gran alegría que nuestro amigo Enrique haya hecho realidad su sueño con la publicación de este libro, así como que siga publicando números de Play Again para nuestro deleite.

Si queréis conocerle, Enrique va a estar este sábado 5 de octubre en Madrid Games Week, presentando su libro a las 17:00h.

Recuera que puedes descargarte todos los números de Play Again.

Play Again nº 7

Dreamcast, el sueño eterno. Nuevo libro de Enrique Segura Alcalde [Espamatica] [Leer]


Y la perseverancia tiene premio. Tanto va el cántaro a la fuente, que al final se llena.

Ese es el caso de Enrique, que tras la edición de Insert Coin, Play Again y Arcade Classics Magazine, ha cumplido un sueño, no solo suyo, pues este sueño es compartido con sus amigos, y ve como Dolmen Editorial publica su nuevo libro.

No nos queda más que desearle suerte en esta nueva aventura, y esperar a que llegue el libro para poder leerlo.

¿Aún no te has enterado? A finales de mes seremos uno más en la familia #DolmenGames ▶ DREAMCAST. El sueño eterno de Enrique Segura.
Os dejamos la cubierta al completo de este libro sobre la última gran consola de SEGA.
¡Un viaje a través del tiempo! pic.twitter.com/77tIkvSyNO

— Dolmen Editorial (@DolmenEditorial) August 5, 2019