Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $03 Dibujando por la pantalla

Primeros pasos dibujando por la pantalla

5
(8)

Ensamblador para ZX Spectrum – Pong: Paso 1, dibujando por la pantalla

En este paso vamos a empezar a dibujar por la pantalla usando ensamblador para ZX Spectrum.

La pantalla del ZX Spectrum está situada, el área de los 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 3 tercios, de 8 líneas cada uno, con 8 scanlines 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 añadir 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 speccy.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.

NextScan

Como su propio nombre indica, obtiene la dirección del scanline siguiente.

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

En la primera instrucción incrementamos el scanline, INC H, 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, por lo que no es necesario ningún cálculo más y salimos de la rutina, RET NZ.

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

0100 0111

Al sumarle uno, deja los bits de 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

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

Sí 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í como esto.

Ensamblador para ZX Spectrum, línea vertical izquierda
Ensamblador para ZX Spectrum, línea vertical izquierda

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 del 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 se pinta scanline a scanline. Si queremos que vuelva a ir rápido, comentamos las 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. Luegos 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 ha 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

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 para ZX Spectrum, línea vertical derecha
Ensamblador para ZX Spectrum, línea vertical derecha

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

En el próximo capítulo de ensamblador para ZX Spectrum, empezaremos con el manejo de las teclas de control.

Enlaces de interés

Ficheros

¿Te ha Resultado útil este artículo?

Ayúdanos a mejorar y danos tu opinión:

Mostrar más

6 comentarios

Deja una respuesta

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

Publicaciones relacionadas

Mira también
Cerrar
Botón volver arriba