Espamática
ZX SpectrumEnsamblador Z80Retro

0x01 Ensamblador ZX Spectrum Pong – Dibujando por la pantalla

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.

Tabla de contenidos

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.

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

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

ASM
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:

ASM
; -----------------------------------------------------------------------------
; 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).

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

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

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

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

ASM
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:

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

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

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:

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

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

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

ASM
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:

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

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

ASM
ld		b, $c0

Y luego hacemos el bucle.

ASM
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:

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

El código completo de Main.asm es:

ASM
; 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.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
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

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

2 comentarios en «0x01 Ensamblador ZX Spectrum Pong – Dibujando por la pantalla»

  • Ante todo quisiera darte las gracias por todo el material que pones a nuestra disposición. Es un placer para mi, 40 años después de tener mi primer spectrum, el poder aprender a programar en Código Máquina.
    Estoy siguiendo el tutorial de Pong y creo que hay una errata en el fichero Video.asm. Concretamente las líneas 39 y 40 de la rutina «Previous Scan» que están intercambiadas de orden.
    En el fichero que me he descargado aparece así:
    39 and $07 ; Se queda con los bits del scanline original
    40 dec h ; Decrementa H para decrementar el scanline
    El fichero compilado de la descarga funciona correctamente.
    Gracias y un saludo.

    Respuesta
    • Hola.
      Efectivamente, el fichero Video.asm que se descarga es incorrecto, primero debe ir DEC H y luego el AND $07, tal como se muestra en el artículo.
      Voy a actualizar las descargas.
      Muchas gracias por el aviso.

      Respuesta

Deja una respuesta

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

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

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

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo