La pantalla del ZX Spectrum

Distribución de la pantalla

La pantalla del ZX Spectrum tiene una resolución de 256*192 píxeles, o lo que es lo mismo 32 columnas y 192 scanlines. Con cada byte podemos configurar 8 píxeles, encendiendo los que están a 1 y apagando los que están a 0; 256 píxeles / 8 píxeles por byte = 32 columnas. El área gráfica ocupa 6.144 bytes ($1800) y comienza en la dirección de memoria 16.384 ($4000).

Una de las particularidades del ZX Spectrum es la forma en la que se distribuye el área gráfica. Cada carácter se compone de 64 píxeles, o lo que es lo mismo 8×8. A cada una de las líneas del carácter se la denomina scanline. Si pintamos en el primer scanline de un carácter y luego sumamos 32 a la posición de memoria, la lógica nos dice que nos situamos en el siguiente scanline del carácter, pero no es así ya que en realidad nos estamos situando en el primer scanline del carácter situado justo debajo del que estamos.

Si pintamos la posición $4000, primer byte de la pantalla, y luego queremos pintar en el primer scanline de la línea 8 de pantalla, la lógica dice que solo habría que sumar 8 veces 32 ($20) a la posición $4000, por lo que el primer scanline del primer carácter de la línea 8 estaría en la posición $4100. Pero esto no es así ya que la posción $4100 corresponde con el segundo scanline del primer carácter de la pantalla.

Este comportamiento es debido a que la pantalla del ZX Spectrum está dividida en tercios. Cada uno de los tercios tiene 8 líneas y cada una de las líneas 8 scanlines. El primer tercio va desde la posición de memoria $4000 hasta la $47FF, el segundo tercio desde la $4800 hasta la $4FFF y el tercer tercio desde la $5000 hasta la $57FF.

Aunque en principio esto resulte extraño, cuando se ve la composición que toman los bytes a la hora de conformar una posición de memoria, todo cobra sentido. La composición de los bytes es la siguiente:

010T TSSS LLLC CCCC

Donde 010 es siempre fijo, TT representa el tercio (de 0 a 2), SSS representa el scanline dentro del carácter (de 0 a 7), LLL representa la línea dentro del tercio (de 0 a 7) y CCCCC representa la columna (de 0 a 31).

Si nos fijamos en la línea, de 0 a 23, los bits 0, 1 y 3 siempre indican la línea dentro del tercio, y los bits 4 y 5 el tercio.

Línea 0:  0000 0000     Línea 0 del tercio 0
Línea 7:  0000 0111     Línea 7 del tercio 0
Línea 8:  0000 1000     Línea 0 del tercio 1
Línea 15: 0000 1111     Línea 7 del tercio 1
Línea 16: 0001 0000     Línea 0 del tercio 2
Línea 23: 0001 0111     Línea 7 del tercio 2

Para ver todo lo expuesto, vamos a realizar un sencillo ejemplo que rellena los bytes de la primera columna con un sencillo patrón $AA (1001 1001).

org  $8000
ld   hl, $4000    ; HL = dirección de inicio de la VideoRAM
ld   bc, $20      ; BC = valor a sumar para pasar al siguiente carácter
ld   a, $08       ; A = para dibujar el primer scanline del primer tercio
loop:
ld   (hl), $aa    ; Carga en pantalla el patrón 10011001
add  hl, bc       ; Avanza HL a la siguiente línea
dec  a            ; Decrementa A
jr   nz, loop     ; Bucle hasta que A llegue a 0
ret               ; Vuelve al Basic
end	 $8000

Si ejecutamos el programa, dibuja el patrón $AA en el primer scanline de las 8 primeras líneas.

Para ver la distribución de la pantalla del ZX Spectrum, solo hay que ir cambiado la línea LD A, $08 e ir cargando múltiplos de $08; $08, $10, $18, $20, $28, $30, $38, $40, $48…

Hasta que no lleguemos a $48, no se verá como se pinta el primer scanline de las 8 líneas del segundo tercio.

A partir de aquí, vamos a ir viendo rutinas que os pueden ayudar a la hora de trabajar con la pantalla del ZX Spectrum. Tened en cuenta que estas rutinas no controlan si las coordenadas están fuera del área de la pantalla.

Dirección de memoria desde coordenada de carácter

La rutina CharCoord2PointerHR, toma unas coordenadas X e Y y retorna la dirección de memoria correspondiente.

; ----------------------------------------------------------------
; CharCoord2PointerHR: Obtiene la dirección de memoria 
; correspondiente a las coordenadas de carácter especificadas. 
; 
; Entrada: B -> Coordenada Y (0 a 23).
;          C -> Coordenada X (0 a 31).
; 
; Salida: HL -> Dirección de memoria que corresponde a las
;               coordenadas. 010T TSSS LLLC CCCC. 
;
; Altera el valor de los registros AF y HL.
; ----------------------------------------------------------------- 
CharCoord2PointerHR:
ld	a, b      ; A = línea. Bits 0, 1 y 2 línea. Bits 3 y 4 tercio.
and $18       ; Se queda con el tercio 0001 1000. 
              ; Scanline siempre a 0.
or 	$40       ; Agrega la parte fija 0100 0000.
ld 	h, a      ; H = A. Parte alta de la dirección calculada.
ld 	a, b      ; A = línea.
and $07       ; Se queda con la línea dentro del tercio.
rrca
rrca
rrca          ; Rota tres veces para pasar valor a bits 5, 6 y 7.
or 	c         ; Agrega la columna.
ld 	l, a      ; L = A. Parte baja de la dirección calculada.
ret

Para probar esta rutina podemos ir pasándole posiciones y luego cargando un patrón en la dirección devuelta y así ver donde dibuja.

org $8000

ld b, $10                     ; Carga en B la línea.
ld c, $10                     ; Carga en C la columna.
call CharCoord2PointerHR      ; Calcula la posición de memoria.
ld (hl), $ff                  ; Activa todos los píxeles en la
                              ; posición de memoria.

inc b                         ; Incrementa la línea.
inc c                         ; Incrementa la columna.
call CharCoord2PointerHR      ; Calcula la posición de memoria.
ld (hl), $ff                  ; Activa todos los píxeles en la
                              ; posición de memoria.

inc b                         ; Incrementa la línea.
dec c                         ; Decrementa la columna.
call CharCoord2PointerHR      ; Calcula la posición de memoria.
ld (hl), $ff                  ; Activa todos los píxeles en la
                              ; posición de memoria.

ret

end	 $8000

Coordenada de carácter desde dirección de memoria

La rutina PointerHR2CharCoord, toma una dirección de memoria y retorna las coordenadas de carácter correspondientes.

; ---------------------------------------------------------------- 
; PointerHR2CharCoord: Obtiene las coordenadas de carácter
; correspondientes a la dirección de memoria especificada.
;
; Entrada: HL -> Puntero a memoria. 010T TSSS LLLC CCCC.
;
; Salida: B -> Coordenada Y (0 a 23).
;         C -> Coordenada X (0 a 31).
;
; Altera el valor de los registros AF y BC.  
; ---------------------------------------------------------------- 
PointerHR2CharCoord: 
ld a, h     ; A = parte alta de la dirección de memoria. 010T TSSS.
and $18     ; Se queda con el tercio. Bits 4 y 5 de la
            ; coordenada Y. Scanline siempre a 0, son coordenadas
            ; de carácter.
ld b, a     ; B = A.

ld a, l     ; A = parte baja de la dirección de memoria. LLLC CCCC.
and $e0     ; Se queda con la línea dentro del tercio.
rrca
rrca
rrca        ; Rota tres veces para pasar valor a bits 0, 1 y 3.
or b        ; Agrega los bits 4 y 5.
ld b, a     ; B = A. Coordenada Y calculada.

ld a, l     ; A = parte baja de la dirección de memoria. LLLC CCCC.
and $1f     ; Se queda con la columna.
ld c, a     ; C = A. Coordenada X calculada.

ret

Dirección de memoria de la siguiente línea

La rutina PointerHRNextLine, toma una dirección de memoria y retorna la dirección de memoria correspondiente a la línea siguiente.

; ----------------------------------------------------------------
; PointerHRNextLine: obtiene la dirección de memoria
; correspondiente a la siguiente línea.
;
; Entrada: HL -> Dirección actual. 010T TSSS LLLC CCCC.
;
; Salida: HL -> Dirección de la línea siguiente.
;               010T TSSS LLLC CCCC.
;
; Altera el valor de los registros AF y HL.
; ---------------------------------------------------------------- 
PointerHRNextLine:
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
add a, $20  ; Añade una línea (LLLC CCCC + 0010 0000).
ld l, a     ; L = A.
ret nc      ; Si no hay acarreo, la línea sigue entre 0 y 7, sale.

; Hay acarreo, hay que cambiar el tercio de la pantalla.
ld a, h     ; A = parte alta de la dirección. 010T TSSS.
add a, $08  ; Añade un tercio (010T TSSS + 0000 1000).
ld h, a     ; H = A.

ret

Dirección de memoria de la línea anterior

La rutina PointerHRPreviousLine, toma una dirección de memoria y retorna la dirección de memoria correspondiente a la línea anterior.

; ----------------------------------------------------------------
; PointerHRPreviousLine: obtiene la dirección de memoria
; correspondiente a la línea anterior de carácter.
;
; Entrada: HL -> Dirección actual. 010T TSSS LLLC CCCC.
;
; Salida: HL -> Dirección de la línea anterior.
;               010T TSSS LLLC CCCC.
;
; Altera el valor de los registros AF y HL.
; ---------------------------------------------------------------- 
PointerHRPreviousLine:
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
and $e0     ; Se queda con la parte de la línea (1110 0000).
jr z, PointerHRPreviousLine_continue   ; Si línea es 0,
                                       ; cambio de tercio.

; No hay cambio de tercio.
ld a, l     ; A = parte baja de la dirección, LLLC CCCC.
sub $20     ; Resta una línea (LLLC CCCC - 0010 0000).
ld l, a     ; L = A. Dirección calculada.

ret

PointerHRPreviousLine_continue:
; Hay que cambiar el tercio.
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
or $e0      ; Pone la línea a 7 (LLLC CCCC or 1110 0000).
ld l, a     ; L = A. Parte baja de la dirección calculada.

ld a, h     ; A = parte alta de la dirección. 010T TSSS.
sub $08     ; Resta uno al tercio (010T TSSS - 0000 1000).
ld h, a     ; H = A. Parte baja de la dirección calculada.

ret

Siguiente scanline

La rutina PointerHRNextScanLine, toma una dirección de memoria y devuelve la del siguiente scanline.

; ----------------------------------------------------------------
; PointerHRNextScanLine: obtiene la dirección de memoria
; correspondiente al siguiente scanline.
;
; Entrada: HL -> dirección actual. 010T TSSS LLLC CCCC.
;
; Salida: HL -> dirección del siguiente scanline.
;               010T TSSS LLLC CCCC.
;
; Altera el valor de los registros AF y HL.
; ---------------------------------------------------------------- 
PointerHRNextScanLine:
ld a, h     ; A = parte alta de la dirección. 010T TSSS.
and $07     ; Se queda con el scanline.
cp $07      ; Comprueba si el scanline es 7.
jr z, PointerHRNextScanLine_continue     ; Es 7, cambio de línea.

; El scanline no es 7.
inc h       ; Aumenta en 1 el scanline y sale.

ret

PointerHRNextScanLine_continue:
; Hay que cambiar la línea.
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
add a, $20  ; Añade una línea (LLLC CCCC + 0010 0000).
ld l, a     ; L = A.
ld a, h     ; A = parte alta de la dirección. 010T TSSS.
jr nc, PointerHRNextScanLine_end  ; Si no hay acarreo, salta
                                  ; para terminar el cálculo.

; Hay acarreo, hay que cambiar el tercio.
add a, $08  ; Añade uno al tercio (010T TSSS + 0000 1000).

PointerHRNextScanLine_end:
and $f8     ; Se queda con la parte fija y el tercio.
            ; Deja el scanline a 0.
ld h, a     ; H = A. Dirección calculada.

ret

Para probar la rutina, vamos a utilizar un pequeño programa para ver como dibuja. Al contrario de lo que vimos en el primer ejemplo, ahora si se pinta seguido, un scanline seguido del que va justo detrás.

org $8000

ld hl, $4000    ; HL = dirección de inicio de la VideoRAM.
ld b, $08       ; B = para dibujar 8 scanlines.

loop:
ld (hl), $aa    ; Carga en pantalla el patrón $AA.
call PointerHRNextScanLine  ; Avanza al siguiente scanline.
djnz loop       ; Bucle hasta que B llegue a 0.

ret

end	 $8000

Si ejecutamos el programa, dibuja el patrón $AA en el primer carácter. El primer ejemplo lo hacía en el primer scanline de las 8 primeras líneas.

Al igual que hicimos en el primer ejemplo, podemos ir cargando múltiplos de $08 en B para ver como va dibujando el programa; $08, $10, $18, $20, $28, $30, $38, $40, $48…

Scanline anterior

La rutina PointerHRPreviousScanLine, toma una dirección de memoria y retorna la dirección de memoria correspondiente al scanline anterior.

; ----------------------------------------------------------------
; PointerHRPreviousScanLine: obtiene la dirección de memoria
; correspondiente al scanline anterior.
;
; Entrada: HL -> dirección actual. 010T TSSS LLLC CCCC.
;
; Salida: HL -> Dirección del scanline anterior.
;               010T TSSS LLLC CCCC.
;
; Altera el valor de los registros AF y HL.
; ---------------------------------------------------------------- 
PointerHRPreviousScanLine:
ld a, h     ; A = parte alta de la dirección. 010T TSSS.
and $07     ; Se queda con el scanline.
or a        ; Comprueba si está a 0.
jr z, PointerHRPreviousScanLine_continue  ; Si es 0, cambiar línea.

; No hay cambio de línea.
dec h       ; Decrementa el scanline y sale.

ret

PointerHRPreviousScanLine_continue:
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
and $e0     ; Se queda con la parte de la línea.
or a        ; Comprueba si está a cero.
jr z, PointerHRPreviousScanLine_change  ; Si es 0, cambiar tercio.

; No hay que cambiar tercio.
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
sub $20     ; Resta un línea, (LLLC CCCC - 0010 0000).
ld l, a     ; L = A.
jr PointerHRPreviousScanLine_end  ; Salta a fin de cálculo.

PointerHRPreviousScanLine_change:
; Hay que cambiar de tercio.
ld a, l     ; A = parte baja de la dirección. LLLC CCCC.
or $e0      ; Pone la línea en 7.
ld l, a     ; L = A.

ld a, h     ; A = parte alta de la dirección. 010T TSSS.
sub $08     ; Resta uno al tercio.
ld h, a     ; H = A.

PointerHRPreviousScanLine_end:
ld a, h     ; A = parte alta de la dirección. 010T TSSS.
and $F8     ; Se queda con la parte fija y el tercio.
or $07      ; Pone el scanline a 7.
ld h, a     ; H = A.

ret

Y para poder probar la rutina, otro sencillo programa.

org $8000

ld hl, $57ff    ; HL = dirección de fin de la VideoRAM.
ld b, $08       ; B = para dibujar 8 scanlines.

loop:
ld (hl), $aa    ; Carga en pantalla el patrón $AA.
call PointerHRPreviousScanLine  ; Retrocede al scanline anterior.
djnz loop       ; Bucle hasta que B llegue a 0.

ret

end	 $8000

Conclusión

Todo lo que he puesto aquí, lo he aprendido siguiendo el curso de ensamblador que se encuentra en speccy.org y leyendo el libro Código Máquina del ZX-Spectrum de Jesús Alonso Cano.

Las rutinas aquí expuestas son los ejercicios que he realizado para practicar lo aprendido.

Un comentario en «La pantalla del ZX Spectrum»

  • el 24 de mayo de 2019 a las 18:58
    Enlace permanente

    La visualizacion en una pantalla PAL estaria sujeta a la correccion gamma y por ello los valores sin brillo parecerian mas claros. Cada modelo de ZX Spectrum uso tensiones diferentes para los colores, por lo que los valores aqui mostrados son solo indicativos.

    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
A %d blogueros les gusta esto: