Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 537

Curso de Ensamblador Z80

1. Prólogo y objetivos del curso.


2. Introducción y conceptos básicos.
3. Arquitectura y Funcionamiento del Spectrum.
4. Lenguaje Ensamblador del Z80 (I): Arquitectura del Z80 e Instrucciones básicas.
5. Lenguaje Ensamblador del Z80 (II): Desplazamientos de memoria, bits y operaciones lógicas.
6. Lenguaje Ensamblador del Z80 (III): Instrucciones condicionales.
7. Lenguaje Ensamblador del Z80 (IV): La pila y las llamadas a subrutinas.
8. Lenguaje Ensamblador del Z80 (V): Puertos de E/S y Tabla de Opcodes.
9. Rutinas de carga: Save y Load (almacenamiento en cinta).
10. Lectura del teclado en el Spectrum: teoría y rutinas.
11. Interrupciones del microprocesador Z80: rutinas ISR.
12. Paginación de memoria: Paginación de bancos de memoria en modelos de 128K.
13. Gráficos en el Spectrum (I): la videomemoria: imagen y atributos.
14. Gráficos en el Spectrum (y II): Cálculo de direcciones y coordenadas.
15. Gráficos en el Spectrum (y III): Sprites y gráficos en baja resolución (gráficos de bloques).
16. Gráficos en el Spectrum (y IV): Fuentes de texto.
17. Gráficos en el Spectrum (y V): Técnicas de mapeado por bloques (tilemaps).
18. Compresión y Descompresión RLE de gráficos, sonido, y datos.
Prólogo
“Programación en ensamblador de Z80 para el microordenador Sinclair ZX Spectrum.”

Mucha gente se preguntará cómo es posible que, en pleno 2011, exista alguien con interés en escribir y publicar
un curso sobre esta temática. Apenas un par de centenares o miles de personas en todo el mundo pueden estar
realmente interesadas en la lectura de un curso como este.

Sin embargo, he dedicado gran cantidad de horas a escribir y depurar este texto y sus ejemplos. ¿El motivo?
Simplemente, no se me ocurre una mejor forma de concentrar en un único elemento mi pasión por el ZX
Spectrum, la programación en ensamblador, el desarrollo de programas y los videojuegos.

Puedo y debo decir que el ZX Spectrum cambió mi vida. Aquella tarde de viernes de 1989 en la que mis padres
aparecieron por la puerta con un Spectrum +2A de segunda mano, junto a una caja llena de revistas
Microhobby y cintas de juegos y programas, cambió el que hubiera sido mi futuro profesional, orientándolo
hacia el mundo de la Ingeniería, la Electrónica y las Telecomunicaciones.

Como todos, empecé exprimiendo el Spectrum a través de los juegos profesionales que se vendían para la
popular máquina de Sinclair. En paralelo a los juegos, comencé a leer los ejemplares de las revistas
Microhobby que habíamos adquirido junto al ordenador.

Mi relación inicial con Microhobby fue las que supongo que tendrían muchos usuarios sin interés por la
programación: directo a las páginas con análisis, fotos y notas de juegos. Como mucho, como curiosidad
tecleaba alguno de los listados en BASIC de la sección de trucos, maravillándome con sencillas melodías, o
psicodélicos efectos de colores con el borde.

Esos listados en BASIC, tan sencillos, despertaron mi curiosidad por “cómo se hacen estos juegos”. Poco a
poco se produjo el cambio: mi interés por jugar pasó a ser interés, mucho interés, por desarrollar.

Microhobby fue la herramienta mediante la cual aprendí BASIC y ensamblador de Z80. Como la completa
revista que era, entre sus páginas de análisis de juegos podías encontrar fantásticos artículos y listados
animándote a programar pequeñas rutinas y juegos.

Casi sin darme tiempo para disfrutar de lo que estaba aprendiendo, llegó el fin de la revista Microhobby y el
ocaso comercial del Spectrum en España. Las consolas ocuparon el espacio lúdico del Spectrum y el PC se
convirtió en la herramienta de programación estándar. El Spectrum pasó para mí al olvido hasta que la revista
Micromanía publicó el emulador “SPECTRUM” de Pedro Gimeno.

Este emulador, y todos los que aparecieron en la década de los 90, sirvió para que la gente no olvidara el
Spectrum y todo el legado que nos había dejado.

Ya a principios del siglo XXI, el Spectrum volvió a ser mi centro de atención: inicialmente, desarrollé el
emulador ASpectrum con el que mejoré en gran parte mis conocimientos sobre la arquitectura del Spectrum y
la programación en lenguaje ensamblador de Z80.

Una vez ASpectrum fue una realidad, comencé a realizar sencillos juegos con Z88DK en C con pequeñas
rutinas en ensamblador integradas en el código. Se despertó de nuevo en mí el interés por desarrollar juegos de
Spectrum y de escribir tutoriales y cursos con todo lo que iba rememorando o aprendiendo.

En esa época (años 2002 - 2003) se fundó Compiler Software y se editó la revista MagazineZX en el portal
Speccy.org, incluyendo diversos cursos de programación en C con Z88DK y en ensamblador de Z80 con
pasmo. Estos cursos, finalmente, se han ampliado y materializado en el texto que estáis leyendo.
Objetivos y desarrollo del curso
El objetivo principal de este curso, libro, o gran tutorial es que un lector con conocimientos básicos de
programación pueda aprender fácilmente ensamblador de Z80 aplicado al desarrollo de juegos y utilidades de
Spectrum.

Con este curso pretendemos enseñar al lector:

• La arquitectura del Sinclair ZX Spectrum: se describen sus componentes internos y cómo se


interrelacionan.
• La arquitectura del microprocesador Z80: sus registros y su juego de instrucciones.
• La sintaxis del lenguaje ensamblador de Z80: nmemónicos del lenguaje.
• Cómo utilizar el ensamblador PASMO para ensamblar nuestros programas en ASM de Z80.
• Acceso a los periféricos del Spectrum: Teclado, Joystick, etc.
• Gráficos en el Spectrum: Sprites, Fuentes de texto, Impresión de mapeados, etc.
• Funciones avanzadas de los modelos 128K: paginación de memoria.
• Rutinas auxiliares: subrutinas de carga, compresión RLE, Interrupciones del procesador.
• Subrutinas útiles para el desarrollo de programas.

Al escribirlo he intentado ponerme en la piel del programador que desea empezar con el lenguaje ensamblador,
por lo que los dos primeros capítulos describen la arquitectura del Spectrum y del Z80. Los siguientes cinco
capítulos tratan sobre la sintaxis del lenguaje ensamblador, donde el lector aprenderá las “piezas básicas” con
las que construir programas en ensamblador para cualquier microordenador basado en el procesador Z80 de
Zilog.

A partir del octavo capítulo nos centramos única y exclusivamente en el Spectrum, profundizando en todas las
diferentes áreas que puedan sernos de utilidad para el desarrollo de juegos o programas: lectura del teclado,
temporización, impresión de gráficos, técnicas de mapeado, carga desde cinta, etc.

A lo largo del texto se presentan múltiples ejemplos y rutinas para que el lector pueda verificar la teoría descrita
así como utilizarlas directamente en sus propios programas.

Cuando se escribe una rutina para un procesador tan “limitado” como el Z80 suelen presentarse 2 opciones:
escribir una rutina comprensible, o escribir una rutina optimizada. El objetivo del curso es que el lector aprenda
programación en ensamblador y por lo tanto debe de poder comprender las rutinas que se presentan, por lo que
en el desarrollo de los ejemplos y las rutinas ha primado la comprensión frente a la optimización en aquellos
casos en que ambas opciones chocaban.

Esto no quiere decir que las rutinas no sean óptimas: al contrario, se han diseñado para que sean siempre lo más
óptimas posible siempre y cuando eso no implique hacerlas incomprensibles para el lector. Aún así, un
programador avanzado podrá (y deberá) darles una pequeña vuelta de tuerca adicional para exprimir ciclos de
reloj a la rutina y hacerla aún un poco más rápida. Ese podría ser el objetivo del lector una vez acabado el curso
y de cara al diseño de un programa.

Si un lector sin conocimientos de ensamblador, tras leer el curso, acaba decidiendo programar un juego y utiliza
o mejora las rutinas que se presentan en este texto, podremos decir que el curso ha conseguido su objetivo.
Prólogo
“Programación en ensamblador de Z80 para el microordenador Sinclair ZX Spectrum.”

Mucha gente se preguntará cómo es posible que, en pleno 2011, exista alguien con interés en escribir y publicar
un curso sobre esta temática. Apenas un par de centenares o miles de personas en todo el mundo pueden estar
realmente interesadas en la lectura de un curso como este.

Sin embargo, he dedicado gran cantidad de horas a escribir y depurar este texto y sus ejemplos. ¿El motivo?
Simplemente, no se me ocurre una mejor forma de concentrar en un único elemento mi pasión por el ZX
Spectrum, la programación en ensamblador, el desarrollo de programas y los videojuegos.

Puedo y debo decir que el ZX Spectrum cambió mi vida. Aquella tarde de viernes de 1989 en la que mis padres
aparecieron por la puerta con un Spectrum +2A de segunda mano, junto a una caja llena de revistas
Microhobby y cintas de juegos y programas, cambió el que hubiera sido mi futuro profesional, orientándolo
hacia el mundo de la Ingeniería, la Electrónica y las Telecomunicaciones.

Como todos, empecé exprimiendo el Spectrum a través de los juegos profesionales que se vendían para la
popular máquina de Sinclair. En paralelo a los juegos, comencé a leer los ejemplares de las revistas
Microhobby que habíamos adquirido junto al ordenador.

Mi relación inicial con Microhobby fue las que supongo que tendrían muchos usuarios sin interés por la
programación: directo a las páginas con análisis, fotos y notas de juegos. Como mucho, como curiosidad
tecleaba alguno de los listados en BASIC de la sección de trucos, maravillándome con sencillas melodías, o
psicodélicos efectos de colores con el borde.

Esos listados en BASIC, tan sencillos, despertaron mi curiosidad por “cómo se hacen estos juegos”. Poco a
poco se produjo el cambio: mi interés por jugar pasó a ser interés, mucho interés, por desarrollar.

Microhobby fue la herramienta mediante la cual aprendí BASIC y ensamblador de Z80. Como la completa
revista que era, entre sus páginas de análisis de juegos podías encontrar fantásticos artículos y listados
animándote a programar pequeñas rutinas y juegos.

Casi sin darme tiempo para disfrutar de lo que estaba aprendiendo, llegó el fin de la revista Microhobby y el
ocaso comercial del Spectrum en España. Las consolas ocuparon el espacio lúdico del Spectrum y el PC se
convirtió en la herramienta de programación estándar. El Spectrum pasó para mí al olvido hasta que la revista
Micromanía publicó el emulador “SPECTRUM” de Pedro Gimeno.

Este emulador, y todos los que aparecieron en la década de los 90, sirvió para que la gente no olvidara el
Spectrum y todo el legado que nos había dejado.

Ya a principios del siglo XXI, el Spectrum volvió a ser mi centro de atención: inicialmente, desarrollé el
emulador ASpectrum con el que mejoré en gran parte mis conocimientos sobre la arquitectura del Spectrum y
la programación en lenguaje ensamblador de Z80.

Una vez ASpectrum fue una realidad, comencé a realizar sencillos juegos con Z88DK en C con pequeñas
rutinas en ensamblador integradas en el código. Se despertó de nuevo en mí el interés por desarrollar juegos de
Spectrum y de escribir tutoriales y cursos con todo lo que iba rememorando o aprendiendo.

En esa época (años 2002 - 2003) se fundó Compiler Software y se editó la revista MagazineZX en el portal
Speccy.org, incluyendo diversos cursos de programación en C con Z88DK y en ensamblador de Z80 con
pasmo. Estos cursos, finalmente, se han ampliado y materializado en el texto que estáis leyendo.
Objetivos y desarrollo del curso
El objetivo principal de este curso, libro, o gran tutorial es que un lector con conocimientos básicos de
programación pueda aprender fácilmente ensamblador de Z80 aplicado al desarrollo de juegos y utilidades de
Spectrum.

Con este curso pretendemos enseñar al lector:

• La arquitectura del Sinclair ZX Spectrum: se describen sus componentes internos y cómo se


interrelacionan.
• La arquitectura del microprocesador Z80: sus registros y su juego de instrucciones.
• La sintaxis del lenguaje ensamblador de Z80: nmemónicos del lenguaje.
• Cómo utilizar el ensamblador PASMO para ensamblar nuestros programas en ASM de Z80.
• Acceso a los periféricos del Spectrum: Teclado, Joystick, etc.
• Gráficos en el Spectrum: Sprites, Fuentes de texto, Impresión de mapeados, etc.
• Funciones avanzadas de los modelos 128K: paginación de memoria.
• Rutinas auxiliares: subrutinas de carga, compresión RLE, Interrupciones del procesador.
• Subrutinas útiles para el desarrollo de programas.

Al escribirlo he intentado ponerme en la piel del programador que desea empezar con el lenguaje ensamblador,
por lo que los dos primeros capítulos describen la arquitectura del Spectrum y del Z80. Los siguientes cinco
capítulos tratan sobre la sintaxis del lenguaje ensamblador, donde el lector aprenderá las “piezas básicas” con
las que construir programas en ensamblador para cualquier microordenador basado en el procesador Z80 de
Zilog.

A partir del octavo capítulo nos centramos única y exclusivamente en el Spectrum, profundizando en todas las
diferentes áreas que puedan sernos de utilidad para el desarrollo de juegos o programas: lectura del teclado,
temporización, impresión de gráficos, técnicas de mapeado, carga desde cinta, etc.

A lo largo del texto se presentan múltiples ejemplos y rutinas para que el lector pueda verificar la teoría descrita
así como utilizarlas directamente en sus propios programas.

Cuando se escribe una rutina para un procesador tan “limitado” como el Z80 suelen presentarse 2 opciones:
escribir una rutina comprensible, o escribir una rutina optimizada. El objetivo del curso es que el lector aprenda
programación en ensamblador y por lo tanto debe de poder comprender las rutinas que se presentan, por lo que
en el desarrollo de los ejemplos y las rutinas ha primado la comprensión frente a la optimización en aquellos
casos en que ambas opciones chocaban.

Esto no quiere decir que las rutinas no sean óptimas: al contrario, se han diseñado para que sean siempre lo más
óptimas posible siempre y cuando eso no implique hacerlas incomprensibles para el lector. Aún así, un
programador avanzado podrá (y deberá) darles una pequeña vuelta de tuerca adicional para exprimir ciclos de
reloj a la rutina y hacerla aún un poco más rápida. Ese podría ser el objetivo del lector una vez acabado el curso
y de cara al diseño de un programa.

Si un lector sin conocimientos de ensamblador, tras leer el curso, acaba decidiendo programar un juego y utiliza
o mejora las rutinas que se presentan en este texto, podremos decir que el curso ha conseguido su objetivo.
Lenguaje Ensamblador del Z80 (I)
Arquitectura del Z80 e Instrucciones básicas

En este capítulo explicaremos la sintaxis utilizada en los programas en ensamblador. Para ello comenzaremos
con una definición general de la sintaxis para el ensamblador Pasmo, que será el “traductor” que usaremos entre
el lenguaje ensamblador y el código máquina del Z80.

Posteriormente veremos en detalle los registros: qué registros hay disponibles, cómo se agrupan, y el registro
especial de Flags, enlazando el uso de estos registros con las instrucciones de carga, de operaciones aritméticas,
y de manejo de bits, que serán las que trataremos hoy.

Esta entrega del curso es delicada y complicada: por un lado, tenemos que explicar las normas y sintaxis del
ensamblador cruzado PASMO antes de que conozcamos la sintaxis del lenguaje ensamblador en sí, y por el
otro, no podremos utilizar PASMO hasta que conozcamos la sintaxis del lenguaje.

Además, el lenguaje ensamblador tiene disponibles muchas instrucciones diferentes, y nos resultaría imposible
explicarlas todas en un mismo capítulo, lo que nos fuerza a explicar las instrucciones del microprocesador en
varias entregas. Esto implica que hablaremos de PASMO comentando reglas, opciones de instrucciones y
directivas que todavía no conocemos.

Es por esto que recomendamos al lector que, tras releer anteriores capítulos de este libro, se tome esta entrega
de una manera especial, leyéndola 2 veces. La “segunda pasada” sobre el texto permitirá enlazar todos los
conocimientos dispersos en el mismo, y que no pueden explicarse de una manera lineal porque están totalmente
interrelacionados. Además, la parte relativa a la sintaxis de PASMO será una referencia obligada para
posteriores capítulos (mientras continuemos viendo diferentes instrucciones ASM y ejemplos).

Sintaxis del lenguaje ASM en PASMO


En anteriores capítulos ya hablamos de PASMO, el ensamblador cruzado que recomendamos para el desarrollo
de programas para Spectrum. Este ensamblador traduce nuestros ficheros de texto .asm con el código fuente de
programa (en lenguaje ensamblador) a ficheros .bin (o .tap/.tzx) que contendrán el código máquina
directamente ejecutable por el Spectrum.

Supondremos para el resto del capítulo que ya tenéis instalado PASMO (ya sea la versión Windows o la de
UNIX/Linux) en vuestro sistema y que sabéis utilizarlo de forma básica (bastará con saber realizar un simple
ensamblado de programa, como ya vimos en el primer capítulo), y que podéis ejecutarlo dentro del directorio
de trabajo que habéis elegido.

El ciclo de desarrollo con PASMO será el siguiente:

• Con un editor de texto, tecleamos nuestro programa en un fichero .ASM con la sintaxis que veremos a
continuación.
• Salimos del editor de texto y ensamblamos el programa:
o Si queremos generar un fichero .bin de código objeto cuyo contenido POKEar en memoria (o
cargar con LOAD “” CODE) desde un cargador BASIC, lo ensamblamos con: “pasmo
ejemplo1.asm ejemplo1.bin”
o Si queremos generar un fichero .tap directamente ejecutable (de forma que sea pasmo quien
añada el cargador BASIC), lo ensamblamos con “pasmo –tapbas ejemplo1.asm ejemplo1.tap”
Todo esto se mostró bastante detalladamente en su momento en el primer capítulo del curso.

Con esto, ya sabemos ensamblar programas creados adecuadamente, de modo que la pregunta es: ¿cómo debo
escribir mi programa para que PASMO pueda ensamblarlo?

Es sencillo: escribiremos nuestro programa en un fichero de texto con extensión .asm. En este fichero de texto
se ignorarán las líneas en blanco y los comentarios, que en ASM de Z80 se introducen con el símbolo “;”
(punto y coma), de forma que todo lo que el ensamblador encuentre a la derecha de un ; será ignorado (siempre
que no forme parte de una cadena). Ese fichero de texto será ensamblado por PASMO y convertido en código
binario.

Lo que vamos a ver a continuación son las normas que debe cumplir un programa para poder ser ensamblado en
PASMO. Es necesario explicar estas reglas para que el lector pueda consultarlas en el futuro, cuando esté
realizando sus propios programas. No te preocupes si no entiendes alguna de las reglas, cuando llegues al
momento de implementar tus primeras rutinas, las siguientes normas te serán muy útiles:

• Normas para las instrucciones:


o Pondremos una sóla instrucción de ensamblador por línea.
o Como existen diferencias entre los “fin de línea” entre Linux y Windows, es recomendable que
los programas se ensamblen con PASMO en la misma plataforma de S.O. en que se han escrito.
Si PASMO intenta compilar en Windows un programa ASM escrito en un editor de texto de
Linux (con retornos de carro de Linux) es posible que obtengamos errores de ensamblado
(aunque no es seguro). Si os ocurre al compilar los ejemplos que os proporcionamos (están
escritos en Linux) y usáis Windows, lo mejor es abrir el fichero .ASM con notepad y grabarlo de
nuevo (lo cual lo salvará con formato de retornos de carro de Windows). El fichero “regrabado”
con Notepad podrá ser ensamblado en Windows sin problemas.
o Además de una instrucción, en una misma línea podremos añadir etiquetas (para referenciar a
dicha línea, algo que veremos posteriormente) y comentarios (con ';').

• Normas para los valores numéricos:


o Todos los valores numéricos se considerarán, por defecto, escritos en decimal.
o Para introducir valores números en hexadecimal los precederemos del carácter “$”, y para
escribir valores numéricos en binario lo haremos mediante el carácter “%”.
o Podremos también especificar la base del literal poniendoles como prefijo las cadena &H ó 0x
(para hexadecimal) o &O (para octal).
o Podemos especificar también los números mediante sufijos: Usando una “H” para hexadecimal,
“D” para decimal, “B” para binario u “O” para octal (tanto mayúsculas como minúsculas).

• Normas para cadenas de texto:


o Podemos separar las cadenas de texto mediante comillas simples o dobles.
o El texto encerrado entre comillas simples no recibe ninguna interpretación, excepto si se
encuentran 2 comillas simples consecutivas, que sirven para introducir una comilla simple en la
cadena.
o El texto encerrado entre comillas dobles permite introducir caracteres especiales al estilo de
C/C++ como \n, \r o \t (nueva línea, retorno de carro, tabulador…).
o El texto encerrado entre comillas dobles también admite \xNN para introducir el carácter
correspondiente a un número hexadecimal NN.
o Una cadena de texto de longitud 1 (un carácter) puede usarse como una constante (valor ASCII
del carácter) en expresiones como, por ejemplo, 'C'+10h.
• Normas para los nombres de ficheros:
o Si vemos que nuestro programa se hace muy largo y por lo tanto incómodo para editarlo,
podemos partir el fichero en varios ficheros e incluirlos mediante directivas INCLUDE (para
incluir ficheros ASM) o INCBIN (para incluir código máquina ya compilado). Al especificar
nombres de ficheros, deberán estar entre dobles comillas o simples comillas.

• Normas para los identificadores:


o Los identificadores son los nombres usados para etiquetas y también los símbolos definidos
mediante EQU y DEFL.
o Podemos utilizar cualquier cadena de texto, excepto los nombres de las palabras reservadas de
ensamblador.

• Normas para las etiquetas:


o Una etiqueta es un identificador de texto que ponemos poner al principio de cualquier línea de
nuestro programa, por ejemplo: “bucle:”
o Podemos añadir el tradicional sufijo “:” a las etiquetas, pero también es posible no incluirlo si
queremos compatibilidad con otros ensambladores que no lo soporten (por si queremos
ensamblar nuestro programa con otro ensamblador que no sea pasmo).
o Para PASMO, cualquier referencia a una etiqueta a lo largo del programa se convierte en una
referencia a la posición de memoria de la instrucción o dato siguiente a donde hemos colocado la
etiqueta. Podemos utilizar así etiquetas para hacer referencia a nuestros gráficos, variables,
datos, funciones, lugares a donde saltar, etc.

• Directivas:
o Tenemos a nuestra disposición una serie de directivas para facilitarnos la programación, como
DEFB o DB para introducir datos en crudo en nuestro programa, ORG para indicar una
dirección de inicio de ensamblado, END para finalizar el programa e indicar una dirección de
autoejecución, IF/ELSE/ENDIF en tiempo de compilación, INCLUDE e INCBIN, MACRO y
REPT.
o La directiva END permite indicar un parámetro numérico (END XXXX) que “pasmo –tapbas”
toma para añadir al listado BASIC de arranque el RANDOMIZE USR XXXX correspondiente.
De esta forma, podemos hacer que nuestros programas arranquen en su posición correcta sin que
el

usuario tenga que teclear el “RANDOMIZE USR DIRECCION_INICIO”.

• Una de las directivas más importantes es ORG, que indica la posición origen donde almacenar el código
que la sigue. Podemos utilizar diferentes directivas ORG en un mismo programa. Los datos o el código
que siguen a una directiva ORG son ensamblados a partir de la dirección que indica éste.
• Iremos viendo el significado de las directivas conforme las vayamos usando, pero es aconsejable
consultar el manual de PASMO para conocer más sobre ellas.

• Operadores
o Podemos utilizar los operadores típicos +, -, *. /, así como otros operadores de desplazamiento
de bits como » y «.
o Tenemos disponibles operadores de comparación como EQ, NE, LT, LE, GT, GE o los clásicos
=, !=, <, >, ⇐, >=.
o Existen también operadores lógicos como AND, OR, NOT, o sus variantes &, |, !.
o Los operadores sólo tienen aplicación en tiempo de ensamblado, es decir, no podemos
multiplicar o dividir en tiempo real en nuestro programa usando * o /. Estos operadores están
pensados para que podamos poner expresiones como ((32*10)+12), en lugar del valor numérico
del resultado, por ejemplo.

Aspecto de un programa en ensamblador


Veamos un ejemplo de programa en ensamblador que muestra el uso de algunas de estas normas, para que las
podamos entender fácilmente mediante los comentarios incluidos:

; Programa de ejemplo para mostrar el aspecto de


; un programa típico en ensamblador para PASMO.
; Copia una serie de bytes a la videomemoria con
; instrucciones simples (sin optimizar).
ORG 40000

valor EQU 1
destino EQU 18384

; Aqui empieza nuestro programa que copia los


; 7 bytes desde la etiqueta "datos" hasta la
; videomemoria ([16384] en adelante).

LD HL, destino ; HL = destino (VRAM)


LD DE, datos ; DE = origen de los datos
LD B, 6 ; numero de datos a copiar

bucle: ; etiqueta que usaremos luego

LD A, (DE) ; Leemos un dato de [DE]


ADD A, valor ; Le sumamos 1 al dato leído
LD (HL), A ; Lo grabamos en el destino [HL]
INC DE ; Apuntamos al siguiente dato
INC HL ; Apuntamos al siguiente destino

DJNZ bucle ; Equivale a:


; B = B-1
; if (B>0) goto Bucle
RET

datos DEFB 127, %10101010, 0, 128, $FE, %10000000, FFh

END

Algunos detalles a tener en cuenta:

• Se utiliza una instrucción por línea.


• Los comentarios pueden ir en sus propias líneas, o dentro de líneas de instrucciones (tras ellas).
• Podemos definir “constantes” con EQU para hacer referencia a ellas luego en el código. Son constantes,
no variables, es decir, se definen en tiempo de ensamblado y no se cambian con la ejecución del
programa. Su uso está pensado para poder escribir código más legible y que podamos cambiar los
valores asociados posteriormente de una forma sencilla (es más fácil cambiar el valor asignado en el
EQU, que cambiar un valor en todas sus apariciones en el código).
• Podemos poner etiquetas (como “bucle” y “datos” -con o sin dos puntos, son ignorados-) para
referenciar a una posición de memoria. Así, la etiqueta “bucle” del programa anterior hace referencia a
la posición de memoria donde se ensamblaría la siguiente instrucción que aparece tras ella. Las etiquetas
se usan para poder saltar a ellas (en los bucles y condiciones) mediante un nombre en lugar de tener que
calcular nosotros la dirección del salto a mano y poner direcciones de memoria. Es más fácil de entender
y programar un “JP bucle” que un “JP $40008”, por ejemplo. En el caso de la etiqueta “datos”, nos
permite referenciar la posición en la que empiezan los datos que vamos a copiar.
• Los datos definidos con DEFB pueden estar en cualquier formato numérico, como se ha mostrado en el
ejemplo: decimal, binario, hexadecimal tanto con prefijo “$” como con sufijo “h”, etc.
Podéis ensamblar el ejemplo anterior mediante:

pasmo --tapbas ejemplo.asm ejemplo.tap

Una vez cargado y ejecutado el TAP en el emulador de Spectrum, podréis ejecutar el código máquina en
BASIC con un “RANDOMIZE USR 40000”, y deberéis ver una pantalla como la siguiente:

Los píxeles que aparecen en el centro de la pantalla (dirección de memoria 18384) se corresponden con los
valores numéricos que hemos definido en “datos”, ya que los hemos copiado desde “datos” hasta la
videomemoria. No os preocupéis por ahora si no entendéis alguna de las instrucciones utilizadas, las iremos
viendo poco a poco y al final tendremos una visión global y concreta de todas ellas.

Si cambiáis el END del programa por END 40000, no tendréis la necesidad de ejecutar RANDOMIZE USR
40000 y que pasmo lo introducirá en el listado BASIC de “arranque”. El tap resultante contendrá un cargador
que incluirá el RANDOMIZE USR 40000.

Los registros
Como ya vimos en la anterior entrega, todo el “trabajo de campo” lo haremos con los registros de la CPU, que
no son más que variables de 8 y 16 bits integradas dentro del Z80 y que por tanto son muy rápidos para realizar
operaciones con ellos.

El Z80 tiene una serie de registros de 8 bits con nombres específicos:

• A: El Registro A (de 8 bits) es el acumulador. Es un registro que se utiliza generalmente como destino
de muchas operaciones aritméticas y de comparaciones y testeos.
• B, C, D, E, H, L: Registros de propósito general, utilizables para gran cantidad de operaciones,
almacenamiento de valores, etc.
• I: Registro de interrupción, no lo utilizaremos en nuestros primeros programas. No debemos modificar
su valor, aunque en el futuro veremos su uso en las interrupciones del Spectrum.
• R: Registro de Refresco de memoria: lo utiliza internamente la CPU para saber cuándo debe refrescar la
RAM. Su valor cambia sólo conforme el Z80 va ejecutando instrucciones, de modo que podemos
utilizarlo (leerlo) para obtener valores pseudo-aleatorios entre 0 y 127 (el Z80 no cambia el bit de mayor
peso de R, sólo los bits del 0 al 6).

Además, podemos agrupar algunos de estos registros en pares de 16 bits para determinadas operaciones:

• AF: Formado por el registro A como byte más significativo (Byte alto) y por F como byte menos
significativo (Byte bajo). Si A vale $FF y F vale $00, AF valdrá automáticamente “$FF00”.
• BC: Agrupación de los registros B y C que se puede utilizar en bucles y para acceder a puertos.
También se utiliza como “repetidor” o “contador” en las operaciones de acceso a memoria (LDIR,
LDDR, etc.).
• DE, HL: Registros de 16 bits formados por D y E por un lado y H y L por otro. Utilizaremos
generalmente estos registros para leer y escribir en memoria en una operación única, así como para las
operaciones de acceso a memoria como LDIR, LDDR, etc.

Aparte de estos registros, existen otra serie de registros de 16 bits:

• IX, IY: Dos registros de 16 bits pensados para acceder a memoria de forma indexada. Gracias a estos
registros podemos realizar operaciones como: “LD (IX+desplazamiento), VALOR”. Este tipo de
registros se suele utilizar pues para hacer de índices dentro de tablas o vectores. El desplazamiento es un
valor numérico de 8 bits en complemento a 2, lo que nos permite un rango desde -128 a +127 (puede ser
negativo para acceder a posiciones de memoria anteriores a IX).
• SP: Puntero de pila, como veremos en su momento apunta a la posición actual de la “cabeza” de la pila.
• PC: Program Counter o Contador de Programa. Como ya vimos en la anterior entrega, contiene la
dirección de la instrucción actual a ejecutar. No modificaremos PC directamente moviendo valores a
este registro, sino que lo haremos mediante instrucciones de salto (JP, JR, CALL…).

Por último, tenemos disponible un banco alternativo de registros, conocidos como Shadow Registers o
Registros Alternativos, que se llaman igual que sus equivalentes principales pero con una comilla simple detrás:
A', F', B', C', D'. E', H' y L'.

En cualquier momento podemos intercambiar el valor de los registros A, B, C, D, E, F, H y L con el valor de


los registros A', B', C', D', E', F', H' y L' mediante la instrucción de ensamblador “EXX”. La utilidad de estos
Shadow Registers es almacenar valores temporales y proporcionarnos más registros para operar: podremos
intercambiar el valor de los registros actuales con los temporales, realizar operaciones con los registros sin
perder los valores originales (que al hacer el EXX se quedarán en los registros Shadow), y después recuperar
los valores originales volviendo a ejecutar un EXX.

Ya conocemos los registros disponibles, veamos ahora ejemplos de operaciones típicas que podemos realizar
con ellos:

• Meter valores en registros (ya sean valores numéricos directos, de memoria, o de otros registros).
• Incrementar o decrementar los valores de los registros.
• Realizar operaciones (tanto aritméticas como lógicas) entre los registros.
• Acceder a memoria para escribir o leer.

Por ejemplo, las siguientes instrucciones en ensamblador serían válidas:

LD C, $00 ; C vale 0
LD B, $01 ; B vale 1
; con esto, BC = $0100
LD A, B ; A ahora vale 1
LD HL, $1234 ; HL vale $1234 o 4660d
LD A, (HL) ; A contiene el valor de (4660)
LD A, (16384) ; A contiene el valor de (16384)
LD (16385), A ; Escribimos en (16385) el valor de A
ADD A, B ; Suma: A = A + B
INC B ; Incrementamos B (B = 1+1 =2)
; Ahora BC vale $0200
INC BC ; Incrementamos BC
; (BC = $0200+1 = $0201)

Dentro del ejemplo anterior queremos destacar el operador “()”, que significa “el contenido de la memoria
apuntado por”. Así, “LD A, (16384)” no quiere decir “mete en A el valor 16384” (cosa que además no se puede
hacer porque A es un registro de 8 bits), sino “mete en A el valor de 8 bits que contiene la celdilla de memoria
16384” (equivalente a utilizar en BASIC las funciones PEEK y POKE, como en LET A=PEEK 16384).

Cabe destacar un gran inconveniente del juego de instrucciones del Z80, y es que no es ortogonal. Se dice que
el juego de instrucciones de un microprocesador es ortogonal cuando puedes realizar todas las operaciones
sobre todos los registros, sin presentar excepciones. En el caso del Z80 no es así, ya que hay determinadas
operaciones que podremos realizar sobre unos registros pero no sobre otros.

Así, si el Z80 fuera ortogonal, podríamos ejecutar cualquiera de estas operaciones:

LD BC, $1234
LD HL, BC
LD SP, BC
EX DE, HL
EX BC, DE
ADD HL, BC
ADD DE, BC

Sin embargo, como el Z80 no tiene un juego de instrucciones (J.I. desde este momento) ortogonal, hay
instrucciones del ejemplo anterior que no son válidas, es decir, que no tienen dentro de la CPU un microcódigo
para que el Z80 sepa qué hacer con ellas:

LD SP, BC ; NO: No se puede cargar el valor un registro en SP,


; sólo se puede cargar un valor inmediato NN

EX BC, DE ; NO: Existe EX DE, HL, pero no EX BC, DE

ADD DE, BC ; NO: Sólo se puede usar HL como operando destino


; en las sumas de 16 bytes con registros de propósito
; general. Una alternativa sería:
;
; LD HL, 0 ; HL = 0
; ADD HL, BC ; HL = HL + BC
; EX DE, HL ; Intercambiamos el valor de HL y DE

LD BC, DE ; NO:, pero se pueden tomar alternativas, como por ej:


;
; PUSH DE
; POP BC

LD DE, HL ; NO: mismo caso anterior.

LD SP, BC ; NO: no existe como instrucción.

La única solución para programar sin tratar de utilizar instrucciones no permitidas es la práctica: con ella
acabaremos conociendo qué operaciones podemos realizar y sobre qué registros se pueden aplicar, y
realizaremos nuestros programas con estas limitaciones en mente. Iremos viendo las diferentes excepciones
caso a caso, pero podemos encontrar las nuestras propias gracias a los errores que nos dará el ensamblador al
intentar ensamblar un programa con una instrucción que no existe para el Z80.

No os preocupéis: es sólo una cuestión de práctica. Tras haber realizado varios programas en ensamblador ya
conoceréis, prácticamente de memoria, qué instrucciones son válidas para el microprocesador y cuáles no.

El registro de flags
Hemos hablado del registro de 8 bits F como un registro especial. La particularidad de F es que no es un
registro de propósito general donde podamos introducir valores a voluntad, sino que los diferentes bits del
registro F tienen un significado propio que cambia automáticamente según el resultado de operaciones
anteriores.
Por ejemplo, uno de los bits del registro F, el bit nº 6, es conocido como “Zero Flag”, y nos indica si el
resultado de la última operación (para determinadas operaciones, como las aritméticas o las de comparación) es
cero o no es cero. Si el resultado de la anterior operación resultó cero, este FLAG se pone a uno. Si no resultó
cero, el flag se pone a cero.

¿Para qué sirve pues un flag así? Para gran cantidad de tareas, por ejemplo para bucles (repetir X veces una
misma tarea poniendo el registro BC al valor X, ejecutando el mismo código hasta que BC sea cero), o para
comparaciones (mayor que, menor que, igual que).

Veamos los diferentes registros de flags (bits del registro F) y su utilidad:

• Flag S (sign o signo): Este flag se pone a uno si el resultado de la operación realizada en complemento
a dos es negativo (es una copia del bit más significativo del resultado). Si por ejemplo realizamos una
suma entre 2 números en complemento a dos y el resultado es negativo, este bit se pondrá a uno. Si el
resultado es positivo, se pondrá a cero. Es útil para realizar operaciones matemáticas entre múltiples
registros: por ejemplo, si nos hacemos una rutina de multiplicación o división de números que permita
números negativos, este bit nos puede ser útil en alguna parte de la rutina.
• Flag Z (zero o cero): Este flag se pone a uno si el resultado de la última operación que afecte a los flags
es cero. Por ejemplo, si realizamos una operación matemática y el resultado es cero, se pondrá a uno.
Este flag es uno de los más útiles, ya que podemos utilizarlo para múltiples tareas. La primera es para
los bucles, ya que podremos programar código como:

; Repetir algo 100 veces


LD B, 100
bucle:
(...) ; código

DEC B ; Decrementamos B (B=B-1)


JR NZ, bucle
; Si el resultado de la operación anterior no es cero (NZ = Non Zero),
; saltar a la etiqueta bucle y continuar. DEC B hará que el flag Z
; se ponga a 1 cuando B llegue a cero, lo que afectará al JR NZ.
; Como resultado, este trozo de código (...) se ejecutará 100 veces.

Como veremos en su momento, existe una instrucción equivalente a DEC B + JR NZ que es más cómoda de
utilizar y más rápida que estas 2 instrucciones juntas (DJNZ), pero se ha elegido el ejemplo que tenéis arriba
para que veáis cómo muchas operaciones (en este caso DEC) afectan a los flags, y la utilidad que estos tienen a
la hora de programar.
Además de para bucles, también podemos utilizarlo para comparaciones. Supongamos que queremos hacer en
ensamblador una comparación de igualdad, algo como:

IF C = B THEN GOTO 1000


ELSE GOTO 2000

Si restamos C y B y el resultado es cero, es que ambos registros contienen el mismo valor:

LD A, C ; A = C
; Tenemos que hacer esto porque no existe
; una instruccion SUB B, C . Sólo se puede
; restar un registro al registro A.

SUB B ; A = A-B
JP Z, Es_Igual ; Si A=B la resta es cero y Z=1
JP NZ, No_Es_Igual ; Si A<>B la resta no es cero y Z=0
(...)

Es_Igual:
(...)
No_Es_Igual:
(...)

Existe una instrucción específica para realizar comparaciones: CP, que es similar a SUB pero que no altera el
valor de A. Hablaremos de CP con más detalle en su momento.

• Flag H (Half-carry o Acarreo-BCD): Se pone a uno cuando en operaciones BCD existe un acarreo del
bit 3 al bit 4.

• Flag P/V (Parity/Overflow o Paridad/Desbordamiento): En las operaciones que modifican el bit de


paridad, este bit vale 1 si el número de unos del resultado de la operación es par, y 0 si es impar. Si, por
contra, el resultado de la operación realizada necesita más bits para ser representado de los que nos
provee el registro, tendremos un desbordamiento, con este flag a 1. Este mismo bit sirve pues para 2
tareas, y nos indicará una u otra (paridad o desbordamiento) según sea el tipo de operación que hayamos
realizado. Por ejemplo, tras una suma, su utilidad será la de indicar el desbordamiento.
El flag de desbordamiento se activará cuando en determinadas operaciones pasemos de valores
11111111b a 00000000b, por “falta de bits” para representar el resultado o viceversa . Por ejemplo, en
el caso de INC y DEC con registros de 8 bits, si pasamos de 0 a 255 o de 255 a 0.

• Flag N (Substract o Resta): Se pone a 1 si la última operación realizada fue una resta. Se utiliza en
operaciones aritméticas.

• Flag C (Carry o Acarreo): Este flag se pone a uno si el resultado de la operación anterior no cupo en el
registro y necesita un bit extra para ser representado. Este bit es ese bit extra. Veremos su uso cuando
tratemos las operaciones aritméticas, en esta misma entrega.

Así pues, resumiendo:

• El registro F es un registro cuyo valor no manejamos directamente, sino que cada uno de sus bits tiene
un valor especial y está a 1 o a 0 según ciertas condiciones de la última operación realizada que afecte a
dicho registro.
• Por ejemplo, si realizamos una operación y el resultado de la misma es cero, se pondrá a 1 el flag de
Zero (Z) del registro F, que no es más que su bit número 6.
• No todas las operaciones afectan a los flags, iremos viendo qué operaciones afectan a qué flags
conforme avancemos en el curso, en el momento en que se estudia cada instrucción.
• Existen operaciones que se pueden ejecutar con el estado de los flags como condición. Por ejemplo,
realizar un salto a una dirección de memoria si un determinado flag está activo, o si no lo está.
Instrucciones LD (instrucciones de carga)
Las operaciones que más utilizaremos en nuestros programas en ensamblador serán sin duda las operaciones de
carga o instrucciones LD. Estas operaciones sirven para:

• Meter un valor en un registro.


• Copiar el valor de un registro en otro registro.
• Escribir en memoria (en una dirección determinada) un valor.
• Escribir en memoria (en una dirección determinada) el contenido de un registro.
• Asignarle a un registro el contenido de una dirección de memoria.

La sintaxis de LD en lenguaje ensamblador es:

LD DESTINO, ORIGEN

Así, gracias a las operaciones LD podemos:

• Asignar a un registro un valor numérico directo de 8 o 16 bits.

LD A, 10 ; A = 10
LD B, 200 ; B = 200
LD BC, 12345 ; BC = 12345

• Copiar el contenido de un registro a otro registro:

LD A, B ; A = B
LD BC, DE ; BC = DE

• Escribir en posiciones de memoria:

LD (12345), A ; Memoria[12345] = valor en A


LD (HL), 10 ; Memoria[valor de HL] = 10

• Leer el contenido de posiciones de memoria:

LD A, (12345) ; A = valor en Memoria[12345]


LD B, (HL) ; B = valor en Memoria[valor de HL]

Nótese cómo el operador () nos permite acceder a memoria. En nuestros ejemplos, LD A, (12345) no significa
meter en A el valor 12345 (cosa imposible al ser un registro de 16 bits) sino almacenar en el registro A el valor
que hay almacenado en la celdilla número 12345 de la memoria del Spectrum.

En un microprocesador con un juego de instrucciones ortogonal, se podría usar cualquier origen y cualquier
destino sin distinción. En el caso del Z80 no es así. El listado completo de operaciones válidas con LD es el
siguiente:

Leyenda:

N = valor numérico directo de 8 bits (0-255)


NN = valor numérico directo de 16 bits (0-65535)
r = registro de 8 bits (A, B, C, D, E, H, L)
rr = registro de 16 bits (BC, DE, HL, SP)
ri = registro índice (IX o IY).
d = desplazamiento respecto a un registro índice.

Listado:

; Carga de valores en registros


LD r, N
LD rr, NN
LD ri, NN

; Copia de un registro a otro


LD r, r
LD rr, rr

; Acceso a memoria
LD r, (HL)
LD (NN), A
LD (HL), N
LD A, (rr) ; (excepto rr=SP)
LD (rr), A ; (excepto rr=SP)
LD A, (NN)
LD rr, (NN)
LD ri, (NN)
LD (NN), rr
LD (NN), ri

; Acceso indexado a memoria


LD (ri+N), r
LD r, (ri+N)
LD (ri+N), N

Además, tenemos una serie de casos “especiales”:

; Manipulación del puntero de pila (SP)


LD SP, ri
LD SP, HL

; Para manipular el registro I


LD A, I
LD I, A

; Para manipular el registro R


LD A, R
LD R, A

Veamos ejemplos válidos y cuál sería el resultado de su ejecución:

; Carga de valores en registros


; registro_destino = valor
LD A, 100 ; LD r, N
LD BC, 12345 ; LD rr, NN

; Copia de registros en registros


; registro_destino = registro_origen
LD B, C ; LD r, r
LD A, B ; LD r, r
LD BC, DE ; LD rr, rr

; Acceso a memoria
; (Posicion_memoria) = VALOR o bien
; Registro = VALOR en (Posicion de memoria)
LD A, (HL) ; LD r, (rr)
LD (BL), B ; LD (rr), r
LD (12345), A ; LD (NN), A
LD A, (HL) ; LD r, (rr)
LD (DE), A ; LD (rr), r
LD (BC), 1234h ; LD (BC), NN
LD (12345), DE ; LD (NN), rr
LD IX, (12345) ; LD ri, (NN)
LD (34567), IY ; LD (NN), ri

; Acceso indexado a memoria


; (Posicion_memoria) = VALOR o VALOR = (Posicion_memoria)
; Donde la posicion es IX+N o IY+N:
LD (IX+10), A ; LD (ri+N), r
LD A, (IY+100) ; LD r, (ri+N)
LD (IX-30), 100 ; LD (ri+N), N

Hagamos hincapié de nuevo en el mismo detalle: debido a que el juego de instrucciones del Z80 no es
ortogonal, en ocasiones no podemos ejecutar ciertas operaciones que podrían sernos útiles con determinados
registros. En ese caso tendremos que buscar una solución mediante los registros y operaciones válidas de que
disponemos.

Un detalle muy importante respecto a las instrucciones de carga: en el caso de las operaciones LD, el registro F
no ve afectado ninguno de sus indicadores o flags en relación al resultado de la ejecución de las mismas (salvo
en el caso de “LD A, I” y “LD A, R”).

Flags
Instrucción |S Z H P N C|
----------------------------------
LD r, r |- - - - - -|
LD r, N |- - - - - -|
LD rr, rr |- - - - - -|
LD (rr), N |- - - - - -|
LD (rr), N |- - - - - -|
LD ri, (NN) |- - - - - -|
LD (NN), ri |- - - - - -|
LD (ri+d), N |- - - - - -|
LD (ri+d), r |- - - - - -|
LD r, (ri+d) |- - - - - -|
LD A, I |* * 0 * 1 0|
LD A, R |* * 0 * 1 0|

Esto quiere decir que una operación como “LD A, 0”, por ejemplo, no activará el flag de Zero del registro F.

CPU Z80: Low Endian

Un detalle curioso sobre el Z80 es que a la hora de trabajar con datos de 16 bits (por ejemplo, leer o escribir de
memoria) conviene tener en cuenta que nuestro Z80 es una CPU del tipo LOW-ENDIAN, es decir, que si
almacenamos en la posición de memoria 5000h el valor “$1234”, el contenido de las celdillas de memoria sería:

Posición Valor
$5000 $34
$5001 $12

En otro tipo de procesadores del tipo BIG-ENDIAN, los bytes aparecerían escritos en memoria de la siguiente
forma:

Posición Valor
$0000 $12
$0001 $34

Debemos tener en cuenta este dato a la hora de escribir valores de 16 bits en memoria y recuperarlos
posteriormente mediante operaciones de acceso a la memoria.
Incrementos y decrementos
Entre las operaciones disponibles, tenemos la posibilidad de incrementar (INC) y decrementar (DEC) en 1
unidad el contenido de determinados registros de 8 y 16 bits, así como de posiciones de memoria apuntadas por
HL o por IX/IY más un offset (desplazamiento de 8 bits).

Por ejemplo:

LD A, 0 ; A = 0
INC A ; A = A+1 = 1
LD B, A ; B = A = 1
INC B ; B = B+1 = 2
INC B ; B = B+1 = 3
LD BC, 0
INC BC ; BC = 0001h
INC B ; BC = 0101h (ya que B=B+1 y es la parte alta)
DEC A ; A = A-1 = 0

Veamos las operaciones INC y DEC permitidas:

INC r
DEC r
INC rr
DEC rr

Donde r puede ser A, B, C, D, E, H o L, y 'rr' puede ser BC, DE, HL, SP, IX o IY. Esta instrucción incrementa
o decrementa el valor contenido en el registro especificado.

INC (HL)
DEC (HL)

Incrementa o decrementa el byte que contiene la dirección de memoria apuntada por HL.

INC (IX+N)
DEC (IX+N)
INC (IY+N)
DEC (IY+N)

Incrementa o decrementa el byte que contiene la dirección de memoria resultante de sumar el valor del registro
IX o el registro IY con un valor numérico de 8 bits en complemento a dos.

Por ejemplo, las siguientes instrucciones serían válidas:

INC A ; A = A+1
DEC B ; B = B-1
INC DE ; DE = DE+1
DEC IX ; IX = IX-1
INC (HL) ; (HL) = (HL)+1
INC (IX-5) ; (IX-5) = (IX-5)+1
DEC (IY+100) ; (IY+100) = (IY+100)+1

Unos apuntes sobre la afectación de los flags ante el uso de INC y DEC:

• Si un registro de 8 bits vale 255 ($FF) y lo incrementamos, pasará a valer 0.


• Si un registro de 16 bits vale 65535 ($FFFF) y lo incrementamos, pasará a valer 0.
• Si un registro de 8 bits vale 0 y lo decrementamos, pasará a valer 255 ($FF).
• Si un registro de 16 bits vale 0 ($0) y lo decrementamos, pasará a valer 65535 ($FF).
• En estos desbordamientos no se tomará en cuenta para nada el bit de Carry (acarreo) de los flags
(registro F), ni tampoco lo afectarán tras ejecutarse.
• Las operaciones INC y DEC sobre registros de 16 bits (BC, DE, HL, IX, IY, SP) no afectan a los flags.
Esto implica que no podemos usar como condición de flag zero para un salto el resultado de
instrucciones como “DEC BC”, por ejemplo.
• Las operaciones INC y DEC sobre registros de 8 bits y sobre la memoria no afectan al flag de acarreo,
pero sí que pueden afectar al flag de Zero (Z), al de Paridad/Overflow (P/V), al de Signo (S) y al de
Half-Carry (H).

Lo siguiente que vamos a ver es una tabla de afectación de flags (que encontraremos en muchas tablas de
instrucciones del Z80, y a las que conviene ir acostumbrandose). Esta tabla indica cómo afecta cada instrucción
a cada uno de los flags:

Flags
Instrucción |S Z H P N C|
----------------------------------
INC r |* * * V 0 -|
INC [HL] |* * * V 0 -|
INC [ri+N] |* * * V 0 -|
INC rr |- - - - - -|
DEC r |* * * V 1 -|
DEC rr |- - - - - -|

Donde:

r = registro de 8 bits
rr = registro de 16 bits (BC, DE, HL, IX, IY)
ri = registro índice (IX, IY)
N = desplazamiento de 8 bits (entre -128 y +127).

Y respecto a los flags:

- = El flag NO se ve afectado por la operación.


* = El flag se ve afectado por la operación acorde al resultado.
0 = El flag se pone a cero.
1 = El flag se pone a uno.
V = El flag se comporta como un flag de Overflow acorde al resultado.
? = El flag toma un valor indeterminado.

Operaciones matematicas
Las operaciones aritméticas básicas para nuestro Spectrum son la suma y la resta, tanto con acarreo como sin él.
A partir de ellas deberemos crearnos nuestras propias rutinas para multiplicar, dividir, etc.

Suma: ADD (Add)

Nuestro microprocesador Z80 puede realizar sumas de 8 y 16 bits internamente. La instrucción utilizada para
ello es “ADD” y el formato es:

ADD DESTINO, ORIGEN

Las instrucciones disponibles para realizar sumas se reducen a:

ADD A, s
ADD HL, ss
ADD ri, rr

Donde:
s: Cualquier registro de 8 bits (A, B, C, D, E, H, L),
cualquier valor inmediato de 8 bits (en el rango 0-255 o -128+127
en complemento a dos), cualquier dirección de memoria apuntada por
HL, y cualquier dirección de memoria apuntada por un registro
índice con desplazamiento de 8 bits.
ss: Cualquier registro de 16 bits de entre los siguientes: BC, DE, HL, SP.
ri: Uno de los 2 registros índices (IX o IY).
rr: Cualquier registro de 16 bits de entre los siguientes excepto el mismo
registro índice origen: BC, DE, HL, IX, IY, SP.

Esto daría la posibilidad de ejecutar cualquiera de las siguientes instrucciones:

; ADD A, s
ADD A, B ; A = A + B
ADD A, 100 ; A = A + 100
ADD A, [HL] ; A = A + [HL]
ADD A, [IX+10] ; A = A + [IX+10]

; ADD HL, ss
ADD HL, BC ; HL = HL + BC
ADD HL, SP ; HL = HL + SP

; ADD ri, rr
ADD IX, BC ; IX = IX + BC
ADD IY, DE ; IY = IY + DE
ADD IY, IX ; IY = IY + IX
ADD IX, IY ; IX = IX + IY

Por contra, estas instrucciones no serían válidas:

ADD B, C ; Sólo A puede ser destino


ADD BC, DE ; Sólo puede ser destino HL
ADD IX, IX ; No podemos sumar un registro índice a él mismo

La afectación de los flags ante las operaciones de sumas es la siguiente:

• Para “ADD A, s”, el registro N (Substraction) se pone a 0 (lógicamente, ya que sólo se pone a uno
cuando se ha realizado una resta). El registro P/V se comporta como un registro de Overflow e indica si
ha habido overflow (desbordamiento) en la operación. El resto de flags (Sign, Zero, Half-Carry y Carry)
se verán afectados de acuerdo al resultado de la operación de suma.

• Para “ADD HL, ss” y “ADD ri, rr”, se pone a 0 el flag N, y sólo se verá afectado el flag de acarreo (C)
de acuerdo al resultado de la operación.

O, en forma de tabla de afectación:

Flags
Instrucción |S Z H P N C|
----------------------------------
ADD A, s |* * * V 0 *|
ADD HL, ss |- - ? - 0 *|
ADD ri, rr |- - ? - 0 *|

Las sumas realizadas por el Spectrum se hacen a nivel de bits, empezando por el bit de más a la derecha y
yendo hacia la izquierda, según las siguientes reglas:

0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 10 (=0 con acarreo)

Al sumar el último bit, se actualizará el flag de acarreo si es necesario.


Por ejemplo:

*
00000100
+ 00000101
-----------
00001001

(* = acarreo de la suma del bit anterior, 1+1=10)

Si la suma del último bit (bit 7) requiere un bit extra, se utilizará el Carry Flag del registro F para almacenarlo.
Supongamos que ejecutamos el siguiente código:

LD A, %10000000
LD B, %10000000
ADD A, B

El resultado de la ejecución de esta suma sería: A=128+128=256. Como 256 (100000000b) tiene 9 bits, no
podemos representar el resultado con los 8 bits del registro A, de modo que el resultado de la suma sería
realmente: A = 00000000 y CarryFlag = 1.

Resta: SUB (Substract)

En el caso de las restas, sólo es posible realizar (de nuevo gracias a la no ortogonalidad del J.I. del Z80) la
operación “A=A-origen”, donde “origen” puede ser cualquier registro de 8 bits, valor inmediato de 8 bits,
contenido de la memoria apuntada por [HL], o contenido de la memoria apuntada por un registro índice más un
desplazamiento. El formato de la instrucción SUB no requiere 2 operandos, ya que el registro destino sólo
puede ser A:

SUB ORIGEN

Concretamente:

SUB r ; A = A - r
SUB N ; A = A - N
SUB [HL] ; A = A - [HL]
SUB [rr+d] ; A = A - [rr+d]

Por ejemplo:

SUB B ; A = A - B
SUB 100 ; A = A - 100
SUB [HL] ; A = A - [HL]
SUB [IX+10] ; A = A - [IX+10]

Es importante recordar que en una operación “SUB X”, la operación realizada es “A=A-X” y no “A=X-A”.

Por otra parte, con respecto a la afectación de flags, es la siguiente:

Flags: S Z H P N C
-----------------------
Afectación: * * * V 1 *

Es decir, el flag de N (substraction) se pone a 1, para indicar que hemos realizado una resta. El flag de P/V
(Parity/Overflow) se convierte en indicar de Overflow y queda afectado por el resultado de la resta. El resto de
flags (Sign, Zero, Half-Carry y Carry) quedarán afectados de acuerdo al resultado de la misma (por ejemplo, si
el resultado es Cero, se activará el Flag Z).
Suma con acarreo: ADC (Add with carry)

Sumar con acarreo dos elementos (ADC) significa realizar la suma de uno con el otro y, posteriormente,
sumarle el estado del flag de Carry. Es decir:

"ADC A, s" equivale a "A = A + s + CarryFlag"


"ADC HL, ss" equivale a "HL = HL + ss + CarryFlag"

(“s” y “ss” tienen el mismo significado que en ADD y SUB).

La tabla de afectación de flags sería la siguiente:

Flags
Instrucción |S Z H P N C|
----------------------------------
ADC A,s |* * * V 0 *|
ADC HL,ss |* * ? V 0 *|

La suma con acarreo se utiliza normalmente para sumar las partes altas de elementos de 16 bits. Se suma la
parte baja con ADD y luego la parte alta con ADC para tener en cuenta el acarreo de la suma de la parte baja.

Resta con acarreo: SBC (Substract with carry)

Al igual que en el caso de la suma con acarreo, podemos realizar restas con acarreo (SBC), que no son más que
realizar una resta de los 2 operandos, tras lo cual restamos además el valor del bit de Carry Flag:

"SBC A, s" equivale a "A = A - s - CarryFlag"


"SBC HL, ss" equivale a "HL = HL - ss - CarryFlag"

La tabla de afectación de flags (en este caso con N=1, ya que es una resta):

Flags
Instrucción |S Z H P N C|
----------------------------------
SBC A,s |* * * V 1 *|
SBC HL,ss |* * ? V 1 *|

Complemento a dos

A lo largo del presente texto hemos hablado de números en complemento a dos. Complemento a dos es una
manera de representar números negativos en nuestros registros de 8 bits, utilizando para ello como signo el bit
más significativo (bit 7) del byte.

Si dicho bit está a 0, el número es positivo, y si está a 1 es negativo. Así:

01111111 (+127)
01111110 (+126)
01111101 (+125)
01111100 (+124)
(...)
00000100 (+4)
00000011 (+3)
00000010 (+2)
00000001 (+1)
00000000 (0)
11111111 (-1)
11111110 (-2)
11111101 (-3)
11111100 (-4)
(...)
10000011 (-125)
10000010 (-126)
10000001 (-127)
10000000 (-128)

Podemos averiguar cuál es la versión negativa de cualquier número positivo (y viceversa), invirtiendo el estado
de los bits y sumando uno:

+17 = 00010001

-17 = 11101110 (Invertimos unos y ceros)


= +1 (Sumamos 1)
= 11101111 (-17 en complemento a dos)

Se eligió este sistema para representar los números negativos para que las operaciones matemáticas estándar
funcionaran directamente sobre los números positivos y negativos. ¿Por qué no utilizamos directamente la
inversión de los bits para representar los números negativos y estamos sumando además 1 para obtenerlos?
Sencillo: si no sumáramos uno y simplemente invirtiéramos los bits, tendríamos 2 ceros (00000000 y
11111111) y además las operaciones matemáticas no cuadrarían (por culpa de los dos ceros). La gracia del
complemento a dos es que las sumas y restas binarias lógicas (ADD, ADC, SUB y SBC) funcionan:

Sumemos -17 y 32:

-17 = 11101111
+ +32 = 00100000
-----------------
1 00001111

El resultado es 00001111, es decir, 15, ya que 32-17=15. El flag de carry se pone a 1, pero lo podemos ignorar,
porque el flag que nos indica realmente el desbordamiento (como veremos a continuación) en operaciones de
complemento a dos es el flag de Overflow.

Sumemos ahora +17 y -17:

+17 = 00010001
+ -17 = 11101111
----------------------
1 00000000

Como podéis ver, al sumar +17 y -17 el resultado es 0. Si representáramos los números negativos simplemente
como la inversa de los positivos, esto no se podría hacer:

+17 = 00010001
+ -17 = 11101110 <--- (solo bits invertidos)
----------------------
1 11111111 <--- Nos da todo unos, el "cero" alternativo.

En complemento a dos, las sumas y restas de números se pueden realizar a nivel lógico mediante las
operaciones estándar del Z80. En realidad para el Z80 no hay más que simples operaciones de unos y ceros, y
somos nosotros los que interpretamos la información de los operandos y del resultado de una forma que nos
permite representar números negativos.

En otras palabras: cuando vemos un uno en el bit más significativo de un resultado, somos nosotros los que
tenemos que interpretar si ese bit representa un signo negativo o no: si sabemos que estamos operando con
números 0-255, podemos tratarlo como un resultado positivo. Si estábamos operando con números en
complemento a dos, podemos tratarlo como un resultado en complemento a dos. Para el microprocesador, en
cambio, no hay más que unos y ceros.

Para acabar, veamos cuál es la diferencia entre el Flag de Carry (C) y el de Overflow (V) a la hora de realizar
sumas y restas. El primero (C) se activará cuando se produzca un desbordamiento físico a la hora de sumar o
restar 2 números binarios (cuando necesitemos un bit extra para representarlo). El segundo (V), se utilizará
cuando se produzca cualquier sobrepasamiento operando con 2 números en complemento a dos.

Como acabamos de ver, en complemento a dos el último bit (el bit 7) nos indica el signo, y cuando operamos
con 2 números binarios que nosotros interpretamos como números en complemento a dos no nos basta con el
bit de Carry. Es el bit de Overflow el que nos dará información sobre el desbordamiento a un nivel lógico.

El bit de Carry se activará si pasamos de 255 a 0 o de 0 a 255 (comportándose como un bit de valor 2 elevado a
8, o 256), y el bit de overflow lo hará si el resultado de una operación en complemento a dos requiere más de 7
bits para ser representado.

Mediante ejemplos:

255+1:

11111111
+ 00000001
-----------
1 00000000

C=1 (porque hace falta un bit extra)


V=0

127+1:

01111111
+ 00000001
-----------
10000000

C=0 (no es necesario un bit extra en el registro)


V=1 (en complemento a dos, no podemos representar +128)

En el ejemplo anterior, V se activa porque no ha habido desbordamiento físico (no es necesario un bit extra para
representar la operación), pero sí lógico: no podemos representar +128 con 7 bits+signo en complemento a dos.

Instrucciones de intercambio
Como ya se ha explicado, disponemos de un banco de registros alternativos (los Shadow Registers), y podemos
conmutar los valores entre los registros estándar y los alternativos mediante unas determinadas instrucciones
del Z80.

El Z80 nos proporciona una serie de registros de propósito general (así como un registro de flags), de nombres
A, B, C, D, E, F, H y L. El micro dispone también de unos registros extra (set alternativo conocido como
Shadow Registers) de nombre A', B', C', D', E', F', H' y L', que aprovecharemos en cualquier momento de
nuestro programa. No obstante, no podremos hacer uso directo de estos registros en instrucciones en
ensamblador. No es posible, por ejemplo, ninguna de las siguientes instrucciones:

LD B', $10
INC A'
LD HL', $1234
LD A', ($1234)
La manera de utilizar estos registros alternativos es conmutar sus valores con los registros estándar mediante la
instrucción “EXX”, cuyo resultado es el intercambio de B por B', C por C', D por D', E por E', H por H' y L por
L'. Supongamos que tenemos los siguientes valores en los registros:

Registro Valor Registro Valor


B $A0 B' $00
C $55 C' $00
D $01 D' $00
E $FF E' $00
H $00 H' $00
L $31 L' $00

En el momento en que realicemos un EXX, los registros cambiarán de valor por la “conmutación” de bancos:

Registro Valor Registro Valor


B $00 B' $A0
C $00 C' $55
D $00 D' $01
E $00 E' $FF
H $00 H' $00
L $00 L' $31

Si realizamos de nuevo EXX, volveremos a dejar los valores de los registros en sus “posiciones” originales.
EXX (mnemónico ensamblador derivado de EXchange), simplemente intercambia los valores entre ambos
bancos.

Aparte de la instrucción EXX, disponemos de una instrucción EX AF, AF' , que, como el lector imagina,
intercambia los valores de los registros AF y AF'. Así, pasaríamos de:

Registro Valor Registro Valor


A 01h A' 00h
F 10h F' 00h

a:

Registro Valor Registro Valor


A 00h A' 01h
F 00h F' 10h

Realizando de nuevo un EX AF, AF' volveríamos a los valores originales en ambos registros.

De esta forma podemos disponer de un set de registros extra Acumulador/Flags con los que trabajar. Por
ejemplo, supongamos que programamos una porción de código donde queremos hacer una serie de cálculos
entre registros y después dejar el resultado en una posición de memoria, pero no queremos perder los valores
actuales de los registros (ni tampoco hacer uso de la pila, que veremos en su momento). En ese caso, podemos
hacer:

; Una rutina a la que saltaremos gracias a la


; etiqueta que definimos aquí:
MiRutina:

; Cambiamos de banco de registros:


EXX
EX AF, AF' ; Intercambiamos AF con AF'

; Hacemos nuestras operaciones


LD A, ($1234)
LD B, A
LD A, ($1235)
INC A
ADD A, B
; (...etc...)
; (...aquí más operaciones...)

; Grabamos el resultado en memoria


LD ($1236), A

; Recuperamos los valores de los registros


EX AF, AF' ; Intercambiamos AF con AF'
EXX

; Volvemos al lugar de llamada de la rutina


RET

Además de EXX y EX AF, AF' tenemos disponibles 3 instrucciones de intercambio más que no trabajan con los
registros alternativos, sino entre la memoria y registros, y la pila (o memoria en general) y los registros HL, IX
e IY.

Instrucción Resultado
EX DE, HL Intercambiar los valores de DE y HL.
Intercambiar el valor de HL con el valor de 16 bits
de la posición de memoria apuntada por el registro SP
EX (SP), HL
(por ejemplo, para intercambiar el valor de HL con el
del último registro que hayamos introducido en la pila).
EX (SP), IX Igual que el anterior, pero con IX.
EX (SP), IY Igual que el anterior, pero con IY.

La primera de estas instrucciones nos resultará muy útil en nuestros programas en ensamblador, ya que nos
permite intercambiar los valores de los registros DE y HL. Las 3 instrucciones restantes permiten intercambiar
el valor apuntado por SP (en memoria) por el valor de los registros HL, IX o IY.

Como ya hemos comentado cuando hablamos del carácter Low-Endian de nuestra CPU, al escribir en memoria
(también en la pila) primero se escribe el Byte Bajo y luego el Byte Alto. Posteriormente lo leeremos de la
misma forma, de tal modo que si los bytes apuntados en la pila (en memoria) son “$FF $00”, al hacer el EX
(SP), HL, el registro HL valdrá “$00FF”.

Nótese que aprovechando la pila (como veremos en su momento) también podemos intercambiar los valores de
los registros mediante:

PUSH BC
PUSH DE
POP BC
POP DE

El siguiente programa muestra el uso de esta técnica:

; Ejemplo que muestra el intercambio de registros


; mediante el uso de la pila (PUSH/POP).
ORG 40000

; Cargamos en DE el valor 12345 y


; realizamos un intercambio de valores
; con BC, mediante la pila:
LD DE, 12345
LD BC, 0

PUSH DE
PUSH BC
POP DE
POP BC

; Volvemos, ahora BC=DE y DE=BC


RET

Lo ensamblamos:

pasmo --tapbas cambio.asm cambio.tap

Tras esto lo cargamos en un emulador de Spectrum (como un fichero TAP), nos vamos al BASIC y tecleamos
“PRINT AT 10, 10; USR 40000”. En pantalla aparecerá el valor “12345”, ya que las rutinas llamadas desde
BASIC devuelven sus resultados en BC, y nosotros hemos hecho un intercambio mediante la pila, entre DE y
BC.

En su momento veremos cómo funciona la pila, por ahora basta con saber que tenemos la posibilidad de
intercambiar registros mediante el uso de la misma. Podríamos haber optado por no explicar este pequeño truco
hasta haber hablado de la pila, pero nos parece más conveniente el hecho de tener toda la información sobre
ensamblador agrupada de forma al buscar información o referencias sobre instrucciones para intercambiar
valores de registros, pueda encontrarse toda junta. Como hemos comentado al principio de este capítulo, resulta
muy complicado explicar un lenguaje tan interrelacionado de forma que no se solapen diferentes áreas, de
modo que la comprensión total de muchos de los conceptos se alcanzará con una segunda lectura del curso
completo.

En resumen
Hemos visto la sintaxis de los programas en ensamblador (o, al menos, la sintaxis general de PASMO, el
ensamblador que recomendamos), así como una descripción completa del juego de registros del Z80,
incluyendo entre ellos el registro de flags F.

Además, hemos comenzado a ver nuestras primeras instrucciones del lenguaje ensamblador, en especial las
instrucciones de carga, incremento y decremento, y aritméticas.

En el próximo capítulo continuaremos detallando las diferentes instrucciones del Z80, ejemplos de uso y su
efecto sobre los flags del registro F.
Lenguaje Ensamblador del Z80 (II)
Desplazamientos de memoria, manipulación de bits
y operaciones lógicas
En el anterior capítulo comenzamos nuestra andadura en el lenguaje ensamblador del Z80 por medio de las
instrucciones de carga (LD), operaciones aritméticas (ADD, ADC, SUB, SBC, INC, DEC) y de intercambio
(EXX y EX). Mientras se introducían las diferentes instrucciones, mostramos la manera de emplear los
registros y cómo los resultados podían afectar a los flags del registro F, mediante las “tablas de afectación de
flags”.

Toda la teoría explicada en el anterior capítulo del curso nos permitirá avanzar ahora mucho más rápido, ya que
con todos los conceptos asimilados podemos ir realizando una rápida introducción a nuevas instrucciones,
bastando ahora con una simple descripción de cada una de ellas. Las tablas de afectación de flags y comentarios
sobre los operandos permitidos (o prohibidos) para cada una de ellas completarán la formación necesaria.

Para poder continuar con éste y posteriores capítulos del curso será imprescindible haber comprendido y
asimilado todos los conocimientos de las entregas anteriores, de modo que si no es así, recomendamos al lector
que relea las entregas 1, 2 y 3, y que se asegure de comprender todos los conceptos explicados.

En esta entrega trataremos las operaciones con bits (NEG, CPL, BIT, SET y RES), las operaciones lógicas
(AND, OR y XOR) y las operaciones de desplazamiento de bits (RR, RL, RLC, RRC, SLA, SRA y SRL).

No obstante, antes de pasar a hablar de las operaciones con bits finalizaremos con la descripción de las
instrucciones de carga (en este caso las repetitivas), y veremos 4 instrucciones muy sencillas: SCF, CCF, NOP
y DAA.

Instrucciones de desplazamiento de memoria

Ya conocemos la existencia de las instrucciones de carga (LD), que nos permitían mover valores entre 2
registros o entre la memoria y los registros. Lo que vamos a ver a continuación es cómo podemos copiar un
byte de una posición de memoria a otra, con una sóla instrucción.

Las 2 instrucciones que vamos a describir: LDI y LDD, no admiten parámetros. Lo que hacen estas
instrucciones es:

LDI (Load And Increment):

• Leer el byte de la posición de memoria apuntada por el registro HL.


• Escribir ese byte en la posición de memoria apuntada por el registro DE.
• Incrementar DE en una unidad (DE=DE+1).
• Incrementar HL en una unidad (HL=HL+1).
• Decrementar BC en una unidad (BC=BC-1).

LDD (Load And Decrement):

• Leer el byte de la posición de memoria apuntada por el registro HL.


• Escribir ese byte en la posición de memoria apuntada por el registro DE.
• Decrementar DE en una unidad (DE=DE-1).
• Decrementar HL en una unidad (HL=HL-1).
• Decrementar BC en una unidad (BC=BC-1).

En pseudocódigo:

LDI: Copiar [HL] en [DE]


DE=DE+1
HL=HL+1
BC=BC-1

LDD: Copiar [HL] en [DE]


DE=DE-1
HL=HL-1
BC=BC-1

Estas instrucciones lo que nos permiten es copiar datos de una zona de la memoria a otra. Por ejemplo,
supongamos que queremos copiar el byte contenido en 16384 a la posición de memoria 40000:

LD HL, 16384
LD DE, 40000
LDI

¿Qué tiene de especial LDI con respecto a realizar la copia a mano con operaciones LD? Pues que al
incrementar HL y DE, lo que hace es apuntar a los siguientes elementos en memoria (HL=16385 y DE=40001),
con lo cual nos facilita la posibilidad de copiar múltiples datos (no sólo 1), con varios LDI. Lo mismo ocurre
con LDD, que al decrementar DE y HL los hace apuntar a los bytes anteriores de origen y destino.

Pero para facilitarnos aún más la tarea de copia (y no tener que realizar bucles manualmente), el Z80 nos
proporciona las instrucciones LDIR y LDDR, que funcionan igual que LDI y LDD pero copiando tantos bytes
como valor contenga el registro BC. Es decir:

LDIR = Repetir LDI hasta que BC valga 0


= Repetir:
Copiar [HL] en [DE]
DE=DE+1
HL=HL+1
BC=BC-1
Hasta que BC = 0

LDDR = Repetir LDD hasta que BC valga 0


= Repetir:
Copiar [HL] en [DE]
DE=DE-1
HL=HL-1
BC=BC-1
Hasta que BC = 0

Estas instrucciones son enormemente útiles porque nos permiten copiar bloques de datos desde una zona de la
memoria a otra. Por ejemplo, podemos hacernos una copia del estado de la pantalla en una zona de memoria
mediante:

LD HL, 16384
LD DE, 50000
LD BC, 6912
LDIR

Con el anterior programa, copiamos los 6912 bytes que hay a partir de la dirección de memoria 16384 (la
pantalla) y los almacenamos a partir de la dirección 50000. De este modo, desde 50000 a 56912 tendremos una
copia del estado de la pantalla (podría servir, por ejemplo, para modificar cosas en esta “pantalla virtual” y
después copiarla de nuevo a la videoram, tomando HL=50000 y DE=16384).

Para demostrar esto, ensamblemos y ejecutemos el siguiente ejemplo:


; Ejemplo de LDIR donde copiamos 6144 bytes de la ROM
; a la videomemoria. Digamos que "veremos la ROM" :)
ORG 40000

LD HL, 0 ; Origen: la ROM


LD DE, 16384 ; Destino: la VideoRAM
LD BC, 6144 ; toda la pantalla
LDIR ; copiar

RET

Este ejemplo copia el contenido de los primeros 6144 bytes de memoria (el inicio de la ROM) sobre la
videomemoria, haciendo aparecer píxeles que se corresponden con los valores que hay en la rom (las
instrucciones de arranque y el intérprete BASIC del Spectrum):

Al probar el equivalente BASIC del ejemplo anterior se puede comprobar la diferencia de velocidad existente:

10 REM Copiamos la ROM en la VideoRAM


20 FOR I=0 TO 6144 : POKE (16384+I), (PEEK I) : NEXT I
30 PAUSE 0
RUN

Concluímos pues que en todas estas instrucciones de copia de memoria o transferencia, HL es el origen, DE el
destino y BC el número de bytes a transferir. Con LDI y LDD sólo copiaremos 1 byte (independientemente del
valor de BC, aunque lo decrementará), y con LDIR y LDDR copiaremos tantos bytes como valga BC,
decrementando BC hasta que su valor llega a cero. Los flags quedarán afectados, especialmente con LDI y
LDD para indicarnos mediante el registro P/V si BC ha llegado a cero.

Flags
Instrucción |S Z H P N C|
----------------------------------
LDI |- - 0 * 0 -|
LDD |- - 0 * 0 -|
LDDR |- - 0 0 0 -|
LDIR |- - 0 0 0 -|

Recordemos el significado de los símbolos de la tabla de afectación de flags (válido para todas las tablas de
instrucciones que utilizaremos a lo largo del curso):

- = El flag NO se ve afectado por la operación.


* = El flag se ve afectado por la operación acorde al resultado.
0 = El flag se pone a cero.
1 = El flag se pone a uno.
V = El flag se comporta como un flag de Overflow acorde al resultado.
P = El flag se comporta como un flag de Paridad acorde al resultado.
? = El flag toma un valor indeterminado.
Una duda que puede asaltarle al lector es: “si tenemos LDIR para copiar bloques, ¿para qué nos puede servir
LDDR? ¿No es una instrucción redundante, que podemos no necesitar nunca gracias a LDIR? La respuesta es
que LDDR es especialmente útil cuando hay que hacer copias de bloques de datos que se superponen.

Supongamos que tenemos que realizar una copia de 1000 bytes desde 25000 hasta 25100. Preparamos para ello
el siguiente código:

LD HL, 25000
LD DE, 25100
LD BC, 1000
LDIR

Este código no funcionará como esperamos: ambas zonas se superponen, con lo cual si lo ejecutamos, ocurrirá
lo siguiente:

• El byte en [25000] se copiará a [25100].


• El byte en [25001] se copiará a [25101].
• etc…

¿Qué ocurrirá cuando LDIR llegue al byte número 25100 y lo intente copiar a 25200? Sencillamente, que
hemos perdido el contenido REAL del byte número 25100, porque fue machacado al principio de la ejecución
del LDIR por el byte contenido en [25000]. No estamos moviendo el bloque correctamente, porque las zonas se
superponen y cuando llegamos a la zona destino, estamos copiando bytes que movimos desde el origen.

Para ello, lo correcto sería utilizar el siguiente código de “copia hacia atrás”:

LD HL, 25999
LD DE, 25099
LD BC, 1000
LDDR

Es decir, apuntamos HL y DE al final de los 2 bloques de copia, y copiamos los bloques desde abajo,
decrementando. De este modo nunca sobreescribimos con un dato ninguna posición de memoria que vayamos a
copiar posteriormente.

En este ejemplo:

• El byte en [26000] se copia en [26100].


• El byte en [25999] se copia en [26099].
• El byte en [25998] se copia en [26098].
• (…)
• El byte en [25001] se copia en [25101].
• El byte en [25000] se copia en [25100].

Que es, efectivamente, lo que queríamos hacer, pero sin perder datos en la copia: copiar 1000 bytes desde
25000 a 25100 (sólo que realizamos la copia de abajo a arriba).

Un ejemplo de rutina con LDIR


Vamos a ver un ejemplo de rutina en ensamblador que utiliza LDIR con un propósito concreto: vamos a cargar
una pantalla de carga (por ejemplo, para nuestros juegos) de forma que no aparezca poco a poco como lo haría
con LOAD ”“ SCREEN$, sino que aparezca de golpe.

Para eso lo que haremos será lo siguiente:


Crearemos una rutina en ensamblador que copiará 6912 bytes desde la dirección 50000 hasta la posición 16384
(la videoram). La rutina ya la hemos visto:

ORG 40000
LD HL, 50000 ; Origen: 50000
LD DE, 16384 ; Destino: la VideoRAM
LD BC, 6912 ; toda la pantalla
LDIR ; copiar
RET

La ensamblamos con pasmo a formato binario (pasmo carga.asm carga.bin) y obtenemos el siguiente código
máquina (que podremos ver con hexedit, hexdump o cualquier otro editor/visor hexadecimal):

33, 80, 195, 17, 0, 64, 1, 0, 27, 237, 176, 201

Nos crearemos un cargador BASIC que realice el trabajo de pokear nuestra rutina en 40000 y cargar la pantalla
en 50000:

10 REM Ejemplo de volcado de pantalla de carga


20 CLEAR 39999
30 DATA 33, 80, 195, 017, 0, 64, 1, 0, 27, 237, 176, 201
40 FOR I=0 TO 11 : READ OPCODE : POKE 40000+I, OPCODE : NEXT I
50 LOAD "" CODE 50000, 6912
60 RANDOMIZE USR 40000
70 PAUSE 0

Grabamos este cargador en cinta (o tap/tzx), y a continuación, tras el cargador, grabamos una pantalla de carga,
que es cargada desde cinta en la dirección de memoria 50000 con la sentencia BASIC LOAD ”“ CODE.

Ejecutamos el programa resultante en emulador o Spectrum, y veremos cómo la carga de la pantalla no puede
verse en el monitor. Cuando está termina su carga, la rutina ensamblador se ejecuta y se vuelca, de golpe, a la
videoram (estad atentos a la carga, porque el volcado es muy rápido).

Algunas instrucciones especiales


Antes de comenzar con las instrucciones de manipulación de registros y datos a nivel de bits vamos a ver una
serie de instrucciones difíciles de encuadrar en futuros apartados y que pueden sernos de utilidad en nuestros
programas:

• SCF: Set Carry Flag : Esta instrucción (que no admite parámetros) pone a 1 el Carry Flag del registro
F. Puede sernos útil en determinadas operaciones aritméticas.
• CCF: Complement Carry Flag : Esta instrucción (que tampoco admite parámetros) invierte el estado
del bit de Carry Flag: si está a 1 lo pone a 0, y viceversa. Puede servirnos para poner a 0 el carry flag
mediante la combinación de SCF + CCF, aunque esta misma operación se puede realizar con un simple
“AND A”.

• NOP: No OPeration : Esta instrucción especial del microprocesador ocupa un byte en el código
(opcode $00) y no efectúa ninguna operación ni afecta a ningún flag. En cambio, se toma 4 t-states (t-
estados, o ciclos del procesador) para ejecutarse, debido al ciclo de fetch/decode/execute del procesador.
¿Para qué puede servir una instrucción que no realiza ninguna acción y que requiere tiempo del
procesador (aunque sea muy poco) para ejecutarse? Por un lado, podemos utilizarla en bucles de
retardos (varios NOPs ejecutados en un bucle que se repita varias veces) para poner retardos en nuestros
programas o juegos. Por otro, como ocupa un byte en memoria (en el código) y no realiza ninguna
operación, podemos utilizarla para rellenar zonas de nuestro código, y así alinear código posterior en
una determinada dirección que nos interese.

• DAA: Decimal Adjust Accumulator : Esta instrucción permite realizar ajustes en los resultados de
operaciones con números BCD (tras operaciones aritméticas). ¿Qué son los números en formato BCD?
Es una manera de representar números en los registros (o memoria) de forma que de los 8 bits de un
byte se utilizan los 4 bits del 0 al 3 para representar un número del 0 al 9 (4 bits = desde 0000 hasta
1111), y los 4 bits del bit 4 al 7 para representar otro número del 0 al 9. A los 2 números BCD juntos se
les llama “Byte BCD” o “números en formato BCD”. Un número BCD puede estar formado por varios
bytes BCD, siendo cada byte 2 cifras del mismo. Así, para representar un número de 10 cifras en BCD
sólo es necesario utilizar 5 bytes. Además, podemos utilizar un byte extra que indique la posición de la
“coma decimal” para así poder trabajar con números decimales en ensamblador. Si queremos realizar
operaciones entre este tipo de números deberemos programarnos nosotros mismos las rutinas para
realizarlas.

A lo largo del curso no utilizaremos números en BCD y por lo tanto es muy probable que no lleguemos
a utilizar DAA, pero conviene saber que el Z80 nos brinda la oportunidad de utilizar números más
grandes de 16 bits, operando con números en BCD. Para realizar juegos normalmente no necesitaremos
de estas instrucciones.

Todas estas instrucciones afectan a los flags de la siguiente manera:

Flags
Instrucción |S Z H P N C|
----------------------------------
SCF |- - 0 - 0 1|
CCF |- - ? - 0 *|
NOP |- - - - - -|
DAA |* * * P - *|

Operaciones con bits


El conjunto de instrucciones que vamos a ver hoy está pensado para trabajar con los bits individuales de un
registro: invertir los bits de un registro, obtener el complemento a dos de un registro y poner a 0 o a 1, o
comprobar, un determinado bit de un registro.

CPL y NEG

CPL es una instrucción que se usa para obtener el inverso en bits del registro A. No admite parámetros (el
operando destino es el registro A) y cuando la ejecutamos, se invierte el estado de cada uno de los bits de A, de
forma que los unos pasan a valer cero, y los ceros, uno.
LD A, %10000001
CPL ; A = %01111110

La tabla de afectación de flags de CPL es:

Flags
Instrucción |S Z H P N C|
----------------------------------
CPL |- - 1 - 1 -|

Es decir, se deja a uno el flag de Resta (N) y el de HalfCarry (H). El resto de flags no se ven afectados.

Existe una instrucción similar a CPL, pero que además de realizar la inversión de unos y ceros suma 00000001
al resultado de la inversión del registro A. Esta instrucción es NEG. El resultado es que en A obtenemos el
valor negativo del número en complemento a dos almacenado en este registro (A = -A).

Por ejemplo:

LD A, 1 ; A = +1
NEG ; A = -1 = %11111111

La tabla de afectación de flags de NEG es:

Flags
Instrucción |S Z H P N C|
----------------------------------
NEG |* * * V 1 *|

SET, RES y BIT

Las siguientes instrucciones que vamos a ver nos permitirán el manejo de cualquiera de los bits de un registro o
posición de memoria: activar un bit (ponerlo a uno), desactivar un bit (ponerlo a cero), o comprobar su valor
(averiguar si es cero o uno) afectando a los flags.

Comencemos con “SET”. Esta instrucción activa (pone a valor 1) uno de los bits de un registro o dirección de
memoria. El formato de la instrucción es:

SET bit, DESTINO

donde Bit es un número entre 0 (el bit menos significativo o bit 0) y 7 (el de más valor o más significativo), y
destino puede ser cualquier registro de 8 bits (A, B, C, D, E, H y L), una dirección de memoria apuntada por
HL (es decir, el destino puede ser [HL]), o una dirección de memoria indexada por [IX+N] o [IY+N]. Con esto,
las siguientes instrucciones serían válidas:

SET 5, A ; Activar el bit 5 del registro A


SET 0, H ; Activar el bit 0 del registro H
SET 7, [HL] ; Activar el bit 7 del dato contenido en
; la dirección de memoria apuntada por HL
SET 1, [IX+10] ; Activar el bit 1 del dato en [IX+10]

La instrucción opuesta a SET es RES (de reset), que pone a cero el bit indicado del destino especificado. Su
formato es igual que el de SET, como podemos ver en los siguientes ejemplos:

RES bit, DESTINO

RES 0, H ; Desactivar el bit 0 del registro H


RES 7, [HL] ; Desactivar el bit 7 del dato contenido en
; la dirección de memoria apuntada por HL
RES 1, [IX-5] ; Desactivar el bit 0 del dato en [IX-5]

SET y RES no afectan a los flags, como podemos ver en su tabla de afectación de indicadores:

Flags
Instrucción |S Z H P N C|
----------------------------------
SET b, s |- - - - - -|
RES b, s |- - - - - -|

La última instrucción de manipulación de bits individuales que veremos en este apartado es BIT. Esta
instrucción modifica el flag de cero (Z) y deja su valor en 0 ó 1 dependiendo del valor del bit que estamos
probando. Si estamos probando, por ejemplo, el bit 5 del registro A, ocurrirá lo siguiente:

• Si el bit 5 del registro A es cero: el Flag Z se pone a 1.


• Si el bit 5 del registro A es uno: el flag Z se pone a 0.

En otras palabras, Z toma la inversa del valor del Bit que comprobamos: esto es así porque Z no es una COPIA
del bit que estamos testeando, sino el resultado de evaluar si dicho bit es cero o no, y una evaluación así pone a
uno el flag Z sólo cuando lo que se evalúa es cero.

Su formato es:

BIT bit, DESTINO

El destino puede ser el mismo que en SET y RES: un registro, posición de memoria apuntado por HL o
posición de memoria apuntada por un registro índice más un desplazamiento.

Por ejemplo:

LD A, 8 ; A = %00001000
BIT 7, A ; El flag Z vale 1
; porque el bit 7 es 0
BIT 3, A ; El flag Z vale 0
; porque el bit 3 no es 0
; (es 1).

El lector se preguntará … ¿cuál es la utilidad de BIT? Bien, el hecho de que BIT modifique el Zero Flag de
acuerdo al bit que queremos comprobar nos permitirá utilizar instrucciones condicionales para realizar muchas
tareas. Por ejemplo, podemos comprobar el bit 0 de un registro (algo que nos permitiría saber si es par o impar)
y en caso de que se active el flag de Zero (Si z=1, el bit 0 vale 0, luego es par), realizar un salto a una
determinada línea de programa.

Por ejemplo:

BIT 0, A ; Que valor tiene el bit 0?


; Ahora Z = NEG del bit 0 de A.
JP Z es_par ; Saltar si esta Z activado
; (si Z=1 -> salta a es_par)
; ya que si Z=1, es porque el bit era 0

Esta instrucción es, como veremos en muchas ocasiones, muy útil, y como ya hemos dicho sí que altera el
registro F:

Flags
Instrucción |S Z H P N C|
----------------------------------
BIT b, s |? * 1 ? 0 -|
Rotacion de bits
El siguiente set de instrucciones que veremos nos permitirá ROTAR (ROTATE) los bits de un dato de 8 bits
(por ejemplo, almacenado en un registro o en memoria) hacia la izquierda o hacia la derecha.

RLC, RRC, RL y RC

Para realizar esta tarea tenemos disponibles 2 instrucciones básicas: RLC y RRC. La primera de ellas, RLC,
rota el registro o dato en un bit a la izquierda (RLC = Rotate Left Circular), y la segunda lo hace a la derecha.

Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
----------------- -> RLC -> -----------------
Valor a b c d e f g h b c d e f g h a

Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
----------------- -> RRC -> -----------------
Valor a b c d e f g h h a b c d e f g

Así, RLC de 00000001 daría como resultado 00000010. Como la rotación es circular, todos los bits se mueven
una posición a la izquierda y el bit 7 se copia en el bit 0. Asímismo, RRC de 00000001 daría como resultado
10000000, ya que el bit 0 al rotarse a la derecha (como todos los demás bits) se copia donde estaba el bit 7.
Cabe destacar que el Carry Flag se vé afectado, ya que el bit 7 en RLC y el 0 en RRC también se copiará allí.

Por ejemplo, supongamos el valor 10000001 almacenado en el registro B:

El resultado las 2 operaciones descritas sería:

LD B, %10000001 ; B = 10000001
RLC B ; B = 00000011

LD B, %10000001 ; B = 10000001
RRC B ; B = 11000000
No sólo podemos rotar registros: en general el destino de la rotación podrá ser un registro, el contenido de la
dirección de memoria apuntada por [HL], o bien el contenido de la memoria apuntada por un registro índice
más desplazamiento ([IX+N] o [IY+N]). Más adelante veremos la tabla de afectación de flags de esta y otras
instrucciones que veremos a continuación.

Además de RLC y RRC (rotación circular), tenemos disponibles 2 instrucciones más que nos permiten
apoyarnos en el Carry Flag del registro F como si fuera un bit más de nuestro registro, comportándose como el
noveno bit (de más valor) del registro: hablamos de las instrucciones RL y RC:

Bit C 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0
---------------------- -> RL -> ---------------------
Valor X a b c d e f g h a b c d e f g h X

Bit C 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0
---------------------- -> RR -> ---------------------
Valor X a b c d e f g h h X a b c d e f g

El CarryFlag hace de bit extra: por un lado se copia al Bit 0 o al Bit 7 según estemos rotando a izquierda o a
derecha, y por otra parte recibe el valor del bit 7 del bit 0 (respectivamente para RL y RR).

Por ejemplo, supongamos el valor 10000001 almacenado en el registro B y que el carry flag estuviera a uno:

El resultado las 2 operaciones descritas sería:

SCF ; Set Carry Flag (hace C=1)


LD B, %00000010 ; B = 00000010
RL B ; B = 00000101 y C=0 (del bit 7)

SCF ; Set Carry Flag (hace C=1)


LD B, %01000001 ; B = 01000000
RR B ; B = 10100000 y C=1 (del bit 0)
Así pues, RLC y RRC son circulares y no utilizan el Carry Flag, mientras que RR y RL sí que lo utilizan, como
un bit extra. Utilizando RR/RL 9 veces o bien RLC/RRC 8 veces sobre un mismo registro obtenemos el valor
original antes de comenzar a rotar.

Veamos la tabla de afectación de flags de estas nuevas instrucciones:

Flags
Instrucción |S Z H P N C| Significado
-----------------------------------------------------------------
RLC s |* * 0 P 0 *| Rotate Left Circular
RRC s |* * 0 P 0 *| Rotate Right Circular
RL s |* * 0 P 0 *| Rotate Left (con Carry)
RR s |* * 0 P 0 *| Rotate Right (con Carry)

El destino “s” puede ser cualquier registro de 8 bits, memoria apuntada por HL o registros índice con
desplazamiento. Como veis hay muchos flags afectados, y en esta ocasión el flag P/V ya no nos sirve para
indicar desbordamientos sino que su estado nos da la PARIDAD del resultado de la operación de rotación. Con
el flag P a uno, tenemos paridad par (even), es decir, el número de bits a uno en el resultado es par. Si está a
cero significa que el número de bits a uno en el resultado es impar.

RLA, RRA, RLCA y RRCA

Aunque pueda parecer sorprendente (ya que podemos utilizar las 4 operaciones anteriores con el registro A
como operando), existen 4 instrucciones más dedicadas exclusivamente a trabajar con “A”: hablamos de RLA,
RRA, RLCA y RRCA. La diferencia entre estas 4 instrucciones y su versión con un espacio en medio (RL A,
RR A, RLC A y RRC A) radica simplemente en que las nuevas 4 instrucciones alteran los flags de una forma
diferente:

Flags
Instrucción |S Z H P N C| Significado
-----------------------------------------------------------------
RLA |- - 0 - 0 *| Rotate Left Accumulator
RRA |- - 0 - 0 *| Rotate Right Accumulator
RLCA |- - 0 - 0 *| Rotate Left Circular Acc.
RRCA |- - 0 - 0 *| Rotate Right Circular Acc.

Como veis, están pensadas para alterar MENOS flags que sus homónimas de propósito general (algo que nos
puede interesar en alguna ocasión).

RLD y RRD

Y para acabar con las instrucciones de rotación, tenemos RLD y RRD, que realiza una rotación entre A y el
contenido de la memoria apuntada por HL.

Concretamente, RRD lo que hace es:

• Leer el dato contenido en la dirección de memoria apuntada por HL.


• Coger los 4 bits más significativos (bit 4-7) de ese valor.
• Rotar A hacia la izquierda 4 veces (copiando los bits 0-3 en las posiciones 4-7).
• Copiar los 4 bits extraídos de la memoria en los 4 bits menos significativos de A.
Resumiendo, supongamos los siguientes valores de A y [HL]:

Registro A: Bit 7 6 5 4 3 2 1 0
--------------------
a b c d e f g h

[HL]: Bit 7 6 5 4 3 2 1 0
--------------------
s t u v w x y z

Resultado de RRD:

Registro A: Bit 7 6 5 4 3 2 1 0
--------------------
e f g h s t u v

Resultado de RLD:

Registro A: Bit 7 6 5 4 3 2 1 0
--------------------
s t u v e f g h

En pseudocódigo C:

RRD: A = ( A<<4 ) | ([HL]>>4)


RLD: A = ( [HL]<<4 ) | (A & 0x0F)

La afectación de flags sería:

Flags
Instrucción |S Z H P N C| Significado
-----------------------------------------------------------------
RLD |* * 0 P 0 -| Rotate Left 4 bits
RRD |* * 0 P 0 -| Rotate Right 4 bits

Aunque ahora todo este conjunto de instrucciones pueda parecernos carente de utilidad, lo que hace el
microprocesador Z80 es proveernos de toda una serie de pequeñas herramientas (como estas de manipulación,
chequeo y rotación de bits) para que con ellas podamos resolver cualquier problema, mediante la combinación
de las mismas. Os aseguramos que en más de una rutina tendréis que usar instrucciones de rotación o
desplazamiento.

Desplazamiento de bits
El siguiente set de instrucciones que veremos nos permitirá DESPLAZAR (SHIFT) los bits de un dato de 8 bits
(por ejemplo, almacenado en un registro o en memoria) hacia la izquierda o hacia la derecha. Desplazar es
parecido a rotar, sólo que el desplazamiento no es circular; es decir, los bits que salen por un lado no entran por
otro, sino que entran ceros en el caso de desplazar a la izquierda, o copias del bit 7 en el caso de desplazar a la
derecha:

00010001 DESPLAZADO A LA IZQUIERDA es 00100010


(movemos todos los bits hacia la izquierda y el bit 0
entra como 0. El bit 7 se copia al Carry)

00001001 DESPLAZADO A LA DERECHA es 00000100


(el 0 del bit 7 del resultado entra nuevo, el 1 del
bit 0 origen se pierde, el cuarto se desplaza)
Las instrucciones de desplazamiento a izquierda y derecha en Z80 se llaman SLA (Shift Left Arithmetic) y
SRA (Shift Right Arithmetic), y su formato es:

SRA operando
SLA operando

Donde operando puede ser el mismo tipo de operando que en las instrucciones de rotación: un registro de 8 bits,
[HL] o [IX/IY+N]. Lo que realizan estas operaciones sobre el dato operando es:

Bit 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0
----------------- -> SLA -> ------------------------
a b c d e f g h a b c d e f g h 0

Literalmente:

• Rotar los bits a la izquierda («).


• El bit “a” (bit 7) se copia al Carry Flag.
• Por la derecha entra un cero.

Bit 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0
----------------- -> SRA -> ------------------------
a b c d e f g h h a a b c d e f g

Literalmente:

• Rotar los bits a la derecha (»).


• El bit “h” (bit 0) se copia al Carry Flag.
• En la izquierda (bit 7) se mantiene su valor anterior.

Nótese pues que SLA y SRA nos permiten trabajar también con números negativos. En el caso de SLA se
utiliza el carry flag para almacenar el estado del bit 7 tras la rotación (con lo cual podemos conservar el signo si
sabemos dónde buscarlo). En el caso de SRA, porque el bit 7 además de desplazarse hacia la derecha se
mantiene en su posición (manteniendo el signo).

El hecho de desplazar un número binario una posición a izquierda o derecha tiene una curiosa propiedad: el
número resultante es el original multiplicado o dividido por 2.

Pensemos un poco en nuestro sistema decimal: si tenemos un determinado número y desplazamos todos los
dígitos una posición a la izquierda y añadimos un cero, lo que está sucediendo es que multiplicamos el valor del
número por la base (10):

1 5 -> Desplazar y añadir cero -> 1 5 0


(equivale a multiplicar por la base, es decir, por 10)

Si desplazamos el número a la derecha, por contra, estamos dividiendo por la base:

1 5 2 -> Desplazar y añadir cero -> 0 1 5


(equivale a dividir por la base, es decir, por 10).

En binario ocurre lo mismo: al desplazar un byte a la izquierda estamos multiplicando por 2 (por la base), y al
hacerlo a la derecha estamos dividiendo por 2 (siempre divisiones enteras). Veamos unos ejemplos:

33 = 00100001
<< 1 (<< significa desplazamiento de bits a izquierda)
------------
01000010 = 66 (33*2)
14 = 00001110
>> 1 (>> significa desplazamiento de bits a derecha)
------------
00000111 = 7 (14/2)

Cada vez que realizamos un desplazamiento estamos multiplicando o dividiendo el resultado por dos, de forma
que:

Dirección Desplaz. Núm. desplazamientos Operación (con SLA)


Izquierda («) 1 N = N*2
Izquierda («) 2 N = (N*2)*2 = N*4
Izquierda («) 3 N = ((N*2)*2)*2 = N*8
Izquierda («) 4 N = (…) N*16
Izquierda («) 5 N = (…) N*32
Izquierda («) 6 N = (…) N*64
Izquierda («) 7 N = (…) N*128
Dirección Desplaz. Núm. desplazamientos Operación (con SRA)
Derecha (») 1 N = N/2
Derecha (») 2 N = (N/2)/2 = N/4
Derecha (») 3 N = ((N/2)/2)/2 = N/8
Derecha (») 4 N = (…) N/16
Derecha (») 5 N = (…) N/32
Derecha (») 6 N = (…) N/64
Derecha (») 7 N = (…) N/128

Así, desplazar una vez a la izquierda equivale a multiplicar por 2. Desplazar 2 veces, por 4. Desplazar 3 veces,
por 8, etc. En resumen, desplazar un registro N veces a la izquierda equivale a multiplicarlo por 2 elevado a N.
Lo mismo ocurre con el desplazamiento a derecha y la división.

De este modo, acabamos de descubrir una manera muy sencilla y efectiva (y rápida, muy rápida para el
microprocesador) de efectuar multiplicaciones y divisiones por 2, 4, 8, 16, 32, 64 y 128.

Existe una pequeña variante de SRA llamada SRL que realiza la misma acción que SRA pero que, a diferencia
de esta, lo que hace es introducir un cero a la izquierda (en lugar de copiar el bit de signo). La diferencia es que
SRA es un desplazamiento aritmético (tiene en cuenta el signo) y SRL es un desplazamiento lógico
(simplemente desplaza los bits):

Bit 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0
----------------- -> SRL -> ------------------------
a b c d e f g h h 0 a b c d e f g

Literalmente:

• Rotar los bits a la derecha (»).


• El bit “h” (bit 0) se copia al Carry Flag.
• Por la izquierda entra un cero.
Veamos nuestra ya conocida tabla de afectación de flags:

Flags
Instrucción |S Z H P N C| Significado
-----------------------------------------------------------------
SLA s |* * 0 P 0 *| Shift Left Arithmetic (s=s*2)
SRA s |* * 0 P 0 *| Shift Right Arithmetic (s=s/2)
SRL s |* * 0 P 0 *| Shift Right Logical (s=s>>1)

Cabe destacar que gracias al Carry flag podremos realizar operaciones de desplazamiento que desborden los 8
bits de que dispone un registro. Por ejemplo, supongamos que queremos realizar una multiplicación por 152 por
2. El resultado del desplazamiento sería:

152 = 10011000
<< 1 (*2)
------------
00110000 = 48

¿Por qué nuestro registro acaba con un valor 48? Porque el resultado es mayor que 255, el valor máximo que
podemos representar con 8 bits. Para representar el resultado (304), necesitaríamos un bit extra (9 bits) que nos
daría acceso a representar números en el rango de 0 a 511. Ese bit extra es el carry flag, ya que en realidad:

152 = 10011000
<< 1 (*2)
---------------------
1 00110000 = 304
(C)

Además, gracias a la combinación de instrucciones de rotación y desplazamiento podemos realizar operaciones


con registros de 16 bits. Por ejemplo, supongamos que queremos multiplicar por 2 el valor positivo que
tenemos en el registro DE:

SLA E
RL D
Lo que hacemos con “SLA E” es desplazar el byte más bajo del registro de 16 bits DE hacia la izquierda,
dejando el bit 7 de “E” en el Carry Flag, y después realizar una rotación de “D” hacia la izquierda
introduciendo el carry flag de la operación anterior en el bit 0 de “D”.

Registro DE original:

D E
DE: --------------------- ---------------------
Bit 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 Carry
------------------------------------------------- -----
a b c d e f g h i j k l m n o p ?

Primero con SLA E rotamos la parte baja, metiendo el bit “i” en el Carry Flag:

SLA E:

D E
DE: --------------------- ---------------------
Bit 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 Carry
------------------------------------------------- -----
a b c d e f g h j k l m n o p 0 i

Ahora con RL D rotamos D introduciendo el bit “i” en su bit 0:

RL D:

D E
DE: --------------------- ---------------------
Bit 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 Carry
------------------------------------------------- -----
b c d e f g h i j k l m n o p 0 a

Podemos repetir la operación para multiplicar por 4, 8, 16, etc. dicho par de registros.

De igual forma, podemos realizar rotaciones de 16 bits a la derecha, haciendo el proceso inverso y comenzando
primero con el byte alto:

SRL D
RR E

Las operaciones de este tipo sobre registros de 16 bits son muy importantes para realizar otro tipo de
operaciones de más amplitud como multiplicaciones y divisiones.

Y, para finalizar, veamos cómo el operando destino de 16 bits puede ser un par de bytes de memoria, como en
el siguiente código de ejemplo:

LD IX, 16384
SLA (IX)
RL (IX+01H)

Recordad para este ejemplo que en memoria se almacena primero el byte menos significativo de la palabra de
16 bits, y en la siguiente posición de memoria el más significativo.

Operaciones logicas: AND, OR y XOR


Para acabar con el artículo de hoy vamos a ver 3 operaciones a nivel de bits: AND, OR y XOR. Estas 3
operaciones lógicas se realizan entre 2 bits, dando un tercer bit como resultado:
Bit 1 Bit 2 Resultado AND
1 1 1
1 0 0
0 1 0
0 0 0
Bit 1 Bit 2 Resultado OR
1 1 1
1 0 1
0 1 1
0 0 0
Bit 1 Bit 2 Resultado XOR
1 1 0
1 0 1
0 1 1
0 0 0

Podría decirse que:

• AND es la multiplicación lógica: si cualquiera de los 2 bits es cero, el resultado es cero (0*0=0, 0*1=0,
1*0=0); dicho resultado sólo será uno cuando ambos bits sean 1 (1*1=1).
• OR es la suma lógica: si alguno de los bits es uno, el resultado es uno (1+1=1, 0+1=1, 1+0=1). Sólo
obtendremos un 0 al hacer un OR entre 2 bits cuando ambos son cero.
• XOR es una operación de “O EXCLUSIVO” (exclusive OR) donde el resultado es cero cuando los 2
bits operandos son iguales, y uno cuando los 2 bits operandos son diferentes.

Ejemplos:

10010101 AND 0000111 = 00000101

00000101 OR 1100000 = 11000101

11000011 XOR 10011001 = 01011010

A la hora de realizar estas operaciones lógicas en nuestro Z80 disponemos de 3 instrucciones cuyos nombres
son, como podéis imaginar, AND, OR y XOR. Las tres tienen el mismo formato:

AND ORIGEN
OR ORIGEN
XOR ORIGEN

Donde ORIGEN puede ser cualquier registro de 8 bits, valor inmediato de 8 bits, contenido de la memoria
apuntada por [HL], o contenido de la memoria apuntada por un registro índice más un desplazamiento. El
formato de la instrucción no requiere 2 operandos, ya que el registro destino sólo puede ser A.

La operación CPL, que vimos al principio de este capítulo, también se considera una operación lógica,
equivalente a NOT (0→1 y 1→0).

Pero continuemos con AND, OR y XOR. Veamos algunos ejemplos de instrucciones válidas:

AND B
OR C
OR [HL]
XOR [IX+10]
AND 45
La operación realizada por estas instrucciones sería:

AND ORIGEN -> A = A & ORIGEN


OR ORIGEN -> A = A | ORIGEN
XOR ORIGEN -> A = A ^ ORIGEN

(Donde & = AND, | = OR y ^ = XOR)

Recordemos que AND, OR y XOR son operaciones lógicas de un sólo bit, de modo que al trabajar con registros
(o memoria, o valores inmediatos), en realidad estamos realizando 8 operaciones AND, OR o XOR, entre los
diferentes bits de los operandos. Por ejemplo, al hacer un AND entre los registros A y B con “AND B”
(A=A&B), realizamos las siguientes operaciones:

Registro A: Bit 7 6 5 4 3 2 1 0
----------------------------
A7 A6 A5 A4 A3 A2 A1 A0

Registro B: Bit 7 6 5 4 3 2 1 0
----------------------------
B7 B6 B5 B4 B3 B2 B1 B0

Resultado:

A7 = A7 AND B7
A6 = A6 AND B6
A5 = A5 AND B5
A4 = A4 AND B4
A3 = A3 AND B3
A2 = A2 AND B2
A1 = A1 AND B1
A0 = A0 AND B0

Es decir, se hace una operación AND entre el bit 7 de A y el bit 7 de B, y se almacena el resultado en el bit 7 de
A, y lo mismo para los bits restantes.

¿Para qué pueden servirnos estas 3 operaciones lógicas? Tenga el lector por seguro que a lo largo de nuestros
programas tendremos que usarlas, y mucho, porque son operaciones muy importantes a la hora de manipular
registros. Por ejemplo, supongamos que queremos eliminar los 4 bits más altos de un registro, dejándolos a
cero, y dejar sin alterar el estado de los 4 bits menos significativos.

Podríamos hacer:

RES 7, A
RES 6, A
RES 5, A
RES 4, A

Pero sería mucho más sencillo:

AND %00001111

O sea, realizar la operación:

A = A AND 00001111b

Veamos un ejemplo del porqué:

Sea A = 10101011
valor = 00001111
------------ <-- Operación AND
00001011
Como AND es la operación lógica de la multiplicación, al hacer un AND de A con 00001111, todos aquellos
bits que son cero en 00001111 quedarán a cero en el resultado, y todos aquellos bits que son uno en 00001111
no modificarán el estado de los bits de A.

De la misma forma, por ejemplo, OR nos permite fusionar 2 cuartetos de bits:

Sea A = 10100000
Sea B = 00001111
------------ <-- Operación OR
10101111

La afectación de flags de las 3 instrucciones es idéntica:

Flags
Instrucción |S Z H P N C|
----------------------------------
AND s |* * * P 0 0|
OR s |* * * P 0 0|
XOR s |* * * P 0 0|

Una curiosidad: XOR A es equivalente a “LD A, 0”. Dejamos como ejercicio al lector comprobar por qué
mediante algún ejemplo práctico con diferentes valores de A.
Lenguaje Ensamblador del Z80 (III)
Instrucciones condicionales
Una vez hemos visto la mayoría de instrucciones aritméticas y lógicas, es el momento de utilizarlas como
condicionales para realizar cambios en el flujo lineal de nuestro programa. En esta entrega aprenderemos a usar
etiquetas y saltos mediante instrucciones condicionales (CP, JR + condición, JP + condición, etc.), lo que nos
permitirá implementar en ensamblador las típicas instrucciones IF/THEN/ELSE y los GOTO de BASIC.

Las etiquetas en los programas ASM


Las etiquetas son unas útiles directivas de los ensambladores que nos permitirán hacer referencia a posiciones
concretas de memoria por medio de nombres, en lugar de tener que utilizar valores numéricos.

Veamos un ejemplo de etiqueta en un programa ensamblador:

ORG 50000

NOP
LD B, 10
bucle:
LD A, 20
NOP
(...)
JP bucle
RET

Este es el código binario que genera el listado anterior al ser ensamblado:

00 06 0a 3e 14 00 (...) c3 53 c3 c9

Concretamente:

DIRECCION OPCODE INSTRUCCION


50000 00 NOP
50001 06 0a LD B, 10
50003 3e 14 LD A, 20
50004 00 NOP
… … …
… c3 53 c3 JP $C353 (53000)
…+1 c9 RET

Si mostramos las direcciones de memoria en que se ensambla cada instrucción, veremos:

50000 NOP ; (opcode = 1 byte)


50001 LD B, 10 ; (opcode = 2 bytes)
50003 LD A, 20 ; (opcode = 2 bytes)
50005 NOP ; (opcode = 1 byte)
50005 (más código)
50006 (más código)
.....
50020 JP bucle
50023 RET
¿Dónde está en ese listado de instrucciones nuestra etiqueta “bucle”? Sencillo: no está. No es ninguna
instrucción, sino, para el ensamblador, una referencia a la celdilla de memoria 50003, donde está la instrucción
que sigue a la etiqueta.

En nuestro ejemplo anterior, le decimos al programa ensamblador mediante ORG 50000 que nuestro código,
una vez ensamblado, debe quedar situado a partir de la dirección 50000, con lo cual cuando calcule las
direcciones de las etiquetas deberá hacerlo en relación a esta dirección de origen. Así, en nuestro ejemplo
anterior la instrucción NOP, que se ensambla con el opcode $00, será “pokeada” (por nuestro cargador BASIC)
en la dirección 50000. La instrucción LD B, 10, cuyo opcode tiene 2 bytes, será “pokeada” en 50001 y 50002, y
así con todas las instrucciones del programa.

Cuando el ensamblador se encuentra la etiqueta “bucle:” después del “LD B, 10”, ¿cómo la ensambla?
Supuestamente le corresponde la posición 50003, pero recordemos que esto no es una instrucción, sino una
etiqueta: no tiene ningún significado para el microprocesador, sólo para el programa ensamblador. Por eso,
cuando el ensamblador encuentra la etiqueta “bucle:”, asocia internamente esta etiqueta (el texto “bucle”) a la
dirección 50003, que es la dirección donde hemos puesto la etiqueta.

Si la etiqueta fuera una instrucción, se ensamblaría en la dirección 50003, pero como no lo es, el programa
ensamblador simplemente la agrega a una tabla interna de referencias, donde lo que anota es:

• La etiqueta “bucle” apunta a la dirección 50003

Lo que realmente ensamblará en la dirección 50003 (y en la 50004) es la instrucción siguiente: “LD A, 20”.

Pero, entonces, ¿para qué nos sirve la etiqueta? Sencillo: para poder hacer referencia en cualquier momento a
esa posición de memoria (del programa, en este caso), mediante una cadena fácil de recordar en lugar de
mediante un número. Es más sencillo recordar “bucle” que recordar “50003”, y si nuestro programa es largo y
tenemos muchos saltos, funciones o variables, acabaremos utilizando decenas y centenares de números para
saltos, con lo que el programa sería inmanejable.

El siguiente programa es equivalente al anterior, pero sin usar etiquetas:

ORG 50000

NOP
LD B, 10
LD A, 20
NOP
(...)
JP 50003
RET

En este caso, “JP 50003” no permite distinguir rápidamente a qué instrucción vamos a saltar, mientras que la
etiqueta “bucle” que utilizamos en el anterior ejemplo marcaba de forma indiscutible el destino del salto.

Las etiquetas son muy útiles no sólo por motivos de legibilidad del código. Imaginemos que una vez acabado
nuestro programa sin etiquetas (utilizando sólo direcciones numéricas), con muchos saltos (JP, CALL, JR,
DJNZ…) a diferentes partes del mismo, tenemos que modificarlo para corregir alguna parte del mismo. Al
añadir o quitar instrucciones del programa, estamos variando las posiciones donde se ensambla todo el
programa. Si por ejemplo, añadiéramos un NOP extra al principio del mismo, ya no habría que saltar a 50003
sino a 50004:

ORG 50000

NOP
NOP ; Un NOP extra
LD B, 10
LD A, 20
NOP
(...)
JP 50004 ; La dirección de salto cambia
RET

Para que nuestro programa funcione, tendríamos que cambiar TODAS las direcciones numéricas de salto del
programa, a mano (recalculandolas todas). Las etiquetas evitan esto, ya que es el programa ensamblador quien,
en tiempo de ensamblado, cuando está convirtiendo el programa a código objeto, cambia todas las referencias a
la etiqueta por el valor numérico correcto (por la posición donde aparece la etiqueta). Un “JP bucle” siempre
saltaría a la dirección correcta (la de la posición de la etiqueta) aunque cambiemos la cantidad de instrucciones
del programa.

Como veremos posteriormente, la instrucción JP realiza un salto de ejecución de código a una posición de
memoria dada. Literalmente, un JP XX hace el registro PC = XX, de forma que alteramos el orden de ejecución
del programa. Las etiquetas nos permiten establecer posiciones donde saltar en nuestro programa para
utilizarlas luego fácilmente:

ORG 50000

; Al salir de esta rutina, A=tecla pulsada


RutinaLeerTeclado:
(instrucciones) ; Aquí código
RET

; Saltar (JP) a esta rutina con:


; HL = Sprite a dibujar
; DE = Direccion en pantalla donde dibujar
RutinaDibujarSprite:
(...)
bucle1:
(instrucciones)
bucle2:
(instrucciones)
pintar:
(instrucciones)
JP bucle1
(...)
salir:
RET

(etc...)

Así, podremos especificar múltiples etiquetas para hacer referencia a todas las posiciones que necesitemos
dentro de nuestro programa.

Lo que nos tiene que quedar claro de este apartado son dos conceptos: cuando el ensamblador encuentra la
definición de una etiqueta, guarda en una tabla interna la dirección de ensamblado de la siguiente instrucción a
dicha etiqueta. Después, cada vez que hay una aparición de esa etiqueta en el código, sustituye la etiqueta por
dicha dirección de memoria. Además, podemos utilizar la etiqueta incluso aunque la definamos después (más
adelante) del código, ya que el ensamblador hace varias pasadas en la compilación: no es necesario primero
definir la etiqueta y después hacer referencia a ella, podemos hacerlo también a la inversa.

Es decir, es válido tanto:

etiqueta:
;;; (más código)
JP etiqueta

Como:

JP etiqueta
;;; (más código)
etiqueta:
Como vamos a ver, también podemos utilizar etiquetas para referenciar a bloques de datos, cadenas de texto,
gráficos y en general cualquier tipo de dato en crudo que queramos insertar dentro de nuestro programa.

Definir datos y referenciarlos con etiquetas

Podemos insertar en cualquier posición de la memoria y de nuestro programa datos en formato numérico o de
texto con directivas como DB (DEFB), DW (DEFW) o DS (DEFS), y hacer referencia a ellos mediante
etiquetas.

Por ejemplo:

; Principio del programa


ORG 50000

; Primero vamos a copiar los datos a la videomemoria.


LD HL, datos
LD DE, 16384
LD BC, 10
LDIR

; Ahora vamos a sumar 1 a cada carácter:


LD B, 27
bucle:
LD HL, texto
LD A, (HL)
INC A
LD (HL), A

DJNZ bucle
RET

datos DB 0, $FF, $FF, 0, $FF, 12, 0, 0, 0, 10, 255


texto DB "Esto es una cadena de texto"

; Fin del programa


END

Como puede verse, con DB hemos “insertado” datos directamente dentro de nuestro programa. Estos datos se
cargarán en memoria (pokeados) también como parte del programa, y podremos acceder a ellos posteriormente.
Los datos, en nuestro programa, están situados en la memoria, justo después de las instrucciones ensambladas
(tras el último RET). Podemos verlo si ensamblamos el programa:

$ pasmo --bin db.asm db.bin


$ hexdump -C db.bin
00000000 21 66 c3 11 00 40 01 0a 00 ed b0 06 1b 21 71 c3 |!f...@.......!q.|
00000010 7e 3c 77 10 f8 c9 00 ff ff 00 ff 0c 00 00 00 0a |~<w.............|
00000020 ff 45 73 74 6f 20 65 73 20 75 6e 61 20 63 61 64 |.Esto es una cad|
00000030 65 6e 61 20 64 65 20 74 65 78 74 6f |ena de texto|
0000003c

Si os fijáis, podemos ver el RET (201, o $C9) justo antes del bloque de datos FF, FF, 0, FF. Concretamente, la
etiqueta “datos” en el programa hará referencia (al pokear el programa a partir de 50000), a la posición de
memoria 50022, que contendrá el 00 inicial de nuestros datos DB.

Cuando en el programa hacemos “LD HL, datos”, el ensamblador transforma esa instrucción en realidad en
“LD HL, 50022” (fijaos en el principio del programa: 21 66 C3, que corresponde a LD HL, C366, que es
50022). Gracias a esto podemos manipular los datos (que están en memoria) y leerlos y cambiarlos, utilizando
un “nombre” como referencia a la celdilla de memoria de inicio de los mismos.

Lo mismo ocurre con el texto que se ha definido entre dobles comillas. A partir de la dirección definida por
“texto” se colocan todos los bytes que forman la cadena “Esto es una cadena de texto”. Cada byte en memoria
es una letra de la cadena, en formato ASCII (La “E” es $45, la “s” es $73“, etc.).

Con DB (o DEFB, que es un equivalente por compatibilidad con otros ensambladores) podremos definir:

• Cadenas de texto (todos los mensajes de texto de nuestros programas/juegos).


• Datos numéricos con los que trabajar (bytes, words, caracteres…).
• Tablas precalculadas para optimizar. Por ejemplo, podemos tener un listado como el siguiente:

numeros_primos DB 1, 3, 5, 7, 11, 13, (etc...)

• Variables en memoria para trabajar en nuestro programa:

vidas DB 3
x DB 0
y DB 0
ancho DB 16
alto DB 16
(...)

LD A, (vidas)
(...)
muerte:
DEC A
LD (vidas), A

• Datos gráficos de nuestros sprites (creados con utilidades como SevenuP o ZXPaintBrush, por ejemplo):

Enemigo:
DB 12, 13, 25, 123, 210 (etc...)

Ahora bien, es muy importante tener clara una consideración: los datos que introducimos con DB (o DW, o
cualquier otra directiva de inclusión) no se ensamblan, pero se insertan dentro del código resultante tal cual. Y
el Z80 no puede distinguir un 201 introducido con DB de un opcode 201 (RET), con lo cual tenemos que
asegurarnos de que dicho código no se ejecute, como en el siguiente programa:

ORG 50000

; Cuidado, al situar los datos aquí, cuando saltemos a 50000


; con RANDOMIZE USR 50000, ejecutaremos estos datos como si
; fueran opcodes.
datos DB 00, 201, 100, 12, 255, 11

LD B, A
(más instrucciones)
RET

Lo correcto sería:

ORG 50000

; Ahora el salto a 50000 ejecutará el LD B, A, no los


; datos que habíamos introducido antes.
LD B, A
(más instrucciones)
RET

; Aquí nunca serán ejecutados, el RET está antes.


datos DB 00, 201, 100, 12, 255, 11

Los microprocesadores como el Z80 no saben distinguir entre datos e instrucciones, y es por eso que tenemos
que tener cuidado de no ejecutar datos como si fueran códigos de instrucción del Z80. De hecho, si hacemos un
RANDOMIZE USR XX (siendo XX cualquier valor de la memoria fuera de la ROM), lo más probable es que
ejecutemos datos como si fueran instrucciones y el Spectrum se cuelgue, ya que los datos no son parte de un
programa, y la ejecución resultante de interpretar esos datos no tendría ningún sentido.

Saltos absolutos incondicionales: JP


Ya sabemos definir etiquetas en nuestros programas y referenciarlas. Ahora la pregunta es: ¿para qué sirven
estas etiquetas? Aparte de referencias para usarlas como variables o datos, su principal uso será saltar a ellas
con las instrucciones de salto.

Para empezar vamos a ver 2 instrucciones de salto incondicionales, es decir, cuando lleguemos a una de esas 2
instrucciones, se modificará el registro PC para cambiar la ejecución del programa. De esta forma podremos
realizar bucles, saltos a rutinas o funciones, etc.

Empecemos con JP (abreviatura de JumP):

; Ejemplo de un programa con un bucle infinito


ORG 50000

XOR A ; A = 0
bucle:
INC A ; A = A + 1
LD (16384), A ; Escribir valor de A en (16384)
JP bucle

RET ; Esto nunca se ejecutará

END 50000

¿Qué hace el ejemplo anterior? Ensamblémoslo con “pasmo –tapbas bucle.asm bucle.tap” y carguémoslo en
BASIC.

Nada más entrar en 50000, se ejecuta un “INC A”. Después se hace un “LD (16384), A”, es decir, escribimos
en la celdilla (16384) de la memoria el valor que contiene A. Esta celdilla se corresponde con los primeros 8
píxeles de la pantalla, con lo cual estaremos cambiando el contenido de la misma.

Tras esta escritura, encontramos un “JP bucle”, que lo que hace es cambiar el valor de PC y hacerlo, de nuevo,
PC=50000. El código se volverá a repetir, y de nuevo al llegar a JP volveremos a saltar a la dirección definida
por la etiqueta “bucle”. Es un bucle infinito, realizado gracias a este salto incondicional (podemos reiniciar el
Spectrum para retomar el control). Estaremos repitiendo una y otra vez la misma porción de código, que cambia
el contenido de los 8 primeros píxeles de pantalla poniendo en ellos el valor de A (que varía desde 0 a 255
continuadamente).

Utilizaremos pues JP para cambiar el rumbo del programa y cambiar PC para ejecutar otras porciones de
código (anteriores o posteriores a la posición actual) del mismo. JP realiza pues lo que se conoce como
“SALTO INCONDICIONAL ABSOLUTO”, es decir, saltar a una posición absoluta de memoria (una celdilla
de 0 a 65535), mediante la asignación de dicho valor al registro PC.

Existen 3 maneras de usar JP:

a.- JP NN:

Saltar a la dirección NN. Literalmente: PC = NN

b.- JP (HL)

Saltar a la dirección contenida en el registro HL (ojo, no a la dirección apuntada por el registro HL, sino
directamente a su valor). Literalmente: PC = HL

c.- JP (registro_indice)

Saltar a la dirección contenida en IX o IY. Literalmente: PC = IX o PC = IY

Ninguna de estas instrucciones afecta a los flags:

Flags
Instrucción |S Z H P N C|
----------------------------------
JP NN |- - - - - -|
JP (HL) |- - - - - -|
JP (IX) |- - - - - -|
JP (IY) |- - - - - -|

Recordemos que nuestra CPU almacena primero en memoria los bytes bajos de los números de 16 bits, por lo
que a la hora de ensamblar un salto como “JP 50000” (JP $C350), dicha instrucción será traducida como:

C3 50 C3

que quiere decir:

JP 50 C3 -> JP $C350 -> JP 50000

Como podéis ver, aparte del código de instrucción (C3) almacenamos un valor numérico, absoluto, de la
posición a la que saltar. Es pues una instrucción de 3 bytes.

Literalmente, JP NN se traduce por PC=NN .


Saltos relativos incondicionales: JR
Además de JP, tenemos otra instrucción para realizar saltos incondicionales: JR. JR trabaja exactamente igual
que JP: realiza un salto (cambiando el valor del registro PC), pero lo hace de forma diferente.

JR son las siglas de “Jump Relative”, y es que esta instrucción en lugar de realizar un salto absoluto (a una
posición de memoria 0-65535), lo hace de forma relativa, es decir, a una posición de memoria alrededor de la
posición actual (una vez decodificada la instrucción JR).

El argumento de JR no es pues un valor numérico de 16 bits (0-65535) sino un valor de 8 bits en complemento
a dos que nos permite saltar desde la posición actual (referenciada en el ensamblador como “$”) hasta 127 bytes
hacia adelante y 127 bytes hacia atrás:

Ejemplos de instrucciones JR:

JR $+25 ; Saltar adelante 25 bytes: PC = PC+25


JR $-100 ; Saltar atrás 100 bytes: PC = PC-100

Nosotros, gracias a las etiquetas, podemos olvidarnos de calcular posiciones y hacer referencia de una forma
más sencilla a posiciones en nuestro programa:

Veamos el mismo ejemplo anterior de JP, con JR:

; Ejemplo de un programa con un bucle infinito


ORG 50000

bucle:
INC A
LD (16384), A
JR bucle

RET ; Esto nunca se ejecutará

Como puede verse, el ejemplo es exactamente igual que en el caso anterior. No tenemos que utilizar el carácter
$ (posición actual de ensamblado) porque al hacer uso de etiquetas es el ensamblador quien se encarga de
traducir la etiqueta a un desplazamiento de 8 bits y ensamblarlo.

¿Qué diferencia tiene JP con JR? Pues bien: para empezar en lugar de ocupar 3 bytes (JP + la dirección de 16
bits), ocupa sólo 2 (JR + el desplazamiento de 8 bits) con lo cual se decodifica y ejecuta más rápido.

Además, como la dirección del salto no es absoluta, sino relativa, y de 8 bits en complemento a dos, no
podemos saltar a cualquier punto del programa, sino que sólo podremos saltar a código que esté cerca de la
línea actual: como máximo 127 bytes por encima o por debajo de la posición actual en memoria.

Si tratamos de ensamblar un salto a una etiqueta que está más allá del alcance de un salto relativo, obtendremos
un error como el siguiente:

ERROR: Relative jump out of range

En ese caso, tendremos que cambiar la instrucción “JR etiqueta” por un “JP etiqueta”, de forma que el
ensamblador utilice un salto absoluto que le permita llegar a la posición de memoria que queremos saltar y que
está más alejada de que la capacidad de salto de JR.

¿Cuál es la utilidad o ventaja de los saltos relativos aparte de ocupar 2 bytes en lugar de 3? Pues que los saltos
realizados en rutinas que usen JR y no JP son todos relativos a la posición actual, con lo cual la rutina es
REUBICABLE. Es decir, si cambiamos nuestra rutina de 50000 a 60000 (por ejemplo), funcionará, porque los
saltos son relativos a “$”. En una rutina programada con JP, si la pokeamos en 60000 en lugar de en 50000,
cuando hagamos saltos (JP 50003, por ejemplo), saltaremos a lugares donde no está el código (ahora está en
60003) y el programa no hará lo que esperamos. En resumen: JR permite programar rutinas reubicables y JP no.

(Nota: se dice que una rutina es reubicable cuando estando programada a partir de una determinada dirección de
memoria, podemos copiar la rutina a otra dirección y sus saltos funcionarán correctamente por no ser
absolutos).

¿Recordáis en los cursos y rutinas de Microhobby cuando se decía ”Esta rutina es reubicable“? Pues quería
decir exactamente eso, que podías copiar la rutina en cualquier lugar de la memoria y llamarla, dado que el
autor de la misma había utilizado sólo saltos relativos y no absolutos, por lo que daría igual la posición de
memoria en que la POKEaramos.

En nuestro caso, al usar un programa ensamblador en lugar de simplemente disponer de las rutinas en código
máquina (ya ensambladas) que nos mostraba microhobby, no se nos plantearán esos problemas, dado que
nosotros podemos usar etiquetas y copiar cualquier porción del código a dónde queramos de nuestro programa.
Aquellas rutinas etiquetadas como “reubicables” o “no reubicables” estaban ensambladas manualmente y
utilizaban direcciones de memoria numéricas o saltos absolutos.

Nuestro ensamblador (Pasmo, z80asm, etc) nos permite utilizar etiquetas, que serán reemplazadas por sus
direcciones de memoria durante el proceso de ensamblado. Nosotros podemos modificar las posibles de
nuestras rutinas en el código, y dejar que el ensamblador las “reubique” por nosotros, ya que al ensamblará
cambiará todas las referencias a las etiquetas que usamos.

Esta facilidad de trabajo contrasta con las dificultades que tenían los programadores de la época que no
disponían de ensambladores profesionales. Imaginad la cantidad de usuarios que ensamblaban sus programas a
mano, usando saltos relativos y absolutos (y como veremos, llamadas a subrutinas), que en lugar de sencillos
nombres (JP A_mayor_que_B) utilizaban directamente direcciones en memoria.

E imaginad el trabajo que suponía mantener un listado en papel todas los direcciones de saltos, subrutinas y
variables, referenciados por direcciones de memoria y no por nombres, y tener que cambiar muchos de ellos
cada vez que tenían que arreglar un fallo en una subrutina y cambiaban los destinos de los saltos por crecer el
código que había entre ellos.

Dejando ese tema aparte, la tabla de afectación de flags de JR es la misma que para JP: nula.

Flags
Instrucción |S Z H P N C|
----------------------------------
JR d |- - - - - -|

(Donde "d" es un desplazamiento de 8 bits)

Literalmente, JR d se traduce por PC=PC+d.

Saltos condicionales con los flags


Ya hemos visto la forma de realizar saltos incondicionales. A continuación veremos cómo realizar los saltos (ya
sean absolutos con JP o relativos con JR) de acuerdo a unas determinadas condiciones.

Las instrucciones condicionales disponibles trabajan con el estado de los flags del registro F, y son:

JP NZ, direccion : Salta si el indicador de cero (Z) está a cero (resultado no cero).
JP Z, direccion : Salta si el indicador de cero (Z) está a uno (resultado cero).
JP NC, direccion : Salta si el indicador de carry (C) está a cero.
JP C, direccion : Salta si el indicador de carry (C) está a uno.
JP PO, direccion : Salta si el indicador de paridad/desbordamiento (P/O) está a cero.
JP PE, direccion : Salta si el indicador de paridad/desbordamiento (P/O) está a uno.
JP P, direccion : Salta si el indicador de signo S está a cero (resultado positivo).
JP M, direccion : Salta si el indicador de signo S está a uno (resultado negativo).

JR NZ, relativo : Salta si el indicador de cero (Z) está a cero (resultado no cero).
JR Z, relativo : Salta si el indicador de cero (Z) está a uno (resultado cero).
JR NC, relativo : Salta si el indicador de carry (C) está a cero.
JR C, relativo : Salta si el indicador de carry (C) está a uno.

Donde “dirección” es un valor absoluto 0-65535, y “relativo” es un desplazamiento de 8 bits con signo -127 a
+127.

(Nota: en el listado de instrucciones, positivo o negativo se refiere a considerando el resultado de la operación


anterior en complemento a dos).

Así, supongamos el siguiente programa:

JP Z, destino
LD A, 10
destino:
NOP

(donde “destino” es una etiqueta definida en algún lugar de nuestro programa, aunque también habríamos
podido especificar directamente una dirección como por ejemplo 50004).

Cuando el procesador lee el “JP Z, destino”, lo que hace es lo siguiente:

• Si el flag Z está activado (a uno), saltamos a “destino” (con lo cual no se ejecuta el “LD A, 10”),
ejecutándose el código a partir del “NOP”.
• Si no está activo (a cero) no se realiza ningún salto, con lo que se ejecutaría el “LD A, 10”, y seguiría
después con el “NOP”.

En BASIC, “JP Z, destino” sería algo como:

IF FLAG_ZERO = 1 THEN GOTO destino

Y “JP NZ, destino” sería:

IF FLAG_ZERO = 0 THEN GOTO destino

Con estas instrucciones podemos realizar saltos condicionales en función del estado de los flags o indicadores
del registro F: podemos saltar si el resultado de una operación es cero, si no es cero, si hubo acarreo, si no lo
hubo…

Y el lector se preguntará: ¿y tiene utilidad realizar saltos en función de los flags? Pues la respuesta es: bien
usados, lo tiene para todo tipo de tareas:

; Repetir 100 veces la instruccion NOP


LD A, 100
bucle:
NOP

DEC A ; Decrementamos A.
; Cuando A sea cero, Z se pondrá a 1
JR NZ, bucle ; Mientras Z=0, repetir el bucle
LD A, 200 ; Aquí llegaremos cuando Z sea 1 (A valga 0)
; resto del programa

Es decir: cargamos en A el valor 100, y tras ejecutar la instrucción “NOP”, hacemos un “DEC A” que
decrementa su valor (a 99). Como el resultado de “DEC A” es 99 y no cero, el flag de Z (de cero) se queda a 0,
(recordemos que sólo se pone a uno cuando la última operación resultó ser cero).

Y como el flag Z es cero (NON ZERO = no activado el flag zero) la instrucción “JR NZ, bucle” realiza un salto
a la etiqueta “bucle”. Allí se ejecuta el NOP y de nuevo el “DEC A”, dejando ahora A en 98.

Tras repetirse 100 veces el proceso, llegará un momento en que A valga cero tras el “DEC A”. En ese momento
se activará el flag de ZERO con lo que la instrucción “JR NZ, bucle” no realizará el salto y continuará con el
“LD A, 200”.

Veamos otro ejemplo más gráfico: vamos a implementar en ASM una comparación de igualdad:

IF A=B THEN GOTO iguales ELSE GOTO distintos

En ensamblador:

SUB B ; A = A-B
JR Z, iguales ; Si Z=1 saltar a iguales
JR NZ, distintos ; Si Z=0 saltar a distintos

iguales:
;;; (código)
JR seguir

distintos:
;;; (código)
JR seguir

seguir:

(Nota: se podría haber usado JP en vez de JR)

Para comparar A con B los restamos (A=A-B). Si el resultado de la resta es cero, es porque A era igual a B. Si
no es cero, es que eran distintos. Y utilizando el flag de Zero con JP Z y JP NZ podemos detectar esa diferencia.

Pronto veremos más a fondo otras instrucciones de comparación, pero este ejemplo debe bastar para demostrar
la importancia de los flags y de su uso en instrucciones de salto condicionales. Bien utilizadas podemos alterar
el flujo del programa a voluntad. Es cierto que no es tan inmediato ni cómodo como los >, <, = y <> de BASIC,
pero el resultado es el mismo, y es fácil acostumbrarse a este tipo de comparaciones mediante el estado de los
flags.

Para finalizar, un detalle sobre DEC+JR: La combinación DEC B / JR NZ se puede sustituir (es más eficiente,
y más sencillo) por el comando DJNZ, que literalmente significa “Decrementa B y si no es cero, salta a
<direccion>”.

DJNZ direccion

Equivale a decrementar B y a la dirección indicada en caso de que B no valga cero tras el decremento.

Esta instrucción se usa habitualmente en bucles (usando B como iterador del mismo) y, al igual que JP y JR, no
afecta al estado de los flags:

Flags
Instrucción |S Z H P N C|
----------------------------------
|JP COND, NN |- - - - - -|
|JR COND, d |- - - - - -|
|DJNZ d |- - - - - -|

El argumento de salto de DJNZ es de 1 byte, por lo que para saltos relativos de más de 127 bytes hacia atrás o
hacia adelante (-127 a +127), DJNZ se tiene que sustituir por la siguiente combinación de instrucciones:

DEC B ; Decrementar B, afecta a los flags


JP NZ, direccion ; Salto absoluto: permite cualquier distancia

DJNZ trabaja con el registro B como contador de repeticiones, lo que implica que podemos realizar de 0 a 255
iteraciones. En caso de necesitar realizar hasta 65535 iteraciones tendremos que utilizar un registro de 16 bits
como BC de la siguiente forma:

DEC BC ; Decrementamos BC -> no afecta a los flags


LD A, B ; Cargamos B en A
OR C ; Hacemos OR a de A y C (de B y C)
JR NZ, direccion ; Si (B OR C) no es cero, BC != 0, saltar

Instruccion de comparacion CP

Comparaciones de 8 bits

Para realizar comparaciones (especialmente de igualdad, mayor que y menor que) utilizaremos la instrucción
CP. Su formato es:

CP origen

Donde “origen” puede ser A, F, B, C, D, E, H, L, un valor numérico de 8 bits directo, (HL), (IX+d) o (IY+d).

Al realizar una instrucción “CP origen”, el microprocesador ejecuta la operación “A-origen”, pero no
almacena el resultado en ningún sitio. Lo que sí que hace es alterar el estado de los flags de acuerdo al
resultado de la operación.

Recordemos el ejemplo de comparación anterior donde realizábamos una resta, perdiendo por tanto el valor de
A:

SUB B ; A = A-B
JR Z, iguales ; Si Z=1 saltar a iguales
JR NZ, distintos ; Si Z=0 saltar a distintos

Gracias a CP, podemos hacer la misma operación pero sin perder el valor de A (por la resta):

CP B ; Flags = estado(A-B)
JR Z, iguales ; Si Z=1 saltar a iguales
JR NZ, distintos ; Si Z=0 saltar a distintos

¿Qué nos permite esto? Aprovechando todos los flags del registro F (flag de acarreo, flag de zero, etc), realizar
comparaciones como las siguientes:

; Comparación entre A Y B (=, > y <)


LD B, 5
LD A, 3

CP B ; Flags = estado(A-B)
JP Z, A_Igual_que_B ; IF(a-b)=0 THEN a=b
JP NC, A_Mayor_o_igual_que_B ; IF(a-b)>0 THEN a>=b
JP C, A_Menor_que_B ; IF(a-b)<0 THEN a<b

A_Mayor_que_B:
;;; (instrucciones)
JP fin

A_Menor_que_B:
;;; (instrucciones)
JP fin

A_Igual_que_B:
;;; (instrucciones)

fin:
;;; (continúa el programa)

Vamos a ilustrar la anterior porción de código con un ejemplo que nos permitirá, además, descubrir una forma
muy singular de hacer debugging en vuestras pruebas aprendiendo ensamblador. Vamos a sacar información
por pantalla de forma que podamos ver en qué parte del programa estamos. Este mismo “sistema” podéis
emplearlo (hasta que veamos cómo sacar texto o gráficos concretos por pantalla) para “depurar” vuestros
programas y hacer pruebas.

Consiste en escribir un valor en la memoria, justo en la zona de la pantalla, para así distinguir las partes de
nuestro programa por las que pasamos. Así, escribiremos 255 (8 pixeles activos) en una línea de la parte
superior de la pantalla izquierda (16960), en el centro de la misma (19056), o en la parte inferior derecha
(21470):

; Principio del programa


ORG 50000

; Comparacion entre A Y B (=, > y <)


LD B, 7
LD A, 5

CP B ; Flags = estado(A-B)
JP Z, A_Igual_que_B ; IF(a-b)=0 THEN a=b
JP NC, A_Mayor_que_B ; IF(a-b)>0 THEN a>b
JP C, A_Menor_que_B ; IF(a-b)<0 THEN a<b

A_Mayor_que_B:
LD A, 255
LD (16960), A ; 8 pixels en la parte sup-izq
JP fin

A_Menor_que_B:
LD A, 255
LD (19056), A ; centro de la pantalla
JP fin

A_Igual_que_B:
LD A, 255
LD (21470), A ; parte inferior derecha

fin:
JP fin ; bucle infinito, para que podamos ver
; el resultado de la ejecucion

END 50000

Lo ensamblamos con: pasmo –tapbas compara.asm compara.tap, y lo cargamos en el Spectrum o emulador. La


sentencia END 50000 nos ahorra el teclear “RANDOMIZE USR 50000” ya que pasmo lo introducirá en el
cargador BASIC por nosotros. Jugando con los valores de A y B del listado deberemos ver cómo cambia el
lugar al que saltamos (representado por el lugar de la pantalla en que vemos dibujada nuestra pequeña línea de
8 píxeles).
Finalmente, destacar que nada nos impide el hacer comparaciones multiples o anidadas:

LD B, 5
LD A, 3
LD C, 6

CP B ; IF A==B
JR Z, A_Igual_a_B ; THEN goto A_Igual_a_B
CP C ; IF A==C
JR Z, A_Igual_a_C ; THEN goto A_Igual_a_C
JP Fin ; si no, salimos
A_Igual_a_B:
;;; (...)
JR Fin

A_Igual_a_C:
;;; (...)

Fin:
(resto del programa)

La instrucción CP afecta a todos los flags:

Flags
Instrucción |S Z H P N C|
----------------------------------
|CP s |* * * V 1 *|

El flag “N” se pone a uno porque, aunque se ignore el resultado, la operación efectuada es una resta.

Comparaciones de 16 bits

Aunque la instrucción CP sólo permite comparar un valor de 8 bits con el valor contenido en el registro A,
podemos realizar 2 comparaciones CP para verificar si un valor de 16 bits es menor, igual o mayor que otro.

Si lo que queremos comparar es un registro con otro, podemos hacerlo mediante un CP de su parte alta y su
parte baja. Por ejemplo, para comparar HL con DE:

;;; Comparacion 16 bits de HL y DE


LD A, H
CP D
JR NZ, no_iguales
LD A, L
CP E
JR NZ, no_iguales
iguales:
;;; (...)

no_iguales:
;;; (...)

Para comparar si el valor de un registro es igual a un valor numérico inmediato (introducido directamente en el
código de programa), utilizaríamos el siguiente código:

;;; Comparacion 16 bits de HL y VALOR_NUMERICO (inmediato)


;;; VALOR_NUMERICO puede ser cualquier valor de 0 a 65535
LD A, H
CP VALOR_NUMERICO / 256 ; Parte alta (VALOR/256)
JR NZ, no_iguales
LD A, L
CP VALOR_NUMERICO % 256 ; Parte baja (Resto de VALOR/256)
JR NZ, no_iguales
iguales:
;;; (...)

no_iguales:
;;; (...)

Consideraciones de las condiciones


A la hora de utilizar instrucciones condicionales hay que tener en cuenta que no todas las instrucciones afectan
a los flags. Por ejemplo, la instrucción “DEC BC” no pondrá el flag Z a uno cuando BC sea cero. Si intentamos
montar un bucle mediante DEC BC + JR NZ, nunca saldremos del mismo, ya que DEC BC no afecta al flag de
zero.

LD BC, 1000 ; BC = 1000


bucle:
(...)

DEC BC ; BC = BC-1 (pero NO ALTERA el Carry Flag)


JR NZ, bucle ; Nunca se pondrá a uno el ZF, siempre salta

Para evitar estas situaciones necesitamos conocer la afectación de los flags ante cada instrucción, que podéis
consultar en todas las tablas que os hemos proporcionado.

Podemos realizar algo similar al ejemplo anterior aprovechándonos (de nuevo) de los flags y de los resultados
de las operaciones lógicas (y sus efectos sobre el registro F). Como ya vimos al tratar la instrucción DJNZ,
podemos comprobar si un registro de 16 bits vale 0 realizando un OR entre la parte alta y la parte baja del
mismo. Esto sí afectará a los flags y permitirá realizar el salto condicional:

LD BC, 1000 ; BC = 1000

bucle:
(...)
DEC BC ; Decrementamos BC. No afecta a F.
LD A, B ; A = B
OR C ; A = A OR C
; Esto sí que afecta a los flags.
; Si B==C y ambos son cero, el resultado
; del OR será cero y el ZF se pondrá a 1.
JR NZ, bucle ; ahora sí que funcionará el salto si BC=0

Más detalles sobre los saltos condicionales: esta vez respecto al signo. Las condiciones P y M (JP P, JP M) nos
permitirán realizar saltos según el estado del bit de signo. Resultará especialmente útil después de operaciones
aritméticas.
Los saltos por Paridad/Overflow (JP PO, JP PE) permitirán realizar saltos en función de la paridad cuando la
última operación realizada modifique ese bit de F según la paridad del resultado. La misma condición nos
servirá para desbordamientos si la última operación que afecta a flags realizada modifica este bit con respecto a
dicha condición.

¿Qué quiere decir esto? Que si, por ejemplo, realizamos una suma o resta, JP PO y JP PE responderán en
función de si ha habido un desbordamiento o no y no en función de la paridad, porque las sumas y restas
actualizan dicho flag según los desbordamientos, no según la paridad.

La importancia de la probabilidad de salto


Ante una instrucción condicional, el microprocesador tendrá 2 opciones, según los valores que comparemos y
el tipo de comparación que hagamos (si es cero, si no es cero, si es mayor o menor, etc.). Al final, sólo habrá 2
caminos posibles: saltar a una dirección de destino, o no saltar y continuar en la dirección de memoria siguiente
al salto condicional.

Aunque pueda parecer una pérdida de tiempo, en rutinas críticas es muy interesante el pararse a pensar cuál
puede ser el caso con más probabilidades de ejecución, ya que el tiempo empleado en la opción ”CONDICION
CIERTA, POR LO QUE SE PRODUCE EL SALTO“ es mayor que el empleado en ”CONDICION FALSA, NO
SALTO Y SIGO“.

Por ejemplo, ante un “JP Z, direccion”, el microprocesador tardará 10 ciclos de reloj en ejecutar un salto si la
condición se cumple, y sólo 1 si no se cumple (ya que entonces no tiene que realizar salto alguno).

Supongamos que tenemos una rutina crítica donde la velocidad es importante. Vamos a utilizar, como ejemplo,
la siguiente rutina que devuelve 1 si el parámetro que le pasamos es mayor que 250 y devuelve 0 si es menor:

; Comparar A con 250:


;
; Devuelve A = 0 si A < 250
; A = 1 si A > 250

Valor_Mayor_Que_250:

CP 250 ; Comparamos A con 250


JP C, A_menor_que_250 ; Si es menor, saltamos
LD A, 1 ; si es mayor, devolvemos 1
RET

A_menor_que_250:
LD A, 0
RET

En el ejemplo anterior se produce el salto si A es menor que 250 (10 t-estados) y no se produce si A es mayor
que 250 (1 t-estado).

Supongamos que llamamos a esta rutina con 1000 valores diferentes. En ese caso, existen más probabilidades
de que el valor esté entre 0 y 250 a que esté entre 250 y 255, por lo que sería más óptimo que el salto que hay
dentro de la rutina se haga no cuando A sea menor que 250 sino cuando A sea mayor, de forma que se
produzcan menos saltos.

Lo normal es que, ante datos aleatorios, haya más probabilidad de encontrar datos del segundo caso (0-250) que
del primero (250-255), simplemente por el hecho de que del primer caso hay 250 probabilides de 255, mientras
que del segundo hay 5 probabilidades de 255.
En tal caso, la rutina debería organizarse de forma que la comparación realice el salto cuando encuentre un dato
mayor de 250, dado que ese supuesto se dará menos veces. Si lo hicieramos a la inversa, se saltaría más veces y
la rutina tardaría más en realizar el mismo trabajo.

; Comparar A con 250:


;
; Devuelve A = 0 si A < 250
; A = 1 si A > 250

Valor_Mayor_Que_250:

CP 250 ; Comparamos A con 250


JP NC, A_mayor_que_250 ; Si es mayor, saltamos
LD A, 0 ; si es menor, devolvemos 1
RET

A_mayor_que_250:
LD A, 1
RET

Eso hace que haya más posibilidades de no saltar que de saltar, es decir, de emplear un ciclo de procesador y no
10 para la mayoría de las ejecuciones.

Instrucciones de comparacion repetitivas


Para acabar con las instrucciones de comparación vamos a ver las instrucciones de comparación repetitivas. Son
parecidas a CP, pero trabajan (igual que LDI, LDIR, LDD y LDDR) con HL y BC para realizar las
comparaciones con la memoria: son CPI, CPD, CPIR y CPDR.

Comencemos con CPI (ComPare and Increment):

CPI:

• Al registro A se le resta el byte contenido en la posición de memoria apuntada por HL.


• El resultado de la resta no se almacena en ningún sitio.
• Los flags resultan afectados por la comparación:
o Si A==(HL), se pone a 1 el flag de Zero (si no es igual se pone a 0).
o Si BC==0000, se pone a 0 el flag Parity/Overflow (a 1 en caso contrario).
• Se incrementa HL.
• Se decrementa BC.

Técnicamente (con un pequeño matiz que veremos ahora), CPI equivale a:

CPI = CP [HL]
INC HL
DEC BC

CPD:
Su instrucción “hermana” CPD (ComPare and Decrement) funciona de idéntica forma, pero decrementando
HL:

CPD = CP [HL]
DEC HL
DEC BC
Y el pequeño matiz: así como CP [HL] afecta al indicador C de Carry, CPI y CPD, aunque realizan esa
operación intermedia, no lo afectan.

Las instrucciones CPIR y CPDR son equivalentes a CPI y CPD, pero ejecutándose múltiples veces: hasta que
BC sea cero o bien se encuentre en la posición de memoria apuntada por HL un valor numérico igual al que
contiene el registro A. Literalmente, es una instrucción de búsqueda: buscamos hacia adelante (CPIR) o hacia
atrás (CPDR), desde una posición de memoria inicial (HL), un valor (A), entre dicha posición inicial (HL) y
una posición final (HL+BC o HL-BC para CPIR y CPDR).

CPIR:

• Al registro A se le resta el byte contenido en la posición de memoria apuntada por HL.


• El resultado de la resta no se almacena en ningún sitio.
• Los flags resultan afectados por la comparación:
o Si A==(HL), se pone a 1 el flag de Zero (si no es igual se pone a 0).
o Si BC==0000, se pone a 0 el flag Parity/Overflow (a 1 en caso contrario).
• Se incrementa HL.
• Se decrementa BC.
• Si BC===0 o A=(HL), se finaliza la instrucción. Si no, repetimos el proceso.

CPDR:
CPDR es, como podéis imaginar, el equivalente a CPIR pero decrementando HL, para buscar hacia atrás en la
memoria.

Como ya hemos comentado, muchos flags se ven afectados:

Flags
Instrucción |S Z H P N C|
----------------------------------
|CPI |* * * * 1 -|
|CPD |* * * * 1 -|
|CPIR |* * * * 1 -|
|CPDR |* * * * 1 -|

Un ejemplo de uso de un CP repetitivo es realizar búsquedas de un determinado valor en memoria.


Supongamos que deseamos buscar la primera aparición del valor “123” en la memoria a partir de la dirección
20000, y hasta la dirección 30000, es decir, encontrar la dirección de la primera celdilla de memoria entre
20000 y 30000 que contenga el valor 123.

Podemos hacerlo mediante el siguiente ejemplo con CPIR:

LD HL, 20000 ; Origen de la busqueda


LD BC, 10000 ; Número de bytes a buscar (20000-30000)
LD A, 123 ; Valor a buscar
CPIR

Este código realizará lo siguiente:

HL = 20000
BC = 10000
A = 123

CPIR =
Repetir:
Leer el contenido de (HL)
Si A==(HL) -> Fin_de_CPIR
Si BC==0 -> Fin_de_CPIR
HL = HL+1
BC = BC-1
Fin_de_CPIR:

Con esto, si la celdilla 15000 contiene el valor “123”, la instrucción CPIR del ejemplo anterior acabará su
ejecución, dejando en HL el valor 15001 (tendremos que decrementar HL para obtener la posición exacta).
Dejará además el flag “P/O” (paridad/desbordamiento) y el flag Z a uno. En BC tendremos restado el número
de iteraciones del “bucle” realizadas.

Si no se encuentra ninguna aparición de “123”, BC llegará a valer cero, porque el “bucle CPI” se ejecutará
10000 veces. El flag P/O estará a cero, al igual que Z, indicando que se finalizó el CPIR y no se encontró nada.

Nótese que si en vez de utilizar CPIR hubiéramos utilizado CPDR, podríamos haber buscado hacia atrás, desde
20000 a 10000, decrementando HL. Incluso haciendo HL=0 y usando CPDR, podemos encontrar la última
aparición del valor de A en la memoria (ya que 0000 - 1 = $FFFF, es decir: 0-1=65535 en nuestros 16 bits).

Un ejemplo con CPIR


Veamos un ejemplo práctico con CPIR. El código que veremos a continuación realiza una búsqueda de un
determinado carácter ASCII en una cadena de texto:

; Principio del programa


ORG 50000

LD HL, texto ; Inicio de la busqueda


LD A, 'X' ; Carácter (byte) a buscar
LD BC, 100 ; Número de bytes donde buscar
CPIR ; Realizamos la búsqueda

JP NZ, No_Hay ; Si no encontramos el caracter buscado


; el flag de Z estará a cero.

; Si seguimos por aquí es que se encontró


DEC HL ; Decrementamos HL para apuntar al byte
; encontrado en memoria.

LD BC, texto
SCF
CCF ; Ponemos el carry flag a 0 (SCF+CCF)
SBC HL, BC ; HL = HL - BC
; = (posicion encontrada) - (inicio cadena)
; = posición de 'X' dentro de la cadena.

LD B, H
LD C, L ; BC = HL

RET ; Volvemos a basic con el resultado en BC

No_Hay:
LD BC, $FFFF
RET

texto DB "Esto es una X cadena de texto."

; Fin del programa


END

Lo compilamos con ”pasmo –tapbas buscatxt.asm buscatxt.tap“, lo cargamos en el emulador y tras un RUN
ejecutamos nuestra rutina como “PRINT AT 10,10 ; USR 50000”. En pantalla aparecerá el valor 12:
¿Qué significa este “12”? Es la posición del carácter 'X' dentro de la cadena de texto. La hemos obtenido de la
siguiente forma:

• Hacemos HL = posición de memoria donde empieza la cadena.


• Hacemos A = 'X'.
• Ejecutamos un CPIR
• En HL obtendremos la posición absoluta + 1 donde se encuentra el carácter 'X' encontrado (o FFFFh si
no se encuentra). Exactamente 50041.
• Decrementamos HL para que apunte a la 'X' (50040).
• Realizamos la resta de Posicion('X') - PrincipioCadena para obtener la posición del carácter dentro de la
cadena. De esta forma, si la 'E' de la cadena está en 50028, y la X encontrada en 50040, eso quiere decir
que la 'X' está dentro del array en la posición 50040-50028 = 12.
• Volvemos al BASIC con el resultado en BC. El PRINT USR 50000 imprimirá dicho valor de retorno.

Nótese que el bloque desde “SCF” hasta “LD C, L” tiene como objetivo ser el equivalente a “HL = HL - BC”, y
se tiene que hacer de esta forma porque no existe “SUB HL, BC” ni “LD BC, HL”:

SUB HL, BC = SCF


CCF ; Ponemos el carry flag a 0 (SCF+CCF)
SBC HL, BC ; HL = HL - BC

LD BC, HL = LD B, H
LD C, L ; BC = HL

(Podemos dar las gracias por estas extrañas operaciones a la no ortogonalidad del juego de instrucciones del
Z80).

En resumen
En este capítulo hemos aprendido a utilizar todas las funciones condicionales y de salto de que nos provee el
Z80. En el próximo trataremos la PILA (Stack) del Spectrum, gracias a la cual podremos implementar en
ensamblador el equivalente a GOSUB/RETURN de BASIC, es decir, subrutinas.
Lenguaje Ensamblador del Z80 (IV)
La pila y las llamadas a subrutinas

La pila del Spectrum


Este capítulo se centra en una de las estructuras más importantes del microprocesador Z80: la pila (o Stack en
inglés).

La pila es una porción de memoria donde se pueden almacenar valores de 16 bits, apilados uno a continuación
del siguiente.

Su nombre viene del hecho que los datos se almacenan unos “encima” de los otros, como, por ejemplo, en una
pila de platos.

Cuando almacenamos un nuevo plato en una pila, lo dejamos en la parte superior de la misma, sobre el plato
anterior. Cuando queremos coger un plato, cogemos el plato de arriba, el situado en la parte superior de la pila.

Es lo que se conoce como una estructura de datos “tipo LIFO” (“Last In, First Out”): el último que entró es el
primero que sale. En nuestro ejemplo de los platos, efectivamente cuando retiramos un plato extraemos el que
está arriba del todo, por lo que el primero en salir (First Out) es el último que habíamos dejado (Last In).

En una pila de ordenador (como en nuestra pila de datos) sólo podemos trabajar con el dato que está arriba del
todo de la pila: no podemos extraer uno de los platos intermedios. Sólo podemos apilar un dato nuevo y
desapilar el dato apilado arriba del todo de la pila.

La pila del Spectrum no es de platos sino de valores numéricos de 16 bits. Introducimos valores y sacamos
valores mediante 2 instrucciones concretas: PUSH <valor> y POP <valor>, donde normalmente <valor> será
un registro (metemos en la pila el valor que contiene un registro de 16 bits, o bien leemos de la pila un valor y
lo asignamos a un registro de 16 bits).

Por ejemplo, podemos guardar el valor que contiene un registro en la pila si tenemos que hacer operaciones con
ese registro para así luego recuperarlo tras realizar una determinada tarea:

LD BC, 1000
PUSH BC ; Guardamos el contenido de BC en la pila

LD BC, 2000
(...) ; Operamos con BC

LD HL, 0
ADD HL, BC ; y ya podemos guardar el resultado de la operación
; (recordemos que no existe "LD HL, BC", de modo que
; lo almacenamos como HL = 0+BC

POP BC ; Hemos terminado de trabajar con BC, ahora


; recuperamos el valor que tenia BC (1000).

La instrucción “PUSH BC” introduce en memoria, en lo alto de la pila, el valor contenido en BC (1000), que
recuperamos posteriormente con el “POP BC”.

La realidad es que el Spectrum no tiene una zona de memoria especial o aislada de la RAM dedicada a la pila.
En su lugar se utiliza la misma RAM del Spectrum (0-65535).
El Z80 tiene un registro conocido como SP (Stack Pointer), o puntero de pila, que es un registro de 16 bits que
contiene una dirección de memoria. Esa dirección de memoria es “la cabeza de la pila”: apunta al próximo
lugar donde almacenaremos un dato.

La peculiaridad de la pila del Spectrum es que crece hacia abajo, en lugar de hacia arriba. Veamos un ejemplo
práctico:

Veámoslo con un ejemplo:

Supongamos que SP (puntero de pila) apunta a 65535 (la última posición de la memoria) y que tenemos los
siguientes valores en BC y DE:

LD BC, $00FF
LD DE, $AABB
LD SP, 65535 ; Puntero de pila al final de la memoria

Si ahora hacemos:

PUSH BC ; Apilamos el registro BC

Lo que estaremos haciendo es:

SP = SP - 2 = 65533
(SP) = BC = $00FF

Con lo que el contenido de la memoria sería:

Celdilla Contenido
-----------------------
65534 $FF
SP -> 65533 $00

Si a continuación hacemos otro PUSH:

PUSH DE ; Apilamos el registro DE

Lo que estaremos haciendo es:

SP = SP - 2 = 65531
(SP) = DE = $AABB

Con lo que el contenido de las celdillas de memoria sería:

Celdilla Contenido
-----------------------
65534 $FF
65533 $00
65532 $AA
SP -> 65531 $BB

Si ahora hacemos un POP:

POP DE

Lo que hacemos es:

DE = (SP) = $AABB
SP = SP + 2 = 65533

Y la memoria queda, de nuevo, como:

Celdilla Contenido
-----------------------
65534 $FF
SP -> 65533 $00

Como podemos ver, PUSH apila valores, haciendo decrecer el valor de SP, mientras que POP recupera
valores, haciendo crecer (en 2 bytes, 16 bits) el valor de SP.

PUSH y POP
Así pues, podemos hacer PUSH y POP de los siguientes registros:

• PUSH: AF, BC, DE, HL, IX, IY


• POP : AF, BC, DE, HL, IX, IY

Lo que hacen PUSH y POP, tal y como funciona la pila, es:

PUSH xx :
SP = SP-2
(SP) = xx

POP xx :
xx = (SP)
SP = SP+2

Nótese cómo la pila se decrementa ANTES de poner los datos en ella, y se incrementa DESPUES de sacar
datos de la misma. Esto mantiene siempre SP apuntando al TOS (Top Of Stack).

Flags
Instrucción |S Z H P N C|
----------------------------------
POP xx |- - - - - -|
PUSH xx |- - - - - -|

Nótese que también podemos apilar y desapilar AF. De hecho, es una forma de manipular los bits del registro F
(hacer PUSH BC con un valor determinado, por ejemplo, y hacer un POP AF).
Utilidad de la pila del Spectrum
La pila resulta muy útil para gran cantidad de tareas en programas en ensamblador. Veamos algunos ejemplos:

• Intercambiar valores de registros mediante PUSH y POP. Por ejemplo, para intercambiar el valor de BC
y de DE:

PUSH BC ; Apilamos BC
PUSH DE ; Apilamos DE
POP BC ; Desapilamos BC
; ahora BC=(valor apilado en PUSH DE)
POP DE ; Desapilamos DE
; ahora DE=(valor apilado en PUSH BC)

• Para manipular el registro F: La instrucción POP AF es la principal forma de manipular el registro F


directamente (haciendo PUSH de otro registro y POP de AF).

• Almacenaje de datos mientras ejecutamos porciones de código: Supongamos que tenemos un registro
cuyo valor queremos mantener, pero que tenemos que ejecutar una porción de código que lo modifica.
Gracias a la pila podemos hacer lo siguiente:

PUSH BC ; Guardamos el valor de BC

(código) ; Hacemos operaciones

POP BC ; Recuperamos el valor que teníamos en BC

Esto incluye, por ejemplo, el almacenaje del valor de BC en los bucles cuando necesitamos operador con B, C o
BC:

LD A, 0
LD B, 100
bucle:
PUSH BC ; Guardamos BC
LD B, 1
ADD A, B
POP BC ; Recuperamos BC
DJNZ bucle

En este sentido, también podremos anidar 2 o más bucles que usen el registro B o BC con PUSH y POPs entre
ellos. Supongamos un bucle BASIC del tipo:

FOR I=0 TO 20:


FOR J=0 TO 100:
CODIGO
NEXT J
NEXT I

En ensamblador podríamos hacer:

LD B, 20 ; repetimos bucle externo 20 veces

bucle_externo:
PUSH BC ; Nos guardamos el valor de BC
LD B, 100 ; Iteraciones del bucle interno
bucle_interno:
(... código ...)
DJNZ bucle_interno ; FOR J=0 TO 100
POP BC ; Recuperamos el valor de B

DJNZ bucle_externo ; FOR I=0 TO 20

Hay que tener en cuenta que PUSH y POP implican escribir en memoria (en la dirección apuntada por SP), por
que siempre serán más lentas que guardarse el valor actual de B en otro registro:

LD B, 20 ; repetimos bucle externo 20 veces

bucle_externo:
LD D, B ; Nos guardamos el valor de B

LD B, 100 ; Iteraciones del bucle interno


bucle_interno:
(... código ...) ; En este codigo no podemos usar D
DJNZ bucle_interno ; FOR J=0 TO 100

LD B, D ; Recuperamos el valor de B
DJNZ bucle_externo ; FOR I=0 TO 20

No obstante, en múltiples casos nos quedaremos sin registros libres donde guardar datos, por lo que la pila es
una gran opción. No hay que obsesionarse con no usar la pila porque implique escribir en memoria. A menos
que estemos hablando de una rutina muy muy crítica, que se ejecute muchas veces por cada fotograma de
nuestro juego, PUSH y POP serán las mejores opciones para preservar valores, con un coste de 11 t-estados
para el PUSH y 10 t-estados para el POP de los registros de propósito general y de 15 y 14 t-estados cuando
trabajamos con IX e IY.

• Almacenaje de datos de entrada y salida en subrutinas: Podemos pasar parámetros a nuestras rutinas
apilándolos en el stack, de forma que nada más entrar en la rutina leamos de la pila esos parámetros.

• Extendiendo un poco más el punto anterior, cuando realicemos funciones en ensamblador embebidas
dentro de otros lenguajes (por ejemplo, dentro de programas en C con Z88DK), podremos recoger
dentro de nuestro bloque en ensamblador los parámetros pasados con llamadas de funciones C.

• Como veremos en el próximo apartado, la pila es la clave de las subrutinas (CALL/RET) en el Spectrum
(equivalente al GOSUB/RETURN de BASIC).

Recordad también que tenéis instrucciones de intercambio (EX) que permiten manipular el contenido de la pila.
Hablamos de:

EX (SP), HL
EX (SP), IX
EX (SP), IY

Los peligros de la pila


Pero como todo arma, las pilas también tienen un doble filo. Mal utilizada puede dar lugar a enormes desastres
en nuestros programas.

Veamos algunos de los más habituales:


• Dado que la pila decrece en memoria, tenemos que tener cuidado con el valor de SP y la posición más
alta de memoria donde hayamos almacenado datos o rutinas. Si ponemos un gráfico o una rutina cerca
del valor inicial de SP, y realizamos muchas operaciones de PUSH, podemos sobreescribir nuestros
datos con los valores que estamos apilando.

• Hacer más PUSH que POP o más POP que PUSH. Recordemos que la pila tiene que ser consistente. Si
hacemos un push, debemos recordar hacer el pop correspondiente (a menos que haya una razón para
ello), y viceversa. Como veremos a continuación, la pila es utilizada tanto para pasar parámetros a
funciones como para volver de ellas, si introducimos un valor en ella con PUSH dentro de una función y
no lo sacamos antes de hacer el RET, nuestro programa continuará su ejecución en algún lugar de la
memoria que no era al que debía volver. Es más, si nuestro programa debe volver a BASIC
correctamente tras su ejecución, entonces es obligatorio que hagamos tantos PUSH como POP para que
el punto final de retorno del programa al BASIC esté en la siguiente posición de la pila cuando nuestro
programa acabe.

• Ampliando la regla anterior, hay que tener cuidado con los bucles a la hora de hacer PUSH y POP.

• Finalmente, no hay que asumir que SP tiene un valor correcto para nosotros. Tal vez tenemos planeado
usar una zona de la memoria para guardar datos o subrutinas y el uso de PUSH y POP pueda
sobreescribir estos datos. Si sabemos dónde no puede hacer daño SP y sus escrituras en memoria, basta
con inicializar la pila al principio de nuestro programa a una zona de memoria libre (por ejemplo, “LD
SP, 49999”, o cualquier otra dirección que sepamos que no vamos a usar). Esto no es obligatorio y
muchas veces el valor por defecto de SP será válido, siempre que no usemos zonas de la memoria que
creemos libres como “almacenes temporales”. Si usamos “variables” creadas en tiempo de ensamblado
(definidas como DB o DW en el ensamblador) no deberíamos tener problemas, al menos con programas
pequeños.

Veamos algunos ejemplos de “errores” con la pila. Empecemos con el típico PUSH del cual se nos olvida hacer
POP:

; Este programa se colgará (probablemente, depende de BC)


; pero en cualquier caso, no seguirá su ejecución normal.
PUSH BC
PUSH DE

(código)

POP DE
RET ; En lugar de volver a la dirección de memoria
; a la que teníamos que volver, volveremos a
; la dirección apuntada por el valor de BC, que
; no hemos recogido de la pila.

También hay que tener cuidado con los bucles:

bucle:
PUSH BC ; Nos queremos guardar BC
(código que usa B)

JR flag, bucle
POP BC

En ese código hacemos múltiples PUSHes pero un sólo POP. Probablemente, en realidad, queremos hacer lo
siguiente:

bucle:
PUSH BC ; Nos queremos guardar BC
(código)
POP BC
JR flag, bucle

O bien:

PUSH BC ; Nos queremos guardar BC


bucle:
(código)

JR flag, bucle
POP BC

Y una curiosidad al respecto de la pila y la sentencia CLEAR de BASIC: en el fondo, lo que realiza la función
CLEAR es cambiar el valor de la variable del sistema RAMTOP, lo que implica cambiar el valor de SP. Así,
con CLEAR XXXX, ponemos la pila colgando de la dirección de memoria XXXX, asegurándonos de que
BASIC no pueda hacer crecer la pila de forma que sobreescriba código máquina que hayamos cargado nosotros
en memoria. Si, por ejemplo, vamos a cargar todo nuestro código a partir de la dirección 50000, en nuestro
cargador BASIC haremos un CLEAR 49999, de forma que BASIC no podrá tocar ninguna dirección de
memoria por encima de este valor.

La ubicación de la pila en el Spectrum


Al cambiar la ubicación de la pila en el Spectrum mediante la modificación del registro SP debemos tener una
consideración especial: no debemos ubicar el stack en la zona de memoria de 16KB entre 16384 y 32767.

Comenzando en la dirección de memoria 16384 está el área de videomemoria del Spectrum, donde se almacena
en forma de datos numéricos el estado de los píxeles y colores de la pantalla. La ULA utiliza esta información
para redibujar en la pantalla el contenido de esta videomemoria, a razón de 50 veces por segundo.

El haz de electrones del monitor se mueve de forma constante recorriendo la pantalla y la ULA, sincronizada
con él, lee regularmente el contenido de la videomemoria para construir la señal de vídeo que debe representar
dicho haz.

Cuando la ULA necesita leer un dato de la videoram bloquea temporalmente el acceso del Z80 al chip de
memoria que contiene los datos de vídeo, ya que el dibujado de la pantalla tiene prioridad (el haz de electrones
del monitor no se puede detener y se le debe proporcionar la información de imagen conforme la necesita).
Cuando tanto la ULA como nuestro programa necesitan acceder a la memoria simultaneamente, es la ULA
quien accede y el Z80 quien espera a que la ULA acabe. Esto es lo que se conoce como “contented memory” o
“memoria contenida”.

Esto implica que las lecturas y escrituras de nuestro programa (ejecutado por el Z80) en la página de memoria
de 16KB que va desde 16384 a 32767 se ven interrumpidas de forma constante por la ULA (aunque de forma
transparente para nuestro programa), por lo que ubicar la pila en esta zona puede suponer una gran ralentización
con respecto a ubicarla más arriba de la dirección 32768. Recuerda que cada operación PUSH y POP es,
físicamente, un acceso de escritura y lectura a memoria, y las rutinas de nuestro programa harán, seguro, gran
uso de ellas, además de los CALLs y RETs (PUSH PC + JP DIR / POP PC).

Por ahora, y hasta que veamos más información respecto a la ULA y la memoria contenida, basta con saber que
debemos evitar el colocar la pila en el bloque de 16KB que comienza en la dirección 16384.
Subrutinas: CALL y RET
Ya de por sí el lenguaje ensamblador es un lenguaje de listados “largos” y enrevesados, y donde teníamos 10
líneas en BASIC podemos tener 100 ó 1000 en ensamblador.

Lo normal para hacer el programa más legible es utilizar bloques de código que hagan unas funciones
concretas y a los cuales podamos llamar a lo largo de nuestro programa. Esos bloques de código son las
funciones o subrutinas.

Las subrutinas son bloques de código máquina a las cuales saltamos, hacen su tarea asignada, y devuelven el
control al punto en que fueron llamadas. A veces, esperan recibir los registros con una serie de valores y
devuelven registros con los valores resultantes.

Para saltar a subrutinas utilizamos la instrucción CALL, y estas deben de terminar en un RET.

El lector podría preguntar, ¿por qué no utilizar las instrucciones de salto JP y JR vistas hasta ahora? La
respuesta es: debido a la necesidad de una dirección de retorno.

Veamos un ejemplo ilustrativo de la importancia de CALL/RET realizando una subrutina que se utilice JP para
su llamada. Supongamos la siguiente “subrutina” sin RET:

; SUMA_A_10
;
; SUMA 10 a A y devuelve el resultado en B
;
; Nota: Modifica el valor de A

SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A

Nuestra función/subrutina de ejemplo espera obtener en A un valor, y devuelve el resultado de su ejecución en


B. Antes de llamar a esta rutina, nosotros deberemos poner en A el valor sobre el que actuar, y posteriormente
interpretar el resultado (sabiendo que lo tenemos en B).

Pero, ¿cómo llamamos a las subrutinas y volvemos de ellas? Comencemos probando con “JP”:

LD A, 35
JP SUMA_A_10
volver1:

(...)

; SUMA_A_10
; SUMA 10 a A y devuelve el resultado en B
; Nota: Modifica el valor de A
SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
JP volver1 ; Volvemos de la subrutina

En este caso, cargaríamos A con el valor 35, saltaríamos a la subrutina, sumaríamos 10 a A (pasando a valer
45), haríamos B = 45, y volveríamos al lugar posterior al punto de llamada.

Pero … ¿qué pasaría si quisieramos volver a llamar a la subrutina desde otro punto de nuestro programa? Que
sería inviable, porque nuestra subrutina acaba con un “JP volver1” que no devolvería la ejecución al punto
desde donde la hemos llamado, sino a “volver1”.

LD A, 35
JP SUMA_A_10
volver1:
LD A, 50
JP SUMA_A_10
; Nunca llegariamos a volver aqui
(...)
SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
JP volver1 ; Volvemos de la subrutina

Para evitar ese enorme problema es para lo que se usa CALL y RET.

Uso de CALL y RET


CALL es, en esencia, similar a JP, salvo porque antes de realizar el salto, introduce en la pila (PUSH) el valor
del registro PC (Program Counter, o contador de programa), el cual (una vez leída y decodificada la instrucción
CALL) apunta a la instrucción que sigue al CALL.

¿Y para qué sirve eso? Para que lo aprovechemos dentro de nuestra subrutina con RET. RET lee de la pila la
dirección que introdujo CALL y salta a ella. Así, cuando acaba nuestra función, el RET devuelve la ejecución a
la instrucción siguiente al CALL que hizo la llamada.

Son, por tanto, el equivalente ensamblador de GO SUB y RETURN en BASIC (o más bien se debería decir que
GO SUB y RETURN son la implantación en BASIC de estas instrucciones del microprocesador).

CALL NN equivale a:
PUSH PC
JP NN

RET equivale a:
POP PC

Veamos la aplicación de CALL y RET con nuestro ejemplo anterior:

LD A, 35
CALL SUMA_A_10

LD A, 50
CALL SUMA_A_10

LD C, B

(...)

SUMA_A_10:
ADD A, 10 ; A = A + 10
LD B, A ; B = A
RET ; Volvemos de la subrutina

En esta ocasión, cuando ejecutamos el primer CALL, se introduce en la pila el valor de PC, que se corresponde
exáctamente con la dirección de memoria donde estaría ensamblada la siguiente instrucción (LD A, 50). El
CALL cambia el valor de PC al de la dirección de “SUMA_A_10”, y se continúa la ejecución dentro de la
subrutina.

Al acabar la subrutina encontramos el RET, quien extrae de la pila el valor de PC anteriormente introducido,
con lo que en el siguiente ciclo de instrucción del microprocesador, el Z80 leerá, decodificará y ejecutará la
instrucción “LD A, 50”, siguiendo el flujo del programa linealmente desde ahí. Con la segunda llamada a
CALL ocurriría lo mismo, pero esta vez lo que se introduce en la pila es la dirección de memoria en la que está
ensamblada la instrucción “LD C, B”. Esto asegura el retorno de nuestra subrutina al punto adecuado.
Al hablar de la pila os contamos lo importante que era mantener la misma cantidad de PUSH que de POPs en
nuestro código. Ahora entenderéis por qué: si dentro de una subrutina hacéis un PUSH que no elimináis
después con un POP, cuando lleguéis al RET éste obtendrá de la pila un valor que no será el introducido por
CALL, y saltará allí. Por ejemplo:

CALL SUMA_A_10
LD C, B ; Esta dirección se introduce en la pila con CALL

SUMA_A_10:
LD DE, $0000
PUSH DE
ADD A, 10
LD B, a
RET ; RET no sacará de la pila lo introducido por CALL
; sino "0000", el valor que hemos pulsado nosotros.

Aquí RET sacará de la pila 0000h, en lugar de la dirección que introdujo CALL, y saltará al inicio del a ROM,
produciendo un bonito reset.

Ni CALL ni RET afectan a la tabla de flags del registro F.

Flags
Instrucción |S Z H P N C|
----------------------------------
CALL NN |- - - - - -|
RET |- - - - - -|

Saltos y retornos condicionales


Una de las peculiaridades de CALL y RET es que tienen instrucciones condicionales con respecto al estado de
los flags, igual que “JP cc” o “JR cc”, de forma que podemos condicionar el SALTO (CALL) o el retorno
(RET) al estado de un determinado flag.

Para eso, utilizamos las siguientes instrucciones:

• CALL flag, NN : Salta sólo si FLAG está activo.


• RET flag : Vuelve sólo si FLAG está activo.

Por ejemplo, supongamos que una de nuestras subrutinas tiene que comprobar que uno de los parámetros que le
pasamos, BC, no sea 0.

; Copia_Pantalla:
;
; Entrada:
; HL = direccion origen
; DE = direccion destino
; BC = bytes a copiar
;
Copia_Pantalla:

; lo primero, comprobamos que BC no sea cero:


LD A, B
OR C ; Hacemos un OR de B sobre C
; Si BC es cero, activará el flag Z
RET Z ; Si BC es cero, volvemos sin hacer nada
(más código)
; Aquí seguiremos si BC no es cero, el
; RET no se habrá ejecutado.

Del mismo modo, el uso de CALL condicionado al estado de flags (CALL Z, CALL NZ, CALL M, CALL P,
etc) nos permitirá llamar o no a funciones según el estado de un flag.

Al igual que CALL y RET, sus versiones condicionales no afectan al estado de los flags.

Flags
Instrucción |S Z H P N C| Pseudocodigo
-----------------------------------------------------------
CALL cc, NN |- - - - - -| IF cc CALL NN
RET cc |- - - - - -| IF cc RET

Pasando parametros a rutinas


Ahora que ya sabemos crear rutinas y utilizarlas, vamos a ver los 3 métodos que hay para pasar y devolver
parámetros a las funciones.

Método 1: Uso de registros

Este método consiste en modificar unos registros concretos antes de hacer el CALL a nuestra subrutina,
sabiendo que dicha subrutina espera esos registros con los valores sobre los que actuar. Asímismo, nuestra
rutina puede modificar alguno de los registros con el objetivo de devolvernos un valor.

Por ejemplo:

;--------------------------------------------------------------
; MULTIPLI: Multiplica DE*BC
; Entrada: DE: Multiplicando,
; BC: Multiplicador
; Salida: HL: Resultado.
;--------------------------------------------------------------
MULTIPLICA:
LD HL, 0
MULTI01:
ADD HL, DE
DEC BC
LD A, B
OR C
JR NZ, MULTI01
RET

Antes de hacer la llamada a MULTIPLICA, tendremos que cargar en DE y en BC los valores que queremos
multiplicar, de modo que si estos valores están en otros registros o en memoria, tendremos que moverlos a DE
y BC.

Además, sabemos que la salida nos será devuelta en HL, con lo que si dicho registro contiene algún valor
importante, deberemos preservarlo previamente.

Con este tipo de funciones resulta importantísimo realizarse cabeceras de comentarios explicativos, que
indiquen:
a.- Qué función realiza la subrutina.
b.- Qué registros espera como entrada.
c.- Qué registros devuelve como salida.
d.- Qué registros modifica además de los de entrada y salida.

Con este tipo de paso de parámetros tenemos el mayor ahorro y la mayor velocidad: no se accede a la pila y no
se accede a la memoria, pero por contra tenemos que tenerlo todo controlado. Tendremos que saber en cada
momento qué parámetros de entrada y de salida utiliza (de ahí la importancia del comentario explicativo, al que
acudiremos más de una vez cuando no recordemos en qué registros teníamos que pasarle los datos de entrada),
y asegurarnos de que ninguno de los registros “extra” que modifica están en uso antes de llamar a la función,
puesto que se verán alterados.

Si no queremos que la función modifique muchos registros además de los de entrada y salida, siempre podemos
poner una serie de PUSH y POP en su inicio y final, al estilo:

MiFuncion:
PUSH BC
PUSH DE ; Nos guardamos sus valores

(...)

POP DE
POP BC ; Recuperamos sus valores
RET

En funciones que no sean críticas en velocidad, es una buena opción porque no tendremos que preocuparnos
por el estado de nuestros registros durante la ejecución de la subrutina: al volver de ella tendrán sus valores
originales (excepto aquellos de entrada y salida que consideremos necesarios).

No nos olvidemos de que en algunos casos podemos usar el juego de registros alternativos (EX AF, AF', EXX)
para evitar algún PUSH o POP.

Método 2: Uso de localidades de memoria

Aunque no es una opción especialmente rápida, el uso de variables o posiciones de memoria para pasar y
recoger parámetros de funciones es bastante efectivo y sencillo. Nos ahorra el uso de muchos registros, y hace
que podamos usar dentro de las funciones prácticamente todos los registros. Se hace especialmente útil usando
el juego de registros alternativos.

Por ejemplo:

LD A, 10
LD (x), A
LD A, 20
LD (y), A
LD BC, 40
LD (size), BC ; Parametros de entrada a la funcion
CALL MiFuncion
(...)

MiFuncion:
EXX ; Preservamos TODOS los registros

LD A, (x)
LD B, A
LD A, (y)
LD BC, (size) ; Leemos los parametros

(Codigo)
LD (salida), a ; Devolvemos un valor
EXX
RET

x DB 0
y DB 0
size DW 0
salida DB 0

Este es un ejemplo exagerado donde todos los parámetros se pasan en variables, pero lo normal es usar un
método mixto entre este y el anterior, pasando cosas en registros excepto si nos quedamos sin ellos (por que una
función requiere muchos parámetros, por ejemplo), de forma que algunas cosas las pasamos con variables de
memoria.

La ventaja del paso de parámetros por memoria es que podemos utilizar las rutinas desde BASIC, POKEando
los parámetros en memoria y llamando a la rutina con RANDOMIZE USR DIRECCION.

Método 3: Uso de la pila (método C)

El tercer método es el sistema que utilizan los lenguajes de alto nivel para pasar parámetros a las funciones: el
apilamiento de los mismos. Este sistema no se suele utilizar en ensamblador, pero vamos a comentarlo de forma
que os permita integrar funciones en ASM dentro de programas escritos en C, como los compilables con el
ensamblador Z88DK.

En C (y en otros lenguajes de programación) los parámetros se insertan en la pila en el orden en que son leídos.
La subrutina debe utilizar el registro SP (una copia) para acceder a los valores apilados en orden inverso. Estos
valores son siempre de 16 bits aunque las variables pasadas sean de 8 bits (en este caso ignoraremos el byte que
no contiene datos, el segundo).

Veamos unos ejemplos:

//-----------------------------------------------------------------
// Sea parte de nuestro programa en C:

int jugador_x, jugador_y;

jugador_x = 10;
jugador_y = 200;
Funcion( jugador_x, jugador_y );
(...)

//-----------------------------------------------------------------
int Funcion( int x, int y )
{

#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (valor de Y)

LD C, (HL)
INC HL
LD B, (HL)
INC HL ; Ahora BC = y

LD E, (HL)
INC HL
LD D, (HL)
INC HL ; Ahora, DE = x
;;; (ahora hacemos lo que queramos en asm)

#endasm
}

No tenemos que preocuparnos por hacer PUSH y POP de los registros para preservar su valor dado que Z88DK
lo hace automáticamente antes y después de cada #asm y #endasm.

El problema es que conforme crece el número de parámetros apilados, es posible que tengamos que hacer
malabarismos para almacenarlos, dado que no podemos usar HL (es nuestro puntero a la pila en las lecturas).
Veamos el siguiente ejemplo con 3 parámetros, donde tenemos que usar PUSH para guardar el valor de DE y
EX DE, HL para acabar asociando el valor final a HL:

//-----------------------------------------------------------------
int Funcion( int x, int y, int z )
{

#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (z)
LD C, (HL)
INC HL
LD B, (HL)
INC HL ; Ahora BC = z

LD E, (HL)
INC HL
LD D, (HL)
INC HL ; Ahora, DE = y

PUSH DE ; Guardamos DE

LD E, (HL)
INC HL
LD D, (HL)
INC HL ; Usamos DE para leer el valor de x

EX DE, HL ; Ahora cambiamos x a HL


POP DE ; Y recuperamos el valor de y en DE

;;; (ahora hacemos lo que queramos en asm)

#endasm
}

La manera de leer bytes (variables de tipo char) pulsados en C es de la misma forma que leemos una palabra de
16 bits, pero ignorando la parte alta. En realidad, como la pila es de 16 bits, el compilador convierte el dato de 8
bits en uno de 16 (rellenando con ceros) y mete en la pila este valor:

//-----------------------------------------------------------------
int Funcion( char x, char y )
{

#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (z)

LD A, (HL) ; Aquí tenemos nuestro dato de 8 bits (y)


LD B, A
INC HL
INC HL ; La parte alta del byte no nos interesa

LD A, (HL) ; Aquí tenemos nuestro dato de 8 bits (x)


LD C, A
INC HL
INC HL ; La parte alta del byte no nos interesa

;;; (ahora hacemos lo que queramos en asm)

#endasm
}

En ocasiones, es posible que incluso tengamos que utilizar variables auxiliares de memoria para guardar datos:

//-----------------------------------------------------------------
int Funcion( int x, int y, char z )
{

#asm
LD HL,2
ADD HL,SP ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (z)

LD C, (HL)
INC HL
LD B, (HL)
INC HL ; Ahora BC = y
LD (valor_y), BC ; nos lo guardamos, BC libre de nuevo

LD C, (HL)
INC HL
LD B, (HL)
INC HL
LD (valor_x), BC ; Nos lo guardamos, BC libre de nuevo

LD A, (HL)
LD (valor_z), A ; Nos guardamos el byte
INC HL
INC HL ; La parte alta del byte no nos interesa

;;; (ahora hacemos lo que queramos en asm)

RET

valor_x DW 0
valor_y DW 0
valor_z DB 0

#endasm
}

Por contra, para devolver valores no se utiliza la pila (dado que no podemos tocarla), sino que se utiliza un
determinado registro. En el caso de Z88DK, se utiliza el registro HL. Si la función es de tipo INT o CHAR en
cuanto a devolución, el valor que dejemos en HL será el que se asignará en una llamada de este tipo:

valor = MiFuncion_ASM( x, y, z);

Hemos considerado importante explicar este tipo de paso de parámetros y devolución de valores porque nos
permite integrar nuestro código ASM en programas en C.

Integracion de ASM en Z88DK


Para aprovechar esta introducción de “uso de ASM en Z88DK”, veamos el código de alguna función en C que
use ASM internamente y que muestre, entre otras cosas, la lectura de parámetros de la pila, el acceso a variables
del código C, el uso de etiquetas, o la devolución de valores.
//
// Devuelve la direccion de memoria del atributo de un caracter
// de pantalla, de coordenadas (x,y). Usando la dirección que
// devuelve esta función (en HL, devuelto en la llamada), podemos
// leer o cambiar los atributos de dicho carácter.
//
// Llamada: valor = Get_LOWRES_Attrib_Address( 1, 3 );
//
int Get_LOWRES_Attrib_Address( char x, char y )
{
#asm

LD HL, 2
ADD HL, SP ; Leemos x e y de la pila
LD D, (HL) ; d = y
INC HL ; Primero "y" y luego "x".
INC HL ; Como son "char", ignoramos parte alta.
LD E, (HL) ; e = x

LD H, 0
LD L, D
ADD HL, HL ; HL = HL*2
ADD HL, HL ; HL = HL*4
ADD HL, HL ; HL = HL*8
ADD HL, HL ; HL = HL*16
ADD HL, HL ; HL = HL*32
LD D, 0
ADD HL, DE ; Ahora HL = (32*y)+x
LD BC, 16384+6144 ; Ahora BC = offset attrib (0,0)
ADD HL, BC ; Sumamos y devolvemos en HL

#endasm
}

//
// Set Border
// Ejemplo de modificación del borde, muestra cómo leer variables
// globales de C en ASM, añadiendo "_" delante.
//

unsigned char bordeactual;

void BORDER( unsigned char value )


{
#asm
LD HL, 2
ADD HL, SP
LD A, (HL)
LD C, 254
OUT (C), A
LD (_bordeactual), A

RLCA ; Adaptamos el borde para guardarlo


RLCA ; en la variable del sistema BORDCR
RLCA ; Color borde -> a zona de PAPER
LD HL, 23624 ; lo almacenamos en BORDCR para que
LD (HL), A ; lo usen las rutinas de la ROM.
#endasm
}

//
// Realización de un fundido de la pantalla hacia negro
// Con esta función se muestra el uso de etiquetas. Nótese
// como en lugar de escribirse como ":", se escriben sin
// ellos y con un punto "." delante.
//
void FadeScreen( void )
{
#asm
LD B, 9 ; Repetiremos el bucle 9 veces

.fadescreen_loop1
LD HL, 16384+6144 ; Apuntamos HL a la zona de atributos
LD DE, 768 ; Iteraciones bucle

HALT
HALT ; Ralentizamos el efecto

.fadescreen_loop2
LD A, (HL) ; Cogemos el atributo
AND 127 ; Eliminamos el bit de flash
LD C, A

AND 7 ; Extraemos la tinta (AND 00000111b)


JR Z, fadescreen_ink_zero ; Si la tinta ya es cero, no hacemos nada

DEC A ; Si no es cero, decrementamos su valor

.fadescreen_ink_zero

EX AF, AF ; Nos hacemos una copia de la tinta en A


LD A, C ; Recuperamos el atributo
SRA A
SRA A ; Pasamos los bits de paper a 0-2
SRA A ; con 3 instrucciones de desplazamiento >>

AND 7 ; Eliminamos el resto de bits


JR Z, fadescreen_paper_zero ; Si ya es cero, no lo decrementamos

DEC A ; Lo decrementamos

.fadescreen_paper_zero
SLA A
SLA A ; Volvemos a color paper en bits 3-5
SLA A ; Con 3 instrucciones de desplazamiento <<

LD C, A ; Guardamos el papel decrementado en A


EX AF, AF ; Recuperamos A
OR C ; A = A OR C = PAPEL OR TINTA

LD (HL), A ; Almacenamos el atributo modificado


INC HL ; Avanzamos puntero de memoria

DEC DE
LD A, D
OR E
JP NZ, fadescreen_loop2 ; Hasta que DE == 0

DJNZ fadescreen_loop1 ; Repeticion 9 veces

#endasm
}

Si tenéis curiosidad por ver el funcionamiento de esta rutina de Fade (fundido), podéis verla integramente en
ASM en el fichero fade.asm. Un detalle a tener en cuenta, en Z88DK se soporta “EX AF, AF”, mientras que
pasmo requiere poner la comilla del shadow-register: “EX AF, AF'”.
En la anterior captura podéis ver el aspecto de uno de los pasos del fundido.

La importancia de usar subrutinas


Usar subrutinas es mucho más importante de lo que parece a simple vista: nos permite organizar el programa
en unidades o módulos funcionales que cumplen una serie de funciones específicas, lo que hace mucha más
sencilla su depuración y optimización.

Si en el menú de nuestro juego estamos dibujando una serie de sprites móviles, y también lo hacemos a lo largo
del juego, resulta absurdo “construir” 2 bloques de código, uno para mover los sprites del menú y otro para los
del juego. Haciendo esto, si encontramos un error en una de las 2 rutinas, o realizamos una mejora, deberemos
corregirlo en ambas.

Por contra, si creamos una subrutina, digamos, DrawSprite, que podamos llamar con los parámetros adecuados
en ambos puntos del programa, cualquier cambio, mejora o corrección que realicemos en DrawSprite afectará a
todas las llamadas que le hagamos. También reducimos así el tamaño de nuestro programa (y con él el tiempo
de carga del mismo), las posibilidades de fallo, y la longitud del listado (haciéndolo más legible y manejable).

Aunque no sea el objetivo de esta serie de artículos, antes de sentarse a teclear, un buen programador debería
coger un par de folios de papel y hacer un pequeño análisis de lo que pretende crear. Este proceso, la fase de
diseño, define qué debe de hacer el programa y, sobre todo, una división lógica de cuáles son las principales
partes del mismo. Un sencillo esquema en papel, un diagrama de flujo, identificar las diferentes partes del
programa, etc.

El proceso empieza con un esbozo muy general del programa, que será coincidente con la gran mayoría de los
juegos: inicialización de variables, menú (que te puede llevar bien a las opciones o bien al juego en sí), y dentro
del juego, lectura de teclado/joystick, trazado de la pantalla, lógica del juego, etc.

Después, se teclea un programa vacío que siga esos pasos, pero que no haga nada; un bucle principal que tenga
un aspecto parecido a:

BuclePrincipal:
CALL Leer_Teclado
CALL Logica_Juego
CALL Comprobar_Estado
jp Bucle_Principal

Leer_Teclado:
RET

Logica_Juego:
RET

Comprobar_Estado:
RET

Tras esto, ya tenemos el “esqueleto del programa”. Y ahora hay que rellenar ese esqueleto, y la mejor forma de
hacerlo es aprovechar esa “modularidad” que hemos obtenido con ese diseño en papel.

Por ejemplo, supongamos que nuestro juego tiene que poder dibujar sprites y pantallas hechas a bases de
bloques que se repiten (tiles). Gracias a nuestro diseño, sabemos que necesitamos una rutina que imprima un
sprite, una rutina que dibuje un tile y una rutina que dibuje una pantalla llena de tiles.

Pues bien, creamos un programa en ASM nuevo, desde cero, y en él creamos una función DrawSprite que
acepte como parámetros la dirección origen de los datos del Sprite, y las posiciones X e Y donde dibujarlo, y la
realizamos. En este nuevo programa, pequeño, sencillo de leer, realizamos todo tipo de pruebas:

ORG 50000

; Probamos de diferentes formas nuestra rutina


LD B, 10
LD C, 15
LD HL, sprite
CALL DrawSprite
RET

; Rutina DrawSprite
; Acepta como parametros ... y devuelve ...
DrawSprite:
(aquí el código)
RET

sprite DB 0,0,255,123,121,123,34, (etc...)

END 50000

Gracias a esto, podremos probar nuestra nueva rutina y trabajar con ella limpiamente y en un fichero de
programa pequeño. Cuando la tenemos lista, basta con copiarla a nuestro programa “principal” y ya sabemos
que la tenemos disponible para su uso con CALL.

Así, vamos creando diferentes rutinas en un entorno controlado y testeable, y las vamos incorporando a nuestro
programa. Si hay algún bug en una rutina y tenemos que reproducirlo, podemos hacerlo en nuestros pequeños
programas de prueba, evitando el típico problema de tener que llegar a un determinado punto de nuestro
programa para chequear una rutina, o modificar su bucle principal para hacerlo.

Además, el definir de antemano qué tipo de subrutinas necesitamos y qué parámetros deben aceptar o devolver
permite trabajar en equipo. Si sabes que necesitarás una rutina que dibuje un sprite, o que lea el teclado y
devuelva la tecla pulsada, puedes decir los registros de entrada y los valores de salida que necesitas, y que la
realice una segunda persona y te envíe la rutina lista para usar.

En ocasiones una excesiva desgranación del programa en módulos más pequeños puede dar lugar a una
penalización en el rendimiento, aunque no siempre es así. Por ejemplo, supongamos que tenemos que dibujar
un mapeado de 10×10 bloques de 8×8 pixeles cada uno. Si hacemos una función de que dibuja un bloque de
8×8, podemos llamarla en un bucle para dibujar nuestros 10×10 bloques.

Hay gente que, en lugar de esto, preferirá realizar una función específica que dibuje los 10×10 bloques dentro
de una misma función. Esto es así porque de este modo te evitas 100 CALLs (10×10) y sus correspondientes
RETs, lo cual puede ser importante en una rutina gráfica que se ejecute X veces por segundo. Por supuesto, en
muchos casos tendrán razón, en ciertas ocasiones hay que hacer rutinas concretas para tareas concretas, aún
cuando puedan repetir parte de otro código que hayamos escrito anteriormente, con el objetivo de evitar
llamadas, des/apilamientos u operaciones innecesarias en una función crítica.
Pero si, por ejemplo, nosotros sólo dibujamos la pantalla una vez cuando nuestro personaje sale por el borde, y
no volvemos a dibujar otra hasta que sale por otro borde (típico caso de juegos sin scroll que muestran pantallas
completas de una sóla vez), vale la pena el usar funciones modulares dado que unos milisegundos más de
ejecución en el trazado de la pantalla no afectarán al desarrollo del juego.

Al final hay que llegar a un compromiso entre modularidad y optimización, en algunos casos nos interesará
desgranar mucho el código, y en otros nos interesará hacer funciones específicas. Y esa decisión no deja de ser,
al fin y al cabo, diseño del programa.

En cualquier caso, el diseño nos asegura que podremos implementar nuestro programa en cualquier lenguaje y
en cualquier momento. Podremos retomar nuestros “papeles de diseño” 3 meses después y, pese a no recordar
en qué parte del programa estábamos, volver a su desarrollo sin excesivas dificultades.

Una de las cosas más complicadas de hacer un juego es el pensar por dónde empezar. Todo este proceso nos
permite empezar el programa por la parte del mismo que realmente importa. Todos hemos empezado alguna
vez a realizar nuestro juego por el menú, perdiendo muchas horas de trabajo para descubrir que teníamos un
menú, pero no teníamos un juego, y que ya estábamos cansados del desarrollo sin apenas haber empezado.

Veamos un ejemplo: suponiendo que realizamos, por ejemplo, un juego de puzzles tipo Tetris, lo ideal sería
empezar definiendo dónde se almacenan los datos del area de juego, hacer una función que convierta esos datos
en imágenes en pantalla, y realizar un bucle que permita ver caer la pieza. Después, se agrega control por
teclado para la pieza y se pone la lógica del juego (realización de líneas al tocar suelo, etc).

Tras esto, ya tenemos el esqueleto funcional del juego y podemos añadir opciones, menúes y demás.
Tendremos algo tangible, funcional, donde podemos hacer cambios que implican un inmediato resultado en
pantalla, y no habremos malgastado muchas horas con un simple menú.

Por otra parte, el diseñar correctamente nuestro programa y desgranarlo en piezas reutilizables redundará en
nuestro beneficio no sólo actual (con respecto al programa que estamos escribiendo) sino futuro, ya que
podremos crearnos nuestras propias “bibliotecas” de funciones que reutilizar en futuros programas.

Aquella rutina de dibujado de Sprites, de zoom de pantalla o de compresión de datos que tanto nos costó
programar, bien aislada en una subrutina y con sus parámetros de entrada y salida bien definidos puede ser
utilizada directamente en nuestros próximos programas simplemente copiando y pegando el código
correspondiente.

Más aún, podemos organizar funciones con finalidades comunes en ficheros individuales. Tendremos así
nuestro fichero / biblioteca con funciones gráficas, de sonido, de teclado/joystick, etc. El ensamblador PASMO
nos permite incluir un fichero en cualquier parte de nuestro código con la directiva “INCLUDE”.

Así, nuestro programa en ASM podría comenzar (o acabar) por algo como:

INCLUDE "graficos.asm"
INCLUDE "sonido.asm"
INCLUDE "teclado.asm"
INCLUDE "datos.asm"

También podemos utilizar este sistema para los programas de prueba y testeo de las funciones que vamos
realizando para el programa principal. Así, podemos verificar con un sencillo programa que incluya algunos
.asm del juego si la rutina que acabamos de crear funciona correctamente.

La organización del código en bibliotecas de funciones contribuye a reducir fallos en la codificación, hacer más
corto el “listado general del programa”, y, sobre todo, reduce el tiempo de desarrollo.
Lenguaje Ensamblador del Z80 (V)

Puertos de E/S y Tabla de Opcodes


En este capítulo se introducirán las instrucciones IN y OUT para la exploración de los puertos del
microprocesador, mostrando cómo el acceso a dichos puertos nos permitirá la gestión de los diferentes
dispositivos conectados al microprocesador (teclado, altavoz, controladora de disco, etc…).

Finalmente, para acabar con la descripción del juego de instrucciones del Z80 veremos algunos ejemplos de
opcodes no documentados, y una tabla-resumen con la mayoría de instrucciones, así como sus tiempos de
ejecución y tamaños.

Los puertos E/S


Como ya vimos en su momento, el microprocesador Z80 se conecta mediante los puertos de entrada/salida de la
CPU a los periféricos externos (teclado, cassette y altavoz de audio), pudiendo leer el estado de los mismos
(leer del teclado, leer del cassette) y escribir en ellos (escribir en el altavoz para reproducir sonido, escribir en el
cassette) por medio de estas conexiones conocidas como “I/O Ports”.

Aunque para nosotros el teclado o el altavoz puedan ser parte del ordenador, para el Z80, el microprocesador en
sí mismo, son tan externos a él como el monitor o el joystick. Nuestro microprocesador accede a todos estos
elementos externos mediante una serie de patillas (buses de datos y direcciones) que son conectadas
eléctricamente a todos los elementos externos con los que queremos que interactúe. La memoria, el teclado, el
altavoz, o los mismos pines del bus trasero del Spectrum, se conectan al Z80 y éste nos permite su acceso a
través de dichas líneas, o de los puertos de entrada/salida (I/O).
IN y OUT
Ya conocemos la existencia y significado de los puertos y su conexión con el microprocesador. Sólo resta
saber: ¿cómo accedemos a un puerto tanto para leer como para escribir desde nuestros programas en
ensamblador?

La respuesta la tienen los comandos IN y OUT del Z80.

Comenzaremos con IN, que nos permite leer el valor de un puerto ya sea directamente, o cargado sobre el
registro BC:

IN registro, (C)
Leemos el puerto “BC” y ponemos su contenido en el registro especificado. En realidad, pese a que
teóricamente el Spectrum sólo tiene acceso a puertos E/S de 8 bits (0-255), para acceder a los puertos, IN r, (C)
pone todo el valor de BC en el bus de direcciones.

IN A, (puerto)
Leemos el puerto “A*256 + Puerto” y ponemos su contenido en A. En esta ocasión, el Spectrum pone en el bus
de direcciones el valor del registro de 16 bits formado por A y (puerto) (en lugar de BC).

Por ejemplo, estas 2 lecturas de puerto (usando los 2 formatos de la instrucción IN vistos anteriormente) son
equivalentes:

; Forma 1
LD BC, FFFEh
IN A, (C) ; A = Lectura de puerto FFFEh

; Forma 2
LD A, FFh
IN A, (FEh) ; A = Lectura de puerto FFFEh

Aunque la instrucción de la “Forma 1” hable del puerto C, en realidad el puerto es un valor de 16 bits y se carga
en el registro BC.

De la misma forma, podemos escribir un valor en un puerto con sus equivalentes “OUT”:

OUT (puerto), A
Escribimos en “puerto” (valor de 8 bits) el valor de A.

OUT (C), registro


Escribimos en el puerto “C” el valor contenido en “registro” (aunque se pone el valor de BC en el bus de
direcciones).

Curiosamente, como se explica en el excelente documento “The Undocumented Z80 Documented” (que habla
de las funcionalidades y opcodes no documentados del Z80), los puertos del Spectrum son oficialmente de 8
bits (0-255) aunque realmente se pone o bien BC o bien (A*256)+PUERTO en el bus de direcciones, por lo que
en el fondo se pueden acceder a todos los 65536 puertos disponibles.

La forma en que estas instrucciones afectan a los flags es la siguiente:

Flags
Instrucción |S Z H P N C|
----------------------------------
IN A, (n) |- - - - - -|
IN r, (C) |* * * P 0 -|
OUT (C), r |- - - - - -|
OUT (n), A |- - - - - -|
Aunque entre los 2 formatos OUT no debería haber ninguna diferencia funcional, cabe destacar que “OUT (N),
A” es 1 t-estado o ciclo de reloj más rápida que “OUT (C), A”, tardando 11 y 12 t-estados respectivamente.

Instrucciones de puerto repetitivas e incrementales


Al igual que LD carga un valor de un origen a un destino, y tiene sus correspondientes instrucciones
incrementales (LDI “carga e incrementa”, LDD “carga y decrementa”) o repetitivas (LDIR “carga, incrementa
y repite BC veces”, LDDR “carga, decrementa, y repite BC veces”), IN y OUT tienen sus equivalentes
incrementales y repetidores.

Así:

• IND :
o Leemos en la dirección de memoria apuntada por HL ([HL]) el valor contenido en el puerto C.
o Decrementamos HL.
o Decrementamos B

• INI :
o Leemos en la dirección de memoria apuntada por HL ([HL]) el valor contenido en el puerto C.
o Incrementamos HL.
o Decrementamos B

• OUTD :
o Escribimos en el puerto C el valor de la dirección de memoria apuntada por HL ([HL])
o Decrementamos HL.
o Decrementamos B

• OUTI :
o Escribimos en el puerto C el valor de la dirección de memoria apuntada por HL ([HL])
o Incrementamos HL.
o Decrementamos B

Y sus versiones repetitivas INDR, INIR, OTDR y OTIR, que realizan la misma función que sus hermanas
incrementales, repitiéndolo hasta que BC sea cero.

Las afectaciones de flags de estas funciones son las siguientes:

Flags:

Flags
Instrucción |S Z H P N C|
----------------------------------
INI |? * ? ? 1 ?|
IND |? * ? ? 1 ?|
OUTI |? * ? ? 1 ?|
OUTD |? * ? ? 1 ?|
INDR |? 1 ? ? 1 ?|
INIR |? 1 ? ? 1 ?|
OTDR |? 1 ? ? 1 ?|
OTIR |? 1 ? ? 1 ?|

Nota: Pese a que la documentación oficial dice que estas instrucciones no afectan al Carry Flag, las pruebas
hechas a posteriori y recopiladas en la información disponible sobre Opcodes No Documentados del Z80
sugieren que sí que son modificados.
Algunos puertos E/S comunes
Para terminar con el tema de los puertos de Entrada y Salida, vamos a hacer referencia a algunos puertos
disponibles en el Sinclair Spectrum (algunos de ellos sólo en ciertos modelos).

Como veremos en capítulo dedicado al teclado, existe una serie de puertos E/S que acceden directamente a la
lectura del estado de las diferentes teclas de nuestro Spectrum. Leyendo del puerto adecuado, y chequeando en
la respuesta obtenida el bit concreto asociado a la tecla que queremos consultar podremos conocer si una
determinada tecla está pulsada (0) o no pulsada (1), como podemos ver en el siguiente ejemplo:

; Lectura de la tecla "P" en un bucle


ORG 50000

bucle:
LD BC, $DFFE ; Semifila "P" a "Y"
IN A, (C) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, salir ; Si esta a 0 (pulsado) salir.
JR bucle ; Si no (a 1, no pulsado) repetimos

salir:
RET

El anterior ejemplo lee constantemente el puerto $DFFE a la espera de que el bit 0 de la respuesta obtenida de
dicha lectura sea 0, lo que quiere decir que la tecla “p” ha sido pulsada.

Aunque los veremos en su momento en profundidad, estos son los puertos asociados a las diferentes filas de
teclas:

Puerto Bits: D4 D3 D2 D1 D0
65278d (FEFEh) Teclas: “V” “C” “X” “Z” CAPS
65022d (FDFEh) Teclas: “G” “F” “D” “S” “A”
64510d (FBFEh) Teclas: “T” “R” “E” “W” “Q”
63486d (F7FEh) Teclas: “5” “4” “3” “2” “1”
61438d (EFFEh) Teclas: “0” “9” “8” “7” “6”
57342d (DFFEh) Teclas: “Y” “U” “I” “O” “P”
49150d (BFFEh) Teclas: “H” “J” “K” “L” ENTER
32766d (7FFEh) Teclas: “B” “N” “M” SYMB SPACE

El bit 6 de los puertos que hemos visto para el teclado tiene un valor aleatorio, excepto cuando se pulsa PLAY
en el cassette, y es a través de dicho bit de donde podremos obtener los datos a cargar.

La escritura en el puerto 00FEh permite acceder al altavoz (bit 4) y a la señal de audio para grabar a cinta (bit
3). Los bits 0, 1 y 2 controlan el color del borde, como podemos ver en el siguiente ejemplo:

; Cambio del color del borde al pulsar espacio


ORG 50000

LD B, 6 ; 6 iteraciones, color inicial borde

bucle:
LD A, $7F ; Semifila B a ESPACIO
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0 (ESPACIO)
JR NZ, bucle ; Si esta a 1 (no pulsado), esperar

LD A, B ; A = B
OUT (254), A ; Cambiamos el color del borde
suelta_tecla: ; Ahora esperamos a que se suelte la tecla
LD A, $7F ; Semifila B a ESPACIO
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, suelta_tecla ; Saltamos hasta que se suelte

djnz bucle ; Repetimos "B" veces

salir:
RET

END 50000 ; Ejecucion en 50000

El puerto 7FFDh gestiona la paginación en los modos de 128K, permitiendo cambiar el modelo de páginas de
memoria (algo que no vamos a ver en este capítulo).

Los puertos BFFDh y FFFDh gestionan el chip de sonido en aquellos modelos que dispongan de él, así como el
RS232/MIDI y el interfaz AUX.

Finalmente, el puerto 0FFDh gestiona el puerto paralelo de impresora, y los puertos 2FFDh y 3FFDh permiten
gestionar la controladora de disco en aquellos modelos de Spectrum que dispongan de ella.

Podéis encontrar más información sobre los puertos de Entrada y Salida en el capítulo 8 sección 23 del manual
del +2A y +3, disponible online en World Of Spectrum.

Tabla de instrucciones, ciclos y tamaños


A continuación se incluye una tabla donde se hace referencia a las instrucciones del microprocesador Z80
(campo Mnemonic), los ciclos de reloj que tarda en ejecutarse (campo Clck), el tamaño en bytes de la
instrucción codificada (Siz), la afectación de Flags (SZHPNC), el opcode y su descripción en cuanto a
ejecución.

La tabla forma parte de un documento llamado “The Complete Z80 OP-Code Reference”, de Devin Gardner.

--------------+----+---+------+------------+---------------------+-----------------------
|Mnemonic |Clck|Siz|SZHPNC| OP-Code | Description | Notes |
--------------+----+---+------+------------+---------------------+-----------------------
|ADC A,r | 4 | 1 |***V0*|88+rb |Add with Carry |A=A+s+CY |
|ADC A,N | 7 | 2 | |CE XX | | |
|ADC A,(HL) | 7 | 1 | |8E | | |
|ADC A,(IX+N) | 19 | 3 | |DD 8E XX | | |
|ADC A,(IY+N) | 19 | 3 | |FD 8E XX | | |
|ADC HL,BC | 15 | 2 |**?V0*|ED 4A |Add with Carry |HL=HL+ss+CY |
|ADC HL,DE | 15 | 2 | |ED 5A | | |
|ADC HL,HL | 15 | 2 | |ED 6A | | |
|ADC HL,SP | 15 | 2 | |ED 7A | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|ADD A,r | 4 | 1 |***V0*|80+rb |Add (8-bit) |A=A+s |
|ADD A,N | 7 | 2 | |C6 XX | | |
|ADD A,(HL) | 7 | 1 | |86 | | |
|ADD A,(IX+N) | 19 | 3 | |DD 86 XX | | |
|ADD A,(IY+N) | 19 | 3 | |FD 86 XX | | |
|ADD HL,BC | 11 | 1 |--?-0*|09 |Add (16-bit) |HL=HL+ss |
|ADD HL,DE | 11 | 1 | |19 | | |
|ADD HL,HL | 11 | 1 | |29 | | |
|ADD HL,SP | 11 | 1 | |39 | | |
|ADD IX,BC | 15 | 2 |--?-0*|DD 09 |Add (IX register) |IX=IX+pp |
|ADD IX,DE | 15 | 2 | |DD 19 | | |
|ADD IX,IX | 15 | 2 | |DD 29 | | |
|ADD IX,SP | 15 | 2 | |DD 39 | | |
|ADD IY,BC | 15 | 2 |--?-0*|FD 09 |Add (IY register) |IY=IY+rr |
|ADD IY,DE | 15 | 2 | |FD 19 | | |
|ADD IY,IY | 15 | 2 | |FD 29 | | |
|ADD IY,SP | 15 | 2 | |FD 39 | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|AND r | 4 | 1 |***P00|A0+rb |Logical AND |A=A&s |
|AND N | 7 | 2 | |E6 XX | | |
|AND (HL) | 7 | 1 | |A6 | | |
|AND (IX+N) | 19 | 3 | |DD A6 XX | | |
|AND (IY+N) | 19 | 3 | |FD A6 XX | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|BIT b,r | 8 | 2 |?*1?0-|CB 40+8*b+rb|Test Bit |m&{2^b} |
|BIT b,(HL) | 12 | 2 | |CB 46+8*b | | |
|BIT b,(IX+N) | 20 | 4 | |DD CB XX 46+8*b | |
|BIT b,(IY+N) | 20 | 4 | |FD CB XX 46+8*b | |
+-------------+----+---+------+------------+---------------------+----------------------+
|CALL NN | 17 | 3 |------|CD XX XX |Unconditional Call |-(SP)=PC,PC=nn |
|CALL C,NN |17/1| 3 |------|DC XX XX |Conditional Call |If Carry = 1 |
|CALL NC,NN |17/1| 3 | |D4 XX XX | |If carry = 0 |
|CALL M,NN |17/1| 3 | |FC XX XX | |If Sign = 1 (negative)|
|CALL P,NN |17/1| 3 | |F4 XX XX | |If Sign = 0 (positive)|
|CALL Z,NN |17/1| 3 | |CC XX XX | |If Zero = 1 (ans.=0) |
|CALL NZ,NN |17/1| 3 | |C4 XX XX | |If Zero = 0 (non-zero)|
|CALL PE,NN |17/1| 3 | |EC XX XX | |If Parity = 1 (even) |
|CALL PO,NN |17/1| 3 | |E4 XX XX | |If Parity = 0 (odd) |
+-------------+----+---+------+------------+---------------------+----------------------+
|CCF | 4 | 1 |--?-0*|3F |Complement Carry Flag|CY=~CY |
+-------------+----+---+------+------------+---------------------+----------------------+
|CP r | 4 | 1 |***V1*|B8+rb |Compare |Compare A-s |
|CP N | 7 | 2 | |FE XX | | |
|CP (HL) | 7 | 1 | |BE | | |
|CP (IX+N) | 19 | 3 | |DD BE XX | | |
|CP (IY+N) | 19 | 3 | |FD BE XX | | |
|CPD | 16 | 2 |****1-|ED A9 |Compare and Decrement|A-(HL),HL=HL-1,BC=BC-1|
|CPDR |21/1| 2 |****1-|ED B9 |Compare, Dec., Repeat|CPD till A=(HL)or BC=0|
|CPI | 16 | 2 |****1-|ED A1 |Compare and Increment|A-(HL),HL=HL+1,BC=BC-1|
|CPIR |21/1| 2 |****1-|ED B1 |Compare, Inc., Repeat|CPI till A=(HL)or BC=0|
+-------------+----+---+------+------------+---------------------+----------------------+
|CPL | 4 | 1 |--1-1-|2F |Complement |A=~A |
+-------------+----+---+------+------------+---------------------+----------------------+
|DAA | 4 | 1 |***P-*|27 |Decimal Adjust Acc. |A=BCD format (dec.) |
+-------------+----+---+------+------------+---------------------+----------------------+
|DEC A | 4 | 1 |***V1-|3D |Decrement (8-bit) |s=s-1 |
|DEC B | 4 | 1 | |05 | | |
|DEC C | 4 | 1 | |0D | | |
|DEC D | 4 | 1 | |15 | | |
|DEC E | 4 | 1 | |1D | | |
|DEC H | 4 | 1 | |25 | | |
|DEC L | 4 | 2 | |2D | | |
|DEC (HL) | 11 | 1 | |35 | | |
|DEC (IX+N) | 23 | 3 | |DD 35 XX | | |
|DEC (IY+N) | 23 | 3 | |FD 35 XX | | |
|DEC BC | 6 | 1 |------|0B |Decrement (16-bit) |ss=ss-1 |
|DEC DE | 6 | 1 | |1B | | |
|DEC HL | 6 | 1 | |2B | | |
|DEC SP | 6 | 1 | |3B | | |
|DEC IX | 10 | 2 |------|DD 2B |Decrement |xx=xx-1 |
|DEC IY | 10 | 2 | |FD 2B | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|DI | 4 | 1 |------|F3 |Disable Interrupts | |
+-------------+----+---+------+------------+---------------------+----------------------+
|DJNZ $+2 |13/8| 1 |------|10 |Dec., Jump Non-Zero |B=B-1 till B=0 |
+-------------+----+---+------+------------+---------------------+----------------------+
|EI | 4 | 1 |------|FB |Enable Interrupts | |
+-------------+----+---+------+------------+---------------------+----------------------+
|EX (SP),HL | 19 | 1 |------|E3 |Exchange |(SP)<->HL |
|EX (SP),IX | 23 | 2 |------|DD E3 | |(SP)<->xx |
|EX (SP),IY | 23 | 2 | |FD E3 | | |
|EX AF,AF' | 4 | 1 |------|08 | |AF<->AF' |
|EX DE,HL | 4 | 1 |------|EB | |DE<->HL |
|EXX | 4 | 1 |------|D9 |Exchange |qq<->qq' (except AF)|
+-------------+----+---+------+------------+---------------------+----------------------+
|HALT | 4 | 1 |------|76 |Halt | |
+-------------+----+---+------+------------+---------------------+----------------------+
|IM 0 | 8 | 2 |------|ED 46 |Interrupt Mode | (n=0,1,2)|
|IM 1 | 8 | 2 | |ED 56 | | |
|IM 2 | 8 | 2 | |ED 5E | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|IN A,(N) | 11 | 2 |------|DB XX |Input |A=(n) |
|IN (C) | 12 | 2 |***P0-|ED 70 |Input* | (Unsupported)|
|IN A,(C) | 12 | 2 |***P0-|ED 78 |Input |r=(C) |
|IN B,(C) | 12 | 2 | |ED 40 | | |
|IN C,(C) | 12 | 2 | |ED 48 | | |
|IN D,(C) | 12 | 2 | |ED 50 | | |
|IN E,(C) | 12 | 2 | |ED 58 | | |
|IN H,(C) | 12 | 2 | |ED 60 | | |
|IN L,(C) | 12 | 2 | |ED 68 | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|INC A | 4 | 1 |***V0-|3C |Increment (8-bit) |r=r+1 |
|INC B | 4 | 1 | |04 | | |
|INC C | 4 | 1 | |0C | | |
|INC D | 4 | 1 | |14 | | |
|INC E | 4 | 1 | |1C | | |
|INC H | 4 | 1 | |24 | | |
|INC L | 4 | 1 | |2C | | |
|INC BC | 6 | 1 |------|03 |Increment (16-bit) |ss=ss+1 |
|INC DE | 6 | 1 | |13 | | |
|INC HL | 6 | 1 | |23 | | |
|INC SP | 6 | 1 | |33 | | |
|INC IX | 10 | 2 |------|DD 23 |Increment |xx=xx+1 |
|INC IY | 10 | 2 | |FD 23 | | |
|INC (HL) | 11 | 1 |***V0-|34 |Increment (indirect) |(HL)=(HL)+1 |
|INC (IX+N) | 23 | 3 |***V0-|DD 34 XX |Increment |(xx+d)=(xx+d)+1 |
|INC (IY+N) | 23 | 3 | |FD 34 XX | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|IND | 16 | 2 |?*??1-|ED AA |Input and Decrement |(HL)=(C),HL=HL-1,B=B-1|
|INDR |21/1| 2 |?1??1-|ED BA |Input, Dec., Repeat |IND till B=0 |
|INI | 16 | 2 |?*??1-|ED A2 |Input and Increment |(HL)=(C),HL=HL+1,B=B-1|
|INIR |21/1| 2 |?1??1-|ED B2 |Input, Inc., Repeat |INI till B=0 |
+-------------+----+---+------+------------+---------------------+----------------------+
|JP $NN | 10 | 3 |------|C3 XX XX |Unconditional Jump |PC=nn |
|JP (HL) | 4 | 1 |------|E9 |Unconditional Jump |PC=(HL) |
|JP (IX) | 8 | 2 |------|DD E9 |Unconditional Jump |PC=(xx) |
|JP (IY) | 8 | 2 | |FD E9 | | |
|JP C,$NN |10/1| 3 |------|DA XX XX |Conditional Jump |If Carry = 1 |
|JP NC,$NN |10/1| 3 | |D2 XX XX | |If Carry = 0 |
|JP M,$NN |10/1| 3 | |FA XX XX | |If Sign = 1 (negative)|
|JP P,$NN |10/1| 3 | |F2 XX XX | |If Sign = 0 (positive)|
|JP Z,$NN |10/1| 3 | |CA XX XX | |If Zero = 1 (ans.= 0) |
|JP NZ,$NN |10/1| 3 | |C2 XX XX | |If Zero = 0 (non-zero)|
|JP PE,$NN |10/1| 3 | |EA XX XX | |If Parity = 1 (even) |
|JP PO,$NN |10/1| 3 | |E2 XX XX | |If Parity = 0 (odd) |
+-------------+----+---+------+------------+---------------------+----------------------+
|JR $N+2 | 12 | 2 |------|18 XX |Relative Jump |PC=PC+e |
|JR C,$N+2 |12/7| 2 |------|38 XX |Cond. Relative Jump |If cc JR(cc=C,NC,NZ,Z)|
|JR NC,$N+2 |12/7| 2 | |30 XX | | |
|JR Z,$N+2 |12/7| 2 | |28 XX | | |
|JR NZ,$N+2 |12/7| 2 | |20 XX | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|LD I,A | 9 | 2 |------|ED 47 |Load* |dst=src |
|LD R,A | 9 | 2 | |ED 4F | | |
|LD A,I | 9 | 2 |**0*0-|ED 57 |Load* |dst=src |
|LD A,R | 9 | 2 | |ED 5F | | |
|LD A,r | 4 | 1 |------|78+rb |Load (8-bit) |dst=src |
|LD A,N | 7 | 2 | |3E XX | | |
|LD A,(BC) | 7 | 1 | |0A | | |
|LD A,(DE) | 7 | 1 | |1A | | |
|LD A,(HL) | 7 | 1 | |7E | | |
|LD A,(IX+N) | 19 | 3 | |DD 7E XX | | |
|LD A,(IY+N) | 19 | 3 | |FD 7E XX | | |
|LD A,(NN) | 13 | 3 | |3A XX XX | | |
|LD B,r | 4 | 1 | |40+rb | | |
|LD B,N | 7 | 2 | |06 XX | | |
|LD B,(HL) | 7 | 1 | |46 | | |
|LD B,(IX+N) | 19 | 3 | |DD 46 XX | | |
|LD B,(IY+N) | 19 | 3 | |FD 46 XX | | |
|LD C,r | 4 | 1 | |48+rb | | |
|LD C,N | 7 | 2 | |0E XX | | |
|LD C,(HL) | 7 | 1 | |4E | | |
|LD C,(IX+N) | 19 | 3 | |DD 4E XX | | |
|LD C,(IY+N) | 19 | 3 | |FD 4E XX | | |
|LD D,r | 4 | 1 | |50+rb | | |
|LD D,N | 7 | 2 | |16 XX | | |
|LD D,(HL) | 7 | 1 | |56 | | |
|LD D,(IX+N) | 19 | 3 | |DD 56 XX | | |
|LD D,(IY+N) | 19 | 3 | |FD 56 XX | | |
|LD E,r | 4 | 1 | |58+rb | | |
|LD E,N | 7 | 2 | |1E XX | | |
|LD E,(HL) | 7 | 1 | |5E | | |
|LD E,(IX+N) | 19 | 3 | |DD 5E XX | | |
|LD E,(IY+N) | 19 | 3 | |FD 5E XX | | |
|LD H,r | 4 | 1 | |60+rb | | |
|LD H,N | 7 | 2 | |26 XX | | |
|LD H,(HL) | 7 | 1 | |66 | | |
|LD H,(IX+N) | 19 | 3 | |DD 66 XX | | |
|LD H,(IY+N) | 19 | 3 | |FD 66 XX | | |
|LD L,r | 4 | 1 | |68+rb | | |
|LD L,N | 7 | 2 | |2E XX | | |
|LD L,(HL) | 7 | 1 | |6E | | |
|LD L,(IX+N) | 19 | 3 | |DD 6E XX | | |
|LD L,(IY+N) | 19 | 3 | |FD 6E XX | | |
|LD BC,(NN) | 20 | 4 |------|ED 4B XX XX |Load (16-bit) |dst=src |
|LD BC,NN | 10 | 3 | |01 XX XX | | |
|LD DE,(NN) | 20 | 4 | |ED 5B XX XX | | |
|LD DE,NN | 10 | 3 | |11 XX XX | | |
|LD HL,(NN) | 20 | 3 | |2A XX XX | | |
|LD HL,NN | 10 | 3 | |21 XX XX | | |
|LD SP,(NN) | 20 | 4 | |ED 7B XX XX | | |
|LD SP,HL | 6 | 1 | |F9 | | |
|LD SP,IX | 10 | 2 | |DD F9 | | |
|LD SP,IY | 10 | 2 | |FD F9 | | |
|LD SP,NN | 10 | 3 | |31 XX XX | | |
|LD IX,(NN) | 20 | 4 | |DD 2A XX XX | | |
|LD IX,NN | 14 | 4 | |DD 21 XX XX | | |
|LD IY,(NN) | 20 | 4 | |FD 2A XX XX | | |
|LD IY,NN | 14 | 4 | |FD 21 XX XX | | |
|LD (HL),r | 7 | 1 |------|70+rb |Load (Indirect) |dst=src |
|LD (HL),N | 10 | 2 | |36 XX | | |
|LD (BC),A | 7 | 1 | |02 | | |
|LD (DE),A | 7 | 1 | |12 | | |
|LD (NN),A | 13 | 3 | |32 XX XX | | |
|LD (NN),BC | 20 | 4 | |ED 43 XX XX | | |
|LD (NN),DE | 20 | 4 | |ED 53 XX XX | | |
|LD (NN),HL | 16 | 3 | |22 XX XX | | |
|LD (NN),IX | 20 | 4 | |DD 22 XX XX | | |
|LD (NN),IY | 20 | 4 | |FD 22 XX XX | | |
|LD (NN),SP | 20 | 4 | |ED 73 XX XX | | |
|LD (IX+N),r | 19 | 3 | |DD 70+rb XX | | |
|LD (IX+N),N | 19 | 4 | |DD 36 XX XX | | |
|LD (IY+N),r | 19 | 3 | |FD 70+rb XX | | |
|LD (IY+N),N | 19 | 4 | |FD 36 XX XX | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|LDD | 16 | 2 |--0*0-|ED A8 |Load and Decrement |(DE)=(HL),HL=HL-1,# |
|LDDR |21/1| 2 |--000-|ED B8 |Load, Dec., Repeat |LDD till BC=0 |
|LDI | 16 | 2 |--0*0-|ED A0 |Load and Increment |(DE)=(HL),HL=HL+1,# |
|LDIR |21/1| 2 |--000-|ED B0 |Load, Inc., Repeat |LDI till BC=0 |
+-------------+----+---+------+------------+---------------------+----------------------+
|NEG | 8 | 2 |***V1*|ED 44 |Negate |A=-A |
+-------------+----+---+------+------------+---------------------+----------------------+
|NOP | 4 | 1 |------|00 |No Operation | |
+-------------+----+---+------+------------+---------------------+----------------------+
|OR r | 4 | 1 |***P00|B0+rb |Logical inclusive OR |A=Avs |
|OR N | 7 | 2 | |F6 XX | | |
|OR (HL) | 7 | 1 | |B6 | | |
|OR (IX+N) | 19 | 3 | |DD B6 XX | | |
|OR (IY+N) | 19 | 3 | |FD B6 XX | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|OUT (N),A | 11 | 2 |------|D3 XX |Output |(n)=A |
|OUT (C),0 | 12 | 2 |------|ED 71 |Output* | (Unsupported)|
|OUT (C),A | 12 | 2 |------|ED 79 |Output |(C)=r |
|OUT (C),B | 12 | 2 | |ED 41 | | |
|OUT (C),C | 12 | 2 | |ED 49 | | |
|OUT (C),D | 12 | 2 | |ED 51 | | |
|OUT (C),E | 12 | 2 | |ED 59 | | |
|OUT (C),H | 12 | 2 | |ED 61 | | |
|OUT (C),L | 12 | 2 | |ED 69 | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|OUTD | 16 | 2 |?*??1-|ED AB |Output and Decrement |(C)=(HL),HL=HL-1,B=B-1|
|OTDR |21/1| 2 |?1??1-|ED BB |Output, Dec., Repeat |OUTD till B=0 |
|OUTI | 16 | 2 |?*??1-|ED A3 |Output and Increment |(C)=(HL),HL=HL+1,B=B-1|
|OTIR |21/1| 2 |?1??1-|ED B3 |Output, Inc., Repeat |OUTI till B=0 |
+-------------+----+---+------+------------+---------------------+----------------------+
|POP AF | 10 | 1 |------|F1 |Pop |qq=(SP)+ |
|POP BC | 10 | 1 | |C1 | | |
|POP DE | 10 | 1 | |D1 | | |
|POP HL | 10 | 1 | |E1 | | |
|POP IX | 14 | 2 |------|DD E1 |Pop |xx=(SP)+ |
|POP IY | 14 | 2 | |FD E1 | | |
|PUSH AF | 11 | 1 |------|F5 |Push |-(SP)=qq |
|PUSH BC | 11 | 1 | |C5 | | |
|PUSH DE | 11 | 1 | |D5 | | |
|PUSH HL | 11 | 1 | |E5 | | |
|PUSH IX | 15 | 2 |------|DD E5 |Push |-(SP)=xx |
|PUSH IY | 15 | 2 | |FD E5 | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|RES b,r | 8 | 2 |------|CB 80+8*b+rb|Reset bit |m=m&{~2^b} |
|RES b,(HL) | 15 | 2 |------|CB 86+8*b | | |
|RES b,(IX+N) | 23 | 4 |------|DD CB XX 86+8*b | |
|RES b,(IY+N) | 23 | 4 |------|FD CB XX 86+8*b | |
+-------------+----+---+------+------------+---------------------+----------------------+
|RET | 10 | 1 |------|C9 |Return |PC=(SP)+ |
|RET C |11/5| 1 |------|D8 |Conditional Return |If Carry = 1 |
|RET NC |11/5| 1 | |D0 | |If Carry = 0 |
|RET M |11/5| 1 | |F8 | |If Sign = 1 (negative)|
|RET P |11/5| 1 | |F0 | |If Sign = 0 (positive)|
|RET Z |11/5| 1 | |C8 | |If Zero = 1 (ans.=0) |
|RET NZ |11/5| 1 | |C0 | |If Zero = 0 (non-zero)|
|RET PE |11/5| 1 | |E8 | |If Parity = 1 (even) |
|RET PO |11/5| 1 | |E0 | |If Parity = 0 (odd) |
+-------------+----+---+------+------------+---------------------+----------------------+
|RETI | 14 | 2 |------|ED 4D |Return from Interrupt|PC=(SP)+ |
|RETN | 14 | 2 |------|ED 45 |Return from NMI |PC=(SP)+ |
+-------------+----+---+------+------------+---------------------+----------------------+
|RLA | 4 | 1 |--0-0*|17 |Rotate Left Acc. |A={CY,A}<- |
|RL r | 8 | 2 |**0P0*|CB 10+rb |Rotate Left |m={CY,m}<- |
|RL (HL) | 15 | 2 | |CB 16 | | |
|RL (IX+N) | 23 | 4 | |DD CB XX 16 | | |
|RL (IY+N) | 23 | 4 | |FD CB XX 16 | | |
|RLCA | 4 | 1 |--0-0*|07 |Rotate Left Cir. Acc.|A=A<- |
|RLC r | 8 | 2 |**0P0*|CB 00+rb |Rotate Left Circular |m=m<- |
|RLC (HL) | 15 | 2 | |CB 06 | | |
|RLC (IX+N) | 23 | 4 | |DD CB XX 06 | | |
|RLC (IY+N) | 23 | 4 | |FD CB XX 06 | | |
|RLD | 18 | 2 |**0P0-|ED 6F |Rotate Left 4 bits |{A,(HL)}={A,(HL)}<- ##|
|RRA | 4 | 1 |--0-0*|1F |Rotate Right Acc. |A=->{CY,A} |
|RR r | 8 | 2 |**0P0*|CB 18+rb |Rotate Right |m=->{CY,m} |
|RR (HL) | 15 | 2 | |CB 1E | | |
|RR (IX+N) | 23 | 4 | |DD CB XX 1E | | |
|RR (IY+N) | 23 | 4 | |FD CB XX 1E | | |
|RRCA | 4 | 1 |--0-0*|0F |Rotate Right Cir.Acc.|A=->A |
|RRC r | 8 | 2 |**0P0*|CB 08+rb |Rotate Right Circular|m=->m |
|RRC (HL) | 15 | 2 | |CB 0E | | |
|RRC (IX+N) | 23 | 4 | |DD CB XX 0E | | |
|RRC (IY+N) | 23 | 4 | |FD CB XX 0E | | |
|RRD | 18 | 2 |**0P0-|ED 67 |Rotate Right 4 bits |{A,(HL)}=->{A,(HL)} ##|
+-------------+----+---+------+------------+---------------------+----------------------+
|RST 0 | 11 | 1 |------|C7 |Restart | (p=0H,8H,10H,...,38H)|
|RST 08H | 11 | 1 | |CF | | |
|RST 10H | 11 | 1 | |D7 | | |
|RST 18H | 11 | 1 | |DF | | |
|RST 20H | 11 | 1 | |E7 | | |
|RST 28H | 11 | 1 | |EF | | |
|RST 30H | 11 | 1 | |F7 | | |
|RST 38H | 11 | 1 | |FF | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|SBC r | 4 | 1 |***V1*|98+rb |Subtract with Carry |A=A-s-CY |
|SBC A,N | 7 | 2 | |DE XX | | |
|SBC (HL) | 7 | 1 | |9E | | |
|SBC A,(IX+N) | 19 | 3 | |DD 9E XX | | |
|SBC A,(IY+N) | 19 | 3 | |FD 9E XX | | |
|SBC HL,BC | 15 | 2 |**?V1*|ED 42 |Subtract with Carry |HL=HL-ss-CY |
|SBC HL,DE | 15 | 2 | |ED 52 | | |
|SBC HL,HL | 15 | 2 | |ED 62 | | |
|SBC HL,SP | 15 | 2 | |ED 72 | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|SCF | 4 | 1 |--0-01|37 |Set Carry Flag |CY=1 |
+-------------+----+---+------+------------+---------------------+----------------------+
|SET b,r | 8 | 2 |------|CB C0+8*b+rb|Set bit |m=mv{2^b} |
|SET b,(HL) | 15 | 2 | |CB C6+8*b | | |
|SET b,(IX+N) | 23 | 4 | |DD CB XX C6+8*b | |
|SET b,(IY+N) | 23 | 4 | |FD CB XX C6+8*b | |
+-------------+----+---+------+------------+---------------------+----------------------+
|SLA r | 8 | 2 |**0P0*|CB 20+rb |Shift Left Arithmetic|m=m*2 |
|SLA (HL) | 15 | 2 | |CB 26 | | |
|SLA (IX+N) | 23 | 4 | |DD CB XX 26 | | |
|SLA (IY+N) | 23 | 4 | |FD CB XX 26 | | |
|SRA r | 8 | 2 |**0P0*|CB 28+rb |Shift Right Arith. |m=m/2 |
|SRA (HL) | 15 | 2 | |CB 2E | | |
|SRA (IX+N) | 23 | 4 | |DD CB XX 2E | | |
|SRA (IY+N) | 23 | 4 | |FD CB XX 2E | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|SLL r | 8 | 2 |**0P0*|CB 30+rb |Shift Left Logical* |m={0,m,CY}<- |
|SLL (HL) | 15 | 2 | |CB 36 | | (SLL instructions |
|SLL (IX+N) | 23 | 4 | |DD CB XX 36 | | are Unsupported) |
|SLL (IY+N) | 23 | 4 | |FD CB XX 36 | | |
|SRL r | 8 | 2 |**0P0*|CB 38+rb |Shift Right Logical |m=->{0,m,CY} |
|SRL (HL) | 15 | 2 | |CB 3E | | |
|SRL (IX+N) | 23 | 4 | |DD CB XX 3E | | |
|SRL (IY+N) | 23 | 4 | |FD CB XX 3E | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|SUB r | 4 | 1 |***V1*|90+rb |Subtract |A=A-s |
|SUB N | 7 | 2 | |D6 XX | | |
|SUB (HL) | 7 | 1 | |96 | | |
|SUB (IX+N) | 19 | 3 | |DD 96 XX | | |
|SUB (IY+N) | 19 | 3 | |FD 96 XX | | |
+-------------+----+---+------+------------+---------------------+----------------------+
|XOR r | 4 | 1 |***P00|A8+rb |Logical Exclusive OR |A=Axs |
|XOR N | 7 | 2 | |EE XX | | |
|XOR (HL) | 7 | 1 | |AE | | |
|XOR (IX+N) | 19 | 3 | |DD AE XX | | |
|XOR (IY+N) | 19 | 3 | |FD AE XX | | |
--------------+----+---+------+------------+---------------------+-----------------------

Leyenda:

+---------------+---------------------------------------------+
| n |Immediate addressing |
| nn |Immediate extended addressing |
| e |Relative addressing (PC=PC+2+offset) |
| (nn) |Extended addressing |
| (xx+d) |Indexed addressing |
| r |Register addressing |
| (rr) |Register indirect addressing |
| |Implied addressing |
| b |Bit addressing |
| p |Modified page zero addressing (see RST) |
| * |Undocumented opcode |
+---------------+---------------------------------------------+
| A B C D E |Registers (8-bit) |
| AF BC DE HL |Register pairs (16-bit) |
| F |Flag register (8-bit) |
| I |Interrupt page address register (8-bit) |
| IX IY |Index registers (16-bit) |
| PC |Program Counter register (16-bit) |
| R |Memory Refresh register |
| SP |Stack Pointer register (16-bit) |
+---------------+---------------------------------------------+
| b |One bit (0 to 7) |
| cc |Condition (C,M,NC,NZ,P,PE,PO,Z) |
| d |One-byte expression (-128 to +127) |
| dst |Destination s, ss, (BC), (DE), (HL), (nn) |
| e |One-byte expression (-126 to +129) |
| m |Any register r, (HL) or (xx+d) |
| n |One-byte expression (0 to 255) |
| nn |Two-byte expression (0 to 65535) |
| pp |Register pair BC, DE, IX or SP |
| qq |Register pair AF, BC, DE or HL |
| qq' |Alternative register pair AF, BC, DE or HL |
| r |Register A, B, C, D, E, H or L |
| rr |Register pair BC, DE, IY or SP |
| s |Any register r, value n, (HL) or (xx+d) |
| src |Source s, ss, (BC), (DE), (HL), nn, (nn) |
| ss |Register pair BC, DE, HL or SP |
| xx |Index register IX or IY |
| + - * / ^ |Add/subtract/multiply/divide/exponent |
| & ~ v x |Logical AND/NOT/inclusive OR/exclusive OR |
| <- -> |Rotate left/right |
| ( ) |Indirect addressing |
| ( )+ -( ) |Indirect addressing auto-increment/decrement |
| { } |Combination of operands |
| # |Also BC=BC-1,DE=DE-1 |
| ## |Only lower 4 bits of accumulator A used |
+---------------+---------------------------------------------+
Unos apuntes sobre esta tabla:

1.- En instrucciones como “ADC A, r” podemos ver una defición del OPCODE como “88+rb”. En este caso, el
opcode final se obtendría sumando a “88h” un valor de 0 a 7 según el registro al que nos referimos:

Registro Valor RB
A 7
B 0
C 1
D 2
E 3
H 4
L 5
(HL) 6

Por ejemplo, “ADC A, B” se codificaría en memoria como “88+0=88”.

2.- En los saltos hay 2 tiempos de ejecución diferentes (por ejemplo, 10/1). En este caso el valor más alto (10)
son los t-estados o ciclos que toma la instrucción cuando el salto se realiza, y el más bajo (1) es lo que tarda la
instrucción cuando no se salta al destino. Como véis, a la hora de programar una rutina que tenga saltos o
bifurcaciones, es interesante programarla de forma que el caso más común, el que se produzca la mayoría de las
veces, no produzca un salto.

3.- La descripción de las afectaciones de flags son las siguientes:

--------+-------+----------------------------------------------
| F | -*01? |Flag unaffected/affected/reset/set/unknown |
| S | S |Sign flag (Bit 7) |
| Z | Z |Zero flag (Bit 6) |
| HC | H |Half Carry flag (Bit 4) |
| P/V | P |Parity/Overflow flag (Bit 2, V=overflow) |
| N | N |Add/Subtract flag (Bit 1) |
| CY | C|Carry flag (Bit 0) |
+---------------+---------------------------------------------+

Instrucciones no documentadas del Z80

En Internet podemos encontrar gran cantidad de documentación acerca del Z80 y su juego de instrucciones,
incluyendo las especificaciones oficiales del microprocesador Z80 de Zilog.

No obstante, existen una serie de instrucciones u opcodes que el microprocesador puede ejecutar y que no están
detallados en la documentación oficial de Zilog. Con respecto a esto, tenemos la suerte de disponer de algo que
los programadores de la época del Spectrum no tenían: una descripción detallada de las instrucciones no
documentadas del Z80. Aunque la mayoría son instrucciones repetidas de sus versiones documentadas, hay
algunas instrucciones curiosas y a las que tal vez le podamos sacar alguna utilidad.

¿Por qué existen estos opcodes y no fueron documentados? Supongo que algunos de ellos no fueron
considerados como “merecedores de utilidad alguna” y los ingenieros de Zilog no los documentaron, o tal vez
sean simplemente un resultado no previsto de la ejecución del Z80 porque los diseñadores no pensaron que al
microprocesador pudieran llegarle dichos códigos. El caso es que para el microprocesador existen “todos” los
opcodes, otra cosa es qué haga al leerlos y decodificarlos. En este caso algunos de ellos realizan funciones
válidas mientras que otros son el equivalente a ejecutar 2 instrucciones NOP, por ejemplo.
¿Cuál es la utilidad de estas instrucciones para los programadores? Para ser sinceros, como programadores con
un ensamblador o un ensamblador cruzado, poca. Si haces tus programas desde cero con un programa
ensamblador, éste se encargará de la conversión de instrucciones estándar a opcodes, aunque no viene mal
conocer la existencia de estas instrucciones. Para los programadores de emuladores y de desensambladores, el
conocimiento de estos opcodes es vital.

El juego Sabre Wulf, por ejemplo, utiliza una de estas instrucciones en la determinación del camino de uno de
los enemigos en pantalla (la instrucción SLL, que veremos a continuación), hasta el punto en que los primeros
emuladores de Spectrum emulaban mal este juego hasta que incluyeron dicha instrucción en la emulación.

Los “undocumented opcodes” son esencialmente opcodes con prefijos CB, ED, DD o FD que hacen unas
determinadas operaciones y que no están incluídos en la “lista oficial” que hemos visto hasta ahora. Todos los
ejemplos que veremos a continuación están extraídos del documento “The Undocumented Z80 Documented”,
de Sean Young.

Prefijo CB

Por ejemplo, los opcodes CB 30, CB 31, CB 32, CB 33, CB 34, CB 35, CB 36 y CB 37 definen una nueva
instrucción: SLL.

OPCODE INSTRUCCION
CB 30 SLL B
CB 31 SLL C
CB 32 SLL D
CB 33 SLL E
CB 34 SLL H
CB 35 SLL L
CB 36 SLL (HL)
CB 37 SLL A

SLL (Shift Logical Left) funciona exactamente igual que SLA salvo porque pone a 1 el bit 0 (mientras que
SLA lo ponía a 0).

Prefijos DD y FD

En general, una instrucción precedida por el opcode DD se ejecuta igual que sin él excepto por las siguientes
reglas:

• Si la instrucción usaba el registro HL, éste se sustituye por IX (excepto en las instrucciones EX DE, HL
y EXX).
• Cualquier uso de (HL) se reemplaza por (IX+d), excepto JP (HL).
• Cualquier acceso a H se reemplaza por IXh (byte alto de IX), excepto en el uso de (IX+d).
• Cualquier acceso a L se reemplaza por IXl (byte alto de IX), excepto en el uso de (IX+d).

Por ejemplo:

Sin el prefijo DD Con el Prefijo DD


LD HL, 0 LD IX, 0
LD H, A LD IXh, A
Sin el prefijo DD Con el Prefijo DD
LD H, (HL) LD H, (IX+d)

El caso de FD es exactamente igual que el de DD, pero usando el registro IY en lugar del IX.

Prefijo ED

Hay una gran cantidad de instrucciones ED XX indocumentadas. Muchos de ellos realizan la misma función
que sus equivalentes sin ED delante, mientras que otros simplemente son leídos y decodificados, resultando, a
niveles prácticos, equivalentes a 2 instrucciones NOP. Veamos algunos de ellos:

OPCODE INSTRUCCION
ED 4C NEG
ED 4E IM 0
ED 44 NEG
ED 45 RETN
ED 5C NEG
ED 5D RETN
ED 64 NEG
ED 65 RETN
ED 66 IM 0
ED 6C NEG
ED 6D RETN
ED 6E IM 0
ED 70 IN (C) / IN F,(C)
ED 71 OUT (C),0
ED 74 NEG
ED 75 RETN
ED 76 IM1
ED 77 NOP
ED 7C NEG
ED 7D RETN
ED 7E IM2
ED 7F NOP

Aparte de los duplicados de NOP, NEG, IM0, etc, podemos ver un par de instrucciones curiosas y que nos
pueden ser de utilidad. Por ejemplo:

ED 70 IN (C)

Esta instrucción lee el puerto C, pero no almacena el resultado de la lectura en ningún lugar. No obstante, altera
los flags del registro F como corresponde al resultado leído. Puede ser interesante si sólo nos interesa, por
ejemplo, si el valor leído es cero o no (flag Z), y no queremos perder un registro para almacenar el resultado.

Prefijos DDCB y FDCB


Las instrucciones DDCB y FDCB no documentadas almacenan el resultado de la operación de la instrucción
equivalente sin prefijo (si existe dicho resultado) en uno de los registros de propósito general: B, C, D, E, H, L,
ninguno o A, según los 3 bits más bajos del último byte del opcode (000=B, 001=C, 010=D, etc).

Así, supongamos el siguiente opcode sí documentado:

DD CB 01 06 RLC (IX+01h)

Si hacemos los 3 últimos bits de dicho opcode 010 (010), el resultado de la operación se copia al registro D
(010 = D en nuestra definición anterior), con lo que realmente, en lugar de “RLC (IX+01h)” se ejecuta:

LD D, (IX+01h)
RLC D
LD (IX+01h), D

La notación que sugiere Sean Young para estos opcodes es: “RLC (IX+01h), D”.

Con el prefijo FDCB ocurre igual que con DDCB, salvo que se usa el registro IY en lugar de IX.

De la teoría a la práctica
Con este capítulo hemos cubierto el 99% de las instrucciones soportadas por el microprocesador Z80. Con la
excepción de los Modos de Interrupciones del Z80 y sus aplicaciones, ya tenemos a nuestra disposición las
piezas básicas para formar cualquier programa o rutina en ensamblador.

No obstante, todavía quedan por delante muchas horas de programación para dominar este lenguaje, así como
diferentes técnicas, trucos, rutinas y mapas de memoria que nos permitan dibujar nuestros gráficos, realizar
rutinas complejas, utilizar el sistema de interrupciones del microprocesador para realizar controles de
temporización de nuestros programas, o reproducir sonido.
Save y Load: almacenamiento en cinta
¿En qué formato se almacenan los datos en una cinta de cassette para que el Spectrum pueda después cargar
desde ellas pantallas de presentación, los personajes de un juego o el código ejecutable de un programa?

En este capítulo mostraremos cómo se graban y estructuran los datos en las cintas, y qué hace el Spectrum para
acceder a ellos mediante las rutinas de que nos provee la ROM. Como aplicación práctica, se incluirá código de
carga y grabación de pantallas junto a un ejemplo completo que hará uso de las mismas.

Formato de los datos en cinta


Supongamos que, desde BASIC, salvamos un bloque de datos en cinta con el comando SAVE. Lo primero que
nos interesa conocer es el formato o estructura de los datos almacenados en la cinta, es decir, ¿qué se guarda
realmente en la cinta (en formato de audio) cuando hacemos un SAVE?

Un SAVE produce 2 bloques de datos en la cinta:

1. Un bloque de 19 bytes de tamaño fijo, conocido como cabecera. Este bloque es cargado muy
rápidamente, debido a su pequeño tamaño. Si pensáis en los cientos ó miles de LOADs desde cinta que
habréis hecho en vuestro Spectrum, recordaréis, nada más pulsar PLAY, la aparición del tono guía
(líneas del borde rojas y cyan) seguido de un brevísimo momento de carga de datos (líneas del borde
azules y amarillas). Es en ese momento en el que aparece en pantalla la información relativa al
juego/programa que estamos cargando.
2. Un bloque de longitud variable que contiene los datos concretos y reales a cargar.

Ambos bloques son en realidad “datos” con el siguiente formato:

• Un byte inicial, que como veremos se llama Flag Byte.


• Los datos en sí mismos: 17 bytes para cabeceras, o la longitud concreta de los datos para los bloques de
datos.
• Un byte de checksum o CRC.

Profundicemos un poco más en estos 2 bloques de datos:

Cada bloque se inicia con una serie de pulsos de 2168 t-stados cada uno, que constituyen el tono guía. La
cantidad de pulsos (la duración) de este tono guía es de 8063 pulsos para los bloques de cabecera, y 3223 pulsos
para los bloques de datos. Es por eso que la duración del tono guía (el famoso pitido inicial de la carga) es
mayor para la carga de la cabecera que para el de los datos en sí mismos. Es decir, el tono guía está presente
tanto para los bloques de cabecera como para los de datos, salvo que su duración es menor en los bloques de
datos.

Un t-stado (t-state) es 1 ciclo de reloj, y equivale a 1 / 3.500.000 segundos.


Tras el tono guía de la cabecera viene la cabecera en sí misma (que no dejan de ser datos). Su carga, como ya
hemos dicho, tarda un tiempo muy corto en realizarse, ya que son sólo 19 bytes a ser leídos desde el cassette.

En la imagen anterior podemos ver el aspecto de la pantalla una vez terminado el tono guía y cargada la
cabecera. Esta cabecera, mediante sus 19 bytes, le indican al Spectrum la naturaleza de los datos a cargar en el
bloque de datos que sigue a la misma, con el siguiente formato:

Byte Longitud Descripción


0 1 Byte Flag ($00 ó $FF)
1 1 Tipo de bloque (0-3)
2 10 Nombre de fichero (rellenado con espacios en blanco)
12 2 Longitud del bloque de datos a cargar
14 2 Parámetro 1
16 2 Parámetro 2
18 1 Checksum / Suma de comprobación

Pasemos a describir los diferentes campos de la cabecera:

El byte de flag (Byte 0) y el de Checksum (byte 18) no forman parte exactamente de la cabecera, sino del
bloque cargado en sí mismo (también están presente cuando cargamos datos y no cabeceras), pero se han
incluído dentro de la tabla para hacerla de lectura más sencilla. Puede decirse que el byte-flag es el byte prefijo
de todo bloque de datos (considerando una cabecera de 17 bytes como un bloque de datos) y el checksum es el
byte sufijo de ese mismo bloque. Concretamente, el valor de Byte-Flag es de $00 para bloques de cabecera y
$FF para bloques de datos.
El byte de tipo de bloque indica qué datos se van a cargar a continuación, según los siguientes valores:

Valor Significado
0 Programa
1 Array de números
2 Array de caracteres
3 CODE (datos a cargar en memoria)

En el caso de bloques de tipo CODE, el byte “Parámetro 1” define la dirección de inicio del bloque de código
cuando se hizo el SAVE, y el parámetro 2 contiene el valor 32768.

Para bloques de tipo PROGRAM, el Parámetro 1 contiene el valor de la línea BASIC de autostart (o un número
mayor de 32768 si no se dio un parámetro LINE al hacer el SAVE), y el Parámetro 2 contiene el inicio del área
de variables relativa al inicio del programa.

Un detalle: una pantalla de datos (SCREEN$) se define en esta cabecera como un bloque de tipo CODE de
6912 bytes a cargar sobre la dirección 16384.

Tras la cabecera (tono guía + bloque datos) viene el bloque de datos en sí mismo, que vuelve a componerse de
un tono guía, el Flag Byte, los datos

Ahora bien … ¿cómo es posible que se almacenen como audio datos digitales? ¿Cómo entiende el Spectrum si
los datos que está cargando son unos o ceros y los agrupa en bloques de bytes que podemos interpretar de forma
lógica?

Como ya hemos comentado al hablar del tono guía, la clave está en la temporización precisa a la hora de leer
datos desde la cinta. Aparte de tonos guía y pulsos de sincronización … ¿qué es un cero en la cinta? ¿qué es un
uno? ¿Cómo se almacena (y lee) un byte de 8 bits? ¿Y cómo se almacena (y lee) un conjunto de bytes?

• Un cero (bit=0) se codifica en cinta como 2 pulsos de una duración de 855 t-stados cada uno.
• Un uno (bit=1) se codifica en cinta como 2 pulsos de una duración de 1710 t-stados cada uno.
• Para almacenar los 8 bits de un byte, se almacenan bit a bit de mayor a menor peso (primero el bit 7,
luego el 6, el 5, el 4, el 3, el 2, el 1 y finalmente el LSB o bit 0).
• Cuando se almacena más de un byte (un bloque) se guardan primero los datos del primer byte del
bloque, luego el segundo, etc.

Los pulsos son ondas con un aspecto como el siguiente (aspecto de un tono guía en TAPER):
Es decir, si tenemos que almacenar en cinta los siguientes bytes:

abcdefgh ijklmnop

Se almacenarían en cinta en este orden:

a b c d e f g h i j k l m n o p

Así pues, la rutina de la ROM del Spectrum se encarga (tanto al grabar como al leer) de codificar pulsos de
diferentes duraciones para almacenar los ceros y unos de forma consecutiva. Nosotros aprovecharemos (como
veremos a continuación) dicha rutina para cargar o salvar bloques de datos a nuestro antojo sin tener que
programar esas temporizaciones y lecturas/escrituras a la cinta. Para nosotros será tan sencillo como cargar los
valores adecuados en ciertos registros y realizar un CALL. No obstante, para los más curiosos, al final de este
capítulo tenéis un enlace a las rutinas de la ROM adecuadamente comentadas de “The Complete Spectrum
ROM Disassembly”, por el Ian Logan y Frank O'Hara (publicado en 1983).

El nivel más bajo al que necesitamos llegar es el siguiente:

• Cada bloque tiene la siguiente estructura física:


o Un tono guía de 8063 (cabeceras) ó 3223 pulsos (datos) de 2168 t-stados cada uno.
o Un pulso de sincronización de 667 t-stados.
o Un segundo pulso de sincronización de 735 t-stados.
o El bloque de datos en sí mismo, bit a bit (0 = 2 pulsos de 855 t-stados, 1 = 2 pulsos de 1710 t-
stados).

• Cada bloque tiene la siguiente estructura lógica (formato de los datos DENTRO de un bloque):
o Flag byte, con un valor de $00 para bloques de cabecera o $FF para bloques de datos.
o Los datos en sí mismos: 17 bytes para cabeceras, o la longitud concreta de los datos para los
bloques de datos.
o Un byte de checksum, calculado de forma que haciendo un XOR de todos los bytes juntos,
incluyendo el flag Byte, produzca $00.

Resumiendo:

• Los datos salvados en cinta constan de tono guía, seguido de un bloque de datos de 19 bytes
denominado cabecera, seguido de un tono guía de menor duración que el inicial (por ser de datos),
seguido de los datos en sí mismos.
• Esa cabecera proporciona información sobre el nombre, duración y ciertos parámetros relativos a los
datos en sí mismos (el segundo bloque cargado).
• Los datos se leen de cinta secuencialmente, del primer al último byte del bloque de datos, y almacenado
cada byte desde el bit 7 al 0 secuencialmente.
• Los 1s y los 0s se almacenan en cinta como pulsos de duraciones concretas.
• Las rutinas de la ROM nos permiten leer y escribir en cinta bloques de datos, realizando ellas la
temporización adecuada para convertir nuestros “datos en memoria” en “pulsos” o viceversa sin
complicación por nuestra parte.
• Sólo necesitaríamos escribir una rutina propia de carga (que detecte pulsos, temporice, etc) si
quisiéramos programar nuestra propia carga, por ejemplo para cargar a diferente velocidad que el
Spectrum (ultracargas), que cargue de dispositivos externos (puerto de joystick, algún pin concreto del
bus de expansión, etc.) o para ejecutar código mientras cargamos el programa (minijuegos durante la
carga, contadores de carga, etc). Para el resto de casos, bastará con usar mediante CALL las rutinas de la
ROM.

Ejemplo de volcado de un SAVE


En la FAQ de comp.sys.sinclair y WorldOfSpectrum tenemos un ejemplo muy interesante que muestra el
formato lógico de los datos grabados con un SAVE en cinta. Supongamos el siguiente comando BASIC:

SAVE "ROM" CODE 0,2

Este comando BASIC salvaría (SAVE), un total de 2 bytes (2) de datos (CODE), empezando en 0 (0) a cinta.
En resumen, salvaría el contenido de la dirección de memoria 0x0000 y 0x0001 en cinta. Esto produciría los
siguientes datos en cinta:

00 03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80 f1 ff f3 af a3

Comencemos mostrando qué representa cada dato poco a poco:

cabecera bloque de datos


00 03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80 f1 ff f3 af a3

Desgranando más la información:

Byte Flag datos cabecera checksum byte flag datos ROM checksum
00 03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80 f1 ff f3 af a3

Concretamente, los datos de la cabecera (ignorando el Byte Flag y el Checksum):

Tipo de bloque Nombre de fichero Longitud bloque Parámetro 1 Parametro 2


03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80

Como véis el nombre “ROM” (52 4F 4D) se completa con espacios en blanco hasta los 10 caracteres. Además
podemos ver la longitud del bloque que se salvó (02 bytes).

Después de estos datos tenemos el checksum (F1) y el bloque de datos en sí mismo:


Byte Flag Datos grabados Checksum
ff f3 af a3

En este caso el byte flag es 0xFF (bloque de tipo “datos”), al cual siguen los 2 bytes tomados de la ROM y
grabados a cinta (0x0000 y 0x0001) y el checksum (0xA3).

Rutinas de carga de la ROM


Ya sabemos cómo se almacenan los datos en cinta, así que nuestra próxima misión es conocer cómo cargarlos o
grabarlos de una manera sencilla. Para hacer esto usaremos las funciones de la ROM del Spectrum para carga y
grabación de datos a cinta: hablamos de 2 subrutinas (de LOAD y SAVE) a las que podremos llamar con unos
parámetros concretos.

Rutina de LOAD de la ROM

La rutina de LOAD comienza en la dirección $0556 (0556h ó 1366d) y requiere los siguientes parámetros:

Registro Valor
IX Dirección inicio de memoria donde almacenar los datos que se van a cargar.
DE Longitud del bloque de datos a cargar.
A Flag Byte, normalmente 0x00 para cargar cabeceras o 0xFF (255) para cargar datos.
CF (CarryFlag) 1=LOAD, 0=VERIFY

La rutina devuelve el CF = 0 si ocurre alguno de los siguientes errores:

• “R-Tape Loading Error” (bien por timeout o bien por byte de paridad incorrecto)
• Flag Byte incorrecto.
• “D BREAK - CONT repeats” (se pulsó la tecla BREAK).

Recuerda que puedes activar el CARRY FLAG con la instruccion “SCF” (Set Carry Flag) y ponerlo a cero con
un simple “AND A”.

Veamos 2 ejemplos, el primero cargaría una pantalla gráfica sobre la videomemoria (el equivalente de un
LOAD “” SCREEN$) siempre y cuando la pantalla se haya grabado sin cabecera:

SCF ; Set Carry Flag -> CF=1 -> LOAD


LD A, 255 ; A = 0xFF (cargar datos)
LD IX, 16384 ; Destino del load = 16384
LD DE, 6912 ; Tamaño a cargar = 6912
CALL 1366 ; Llamamos a la rutina de carga

Este segundo programa cargaría un bloque de código ejecutable en memoria, y saltaría a él (un programa
“cargador”):

SCF ; Set Carry Flag (LOAD)


LD A, 255 ; A = 0xFF (cargar datos)
LD IX, 32768 ; Destino de la carga
LD DE, 12000 ; Nuestro "programa" ocupa 12000 bytes.
CALL 0556 ; Recordemos que 0556h = 1366d
JP 32768 ; Saltamos al programa código máquina cargado
Rutina de SAVE de la ROM

La rutina SAVE de la ROM tiene unos parámetros muy similares a la de LOAD, y está alojada en 1218d
(04c2h):

Registro Valor
IX Dirección inicio de memoria de los datos que se van a grabar.
DE Longitud del bloque de datos a grabar (se grabarán los datos desde IX a IX+DE).
A Flag Byte, 0x00 para grabar cabeceras o 0xFF (255) para grabar datos.
CF (CarryFlag) 0 (SAVE)

Lo normal es que no tengamos que recurrir en prácticamente ninguna ocasión a la rutina de grabación de datos,
de modo que nos centraremos, mediante ejemplos, en la rutina de carga.

Cargando o Ignorando la cabecera

Cuando salvamos datos desde BASIC, lo normal es que se generen 2 bloques de datos, el de la cabecera, y el de
los datos en sí mismos. El bloque de datos de la cabecera, cargado en memoria, nos permite saber el tamaño y
destino de los datos que vendrán en el siguiente bloque. Es decir, cargando el primer bloque obtenemos la
información necesaria para cargar en IX y DE los valores adecuados para la carga del bloque de datos.

En ocasiones, podemos ignorar el bloque de cabecera totalmente, sobre todo cuando sabemos qué vamos a
cargar desde cinta, qué destino tiene, y qué tamaño tiene, y lo especificamos directamente en nuestro programa
ASM. En ese caso, podemos cargar la cabecera con el CARRY FLAG a cero (verify), con lo cual la leemos
pero no la almacenamos en memoria, y después cargar los valores adecuados en IX, DE, A, etc, poner el CF a
1, y cargar los datos que vienen tras la cabecera.

Supongamos que grabamos un bloque de datos, gráficos, una pantalla o música en cinta usando SAVE, rutinas
de la ROM, o desde un emulador o herramienta cruzada de PC. Supongamos que sabemos el tamaño exacto en
cinta de dichos datos, y no necesitamos leer y analizar la cabecera para cargarlos. En tal caso, podemos ejecutar
código como el siguiente:

AND A ; CF = 0 (verify)
CALL 1366 ; Cargamos e ignoramos la cabecera

SCF ; Set Carry Flag -> CF=1 -> LOAD


LD A, 255 ; A = 0xFF (cargar datos)
LD IX, direccion_destino ; Destino del load
LD DE, tamaño_a_cargar ; Tamaño a cargar
CALL 1366 ; Llamamos a la rutina de carga

Posteriormente veremos un ejemplo que ignora la cabecera al cargar una pantalla SCR completa sobre la
videoram.

¿Cuándo nos interesa analizar la cabecera? Principalmente cuando los datos están generados desde nuestro
propio programa y tienen un tamaño variable. Por ejemplo, supongamos que programamos un editor de textos
que da al usuario la oportunidad de salvar y cargar los textos en cinta. En tal caso, necesitaremos leer la
cabecera para saber el tamaño del documento (bloque de datos) a cargar, ya que no lo conocemos de antemano.

No obstante, en el caso de un juego, normalmente se conoce con antelación el tamaño de los datos a cargar, por
lo que se puede ignorar felizmente la cabecera del bloque de cinta..
Convirtiendo datos en cinta
Lo primero que necesitamos saber es, ¿cómo convertimos nuestros datos (gráficos, pantalla de carga, números,
tablas precalculadas, sprites, música, etc) en datos cargables desde las rutinas que hemos visto? Hay múltiples
formas de hacerlo.

Para empezar, podemos hacerlo desde el mismo BASIC del Spectrum, usando el comando SAVE: esto nos
permitirá grabar datos de memoria en cinta:

SAVE "nombre" CODE direccion_inicio, tamaño

En la mayoría de los casos, muchos de nosotros programamos hoy en sistemas PC usando compiladores
cruzados, ensambladores cruzados y emuladores, por lo que normalmente lo que nos interesará es obtener
ficheros TAP para poder concatenarlos con nuestros cargadores o programas.

Supongamos que estamos programando un juego que, nada más acabar, lo primero que hace es cargar desde
cinta los datos del nivel actual (gráficos, mapeado, etc). Esto implica que cuando programemos el juego,
tendremos por un lado el código, que nos proporcionará un fichero TAP (por ejemplo) listo para ejecutar. A ese
fichero TAP tendremos que concatenarle los datos de los diferentes niveles (o gráficos, o los datos que
necesitemos).

Así, nuestro “programa.asm” (código fuente) se convierte en “programa.bin” tras el proceso de ensamblado, y
finalmente obtenemos un “programa.tap” (o .tzx) listo para cargar en un Spectrum.

Pero en dicho TAP o TZX tenemos que añadir (al final del mismo) los datos que el programa espera cargar.
Imaginemos que estos datos son una pantalla gráfica (.scr) de 6912 bytes. Tendremos un fichero “pantalla.scr”,
y tenemos que introducirlo dentro de nuestro fichero TAP, al final, tras el código del programa, para que
cuando este sea ejecutado, lo siguiente que cargue desde cinta nuestro programa sea dicha pantalla SCR.

Para ello, con el objetivo de hacerlo de una manera muy sencilla, utilizaremos ficheros TAP. El formato de este
tipo de ficheros es muy sencillo, simplemente contienen bloques de datos precedidos por 2 bytes que indican el
tamaño del bloque. Supongamos que tenemos en un fichero los 2 primeros bytes de la ROM que vimos
anteriormente:

f3 af

Este fichero de 2 bytes de tamaño (inicio_rom.bin, por ejemplo), guardado en una cinta tendría el formato que
vimos anteriormente: 2 bloques (cabecera y datos)

00 03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80 f1

ff f3 af a3

Es decir, 2 bloques de cinta de 19 y 4 bytes de datos, que conforman un SAVE de nuestros 2 bytes. Pues un
fichero TAP con estos datos sería, sencillamente, el escribir estos 2 bloques en un fichero anteponiendo a cada
bloque el tamaño a cargar:

13 00 00 03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80 f1

04 00 ff f3 af a3

Es decir “13 00” (número 19 en formato WORD, indicando el tamaño del bloque que viene a continuación)
seguido de los 19 bytes, y “04 00” (número 4 en formato WORD) seguido de los 4 bytes del bloque.
El contenido en binario de nuestro inicio_rom.tap sería, pues:

13 00 00 03 52 4f 4d 20 20 20 20 20 20 20 02 00 00 00 00 80 f1 04 00 ff f3 af a3

Y el tamaño resultante en bytes del fichero serían 2 + 19 + 2 + 4= 27 bytes.

Gracias a este formato tan sencillo, podemos unir ficheros TAP simplemente concatenándolos. De esta forma,
si tenemos nuestro “programa.tap” y la “graficos.tap”, y queremos unirlos porque nuestro programa, al
ejecutarse, carga los gráficos esperándolos en cinta tras el código del mismo, bastaría con hacer:

Linux: cat programa.tap graficos.tap > programa_final.tap


Windows: copy /B programa.tap graficos.tap programa_final.tap

Sabemos cómo podemos obtener nuestro programa en formato TAP: cogemos el código fuente, lo compilamos,
y o bien obtenemos un TAP directamente (pasmo –tapbas), o bien obtenemos un BIN que convertimos con
BIN2TAP. Pero … ¿cómo convertimos nuestro “graficos.bin”, “pantalla_carga.bin”, “musica.bin” o cualquier
otro fichero de datos en crudo? No podemos usar el BIN2TAP original porque éste añade un cargador BASIC
al principio del programa… hay múltiples soluciones, pero la más sencilla es utilizar un ensamblador como
pasmo.

Para convertir un fichero .bin en un fichero tap sin cabecera, creamos un pequeño programa ASM (rom.asm)
como el siguiente:

INCBIN "rom.bin"

A continuación, “ensamblamos” ese programa con PASMO indicando que queremos que nos genere un TAP
sin cabecera BASIC:

pasmo --tap rom.asm rom.tap

Con esto, obtendremos un fichero “rom.tap” con el contenido de rom.bin, y sin cargador BASIC, listo para
utilizar.

Ejemplo completo
Finamente, para aquellos programadores que quieran ver un ejemplo de aplicación práctica de la recuperación
de datos desde un binario en ejecución, vamos a juntar todo lo visto para realizar un programa que cargue una
pantalla gráfica completa (fichero .scr) sobre la videoRAM.

Los pasos a seguir para generar el ejemplo son los siguientes:

Primero, buscamos un fichero .SCR de carga (por ejemplo, la pantalla de carga de cualquier juego obtenida
desde WOS InfoSeek ) y lo almacenamos en disco.

Segundo, mediante pasmo obtenemos un TAP con los datos del fichero SCR, sin cabecera BASIC. Dicho TAP
tendrá un tamaño como el siguiente:

$ ls -l zxcolumns.*
-rw-r--r-- 1 sromero sromero 6912 2007-10-08 13:01 zxcolumns.scr
-rw-r--r-- 1 sromero sromero 6937 2007-10-08 13:02 zxcolumns.tap

Ahora ya tenemos una pantalla SCR guardada en formato TAP (en cinta). Nótese cómo podríamos cargar este
TAP desde BASIC con un LOAD “” CODE 16384,6912, y aparecería la imagen en pantalla.
Lo siguiente que necesitamos es el programa propiamente dicho, el cual hará la carga de la pantalla en
videomemoria:

;----------------------------------------------------------------------
; Loadscr.asm : Demostración de las rutinas LOAD de la ROM, con
; la carga de un fichero SCR (desde cinta) en videomemoria.
;----------------------------------------------------------------------

ORG 32000

AND A ; CF = 0 (verify)
CALL 1366 ; Cargamos e ignoramos la cabecera

SCF ; Set Carry Flag -> CF=1 -> LOAD


LD A, 255 ; A = 0xFF (cargar datos)
LD IX, 16384 ; Destino del load = 16384
LD DE, 6912 ; Tamaño a cargar = 6912
CALL 1366 ; Llamamos a la rutina de carga

RET

END 32000

Al respecto del código fuente, como habréis notado, realizamos 2 llamadas a la rutina de la ROM. La primera
carga (pero no almacena en ningún sitio) el primer bloque de datos existente (la cabecera de la pantalla de
carga). La rutina de la ROM ignorará esta carga porque el CARRY FLAG está a cero (0=VERIFY). La segunda
llamada a 1366 realizará la carga de los datos propiamente dichos. Al cargarlos sobre la dirección de destino
16384 (la dirección de la videoram), veremos cómo se van cargando sobre la pantalla directamente desde la
cinta.

Ensamblamos nuestro programa con “pasmo –tapbas loadscr.asm loadscr.tap” y tendremos lo siguiente:

• Un TAP (loadscr.tap) con nuestro programa (pero que no tiene datos después de él).
• Un TAP (zxcolumns.tap) con los datos gráficos (en este caso, una pantalla completa de 6912 bytes).

Si cargamos en el Spectrum, o en un emulador, el fichero loadscr.tap, nos encontraríamos con que intenta
cargar los datos desde cinta, pero no hay datos almacenados tras el programa. Para solucionarlo, concatenamos
los 2 TAPs (con cat en Linux o copy en Windows/DOS):

$ cat loadscr.tap zxcolumns.tap > programa.tap

Ahora sí, cargando “programa.tap” en el emulador, cargaremos nuestro programa, el cual llama a la rutina de la
ROM para cargar los datos que van después del programa (la pantalla de carga) en videomemoria. Si lo probáis
en un emulador, recordad deshabilitar las opciones de carga rápida o carga instantánea si queréis ver el efecto
de la carga.
Si os fijáis durante la carga, veréis como primero se carga el LOADER, luego el código máquina de nuestro
programa, y después la pantalla. Contando los tonos guía de carga también encontraréis el lugar donde se lee,
pero ignora, la cabecera (19 bytes, carga muy corta) de la pantalla SCR.

Un apunte: tanto en el caso de la carga, como de la grabación de datos, recordad que las rutinas de la ROM no
indican al usuario que debe pulsar PLAY o REC, por lo que debemos indicar al usuario cuándo debe pulsar
PLAY o REC dentro de nuestros programas o juegos. Incluso, cuando acabemos de cargar los datos relativos a
nuestro juego, resulta conveniente indicarle al usuario cuándo debe detener la cinta (especialmente en juegos
multicarga) e insertar los segundos de “espacio” que creamos convenientes entre bloques.

Recordad que en este ejemplo hemos cargado 6912 bytes de un fichero SCR directamente sobre la videoRAM,
pero nada nos impide cargar ficheros de cualquier otro tamaño, con cualquier otro contenido (sprites, fondos,
datos, mapeados) sobre cualquier lugar de la memoria (asegurándonos primero que no hay nada en el lugar
destino de la carga, como código, la pila, u otros datos o variables).

Así pues, de la misma forma que hemos cargado una pantalla SCR, podemos organizar los gráficos y mapeados
de nuestro juego en 1 “bloque de datos” por nivel, cargar los datos del nivel 1 tras acabar la carga de nuestro
programa, y sobreescribir estos gráficos y mapeados “en memoria” con los de los diferentes niveles según vaya
avanzando el jugador. En otras palabras, podemos hacer un juego multicarga que nos permita tener más sprites,
pantallas o gráficos disponibles para cada nivel, que los que tendríamos disponibles si cargamos todos los datos
de todos los niveles del juego, ya que usamos toda la memoria para cada nivel, en lugar de dividirla en espacio
para los diferentes niveles. A cambio, el usuario tendrá que cargar desde cinta las diferentes fases conforme
avanza, y rebobinar para cargar los datos del “Nivel 1” cuando deba empezar una nueva partida.

La rutina LD-BYTES (Load)

A continuación podemos ver el código desensamblado de la rutina LD-BYTES ($0556), la que estamos
utilizando para la carga de datos desde cinta. Es una rutina muy interesante y disponer de su código fuente
puede tener 2 usos directos.

El primero es poder comprender de forma exácta cómo funciona la carga de datos desde cinta y los tiempos que
se manejan en dicha carga. El segundo es el de reproducir la rutina en nuestro programa y añadir funciones
adicionales como un contador de carga o incluso algún tipo de minijuego durante la misma.

El código comentado está extraído del documento “The Complete Spectrum ROM Disassembly”, de Ian Logan
y Frank O'Hara.

; THE 'LD-BYTES' SUBROUTINE


; This subroutine is called to LOAD the header information (from 07BE)
; and later LOAD, or VERIFY, an actual block of data (from 0802).
0556 LD-BYTES:
INC D ; This resets the zero flag. (D
; cannot hold +FF.)
EX AF,A'F' ; The A register holds +00 for a
; header and +FF for a block of
; data.
; The carry flag is reset for
; VERIFYing and set for
; LOADing.
DEC D ; Restore D to its original value.

DI ; The maskable interrupt is now


; disabled.
LD A,+0F ; The border is made WHITE.
OUT (+FE),A
LD HL,+053F ; Preload the machine stack
PUSH HL ; with the address - SA/LD-RET.
IN A,(+FE) ; Make an initial read of port '254'
RRA ; Rotate the byte obtained but
AND +20 ; keep only the EAR bit,
OR +02 ; Signal 'RED' border.
LD C,A ; Store the value in the C register. -
; (+22 for 'off' and +02 for 'on'
; - the present EAR state.)
CP A ; Set the zero flag.

; The first stage of reading a tape involves showing that a pulsing


; signal actually exist (i.e. 'On/off' or 'off/on' edges.)

056B LD-BREAK RET NZ ; Return if the BREAK key is


; being pressed.
056C LD-START CALL 05E7,LD-EDGE-1 ; Return with the carry flag reset
JR NC,056B,LD-BREAK ; if there is no 'edge' within
; approx. 14,000 T states. But if
; an 'edge' is found the border
; will go CYAN.

; The next stage involves waiting a while and then showing


; that the signal is still pulsing.

LD HL,+0415 ; The length of this waiting


0574 LD-WAIT DJNZ 0574,LD-WAIT ; period will be almost one
DEC HL ; second in duration.
LD A,H
OR L
JR NZ,0574,LD-WAIT
CALL 05E3,LD-EDGE-2 ; Continue only if two edges are
JR NC,056B,LD-BREAK ; found within the allowed time
; period.

; Now accept only a 'leader signal'.

0580 LD-LEADER LD B,+9C ; The timing constant,


CALL 05E3,LD-EDGE-2 ; Continue only if two edges are
JR NC,056B,LD-BREAK ; found within the allowed time
; period.
LD A,+C6 ; However the edges must have
CP B ; been found within about
JR NC,056C,LD-START ; 3,000 T states of each other
INC H ; Count the pair of edges in the H
JR NZ,0580,LD-LEADER ; register until '256' pairs have
; been found.

; After the leader come the 'off' and 'on' part's of the sync pulse.

058F LD-SYNC LD B,+C9 ; The timing constant.


CALL 05E7,LD-EDGE-1 ; Every edge is considered until
JR NC,056B,LD-BREAK ; two edges are found close
LD A,B ; together - these will be the
CP +D4 ; start and finishing edges of
JR NC,058F,LD-SYNC ; the 'off' sync pulse.
CALL 05E7,LD-EDGE-1 ; The finishing edge of the
RET NC ; 'on' pulse must exist.
; (Return carry flag reset.)

; The bytes of the header or the program/data block can now be LOADed or
; VERIFied. But the first byte is the type flag.

LD A,C ; The border colours from now


XOR +03 ; on will be BLUE & YELLOW.

LD C,A
LD H,+00 ; Initialise the 'parity matching'
; byte to zero.
LD B,+B0 ; Set the timing constant for the
; flag byte.
JR 05C8,LD-MARKER ; Jump forward into the byte
; LOADING loop.

; The byte LOADing loop is used to fetch the bytes one at a time.
; The flag byte is first. This is followed by the data bytes and
; the last byte is the 'parity' byte.

05A9 LD-LOOP EX AF,A'F' ; Fetch the flags.


JR NZ,05B3,LD-FLAG ; Jump forward only when
; handling the first byte.
JR NC,05BD,LD-VERIFY ; Jump forward if VERIFYing a
; tape.
LD (IX+00),L ; Make the actual LOAD when
; required.
JR 05C2,LD-NEXT ; Jump forward to LOAD the
; next byte.
05B3 LD-FLAG RL C ; Keep the carry flag in a safe
; place temporarily.
XOR L ; Return now if the type flag does
RET NZ ; not match the first byte on the
; tape. (Carry flag reset.)
LD A,C ; Restore the carry flag now.
RRA
LD C,A
INC DE ; Increase the counter to
JR 05CA,LD-DEC ; compensate for its 'decrease'
; after the jump.

; If a data block is being verified then the freshly loaded byte is


; tested against the original byte.

05BD LD-VERlFY LD A,(IX+00) ; Fetch the original byte.


XOR L ; Match it against the new byte.
RET NZ ; Return if 'no match'. (Carry
; flag reset.)

; A new byte can now be collected from the tape.

05C2 LD-NEXT INC IX ; Increase the 'destination'.


05C4 LD-DEC DEC DE ; Decrease the 'counter'.
EX AF,A'F' ; Save the flags.
LD B,+B2 ; Set the timing constant.
05C8 LD-MARKER LD L,+01 ; Clear the 'object' register apart
; from a 'marker' bit.

; The 'LD-8-BITS' loop is used to build up a byte in the L register.

05CA LD-8-BITS CALL 05E3,LD-EDGE-2 ; Find the length of the 'off' and
; 'on' pulses of the next bit.
RET NC ; Return if the time period is
; exceeded. (Carry flag reset.)
LD A,+CB ; Compare the length against
; approx. 2,400 T states; resetting
CP B ; the carry flag for a '0' and
; setting it for a '1'.
RL L ; Include the new bit in the L
; register.
LD B,+B0 ; Set the timing constant for the
; next bit.
JP NC,05CA,LD-8-BITS ; Jump back whilst there are still
; bits to be fetched.

; The 'parity matching' byte has to be updated with each new byte.

LD A,H ; Fetch the 'parity matching'


XOR L ; byte and include the new byte.
LD H,A ; Save it once again.
; Passes round the loop are made until the 'counter' reaches zero.
; At that point the 'parity matching' byte should be holding zero.
LD A,D ; Make a further pass if the DE
OR E ; register pair does not hold
JR NZ,05A9,LD-LOOP ; zero.
LD A,H ; Fetch the 'parity matching'
; byte.
CP +01 ; Return with the carry flat set
RET ; if the value is zero.
; (Carry flag reset if in error.)

; THE 'LD-EDGE-2' AND 'LD-EDGE-1' SUBROUTINES


; These two subroutines form the most important part of the LOAD/VERIFY
; operation. The subroutines are entered with a timing constant in the B
; register, and the previous border colour and 'edge-type' in the C register.
; The subroutines return with the carry flag set if the required number
; of 'edges' have been found in the time allowed; and the change to the
; value in the B register shows just how long it took to find the 'edge(s)'.
; The carry flag will be reset if there is an error. The zero flag then
; signals 'BREAK pressed' by being reset, or 'time-up' by being set.
; The entry point LD-EDGE-2 is used when the length of a complete pulse
; is required and LD-EDGE-1 is used to find the time before the next 'edge'.

05E3 LD-EDGE-2 CALL 05E7,LD-EDGE-1 ; In effect call LD-EDGE-1 twice;


RET NC ; returning in between if there
; is an error.
05E7 LD-EDGE-1 LD A,+16 ; Wait 358 T states before
05E9 LD-DELAY DEC A ; entering the sampling loop.
JR NZ,05E9,LD-DELAY
AND A

; The sampling loop is now entered. The value in the B register is


; incremented for each pass; 'time-up' is given when B reaches zero.

05ED LD-SAMPLE INC B ; Count each pass.


RET Z ; Return carry reset & zero set if
; 'time-up'.
LD A,+7F ; Read from port +7FFE.
IN A,(+FE) ; i.e. BREAK & EAR.
RRA ; Shift the byte.
RET NC ; Return carry reset & zero reset
; if BREAK was pressed.
XOR C ; Now test the byte against the
AND +20 ; 'last edge-type'; jump back
JR Z,05ED,LD-SAMPLE ; unless it has changed.

; A new 'edge' has been found within the time period allowed for the search.
; So change the border colour and set the carry flag.

LD A,C ; Change the 'last edge-type'


CPL ; and border colour.
LD C,A
AND +07 ; Keep only the border colour.
OR +08 ; Signal 'MIC off'.
OUT (+FE),A ; Change the border colour (RED/
; CYAN or BLUE/YELLOW).
SCF ; Signal the successful search
RET ; before returning.

; Note: The LD-EDGE-1 subroutine takes 465 T states, plus an additional 58 T


; states for each unsuccessful pass around the sampling loop.

; For example, therefore, when awaiting the sync pulse (see LD-SYNC at 058F)
; allowance is made for ten additional passes through the sampling loop.
; The search is thereby for the next edge to be found within, roughly,
; 1,100 T states (465 + 10 * 58 + overhead). This will prove successful
; for the sync 'off' pulse that comes after the long 'leader pulses'.
Lectura del teclado en el Spectrum
Este capítulo está íntegramente dedicado a la lectura del teclado mediante la instrucción de lectura de puertos
“IN”. A lo largo del mismo veremos cómo responde el estado del puerto a los cambios del teclado, y subrutinas
útiles para la gestión del mismo.

Tras este capítulo seremos capaces de consultar el estado del teclado, ya sean teclas concretas predefinidas en el
código o redefinidas por el usuario, permitiéndonos interactuar con él y cubriendo una de las principales
necesidades a la hora de programar juegos para Spectrum: el control, tanto de los menúes como del juego en sí.

El teclado a nivel hardware


El teclado del Spectrum es una matriz de 40 pulsadores (40 teclas) que proporcionan al microprocesador, a
través de las líneas de Entrada/Salida, para diferentes filas, un valor de 8 bits en el rango 0-255, donde cada bit
indica el estado de una determinada tecla. Estrictamente hablando, la encargada de la lectura del teclado (como
de otros periféricos) es realmente la ULA, pero para nosotros esto es transparente y podremos utilizar las
funciones estándar de IO del microprocesador Z80 para conocer el estado del teclado. Para leer el estado de
cada una de las teclas del teclado (0 = pulsada, 1 = no pulsada), debemos obtener el estado del puerto $FE.

La pregunta en este momento debe de ser: si $FE es el puerto de lectura del teclado, y el Z80A es un
microprocesador con un bus de datos de 8 bits, ¿cómo leemos el estado de 40 teclas en un registro de 8 bits?

La respuesta es: organizando el teclado en filas de teclas y seleccionando qué fila leer mediante el registro B.

Si abrimos físicamente nuestro Spectrum, veremos que el teclado está conectado a la placa base del mismo
mediante 2 cintas de datos: una de ellas de 8 líneas y la otra de 5. La de 8 líneas (8 bits) se puede considerar
como el “byte de direcciones” del teclado, y es la que está unida al byte alto (bits 8 al 15) del bus de direcciones
del Spectrum. La de 5 bits está conectado a los 5 bits inferiores (del 0 al 4) del bus de datos. Mediante la
primera seleccionamos qué fila queremos leer (con el registro B), y mediante la segunda leemos el estado del
teclado (en ceros y unos).
Así, en nuestros programas podemos leer el estado del teclado accediendo a los puertos de Entrada / Salida del
microprocesador a los que están conectadas las diferentes líneas de dicha matriz.
Rescatemos el siguiente programa BASIC de uno de los capítulos iniciales del curso:

5 REM Mostrando el estado de la fila 1-5 del teclado ($F7FE)


10 LET puerto=63486
20 LET V=IN puerto: PRINT AT 20,0; V ; " " : GO TO 20

Este ejemplo lee (en un bucle infinito) una de las filas del teclado, concretamente la fila de las teclas del 1 al 5.
Esta fila tiene sus diferentes bits de estado conectados al puerto 63486 ($F7FEh), y como podéis suponer,
mediante la instrucción IN de BASIC realizamos la misma función que con su equivalente ensamblador:
consultar el valor contenido en dicho puerto.
El valor $FE se corresponde con el puerto del teclado, mientras que $F7 se corresponde con 11110111 en
binario, donde el cero selecciona la fila concreta del teclado que queremos leer, en este caso, la fila de teclas del
1 al 5.

Por defecto, sin pulsar ninguna tecla, los diferentes bits del valor leído en dicho puerto estarán a 1. Cuando
pulsamos una tecla, el valor del bit correspondiente a dicha tecla aparecerá como 0, y soltándola volverá a su
valor 1 original.

Al ejecutar el ejemplo en BASIC, veremos que la pulsación de cualquier tecla modifica el valor numérico que
aparece en pantalla. Si pasamos dicho valor numérico a formato binario veremos cómo el cambio del valor se
corresponde con las teclas que vamos pulsando y liberando.

Si no hay ninguna tecla pulsada, los 5 bits más bajos del byte que hay en el puerto estarán todos a 1, mientras
que si se pulsa alguna de las teclas del 1 al 5, el bit correspondiente a dicha tecla pasará a estado 0. Nosotros
podemos leer el estado del puerto y saber, mirando los unos y los ceros, si las teclas están pulsadas o no. Los 3
bits más altos del byte debemos ignorarlos para este propósito, ya que no tienen relación alguna con el teclado
(son los bits de acceso al cassette, a la unidad de cinta y al altavoz del Spectrum).

Así, por ejemplo, leyendo del puerto 63486 obtenemos un byte cuyos 5 últimos bits tienen como significado el
estado de cada una de las teclas de la semifila del “1” al “5”.

Bits: D7 D6 D5 D4 D3 D2 D1 D0
Teclas: XX XX XX “5” “4” “3” “2” “1”

Como ya hemos dicho, los bits D7 a D5 no nos interesan en el caso del teclado (por ejemplo, D6 tiene relación
con la unidad de cinta), mientras que los bits de D4 a D0 son bits de teclas (0=pulsada, 1=no pulsada). Tenemos
pues el teclado dividido en filas de teclas y disponemos de una serie de puertos para leer el estado de todas
ellas:

Puerto Teclas
65278d ($FEFE) de CAPS SHIFT a V
65022d ($FDFE) de A a G
64510d ($FBFE) de Q a T
63486d ($F7FE) de 1 a 5 (y JOYSTICK 1)
61438d ($EFFE) de 6 a 0 (y JOYSTICK 2)
57342d ($DFFE) de P a Y
49150d ($BFFE) de ENTER a H
32766d ($7FFE) de (space) a B

En el resultado de la lectura de estos puertos, el bit menos significativo (D0) siempre hace referencia a la tecla
más alejada del centro del teclado (“1” en nuestro ejemplo), mientras que el más significativo de los 5 (D5) lo
hace a la tecla más cercana al centro del teclado.

Concretamente:
Puerto Bits: D4 D3 D2 D1 D0
65278d ($FEFE) Teclas: “V” “C” “X” “Z” CAPS
65022d ($FDFE) Teclas: “G” “F” “D” “S” “A”
64510d ($FBFE) Teclas: “T” “R” “E” “W” “Q”
63486d ($F7FE) Teclas: “5” “4” “3” “2” “1”
61438d ($EFFE) Teclas: “6” “7” “8” “9” “0”
57342d ($DFFE) Teclas: “Y” “U” “I” “O” “P”
49150d ($BFFE) Teclas: “H” “J” “K” “L” ENTER
32766d ($7FFE) Teclas: “B” “N” “M” SYMB SPACE
SINCLAIR 1 y 2 (las mismas teclas 0-9)
61438d ($EFFE) SINCL1 LEFT RIGHT DOWN UP FIRE
63486d ($F7FE) SINCL2 FIRE DOWN UP RIGHT LEFT

Como puede verse en la tabla, la parte baja de los 16 bits del puerto representan siempre $FE (254d), el puerto
al que está conectado el teclado.

La parte alta es, la única que varía según la semifila a leer, y su valor consiste, como hemos visto, en la puesta a
cero de la semifila deseada, teniendo en cuenta que cada semifila de teclas está conectada a uno de los bits del
bus de direcciones:

FILA DE TECLAS LINEA BUS VALOR VALOR BINARIO


CAPSSHIFT a V A8 $FE 11111110
A-G A9 $FD 11111101
Q-T A10 $FB 11111011
1-5 A11 $F7 11110111
6-0 A12 $EF 11101111
Y-P A13 $DF 11011111
H-ENTER A14 $BF 10111111
B-SPACE A15 $7F 01111111

Así, al mandar estos valores en la lectura de puerto (en la parte alta del mismo), lo que hacemos realmente es
seleccionar cuál de las líneas del bus de direcciones (cada una de ellas conectada a una fila del teclado)
queremos leer.

En resumen: es posible obtener el estado de una tecla determinada leyendo de un puerto de Entrada / Salida del
microprocesador. Este puerto de 16 bits se compone poniendo en su parte alta el valor de la semifila de teclado
a leer (todo “1”s excepto un “0” en la semifila de interés, según la tabla vista anteriormente), y poniendo en su
parte baja el valor $FE. El IN de dicho puerto nos proporcionará un valor de 8 bits cuyos 5 últimos bits
indicarán el estado de las 5 teclas de la semifila seleccionada (1=no pulsada, 0=pulsada).

(Nótese que es posible leer el estado de 2 o más semifilas simultáneamente haciendo 0 a la vez valor 2 de los
bits del byte alto del puerto. El resultado obtenido será un AND del estado de los bits de todas las semifilas
leídas).

Ejemplo práctico: leyendo el teclado


Veamos un ejemplo en ASM que se queda en un bucle infinito hasta que pulsamos la tecla “p”:
; Lectura de la tecla "P" en un bucle
ORG 50000

bucle:
LD BC, $DFFE ; Semifila "P" a "Y"
IN A, (C) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, salir ; Si esta a 0 (pulsado) salir.
JR bucle ; Si no (a 1, no pulsado) repetimos

salir:
RET

END 50000

De nuevo ensamblamos nuestro programa con “pasmo –tapbas keyb1.asm keyb1.tap”, y lo cargamos en el
Spectrum o en un emulador.

Efectivamente, el programa se mantendrá en un bucle infinito hasta que se ponga a cero el bit 0 del puerto
$DFFE, que se corresponde con el estado de la tecla “P”. Al pulsar esa tecla, la comparación hecha con BIT
hará que el Zero Flag se active y el “JR Z” saldrá de dicho bucle, retornando al BASIC.

Nótese cómo en ciertos casos puede ser más recomendable la utilización de una u otra forma de IN. Por
ejemplo, nuestro ejemplo anterior se podría escribir con “IN A, (N)”, y sería más recomendable, puesto que
evita el tener que utilizar el registro B:

; Lectura de la tecla "P" en un bucle (FORMA 2)


ORG 50000

bucle:
LD A, $DF ; Semifila "P" a "Y"
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, salir ; Si esta a 0 (pulsado) salir.
JR bucle ; Si no (a 1, no pulsado) repetimos

salir:
RET
END 50000

En este ejemplo B no se usa, usamos A para albergar la semifila a leer, que no nos afecta puesto que ya íbamos
a perder su valor tras la lectura (como resultado de ella). Si estamos usando B, por ejemplo como contador de
un bucle, nos interesa más esta forma de uso.

Esperar pulsación y esperar liberación de tecla


Veamos otras 2 rutinas interesantes para nuestros programas. La primera espera a que se pulse cualquier tecla
(por ejemplo, para realizar una pausa), y la segunda espera a que se suelte la tecla pulsada (esta la podemos usar
tras detectar una pulsación para esperar a que el usuario suelte la tecla y no volver a leer la misma tecla en una
segunda iteración de nuestro bucle).

Ambas rutinas se basan en el hecho de que, realmente, es posible leer más de una semifila del teclado
simultáneamente. Como ya vimos, el valor que mandamos como byte alto del puerto tiene a valor 0 el bit que
representa a la línea de datos que queremos leer.

Pero como vamos a ver, podemos leer más de una línea simultáneamente, poniendo a cero tantos bits como
deseemos en la parte alta del puerto. En estos ejemplos, con el XOR A (que equivale a “LD A, 0” dado que un
XOR de un registro con sí mismo es cero) dejamos a 0 todo el byte alto con lo que leemos la información de
todas las semifilas simultáneamente.
Este tipo de “multilectura” no nos permite saber de forma exacta qué tecla ha sido pulsada. Por ejemplo, si
intentamos leer simultaneamente las semifilas 1-5 y Q-T, el bit 0 del resultado valdrá “0” (tecla pulsada) si se
pulsa “1”, se pulsa “Q”, o se pulsan ambas.

Así, podemos saber que una de las teclas de las semifilas está siendo leída, pero no cuál de ellas. Para saber qué
valor debemos enviar al puerto para leer varias semifilas de forma simultánea, recordemos la tabla vista en el
anterior apartado:

FILA DE TECLAS BIT A CERO VALOR BINARIO


CAPSSHIFT a V A8 11111110
A-G A9 11111101
Q-T A10 11111011
1-5 A11 11110111
6-0 A12 11101111
Y-P A13 11011111
H-ENTER A14 10111111
B-SPACE A15 01111111

Así, para leer tanto 1-5 como Q-T, necesitamos tener 2 ceros: uno en el bit 10 y otro en el bit 11. El valor
decimal que corresponde con (11110011d), 243d, nos permite la lectura de ambas semifilas de forma
simultánea.

En el caso de una rutina de espera de pulsación o liberación de tecla, podemos hacer la parte alta del byte igual
a cero (activar todas las semifilas) para leer todo el teclado, ya que no nos importa cuál de las teclas ha sido
pulsada sino el hecho de que lo haya sido o no.

;-----------------------------------------------------------------------
; Esta rutina espera a que haya alguna tecla pulsada para volver,
; consultando las diferentes filas del teclado en un bucle.
;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
XOR A ; A = 0
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed
RET

;-----------------------------------------------------------------------
; Esta rutina espera a que no haya ninguna tecla pulsada para volver,
; consultando las diferentes filas del teclado en un bucle.
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
XOR A
IN A, ($FE)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET

Existe otra forma de codificar Wait_For_Keys_Pressed consistente en utilizar CPL para “complementar” los
bits del resultado leído:

Wait_For_Key_Pressed:
XOR A ; Leer todas las teclas
IN A, ($FE)
CPL
AND 1Fh ; Comprobar todos los unos
JR Z, Wait_For_Key_Pressed
RET

Leyendo todas las direcciones


En el siguiente ejemplo veremos cómo leer las 4 direcciones más la tecla de disparo en un juego y codificar la
información de la lectura en un formato usable en el bucle principal del programa (para poder chequear
cómodamente el estado de las teclas consultadas).

La idea sería que, utilizando los diferentes bits de un byte, podemos codificar el estado de las 5 teclas básicas
(arriba, abajo, izquierda, derecha, disparo) en 5 de los 8 bits de un registro, y que nuestra función de detección
de teclado sea algo parecido a lo siguiente:

; Lee el estado de O, P, Q, A, ESPACIO y devuelve


; en A en A el estado de las teclas (1=pulsada, 0=no pulsada).
; El byte está codificado de forma que:
;
; BITS 4 3 2 1 0
; SIGNIFICADO FIRE LEFT RIGHT DOWN UP
;
LEER_TECLADO:

LD D, 0 ; Keyboard status flag register (D)

LD BC, $FBFE
IN A, (C)
BIT 0, A ; Leemos la tecla Q
JR NZ, Control_no_up ; No pulsada, no cambiamos nada en D
SET 0, D ; Pulsada, ponemos a 1 el bit 0
Control_no_up:

LD BC, $FDFE
IN A, (C)
BIT 0, A ; Leemos la tecla A
JR NZ, Control_no_down ; No pulsada, no cambianos nada en D
SET 1, D ; Pulsada, ponemos a 1 el bit 1
Control_no_down:

LD BC, $DFFE
IN A, (C)
BIT 0, A ; Leemos la tecla P
JR NZ, Control_no_right ; No pulsada
SET 2, D ; Pulsada, ponemos a 1 el bit 2
Control_no_right:
; BC ya vale $DFFE, (O y P en misma fila)
BIT 1, A ; Tecla O
JR NZ, Control_no_left
SET 3, D
Control_no_left:

LD BC, $7FFE
IN A, (C)
BIT 0, A ; Tecla Espacio
JR NZ, Control_no_fire
SET 4, D
Control_no_fire:

LD A, D ; Devolvemos en A el estado de las teclas


RET

El bucle principal del programa deberá llamar a esta función de lectura del teclado y después, con el valor
devuelto en el registro A, actuar consecuentemente de acuerdo al estado de los bits (aprovechando las funciones
de testeo de bits del Z80). De esta forma, al volver de la llamada a esta subrutina sabremos si el usuario
pretende mover el personaje en una dirección u otra, o si ha pulsado disparo, según el estado de los diferentes
bits. La rutina que hemos visto trabaja con 5 teclas, pero todavía tenemos espacio para almacenar el estado de 3
teclas más en los bits 5, 6 y 7 del registro A.

Con esta información sobre el teclado y las instrucciones IN y OUT, nada os impide dotar de interacción y
movimiento vuestros programas, utilizando la lectura del teclado en menúes, acciones sobre los personajes, etc.

Por otra parte, el código que acabamos de ver se basa en leer las semifilas de teclado y comprobar el estado de
teclas concretas y definidas (“hardcodeadas”) en el propio listado del programa. Esto quiere decir que las teclas
de control son fijas y no seleccionables por el usuario. Este es el mecanismo de “lectura de teclado” más rápido
y que menos espacio ocupa puesto que no es necesario crear rutinas para re-definir las teclas de juego,
almacenarlas en variables de memoria y comparar el estado del teclado de los scancodes seleccionados por el
usuario.

No obstante, una de las cosas que más agradecen los usuarios es la posibilidad de que las teclas de control se
puedan redefinir y no sean fijas, por lo que a continuación veremos mecanismos para obtener del usuario las
teclas de control y posteriormente poder detectar su pulsación en el transcurso de la lógica del programa o
juego.

Redefinicion de teclas

Hasta ahora hemos visto cómo verificar el estado de unas teclas predeterminadas del teclado. Normalmente los
juegos o programas suelen incluir la opción de redefinir las teclas asociadas a las acciones del juego, de forma
que sea el jugador quien elija la combinación de teclas con la que se sienta más cómodo.

Para ello necesitaremos una rutina que haga lo siguiente:

• Antes de ser llamada, deberemos llegar a ella con el mensaje apropiado en pantalla (“Pulse tecla para
ARRIBA”, por ejemplo). La rutina se dedicará sólo al escaneo del teclado en sí mismo, de forma que
pueda ser llamada tantas veces como teclas a redefinir. Además, tenemos que asegurarnos de que
cuando la llamamos no haya ninguna tecla pulsada. Para eso podemos usar la rutina
Wait_For_Keys_Released vista previamente.
• La rutina deberá devolver un valor único para cada tecla pulsada, y asegurarse (o informarnos) de que
no está siendo pulsada más de una tecla simultáneamente.
• Dicho valor devuelto por la rutina será almacenado para su posterior chequeo durante el juego,
utilizando una rutina que nos indique si la tecla asociada a ese “valor único” está siendo pulsada.

David Webb nos ofrece el siguiente conjunto de rutinas para este propósito. Existen bastantes posibilidades de
realizar esta tarea (tablas con las semifilas y bits y sus correspondientes en ASCII, modificación en tiempo real
de los opcodes que hacen los testeos, elegir entre un conjunto de combinaciones de teclas predeterminadas, etc),
pero la que nos muestra David Webb es elegante y sencilla de utilizar.

Consiste en escanear el teclado completo y, al detectar la pulsación de una tecla, codificar la semifila y el bit
donde se han detectado en un mismo byte, utilizando los 3 bits más bajos para “el bit de la tecla pulsada” y los
3 siguientes para “la semifila (puerto)” en que se ha detectado la pulsación.

; Chequea el teclado para detectar la pulsación de una tecla.


; Devuelve un código en el registro D que indica:
;
; Bits 0, 1 y 2 de "D": Fila de teclas (puerto) detectada.
; Bits 3, 4 y 5 de "D": Posición de la tecla en esa media fila
;
; Así, el valor devuelto nos indica la semifila a leer y el bit a testear.
; El registro D valdrá 255 ($FF) si no hay ninguna tecla pulsada.
;
; Flags: ZF desactivado: Más de una tecla pulsada
; ZF activado: Tecla correctamente leída
Find_Key:

LD DE, $FF2F ; Valor inicial "ninguna tecla"


LD BC, $FEFE ; Puerto

NXHALF:
IN A, (C)
CPL
AND $1F
JR Z, NPRESS ; Saltar si ninguna tecla pulsada

INC D ; Comprobamos si hay más de 1 tecla pulsada


RET NZ ; Si es así volver con Z a 0

LD H, A ; Cálculo del valor de la tecla


LD A, E

KLOOP:
SUB 8
SRL H
JR NC, KLOOP

RET NZ ; Comprobar si más de una tecla pulsada

LD D, A ; Guardar valor de tecla en D

NPRESS: ; Comprobar el resto de semifilas


DEC E
RLC B
JR C, NXHALF ; Repetimos escaneo para otra semifila

CP A ; Ponemos flag a zero


RET Z ; Volvemos

La forma en que llamaríamos a esta subrutina sería la siguiente:

;; Pedimos tecla ARRIBA


CALL Imprimir_Texto_Pulse_Arriba
CALL Wait_For_Keys_Released ; Esperamos teclado libre

Pedir_Arriba:

CALL Find_Key ; Llamamos a la rutina


JR NZ, Pedir_Arriba ; Repetir si la tecla no es válida
INC D
JR Z, Pedir_Arriba ; Repetir si no se pulsó ninguna tecla
DEC D

LD A, D
LD (tecla_arriba), A

;; Pedimos siguiente tecla (ABAJO)


CALL Imprimir_Texto_Pulse_Abajo
CALL Wait_For_Keys_Released ; Esperamos teclado libre

Pedir_Abajo:

CALL Find_Key ; Llamamos a la rutina


JR NZ, Pedir_Abajo ; Repetir si la tecla no es válida
INC D
JR Z, Pedir_Abajo ; Repetir si no se pulsó ninguna tecla
DEC D
LD A, D
LD (tecla_abajo), A

;;; Repetir el mismo código para IZQ, DERECHA, DISPARO, etc.

tecla_arriba DEFB 0
tecla_abajo DEFB 0

A continuación podemos ver un ejemplo (scancode.asm) que “dibuja” en pantalla de forma gráfica el
SCANCODE que devuelve la función Find_Key:

; Visualizando los scancodes de las teclas codificadas con "Find_Key"

ORG 50000

Bucle_entrada:
CALL Wait_For_Keys_Released

Pedir_Tecla:
CALL Find_Key ; Llamamos a la rutina
JR NZ, Pedir_Tecla ; Repetir si la tecla no es valida
INC D
JR Z, Pedir_Tecla ; Repetir si no se pulsa ninguna tecla
DEC D

LD A, D ; Guardamos en A copia del resultado


CALL PrintBin ; Imprimimos el scancode bin en pantalla

LD A, D ; Guardamos en A copia del resultado


CALL PrintHex ; Imprimimos el scancode hex en pantalla

CP $21 ; Comprobamos si A == 21h (enter)


JR NZ, Bucle_entrada ; Si no lo es, repetir

RET ; Si es enter, fin del programa

;-----------------------------------------------------------------------
; PrintBin: Imprime en la pantalla un patron para visualizar el valor
; de A en binario, usando 8 pixels "puros" para "1" y punteados para "0"
;
; Entrada: A = valor a "imprimir" en binario
;-----------------------------------------------------------------------
PrintBin:
PUSH AF
PUSH HL
PUSH BC ; Preservamos los registros que se usará

LD HL, 20704 ; Esquina (0,24) de la pantalla


LD C, A ; Guardamos en C copia de A
LD B, 8 ; Imprimiremos el estado de los 8 bits

printbin_loop:
LD A, $FF ; Para bit = 1, todo negro
BIT 7, C ; Chequeamos el estado del bit 7
JR NZ, printbin_es_uno ; Dejamos A = 255
LD A, $55 ; Para bit = 0, punteado/gris

printbin_es_uno:
LD (HL), A ; Lo "imprimimos" (A) y pasamos a la
INC HL ; Siguiente posició en memoria
RLC C ; Rotamos C a la izq para que podamos
; usar de nuevo el BIT 7 en el bucle
DJNZ printbin_loop ; Repetimos 8 veces

POP BC
POP HL
POP AF
RET

;-----------------------------------------------------------------------
; PrintHex: Imprime en la pantalla un numero de 1 byte en hexadecimal.
; Para ello convierte el valor numérico en una cadena llamando
; a Byte2ASCII_Hex y luego llama a RST 16 para imprimir cada
; caracter por separado. Imprime un $ delante y ESPACIO detrás.
;
; Entrada: A = valor a "imprimir" en binario
;-----------------------------------------------------------------------
PrintHex:
PUSH HL
PUSH AF
PUSH DE

LD H, A
CALL Byte2ASCII_Hex ; Convertimos A en Cadena HEX
LD HL, Byte2ASCII_output ; HL apunta a la cadena

LD A, "$"
RST 16 ; Imprimimos un "$"

LD A, (HL)
RST 16 ; Imprimimos primer valor HEX

INC HL ; Avanzar en la cadena


LD A, (HL)
RST 16 ; Imprimimos segundo valor HEX

LD A, " "
RST 16 ; Imprimimos un espacio

POP DE
POP AF
POP HL

RET

;-----------------------------------------------------------------------
; Byte2ASCII_Hex: Convierte el valor del registro H en una cadena
; de texto de max. 2 caracteres hexadecimales, para poder imprimirla.
; Rutina adaptada de Num2Hex en http://baze.au.com/misc/z80bits.html .
;
; IN: H = Numero a convertir
; OUT: [Byte2ASCII_output] = Espacio de 2 bytes con los ASCIIs
;-----------------------------------------------------------------------
Byte2ASCII_Hex:

ld de, Byte2ASCII_output
ld a, h
call B2AHex_Num1
ld a, h
call B2AHex_Num2
ret

B2AHex_Num1:
rra
rra
rra
rra

B2AHex_Num2:
or $F0
daa
add a, $A0
adc a, $40
ld (de), a
inc de
ret
Byte2ASCII_output DB 0, 0
;-----------------------------------------------------------------------

; Debemos incluir, además, el código de Wait_For_Keys_Released y


; de Find_Key dentro de este ejemplo para que ensamble correctamente.

; Nota: recuerda que acabando el programa con END 50000 no sería necesario
; ejecutarlo manualmente con randomize usr 50000 al ensamblarlo con PASMO.

Este ejemplo proporcionará en pantalla (hasta que se pulse ENTER) una salida como la siguiente:

Mediante Byte2ASCII_Hex convertimos el scancode en una cadena de 2 caracteres, que imprimimos en


pantalla con PrintHex. Esta función, PrintHex, hace uso de la RST 16 para trazar caracteres por pantalla en la
posición actual del cursor.

Por otra parte, se incluye una rutina PrintBin para mostrar el estado de los diferentes bits del valor del scancode
mediante pixeles “encendidos” y “apagados”. Como véis, la rutina PrintBin es una forma rudimentaria de
mostrar en pantalla el valor del registro A en binario. Lo que muestra es una representación gráfica binaria de
las teclas pulsadas.

Los scancodes asociados a las diferentes teclas son:

Teclas: 1 2 3 4 5 6 7 8 9 0
Scancodes: $24 $1C $14 $0C $04 $03 $0B $13 $1B $23
Teclas: Q W E R T Y U I O P
Scancodes: $25 $1D $15 $0D $05 $02 $0A $12 $1A $22
Teclas: A S D F G H J K L ENTER
Scancodes: $26 $1E $16 $0E $06 $01 $09 $11 $19 $21
Teclas: CAPS Z X C V B N M SYMB SPACE
Scancodes: $27 $1F $17 $0F $07 $00 $08 $10 $18 $20

Estos valores nos serán necesarios si queremos establecer unos scancodes por defecto para las teclas del
programa, de forma que si el usuario no las redefine, tengan unos valores de comprobación determinados para
la rutina de chequeo que veremos a continuación.
Chequeando las teclas redefinidas
Llegados a este punto tenemos una función que nos devuelve un “scancode” propio (creado a nuestra medida)
de una tecla pulsada. De esta forma, podemos almacenar en variables de memoria (por ejemplo: “tecla_arriba
DEFB 0”) los valore que nos devuelve dicha función. En lugar de dar a estas variables un valor de 0 por
defecto, tenemos una tabla de “scancodes” que nos permitiría definir unas “teclas iniciales” como:

tecla_arriba DEFB $25


tecla_abajo DEFB $26
tecla_izq DEFB $1A
tecla_der DEFB $22
tecla_disp DEFB $20

Dichos valores podrán ser modificados (o no) por la rutina de redefinición del teclado.

Lo único que nos falta para un control total del teclado en nuestro juego sería una rutina que reciba un scancode
y nos indique si dicho scancode está pulsado o no. De esta forma, llamaríamos a la rutina 5 veces, poniendo el
valor de las diferentes teclas (tecla_arriba, tecla_abajo, etc.) en el registro A antes de cada llamada, para
conocer el estado de las mismas.

Llamaremos a esta rutina Check_Key:

; Chequea el estado de una tecla concreta, aquella de scancode


; codificado en A (como parametro de entrada).
;
; Devuelve: CARRY FLAG = 0 -> Tecla pulsada
; CARRY FLAG = 1 y BC = 0 -> Tecla no pulsada
;
Check_Key:
LD C, A ; Copia de A

AND 7
INC A
LD B, A ; B = 16 - (num. linea dirección)

SRL C
SRL C
SRL C
LD A, 5
SUB C
LD C, A ; C = (semifila + 1)

LD A, $FE

CKHiFind: ; Calcular el octeto de mayor peso del puerto


RRCA
DJNZ CKHiFind

IN A, ($FE) ; Leemos la semifila

CKNXKey:
RRA
DEC C
JR NZ, CKNXKey ; Ponemos el bit de tecla en el CF

RET

La forma en que se debe llamar a esta rutina sería la siguiente:

Comprobar_tecla_izquierda:
LD A, (teclaizq)
CALL Check_Key
JR C, izq_no_pulsada ; Carry = 1, tecla no pulsada
(acciones a realizar si se pulso izq)

izq_no_pulsada:

Comprobar_tecla_derecha:
LD A, (teclader)
CALL Check_Key
JR C, der_no_pulsada ; Carry = 1, tecla no pulsada

(acciones a realizar si se pulso der)

; Repetir para arriba, abajo, disparo, etc.

También podemos generar una rutina que combine lo que acabamos de ver con la codificación de las
direcciones en los 5 bits de un único byte, de la misma forma que lo realizamos con teclas predefinidas.

Por supuesto, estas rutinas son sólo de ejemplo y pueden ser modificadas para que devuelvan los resultados en
otros flags (Zero, Carry), en otros registros, sean llamadas con diferentes parámetros, etc.

Veamos a continuación un ejemplo final que permite modificar el valor de una variable en memoria (“valor”)
mediante las teclas Q y A (sumando o restando 1 a su valor de 8 bits). Cada vez que el valor de la variable
cambie, se mostrará en pantalla con nuestra la sencilla rutina PrintBin.

; Controlando el valor de "valor" con Q y A


ORG 50000

LD A, (valor)
CALL PrintBin ; Imprimimos el scancode en pantalla

Bucle_entrada:

LD BC, 20000 ; Retardo (bucle 20000 iteraciones)


retardo:
DEC BC
LD A, B
OR C
JR NZ, retardo ; Fin retardo

Comprobar_tecla_mas:
LD A, (tecla_mas)
CALL Check_Key
JR C, mas_no_pulsado ; Carry = 1, tecla_mas no pulsada

LD A, (valor)
INC A
LD (valor), A ; Incrementamos (valor)
JR Print_Valor

mas_no_pulsado:

Comprobar_tecla_menos:
LD A, (tecla_menos)
CALL Check_Key
JR C, menos_no_pulsado ; Carry = 1, tecla_menos no pulsada

LD A, (valor)
DEC A
LD (valor), A ; Decrementamos (valor)
JR Print_Valor

menos_no_pulsado:

JR Bucle_entrada ; Repetimos continuamente hasta que se


; pulse algo (tecla_mas o tecla_menos)

Print_Valor:
LD A, (valor) ; Guardamos en A copia del resultado
CALL PrintBin ; Imprimimos el scancode en pantalla

LD A, (valor) ; Guardamos en A copia del resultado


CALL PrintHex ; Imprimimos el scancode HEX en pantalla

JR Bucle_entrada ; Repetimos

valor DEFB 0
tecla_mas DEFB $25
tecla_menos DEFB $26

;-----------------------------------------------------------------------
; Chequea el estado de una tecla concreta, aquella de scancode
; codificado en A (como parametro de entrada).
;
; Devuelve: CARRY FLAG = 0 -> Tecla pulsada
; CARRY FLAG = 1 y BC = 0 -> Tecla no pulsada
;-----------------------------------------------------------------------
Check_Key:
LD C, A ; Copia de A

AND 7
INC A
LD B, A ; B = 16 - (num. linea direcció
SRL C
SRL C
SRL C
LD A, 5
SUB C
LD C, A ; C = (semifila + 1)

LD A, $FE

CKHiFind: ; Calcular el octeto de mayor peso del puerto


RRCA
DJNZ CKHiFind

IN A, ($FE) ; Leemos la semifila

CKNXKey:
RRA
DEC C
JR NZ, CKNXKey ; Ponemos el bit de tecla en el CF

RET

;; Nota: Incluir en el código las siguientes rutinas para re-ensamblarlo:


;; Wait_For_Keys_Released, PrintBin, PrintHex y Byte2ASCII_Hex.

END 50000

Queda como ejercicio para el lector la modificación del programa para que, antes de entrar en el bucle
principal, lea 2 teclas válidas y diferentes del teclado para permitir la redefinición de “tecla_mas” y
“tecla_menos” con respecto a sus valores por defecto.

Redefinición de las teclas


En las secciones anteriores hemos visto las rutinas Find_Key y Check_Key para detectar las pulsaciones de
teclas y chequear el estado de una tecla concreta. Estas teclas concretas las guardamos en variables de memoria
y así podemos permitir al jugador redefinirlas.
El menú principal deberá de tener una opción que permita modificar el contenido de estas variables de memoria
con aquellos scancodes que el jugador elija para controlar el juego.

El sistema de redefinición de teclas debe:

A.- Establecer en el arranque del programa unos valores por defecto para las teclas:

tecla_arriba DEFB $25


tecla_abajo DEFB $26
tecla_izq DEFB $1A
tecla_der DEFB $22
tecla_disp DEFB $20

B.- Repetir N veces (uno por cada control a redefinir):

• Esperar a que ninguna tecla del teclado esté pulsada (para evitar que la tecla de selección del menú para
entrar en la redefinición, o la anterior tecla pulsada, se seleccione como tecla pulsada por el usuario).

• Mostrar por pantalla el mensaje de “Pulse una tecla para (dirección a redefinir)”.

• Esperar una pulsación de teclado del usuario.

• Opcionalmente, comprobar que esa pulsación no se corresponda con ninguna de las teclas anteriores,
para evitar que el usuario seleccione la misma dirección para, por ejemplo, izquierda y derecha. Este
paso es opcional porque el usuario, si se equivoca, siempre puede redefinir de nuevo el teclado con las
teclas adecuadas, y para nosotros esta comprobación representa tiempo de programación y espacio
ocupado innecesariamente en el programa.

• Mostrar al usuario la tecla que ha pulsado a la derecha del mensaje impreso pidiendo dicha tecla. Para
eso tenemos que convertir el Scancode en un código ASCII imprimible en pantalla.

• Modificar la variable en memoria que deba almacenar el scancode de la tecla pulsada para poder usarla
posteriormente en el transcurso del juego (es decir, guardar el scancode obtenido en tecla_arriba,
tecla_abajo, tecla_izq, o en la variable que corresponda).

Hasta ahora tenemos todos los mecanismos necesarios para crear nuestra propia rutina de redefinición de teclas,
salvo la rutina para convertir un scancode en su correspondiente ASCII. A continuación tenemos una rutina
Scancode2Ascii basada en una tabla que relaciona cada scancode con su ASCII (40 bytes más adelante en la
misma tabla):

;-----------------------------------------------------------------------
; Scancode2Ascii: convierte un scancode en un valor ASCII
; IN: D = scancode de la tecla a analizar
; OUT: A = Codigo ASCII de la tecla
;-----------------------------------------------------------------------
Scancode2Ascii:

push hl
push bc

ld hl,0
ld bc, TABLA_S2ASCII
add hl, bc ; hl apunta al inicio de la tabla

; buscamos en la tabla un max de 40 veces por el codigo


; le sumamos 40 a HL, leemos el valor de (HL) y ret A
SC2Ascii_1:
ld a, (hl) ; leemos un byte de la tabla
cp "1" ; Si es "1" fin de la rutina (porque en
; (la tabla habriamos llegado a los ASCIIs)
jr z, SC2Ascii_Exit ; (y es condicion de forzado de salida)
inc hl ; incrementamos puntero de HL
cp d ; comparamos si A==D (nuestro scancode)
jr nz, SC2Ascii_1

SC2Ascii_Found:
ld bc, 39 ; Sumamos 39(+INC HL=40) para ir a la seccion
add hl, bc ; de la tabla con los codigos ASCII
ld a,(hl) ; leemos el codigo ASCII de esa tabla

SC2Ascii_Exit:
pop bc
pop hl
ret

; 40 scancodes seguidos de sus ASCIIs equivalentes


TABLA_S2ASCII:
defb $24, $1C, $14, $0C, $04, $03, $0B, $13, $1B, $23
defb $25, $1D, $15, $0D, $05, $02, $0A, $12, $1A, $22
defb $26, $1E, $16, $0E, $06, $01, $09, $11, $19, $21
defb $27, $1F, $17, $0F, $07, $00, $08, $10, $18, $20
defm "1234567890QWERTYUIOPASDFGHJKLecZXCVBNMys"

La rutina recibe en el registro D el scancode obtenido con la rutina Find_Key y devuelve en el registro A el
código ASCII correspondiente directamente imprimible. Los primeros 40 bytes de la tabla contienen los
Scancodes y la última línea defm (últimos 40 bytes) los ASCIIs a los que corresponden, en orden, los anterior
40 códigos.

Nótese que las teclas se devuelven como ASCIIs en mayúsculas, aprovechando las letras minúsculas para los
caracteres especiales no directamente representables:

• e = ENTER
• c = CAPS SHIFT
• y = SYMBOL SHIFT
• s = SPACE

De esta forma, “E” se corresponde a la tecla E y “e” a la tecla de ENTER. Podemos utilizar estos ASCIIs en
minúsculas para mostrar en pantalla cadenas como “ENTER” o “SPACE” durante la redefinición de las teclas.

A continuación podemos ver un ejemplo que utiliza las rutinas Find_Key y Scancode2Ascii para mostrar en
pantalla el código ASCII de cualquier tecla pulsada:

;--------------------------------------------------------------
; Prueba de conversion de Scancode a ASCII
; Recuerda que para compilarla necesitarás añadir las rutinas
; Find_Key y Scancode2ASCII a este listado, se han eliminado
; del mismo para reducir la longitud del programa.
;--------------------------------------------------------------

ORG 32768

START:
CALL Wait_For_Keys_Released

chequear_teclas:
CALL Find_Key ; Llamamos a la rutina
JR NZ, chequear_teclas ; Repetir si la tecla no es válida
INC D
JR Z, chequear_teclas ; Repetir si no se pulsó ninguna tecla
DEC D
; En este punto D es un scancode valido
call Scancode2Ascii

; En este punto A contiene el ASCII del scancode en D


; lo imprimimos por pantalla con rst 16.
rst 16

CALL Wait_For_Keys_Released
jr START ; vuelta a empezar

;-----------------------------------------------------------------------
; Esta rutina espera a que no haya ninguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
XOR A
IN A, ($FE)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET

;-----------------------------------------------------------------------
;--- Introducir aquí las rutinas Find_Keys y Scancode2ASCII ------------
;-----------------------------------------------------------------------

END 32768

Una vez en ejecución y tras pulsar múltiples teclas, este es el aspecto del programa anterior:

ISSUE 2 vs ISSUE 3
Una recomendación a la hora de verificar el estado de las teclas es que utilicemos las herramientas de que nos
provee el Z80 para testear los bits del valor devuelto por IN, en lugar de, simplemente tratar de comparar el
valor del estado del teclado con algún valor predefinido. Esto evitará que nuestro programa funcione de forma
diferente en Spectrums con teclado ISSUE 2, ISSUE 3, o con algún periférico conectado.

Si alguna vez has cargado un snapshot de algún juego en un emulador y has visto que el personaje se movía
“sólo”, como si alguien estuviera pulsando teclas que realmente no están pulsadas (por ejemplo, Abu Simbel
Profanation), y has tenido que activar la opción “ISSUE 2 KEYBOARD EMULATION” para que funcione
adecuadamente, entonces ya has sufrido los efectos de una incorrecta lectura del teclado.

Ahora mismo veremos por qué, y empezaremos para ello recordando uno de los primeros párrafos de esta
entrega:

Así, leyendo del puerto 63486 obtenemos un byte cuyos 8 bits tienen como significado el estado de cada una de
las teclas de la semifila del “1” al “5”.
Bits: D7 D6 D5 D4 D3 D2 D1 D0
Teclas: XX XX XX “5” “4” “3” “2” “1”

Con esta información “rescatada”, volvamos al punto en que estábamos: A la hora de comprobar si la tecla “2”
está pulsada, lo recomendable es testear (por ejemplo, con el nmemónico *BIT*), el bit 1 del valor devuelto por
un IN del puerto 63486. Recordemos que dicho bit valdrá 0 si la tecla está pulsada, y 1 si no lo está.

Teniendo en cuenta que un bit a “1” significa tecla no pulsada y “0” significa pulsada, si en nuestro teclado no
está pulsada ninguna tecla del 1 al 5, los últimos 5 bits del valor leído del puerto serán “11111b”, y que si está
pulsada la tecla “2”, tendremos “11101b”.

Viendo esto, podría surgirnos la tentación de utilizar COMPARACIONES para chequear el estado de la tecla
“2”. Pulsamos “2” en nuestro Spectrum, leemos el valor del puerto, y obtenemos 253 (“11111101b”), con lo
cual basamos el chequeo de teclas de nuestro programa en cosas como el siguiente pseudocódigo:

valor = IN(63486)
SI valor == 253 ENTONCES: TECLA_DOS_PULSADA

Comparando con 253 (11111101b), estamos asumiendo que los bits D7, D6 y D5 valen siempre 1, porque en
*nuestro* Spectrum es así, pero … ¿Qué valor tienen los bits D7, D6 y D5? La realidad es que la gran mayoría
de las veces será, efectivamente, 1, pero este valor puede verse alterado si tenemos determinados periféricos
hardware conectados al bus de expansión trasero, e incluso existen unos determinados modelos de placas
(ISSUE 2) que contienen otros valores en estos bits.

Uno de los componentes del grupo Mojon Twins, na_th_an, nos proporciona a través de su blog la siguiente
prueba de concepto BASIC:

Una ligera prueba en Spectaculator, que puede configurarse para que use el teclado de issue 2, nos da los
valores que buscamos para los bits que desconocemos. Sólo tenemos que teclear y ejecutar este pequeño
programa, y fijarnos como normalmente obtenemos 253 y 254 (255 sin pulsar nada) para las pulsaciones de O
y P, respectivamente, y otros valores diferentes si activamos el teclado issue 2 en las opciones del emulador:

10 PRINT AT 0,0; IN 57342; " "


20 GOTO 10

Los valores que obtenemos para estas pulsaciones son 189 y 190 (con 191 sin pulsar), lo que significa que los
bits desconocidos son nada más y nada menos que XXX = 101. Podremos garantizar que nuestro programa
funcionará en todos los Spectrum si comparamos siempre con ambos valores (por ejemplo, para detectar O
deberíamos mirar si IN 57342=253 OR IN 57342=189).

La solución que Na_th_an nos expone en el párrafo anterior está orientada a la creación de programas en
BASIC, dado que la variante “ZX Spectrum” de este lenguaje no dispone de operaciones de testeo de bits, pero
en nuestro caso, en ensamblador, la mejor opción para leer una tecla (pongamos “P” en el siguiente ejemplo)
sería:

LD A, $DF ; Semifila "P" a "Y"


IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, pulsado ; Si esta a 0 (pulsado) salir.

Curiosidades con el teclado


1.- Debido al diseño del Spectrum, existen combinaciones de teclas que siendo pulsadas simultáneamente no
permiten la detección de teclas adicionales en la misma u otras filas. A la hora de definir unas teclas por
defecto, deberemos de realizar pruebas con nuestro programa para asegurarnos de que podemos pulsar todas las
combinaciones de teclas elegidas.

2.- Aparte, existen al menos 4 combinaciones de teclas que producen el mismo efecto que pulsar la tecla
BREAK:

CAPS SHIFT + Z + SYMBOL SHIFT


CAPS SHIFT + X + M
CAPS SHIFT + C + N
CAPS SHIFT + V + B

3.- Para los casos en los cuales queramos comprobar sólo el bit 0 de una semifila, podemos ahorrarnos la
sentencia BIT utilizando RRA para mover el bit b0 al carry flag. ¿La utilidad de esto? Sencillamente que RRA
se ejecuta en 4 ciclos de reloj mientras que BIT en 8.

; Ejemplo: leyendo la tecla espacio (bit 0)


LD A, $7F
IN A, ($FE)
RRA ; pone b0 en el carry flag
JP NC, key_pressed

4.- Algunas compañías de videojuegos (por ejemplo, ULTIMATE), seleccionaban para los juegos teclas como
Q, W, E, R y T. Como habéis podido ver en este capítulo, esa selección no es casualidad: todas estas teclas
están en la misma semifila del teclado, con lo que se puede leer el estado de todas ellas con una sóla lectura de
puerto. Esto permitía ahorrar tanto memoria como tiempo de proceso.

En ese sentido, la lectura de los joysticks Sinclair (1-5 y 0-9) también es muy cómoda para nuestros programas.

Microrebotes y Ghosting en la lectura del teclado


Miguel A. Rodríguez Jódar nos cuenta dos detalles a tener en cuenta a la hora de leer las teclas en ensamblador.

El primero está relacionado con la lectura de cadenas de texto y no simplemente “controlar” un personaje.

El problema: los microrebotes del teclado, provocarán el conocido efecto de “repetición de teclas” aún cuando
sólo hayamos pulsado y liberado una tecla. Citando a Miguel Ángel:

Cuando se usa el teclado para mover un personaje, lo que se busca es ver qué tecla
se pulsa, y mientras esté pulsada se mueve en una determinada dirección. Si se suelta
un momento pero después se vuelve a pulsar, el personaje se sigue moviendo, así que
en este caso los microrrebotes parecen no afectar.

Otra cosa es cuando se usa el teclado para "teclear". En este caso la secuencia es:
esperar a que se pulse una tecla, recoger qué tecla es, almacenarla, esperar a que se
suelte, y volver al principio.

En este caso es cuando ocurre el problema: si el bucle que implementa el algoritmo


anterior es muy rápido, es posible escanear el teclado cada pocos microsegundos. Si
una trama de microrrebotes dura más que el tiempo entre escaneos de teclado, el programa
puede detectar pulsaciones incorrectas, al estar leyendo datos que corresponden a un
microrrebote. Dado que lo que buscamos son secuencias pulsado/no pulsado, estos
microrrebotes se interpretarán erróneamente como pulsaciones y nos podemos encontrar
con que lo que tecleamos aparece repetido dos o tres veces.

Bucle:
EsperaSoltar: xor a
in a,(254)
and 00011111b
cp 00011111b
jr nz,EsperaSoltar
EsperaPulsar: xor a
in a,(254)
and 00011111b
cp 00011111b
jr z,EsperaPulsar

;Se registra la pulsacion...


jr Bucle

Un bucle así ejecutándose en un Spectrum real podría enfrentarse con la siguiente


pulsación de teclado:

Teclado: 11111111111111111111111001011010000000000000000000000000
Lectura: ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^

Esto es lo que quería destacar: si el intervalo entre dos lecturas es menor que el
tiempo que dura una trama de microrrebotes, se interpretarán como pulsaciones
independientes. Aquí se comienza con la tecla soltada. En un determinado momento,
el usuario la pulsa y genera la secuencia que se ve. El bucle de lectura detecta
una pulsación dentro de la trama de microrrebotes, en la siguiente lectura
detecta una no-pulsación, y en la siguiente, otra pulsación. El resultado final
es que se almacenan dos pulsaciones de tecla en lugar de una.

La descripción técnica del Spectrum apunta a que la supresión de rebotes de teclado


se hace por software, en la rutina de lectura de la ROM. Pero si no se usa dicha
rutina y se lee el teclado directamente, hay que tener en cuenta esto. En nuestro
software basta con insertar una pausa de 1ms en la que se ignora al teclado.
1 milisegundo cuando el teclado se usa no para mover un personaje en un arcade,
sino para registrar entrada del usuario, no afecta a la respuesta del programa.

Bucle:
EsperaSoltar: xor a
in a,(254)
and 00011111b
cp 00011111b
jr nz,EsperaSoltar

;Hacer pausa AQUI

EsperaPulsar: xor a
in a,(254)
and 00011111b
cp 00011111b
jr z,EsperaPulsar

;Hacer pausa AQUI

;Se registra la pulsacion...


jr Bucle

El efecto que esto tiene sobre el comportamiento de las lecturas es el siguiente:

Teclado: 11111111111111111111111001011010000000000000000000000000
Lectura: ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^

Cuando se detecta una pulsación (sea en medio de un microrrebote o no), el


teclado deja de explorarse durante 1 ms (o quizás baste con menos). Al soltar
también se generar microrrebotes, que se amortiguarían con la segunda pausa.

Resumiendo: la lectura del teclado en ensamblador sólo está limitada, “físicamente”, por la velocidad con la
que responde la lectura del puerto con el comando IN. Como explica Miguel Ángel, electrónicamente existen
una serie de rebotes de la señal que pueden inducir a generarnos pulsaciones de teclado “residuales” que
realmente no se han dado. Esto hace necesario insertar “pausas” entre lecturas para no leer “microrebotes” de
los estados del teclado al tomar caracteres de teclado en determinadas circunstancias (lectura de “cadenas de
texto”, por ejemplo).
El segundo problema se refiere al “Ghosting”. Debido al funcionamiento interno del teclado, como matriz de
pulsadores sin tener aislado cada uno de ellos con diodos (que hubieran encarecido el producto final al tener
que acoplarlos al teclado de membrana del Spectrum), el estado de “0 voltios” se propaga por todas las líneas
conectadas entre sí mediante los circuitos que han cerrado los pulsadores (teclas), por lo que en ciertas
combinaciones de teclas podemos encontrar teclas no pulsadas con su línea a 0 voltios, interpretando
erróneamente nuestro programa que dicha tecla está realmente pulsada.

Tal y como nos cuenta Miguel A. Rodríguez Jódar en los foros de Speccy.org:

Esto implica, por ejemplo, que al pulsar tres teclas que forman los tres vértices
de un cuadrado en la matriz, la cuarta tecla perteneciente al cuarto vértice también
aparece como pulsada, y por tanto no se puede detectar cuando NO está pulsada.

Lo que ocurre exactamente es lo siguiente: cuando se pulsan dos teclas que pertenecen
a distintas filas, pero que pertenecen a la misma columna las filas de ambas teclas
adquieren el potencial de 0 voltios, así que aunque nosotros hayamos seleccionado
una fila para leer, en realidad se están seleccionando dos filas para leer. Si en la
fila que no pretendíamos leer hay más de una tecla pulsada (la I), ésta obviamente
aparecerá en la línea de salida.

Esto es el "ghosting" en un teclado de matriz. Para detener este efecto, es necesario


impedir que se formen circuitos cerrados allí donde no queremos, o al menos que si
se forman sea porque la corriente deba circular por ese circuito en el sentido adecuado.

La solución es meramente hardware, por lo que a nosotros nos queda simplemente la posibilidad de modificar
la rutina de redefinición de teclas para impedir que el usuario seleccione teclas cuya combinación provoque la
pulsación no real a nivel de línea de otra.

El propio Miguel A. nos propone un programa en BASIC que nos puede mostrar las combinaciones de teclado
que producen Ghosting y que podemos implementar en ASM si consideramos necesario que nuestro programa
tenga en cuenta esta particularidad (Nota: se han partido los comentarios REM y las líneas largas en líneas
múltiples para facilitar la lectura):

1 REM Datos de la matriz a cargar en T. No podremos usar CAPS SHIT y SYMBOL


SHIFT porque la rutina de la ROM que usamos no las puede detectar
"aisladas" asi que en su lugar ponemos CHR$ 0 (NOTA: ambas a la vez
si puede, es CHR$ 14)
2 DATA "b","n","m",CHR$ 0," ","h","j","k","l",CHR$ 13,"y","u","i","o","p",
"6","7","8","9","0","5","4","3","2","1","t","r","e","w","q","g","f",
"d","s","a","v","c","x","z",CHR$ 0
10 DIM t(8,5,2):
REM Estado de la matriz. t(f,c,s) es:f=fila, c=columna, s=codigo ascii tecla
15 DIM r(7,2):
REM Nuestra seleccion de teclas. Para cada una se guarda su fila y columna.
17 FOR f=1 TO 8: FOR c=1 TO 5: READ t$: LET t(f,c,2)=CODE t$: NEXT c: NEXT f:
REM Rellenamos la matriz T
20 DATA "Arriba","Abajo","Izquerda","Derecha","Fuego","Pausa","Abortar"
30 FOR n=1 TO 7
40 READ t$
50 PRINT "Elige tecla para ";t$;": ";
60 PAUSE 0: LET tecl=PEEK 23560:
REM Leemos tecla. Valdria tambien hacer LET tecl=CODE INKEY$
65 BEEP .05,0:
REM pitido de realimentacion al usuario para que sepa que su tecla
ha sido leida y va a ser procesada
70 FOR f=1 TO 8: FOR c=1 TO 5: IF t(f,c,2)=tecl THEN GO TO 90:
REM La buscamos en la matriz
80 NEXT c: NEXT f: PRINT "Fallo en la matriz! :(": STOP :
REM Esto no deberia pasar...
90 IF t(f,c,1)=1 THEN BEEP .5,-20: GO TO 60:
REM Si ya estaba marcada, error! y a elegir otra
100 LET t(f,c,1)=1:
REM No esta marcada, asi que la aceptamos y la marcamos
105 IF tecl=13 THEN PRINT "ENTER": GO TO 110
106 IF tecl=32 THEN PRINT "SPACE": GO TO 110
108 PRINT CHR$ tecl
110 BEEP .1,20:
REM pitido para indicar tecla OK
120 LET r(n,1)=f: LET r(n,2)=c:
REM La guardamos en nuestra matriz de teclas seleccionadas.
130 FOR m=1 TO n: LET fil=r(m,1): LET col=r(m,2): GO SUB 900: NEXT m:
REM Repasamos la lista de teclas seleccionadas hasta el momento para
actualizar la matriz con las teclas "fantasma" que encontremos
140 NEXT n: STOP
900 FOR i=1 TO 8:
REM recorremos todas las teclas de la misma columna que nuestra tecla
910 IF t(i,col,1)=1 THEN GO SUB 1000:
REM si alguna esta seleccionada, significa que tenemos dos teclas en
una misma columna. Miramos si hay una tercera en la misma fila
920 NEXT i: RETURN
1000 FOR j=1 TO 5:
REM Recorremos una fila buscando una tercera tecla seleccionada
1010 IF t(i,j,1)=1 THEN LET t(fil,j,1)=1:
REM Si la encontramos, entonces tenemos tres teclas en un cuadrado.
Marcamos como seleccionada la cuarta tecla del cuadrado, para que
no podamos elegirla
1020 NEXT j: RETURN

Por otra parte, es bastante complicado que los usuarios seleccionen combinaciones de teclado no estándar
(OPQA, 6789, etc.) y que puedan suponer problemas de ghosting, por lo que lo más normal para evitar la
inclusión de código adicional en nuestro programa será permitir al usuario que seleccione las teclas sin este tipo
de comprobación.

Uso de las rutinas de la ROM


La ROM dispone de diferentes rutinas para la lectura del teclado y el tratamiento de scancodes (por ejemplo,
conversión a ASCII, guardar la última tecla pulsada en una variable de sistema, etc).

Como veremos en la entrega dedicada a las Interrupciones del procesador, ciertas rutinas de servicio (ISR) son
llamadas regularmente por el procesador (RST $38). Estas rutinas son utilizadas por el intérprete de BASIC,
por ejemplo, pero pueden ser utilizadas por nosotros siempre y cuando no cambiemos del modo de Interrupción
1 (por defecto) al modo de Interrupción 2 (un modo que nos permite programar nuestras propias rutinas ISR
personalizadas).

Estando en modo 1, la ROM lee regularmente el teclado y actualiza ciertas variables del sistema como
LAST_K (23560), que contiene el código ASCII de la última tecla que se haya pulsado. La tecla pulsada
permanece en dicha variable del sistema incluso aunque ya se haya liberado, por lo que podemos forzar una
lectura de teclado estableciéndola dicha celdilla de memoria a 0 e invocando una RST $38 (lo que provocaría su
actualización a un valor ASCII válido en caso de que se esté pulsando una tecla, o 0 en caso contrario). La
rutina en RST $38 ya nos asegura una correcta gestión de los microrebotes del teclado y su conversión a un
ASCII válido.

No obstante, no es lo normal utilizar las rutinas de teclado de la ROM en juegos, puesto que en la mayoría de
ellos necesitaremos entrar en el modo 2 de Interrupciones, en el cual no se ejecuta RST $38 y por lo tanto no se
actualizan variables como LAST_K. Además, es posible que nuestro programa tenga que hacer uso de la zona
de memoria en que las rutinas de la ROM guardan los datos de las teclas leídas (de 23552 a 23560), lo que no
haría factible el uso de estas rutinas (ni siquiera llamando a RST $38) a menos que preservemos el contenido de
esta zona de memoria y de otros registros del procesador antes de volver a a IM1, llamar a RST $38, y saltar de
nuevo a IM 2. Seguramente todo este proceso es totalmente innecesario en un juego si éste ya dispone de
rutinas para leer el teclado y podemos aprovecharlas.

Nuestro curso está orientado a no utilizar las rutinas de la ROM a menos que sea estríctamente necesario, como
en el caso de la utilización de RST 16 en alguno de los ejemplos mostrados para imprimir texto por pantalla,
dado que todavía no hemos llegado a la sección del curso dedicada a trazado de texto e imágenes en la
videomemoria. Habrá otras ocasiones en que alguna rutina de la ROM sea más útil o rápida que la que podamos
escribir nosotros, o, sencillamente, dado que está disponible en la ROM nos pueda ahorrar espacio en nuestro
programa. En ese caso está justificada su utilización siempre y cuando asumamos que a la hora de portar
nuestro programa a otras plataformas Z80 (Amstrad, MSX), dicha rutina no estará disponible y tendremos que
reescribirla adecuándola a la plataforma destino.

En la sección de ficheros se incluye un listado de las rutinas de la ROM desensambladas y comentadas,


obtenidas del libro “The Complete Spectrum ROM Disassembly”. Estas rutinas incluyen funciones de lectura de
scancodes y decodificación de los mismos para convertirlos en ASCIIs. Podemos aprovecharlas en programas
que no requieran precisión con el teclado (por ejemplo, para aplicaciones en lugar de para juegos).

En cualquier caso, en el capítulo dedicado a la impresión de texto veremos un ejemplo de utilización de la


rutina de la ROM KEY_SCAN para la lectura de cadenas de texto tecleadas por el usuario (INPUT).
Interrupciones del procesador Z80
En este capítulo vamos a ver qué son las interrupciones del microprocesador Z80 y cómo utilizarlas en nuestro
beneficio para ejecutar rutinas de servicio “en paralelo” al flujo del programa. Aunque la introducción inicial
sera básicamente teórica, la aplicación práctica es bastante sencilla, pudiendo utilizar las rutinas de esta entrega
directamente sin conocimiento total de la información teórica presentada.

Los microprocesadores suelen disponer como mínimo de una señal de Interrupción. Esta señal, normalmente es
invocada externamente por dispositivos de I/O que requieren la atención del procesador, solicitando la atención
del mismo por algún tipo de evento.

De esta forma, no es necesario que sea nuestro programa el encargado de comprobar continuamente si ha
ocurrido un evento concreto.

Una señal de interrupción provoca que el procesador termine de ejecutar la instrucción en curso y ya no
continúe con la ejecución de la siguiente instrucción apuntada por PC (el contador de programa). En lugar de
esto, temporalmente, lanza una porción de código definida como ISR (Interrupt Service Program), en la que
podemos realizar determinadas tareas regulares, desde actualizar variables de ticks/segundos/minutos/horas con
precisión, hasta enviar datos musicales al chip de música AY, por ejemplo.

Cuando se finaliza la ejecución de la rutina ISR, el procesador continúa la ejecución desde donde se detuvo al
llegarle la señal de interrupción. Para nuestro programa la ejecución de la ISR es “transparente”. No obstante,
es importante que estas rutinas ISR sean lo más reducidas y rápidas posibles para no afectar a la velocidad de
ejecución del programa principal.

El Z80A (el corazón del ZX Spectrum) dispone de 2 tipos de señales de interrupción: una señal de alta
prioridad (NMI, Non-mascarable-Interrupt), y otra señal enmascarable de menor prioridad (MI). El procesador,
como hemos dicho, lee el estado de las señales /NMI e /INT al acabar la ejecución de cada instrucción (salvo en
el caso de instrucciones repetitivas como LDDR, por ejemplo, que lo realiza al acabar cada subinstrucción
como LDD).
Veamos a continuación los 2 tipos de interrupciones, en qué modos pueden operar, y qué podemos hacer con
ellas.

Interrupciones NMI
Las interrupciones no enmascarables (NMI) permiten que dispositivos I/O ajenos al procesador le interrumpan
solicitando atención por parte del mismo. El microprocesador Z80 verifica el estado de la señal de interrupción
NMI en el correspondiente pin del procesador en el último T-estado del ciclo de ejecución actual (incluyendo
las instrucciones con prefijo al opcode). En ese momento realiza un “PUSH PC” y salta a la dirección de la ISR
(hacia $0066). Tras 11 ciclos de reloj, se ejecuta el código correspondiente, se recupera “PC” de la pila y se
continúa la ejecución del programa original. En el caso de que ocurran 2 interrupciones simultáneamente (una
de tipo NMI y otra de tipo MI), la interrupción NMI tiene prioridad.

En el caso del Sinclair ZX Spectrum, las NMI simplemente provocan un RESET del ordenador, puesto que
$0066 es una posición de memoria que cae dentro de la ROM y que no puede modificarse salvo hacia $0000,
dirección de inicio del ciclo de ejecución y que provoca el mencionado reset.

Interrupciones MI
Las interrupciones enmascarables (MI, INT o INTRQ) se denominan así porque, al contrario que las NMI,
pueden ser ignoradas por el procesador cuando han sido deshabilitadas con la instrucción DI (Disable
Interrupt).

Cuando el procesador recibe una de estas interrupciones actúa de 3 formas diferentes según el modo actual de
interrupción en que esté. El Z80 puede estar en 3 modos de interrupción o IM (Interrupt Mode):

• Modo 0: En este modo de interrupción, el dispositivo que desea interrumpir al procesador activa la
pantilla /INT del mismo y durante el ciclo de reconocimiento de interrupción del procesador coloca el
opcode de una instrucción en el bus de datos del Z80. Normalmente será una instrucción de 1 sólo byte
(normalmente un RST XX) ya que esto sólo hace necesario escribir y mantener un opcode en el bus de
datos (aunque puede ser, en periféricos más complejos y con una correcta temporización, un JP o CALL
seguido de la dirección de salto). Este modo de interrupción existe principalmente por compatibilidad
con el procesador 8080.

• Modo 1: Cuando se recibe una señal INT y el procesador está en IM 1, el Z80 ejecuta un DI (Disable
Interrupts), se salva en la pila el valor actual de PC, y se realiza un salto a la ISR ubicada en la dirección
$0038 (en la ROM). Es el modo de interrupción por defecto del Spectrum (por ejemplo, en el intérprete
BASIC), y en este modo el Spectrum no sabe qué dispositivo ha causado la interrupción y es la rutina
ISR la encargada de determinar qué dispositivo externo (o proceso interno) es el que requiere la
atención del procesador.

• Modo 2: Es el modo más utilizado en los programas comerciales e implica el uso del registro I y el bus
de datos para generar un vector de salto. Los 8 bits superiores de la dirección de salto del ISR se cargan
en el registro I. El dispositivo que desea interrumpir al procesador colocar los 8 bits bajos de la
dirección (por convención, un número par, es decir, con el bit 0 a 0) en el bus de datos.

La lectura de la parte baja de la dirección de salto desde el bus de datos permite que cada dispositivo pueda
tener su propia rutina ISR y solicitar una interrupción al procesador y que ésta sea atendida por la rutina
especializada adecuada.

El vector resultante de combinar I y BUS_DATOS no apunta a la ISR en sí misma, sino a una dirección de 2
bytes que es la que realmente contiene la dirección de la ISR. Esto nos permite también utilizar nuestras propias
ISRs por software, desde nuestros propios programas. Concretamente I*256 apunta a una tabla de direcciones
de ISRs (Tabla de Vectores de Interrupción) que será indexada por el byte bajo de la dirección (lo que nos da
un total de 128 ISRs posibles de 2 bytes de dirección de inicio absoluta cada una). Es por esto que la dirección
del byte bajo es, por convención, un número par, de forma que siempre accedamos a las direcciones de 16 bits
correctas en la tabla (I*256+N y I*256+N+1 siendo N par) y no a media dirección de una ISR y media de otra
(como veremos más adelante).

Así pues, se puede definir la dirección de salto de la interrupción en modo IM2 como:

DIR_SALTO = [ (I*256)+VALOR_EN_BUS_DE_DATOS ]

Finalmente, este es el coste en T-estados de la aceptación de interrupciones en cada modo:

• NMI: 11 t-estados
• INT IM 0: 13 t-estados (si la instrucción del bus es un RST)
• INT IM 1: 13 t-estados
• INT IM 2: 19 t-estados

Instrucciones relacionadas con las interrupciones

Cambio del modo de interrupción (IM)

Podemos cambiar el modo de interrupcion en el Spectrum con la instrucción del procesador “IM”:

IM 0 ; Cambiar a modo IM 0 (8 T-Estados).


IM 1 ; Cambiar a modo IM 1 (8 T-Estados).
IM 2 ; Cambiar a modo IM 2 (8 T-Estados).

Como ya hemos dicho, el Spectrum opera normalmente en IM 1, donde se llama regularmente a una ISR que
actualiza lee el estado del teclado y actualiza ciertas variables del sistema (LAST_K, FRAMES, etc) para la
conveniencia del intérprete de BASIC (y, en algunos casos, de nuestros propios programas). Esta ISR (la RST
$38) pretende hacer uso exclusivo del registro IY por lo que si nuestro programa necesita hacer uso de este
registro es importante hacerlo entre un DI y un EI para evitar que pueda ocurrir una interrupción con su valor
modificado por nosotros y provocar un reset en el Spectrum. También tenemos que tener en cuenta esto si
estando en modo IM 2 llamamos manualmente a la RST $38 para actualizar variables del sistema (aunque no es
habitual que necesitemos ejecutar la ISR que usa el intérprete de BASIC).

En el caso de aplicaciones y juegos, lo normal es cambiar a IM 2 con una rutina propia de ISR que realice las
tareas que nosotros necesitemos, especialmente temporización, actualización del buffer del chip AY de audio
para reproducir melodías, etc.

Activar y desactivar las interrupciones del procesador

Existen también 2 instrucciones especiales para DESACTIVAR las interrupciones (DI, Disable Interrupts), y
ACTIVARLAS (EI, Enable Interrupts), manipulando el flip-flop del procesador IFF.

DI ; Disable Interrupts (4 T-estados) -> IFF=0


EI ; Enable Interrupts (4 T-estados). -> IFF=1

Nótese el hecho importantísimo de que las interrupciones no se habilitan de nuevo al final la ejecución del EI,
sino tras la ejecución de la instrucción que lo sigue en el flujo del programa. Más adelante veremos por qué.
Instrucción HALT

La instrucción HALT es una instrucción muy útil que detiene el proceso de ejecución la CPU. Al llamarla, la
CPU comienza a ejecutar continuamente NOPs de 4 t-estados (sin incrementar el contador de programa), hasta
que se vea interrumpido por una NMI o una MI (INT), en cuyo momento se incrementa PC y se procesa la
interrupción. Al volver de la ISR, el procesador continúa la ejecución del programa en la instrucción siguiente
al HALT.

HALT ; Halt computer and wait for INT (4 T-Estados).

Como veremos más adelante, la instrucción HALT nos será especialmente útil en determinadas ocasiones al
trabajar con la manipulación del área de datos de la videomemoria.

Instrucciones RST

Las instrucciones RST (ReSTart) que se utilizan para realizar un salto a una dirección concreta y específica
mediante una instrucción de un sólo opcode. Existen las siguientes posibles instrucciones RST:

RST 0 ; Opcode C7 (11 T-estados).


RST 8 ; Opcode CF (11 T-estados).
RST 10h ; Opcode D7 (11 T-estados).
RST 18h ; Opcode DF (11 T-estados).
RST 20h ; Opcode E7 (11 T-estados).
RST 28h ; Opcode EF (11 T-estados).
RST 30h ; Opcode F7 (11 T-estados).
RST 38h ; Opcode FF (11 T-estados).

El equivalente de esta instrucción de 1 sólo opcode es un “CALL 00XXh”, y su existencia está justificada en
que es necesario disponer de estas instrucciones de un sólo byte para que puedan así ser emplazadas en el bus
de datos y leídas en el modo de interrupción IM 0, algo que no se podría hacer de una forma tan sencilla con la
instrucción multibyte CALL.

En nuestros programas podemos utilizar estas instrucciones RST si queremos llamar manualmente a alguna de
las rutinas de la ROM a la que hacen referencia, como RST $10 (o RST 16), que utilizamos en la entrega sobre
el teclado para llamar a $0010, que aloja la rutina PRINT-A (la cual imprime en pantalla el carácter ASCII
correspondiente al valor del registro A).

Instrucciones "LD A, R" y "LD A, I"

Una instrucción de uso infrecuente con una peculiar utilidad es LD A, R. Con esta instrucción cargamos el
valor del registro interno del procesador R (utilizado para el refresco de la DRAM) en el acumulador.
Comunmente se utiliza para obtener algún tipo de valor “variable” como semilla o parte del proceso de
generación de números aleatorios.

No obstante, esta instrucción de 2 bytes ($ED $5F) y 9 t-estados de ejecución tiene la particular utilidad de
copiar en el flag P/V el contenido del flip-flop IFF2, por lo que podemos utilizarla para conocer el estado de las
interrupciones enmascarables.

Así, una vez ejecuado un “LD A, R”, sabemos que si la bandera está a 1 es que las interrupciones están
habilitadas, mientras que si están a cero, es porque han sido deshabilitadas.
Como curiosidad, la instrucción LD A, I produce la misma afectación de P/V que LD A, R. Otros flags
afectados por ambas instrucciones son “S”, “C” (reseteado) y “z”.

Las interrupciones de la ULA


Como ya hemos dicho, las interrupciones están diseñadas para que los dispositivos externos puedan interrumpir
al procesador Z80. En el caso del Spectrum, existe un dispositivo externo común a todos los modelos y que
tiene funciones críticas para el sistema. Hablamos de la ULA, que en un Spectrum sin dispositivos conectados
al puerto de expansión es el único periférico que provoca señales de interrupción al procesador.

La ULA, como encargada de gestionar la I/O, el teclado, y de refrescar el contenido de la pantalla usando los
datos almacenados en el área de videoram del Spectrum, interrumpe al procesador de forma constante, a razón
de 50 veces por segundo en sistemas de televisión PAL (Europa y Australia) y 60 veces por segundo en
sistemas NTSC (USA).

Esto quiere decir que cada 1/50 (o 1/60) segundos, la ULA produce una señal INT (interrupción enmascarable),
que provoca la ejecución de la ISR de turno (RST $38 en modo IM 1 ó la ISR que hayamos definido en modo
IM 2).

En el modo IM 1 (el modo en que arranca el Spectrum), el salto a RST $38 provocado por las interrupciones
generadas por la ULA produce la ejecución regular y continua cada 1/50 segundos de las rutinas de lectura del
teclado, actualización de variables del sistema de BASIC y del reloj del sistema (FRAMES) requeridas por el
intérprete de BASIC para funcionar.

En cuanto al modo IM 2, el que nos interesa principalmente para la realización de programas y juegos, la
dirección de salto del ISR se compone como 16 bits a partir del registro I (en la parte alta de la dirección), y el
identificador de dispositivo (par) en el bus de datos, utilizado como parte baja del vector de salto.

Como ya hemos visto en la definición del modo IM 2, la dirección resultante


((I*256)+ID_DE_DISPOSITIVO_EN_BUS_DATOS) se utiliza para consultar una tabla de vectores de
interrupción para saltar. A partir de la dirección I*256, debe de haber una tabla de 256 bytes con 128
direcciones de salto absolutas de 2 bytes cada una.

De esta forma, cada dispositivo de hasta un total de 128 puede colocar su ID en el bus de datos y tener su propia
ISR en la tabla:

• Un dispositivo con ID 0 tendría su dirección de salto a la ISR en (I*256+0 e I*256+1).


• Un dispositivo con ID 2 tendría su dirección de salto a la ISR en (I*256+2 e I*256+3).
• Un dispositivo con ID 4 tendría su dirección de salto a la ISR en (I*256+4 e I*256+5).
• (…)
• Un dispositivo con ID 254 tendría su dirección de salto a la ISR en (I*256+254 e I*256+255).

Debido a que la tabla de saltos requiere 2 bytes por cada dirección y que existen 256 posibles valores en el bus
de datos, el identificador de dispositivo tiene que ser un valor PAR, ya que si un dispositivo introdujera un
valor IMPAR en el bus de datos, el procesador podría realizar un salto a una dirección compuesta a partir de los
datos de salto de 2 dispositivos diferentes. Comprenderemos este problema con un ejemplo muy sencillo:

• Un dispositivo con ID 1 tendría su dirección de salto a la ISR en (I*256+1 e I*256+2) (que forman parte
de las direcciones de salto de los dispositivos con ID 0 e ID 2).
• Un dispositivo con ID 255 tendría su dirección de salto a la ISR en (I*256+255 e I*256+256), lo que
implicaría tratar de utilizar parte de la dirección de salto del dispositivo con ID 254 además de un byte
de fuera de la tabla de vectores de interrupción.
Así pues, por convención, todos los dispositivos que se conectan a un Z80 tienen que colocar como ID de
dispositivo en el bus de datos un identificador único par, que asegure que los vectores de salto de 2 dispositivos
nunca puedan solaparse.

Existe una excepción notable a esta regla, y no es otra que la propia ULA. La ULA no está diseñada para
funcionar en modo IM 2, ya que no coloca ningún identificador de dispositivo en el bus de datos cuando genera
interrupciones. Está diseñada para funcionar en modo 1, donde no se espera este identificador y siempre se
produce el salto a RST $38, sea cual sea el dispositivo que solicita la interrupción.

Por suerte, cuando no se coloca ningún valor en el bus de datos del Spectrum, éste adquiere el valor de 8
señales uno (11111111b, 255d o FFh), debido a las resistencias de pull-up al que están conectadas las líneas de
dicho bus. Por lo tanto, nuestro procesador Z80A obtendrá como device-id del dispositivo que interrumpe un
valor FFh (con cierta particularidad que veremos en la sección sobre Compatibilidad).

Este valor, impar, produce el siguiente valor dentro de la tabla de vectores de interrupción: (I*256+255) e
(I*256+255+1), lo que produce la lectura dentro de la tabla de vectores del campo 255 y del 256, provocando la
necesidad de que nuestra tabla de vectores requiera 257 en lugar de 256 bytes.

La interrupción de la ULA y el VSync de vídeo

Como acabamos de ver, la ULA provee al procesador en modo IM1 de un mecanismo para, regularmente,
escanear el teclado y evitar así que sean los propios programas quienes tengan que realizar esa tarea por
software.

La interrupción generada por la ULA debe de ser de una regularidad tal que se ejecute suficientes veces por
segundo para que el escaneo del teclado no pierda posibles pulsaciones de teclas del usuario, pero no tan
frecuente como para que requiera gran cantidad de tiempo de procesador ejecutando una y otra vez la ISR
asociada.

Como nos cuenta el libro sobre la ULA de Chris Smith, el ingenierio de Sinclair, Richard Altwasser, aprovechó
la señal de VSYNC que la ULA genera como señal de sincronización para el televisor como lanzador de la
señal de interrupción. Esta señal se genera 50 veces por segundo para televisiones PAL y 60 para televisiones
NTSC, y tiene la duración adecuada para que el procesador detecte la interrupción en su patilla INTrq.

Este es el precisamente el motivo por el cual la interrupción generada por la ULA se genera 50 (ó 60 veces por
segundo): un aprovechamiento de la señal de VSYNC para los televisores, con el consiguiente ahorro de
electrónica adicional que supondría generar otra señal adicional para INTrq.

Por otra parte, para los programadores es una enorme ventaja el saber que la interrupción del procesador por
parte de la ULA coincide con el VSYNC, ya que nos permite el uso de la instrucción HALT en nuestro
programa para forzar al mismo a esperar a dicha interrupción y poder ejecutar código después del HALT que
trabaje sobre la pantalla sabiendo que el haz de electrones no la está redibujando.

Como veremos en el capítulo dedicado a la memoria de vídeo, la ULA lee regularmente un área de aprox. 7 KB
que empieza en la dirección de memoria $4000 y con los datos que hay en ese área alimenta al haz de
electrones del monitor para que forme la imagen que aparece en pantalla. Como parte del proceso de generación
de la imagen, 50 veces por segundo (una vez por cada “cuadro de imagen”) la ULA debe de generar un pulso de
VSYNC durante 256 microsegundos para el monitor que asegure que el inicio de la generación de la imagen
está sincronizado con las líneas de vídeo que se le envían. Durante ese período, la ULA no está generando señal
de vídeo sobre el televisor y podemos alterar el contenido de la videoram con seguridad.

¿Por qué es necesario tener esta certeza acerca de la ubicación del haz de electrones? La respuesta es que si
alteramos el contenido de la videoram durante la generación de la imagen, es posible que se muestren en
pantalla datos de la imagen del cuadro de vídeo anterior (datos de videoram ya trazados por el haz de
electrones) junto a datos de la imagen del cuadro de vídeo que estamos generando, mostrando un efecto
“cortinilla” o “efecto nieve”.

Por mostrarlo de una manera gráfica (y con un ejemplo “teórico”), supongamos que tenemos una pantalla de
color totalmente azul y queremos cambiarla a una pantalla de color totalmente verde. Imaginemos que cuando
el haz de electrones ha mostrado la mitad de la pantalla nosotros cambiamos el contenido de la zona de
atributos para que la pantalla completa sea verde. Con el haz en el centro de la pantalla, todavía recorriendo la
videoram y “trazando” los colores en pantalla, todos los datos mostrados a partir de ese momento serán píxeles
verdes, por lo que durante ese cuadro de imagen tendremos el 50% inicial de la pantalla en color azul y el 50%
restante en verde, y no toda verde como era nuestra intención. Será en el próximo cuadro de retrazado de la
pantalla cuando se leerán los valores de color verde de la VRAM de la zona superior de la pantalla y se
retrazarán dichos píxeles en verde, dejándonos la pantalla totalmente de dicho color.

¿Cómo podemos evitar este efecto? Mediante la instrucción HALT.

El haz de electrones del monitor barre la pantalla empezando en la esquina superior izquierda de la misma,
recorriendola de derecha a izquierda, trazando líneas horizontales con el contenido de la videomemoria. Cuando
el haz llega a la derecha del televisor, baja a la siguiente línea de pantalla retrocediendo a la izquierda de la
misma y se sincroniza con la ULA mediante una señal de HSYNC. De nuevo el haz de electrones traza una
nueva línea horizontal hacia la derecha, repitiendo el proceso una y otra vez hasta llegar a la esquina inferor
derecha. El haz de electrones debe entonces volver a la parte superior izquierda de la pantalla (mediante una
diagonal directa) y sincronizarse con la ULA mediante un pulso VSYNC.

Sabemos que la interrupción generada por la ULA llega al procesador cuando se realiza el VSYNC con el
monitor (cuando el haz de electrones está en el punto superior de su retroceso a la esquina superior izquierda de
la pantalla), así que podemos utilizar HALT en nuestro programa para forzar al mismo a esperar una
interrupción, es decir, a que se finalice el trazado del cuadro actual, asegurándonos que no se está escribiendo
en pantalla. Esto nos deja un valioso pero limitado tiempo para realizar actualizaciones de la misma antes de
que el haz de electrones comience el retrazado o incluso alcance el punto que queremos modificar.
En nuestro ejemplo anterior de la pantalla azul y verde, un HALT antes de llamar a la rutina que pinta la
pantalla de verde aseguraría que la pantalla se mostrara completamente en verde (y no parcialmente) al haber
realizado el cambio de los atributos tras el HALT (durante el VSYNC) y no durante el retrazado de la pantalla
en sí misma.

Hay que tener en cuenta un detalle para temporizaciones precisas: aunque el microprocesador siempre recibe la
señal de interrupción en el mismo instante de retorno del haz, la señal sólo es leída por el microprocesador al
acabar la ejecución de la instrucción en curso, por lo que dependiendo del estado actual de ejecución y del tipo
de instrucción (su tamaño y tiempo de ejecución) puede haber una variación de hasta 23 t-estados en el tiempo
de procesado de la INT.

Una vez se produce la interrupción, tenemos un tiempo finito para trabajar sobre la pantalla antes de que
comience el redibujado de la misma:

Modelo de Spectrum t-estados disponibles (+-1 t-estado)


16K 14336 t-estados
48K 14336 t-estados
128K 14361 t-estados
+2 14361 t-estados
+2A 14364 t-estados
+3 14364 t-estados

Detalles de temporización con este son los que permiten a algunos juegos realizar auténticas virguerías con el
borde (como Aquaplane), o generar rutinas que permitan varios colores por carácter controlando la posición
exacta del haz de electrones y cambiando los atributos mientras el haz está trazando un determinado scanline.

Las rutinas de ISR


Hemos hablado ya de las rutinas de ISR y de cómo son llamadas 50 (o 60) veces por segundo. Lo normal en el
desarrollo de un juego o programa medianamente complejo es que utilicemos el modo IM 2 y desarrollemos
nuestra propia rutina ISR para que cumpla nuestras necesidades.

Las ISRs deben de optimizarse lo máximo posible, tratando de que sean lo más rápidas y óptimas posibles, ya
que nuestro programa se ha visto interrumpido y no se continuará su ejecución hasta la salida de la ISR. Si
tenemos en cuenta que normalmente nuestras ISRs se ejecutarán 50 veces por segundo, es importante no
ralentizar la ejecución del programa principal llenando de código innecesario la ISR.

Es crítico también que en la salida de la ISR no hayamos modificado los valores de los registros con respecto a
su entrada. Para eso, podemos utilizar la pila y hacer PUSH + POP de los registros utilizados o incluso utilizar
los Shadow Registers si sabemos a ciencia cierta que nuestro programa no los utiliza (con un EXX y un EX AF,
AF al principio y al final de nuestra ISR).

Al principio de nuestra ISR no es necesario desactivar las interrupciones con DI, ya que el Z80 las deshabilita al
aceptar la interrupción. Debido a este “DI” automático realizado por el procesador, las rutinas de ISR deben
incluir un EI antes del RET/RETI.

Así pues, de las rutinas ISR llamadas en las interrupciones se debe de volver con una instrucción RETN en las
interrupciones no enmascarables y un EI + RETI en las enmascarables (aunque en algunos casos, según el
periférico que provoca la interrupción, también se puede utilizar EI+RET, que es ligeramente más rápido y que
tiene el mismo efecto en sistemas como el Spectrum).

RETI ; Return from interrupt (14 T-Estados).


RETN ; Return from non-maskable interrupt (14 T-Estados).
Existe un motivo por el cual existe RETI y no se utiliza simplemente RET, y es que existen unos flip-flops
internos en el procesador que le marcan cierto estados al procesador y que en el caso de salida de una
interrupción deben resetearse.

Citando el documento z80undoc3.txt de Z80.info (por Sean Young):

3.1) Non-maskable interrupts (NMI)

When a NMI is accepted, IFF1 is reset. At the end of the routine, IFF1 must
be restored (so the running program is not affected). That's why IFF2 is
there; to keep a copy of IFF1.

An NMI is accepted when the NMI pin on the Z80 is made low. The Z80 responds
to the /change/ of the line from +5 to 0. When this happens, a call is done
to address 0066h and IFF1 is reset so the routine isn't bothered by maskable
interrupts. The routine should end with an RETN (RETurn from Nmi) which is
just a usual RET, but also copies IFF2 to IFF1, so the IFFs are the same as
before the interrupt.

3.2) Maskable interrupts (INT)

At the end of a maskable interrupt, the interrupts should be enabled again.


You can assume that was the state of the IFFs because otherwise the interrupt
wasn't accepted. So, an INT routine always ends with an EI and a RET
(RETI according to the official documentation, more about that later):

INT: .
.
.
EI
RETI (or RET)

Note a fact about EI: a maskable interrupt isn't accepted directly after it,
so the next opportunity for an INT is after the RETI. This is very useful;
if the INT is still low, an INT is generated again. If this happens a lot and
the interrupt is generated before the RETI, the stack could overflow (since
the routine is called again and again). But this property of EI prevents this.

You can use RET in stead of RETI too, it depends on hardware setup. RETI
is only useful if you have something like a Z80 PIO to support daisy-chaining:
queueing interrupts. The PIO can detect that the routine has ended by the
opcode of RETI, and let another device generate an interrupt. That is why
I called all the undocumented EDxx RET instructions RETN: All of them
operate like RETN, the only difference to RETI is its specific opcode.
(Which the Z80 PIO recognises.)

Es decir, para aquellos sistemas basados en Z80 con hardware PIO que soporte múltiples dispositivos I/O
encadenando sus interrupciones, se define un opcode especial RETI distinto de RET de forma que el PIO pueda
detectar el fin de la ISR y pueda permitir a otros dispositivos generar una interrupción.

En el caso del Spectrum con la ULA como (habitualmente) único dispositivo que interrumpe, se utiliza
normalmente RET en lugar de RETI por ser ligeramente más rápida en ejecución. No obstante, en nuestros
ejemplos hemos utilizado RETI para acomodarlos a la teoría mostrada.

Como ya hemos comentado antes, EI no activa las interrupciones al acabar su ejecución, sino al acabar la
ejecución de la siguiente instrucción. El motivo de esto es evitar que se pueda recibir una interrupción estando
dentro de una ISR entre el EI y el RET:

Nuestra_ISR:
PUSH HL

(código de la ISR)

POP HL
EI
RETI

Si EI habilitar las interrupciones de forma instantánea y se recibiera una interrupción entre la instrucción EI y el
RETI, se volvería a entrar en la ISR, y por lo tanto se volvería a realizar el PUSH de PC y el PUSH de HL,
rompiendo el flujo correcto del programa. Por contra, tal y como funciona EI sólo se habilitarán de nuevo las
interrupciones tras la ejecución de RETI y la recuperación de PC de la pila, permitiendo así la ejecución de una
nueva interrupción sin corromper el contenido del stack.

Finalmente, es importantísimo en los modelos de más de 16K (48K y 128K paginados) utilizar una tabla de
vector de interrupciones ubicada en la página superior de la RAM (memoria por encima de los 32K), ya que la
utilización del bloque inferior de memoria (dejando de lado la ROM, el bloque desde 16K a 32K) provocaría un
efecto nieve en la pantalla. La elección estándar de la dirección de la tabla de vector de interrupciones recae en
direcciones a partir de $FE00, por motivos que veremos al hablar sobre la ULA. Por otra parte, en la sección de
Consideraciones y Curiosidades veremos cómo solucionar este problema en sistemas de 16K de memoria
RAM.

Ninguna de las instrucciones que hemos visto (RST XX, EI, DI, RETI, RETN, HALT o IM XX) produce
afectación alguna en los flags, salvo LD A, R, que altera el flag P/V con la utilidad que ya hemos visto.

La ISR de IM 1
A modo de curiosidad, vamos a ver el código de la ISR que se ejecuta en modo 1 (RST $38), tomado del
documento The Complete Spectrum ROM Disassembly con alguna modificación en los comentarios:

; THE 'MASKABLE INTERRUPT' ROUTINE


; The real time clock (FRAMES) is incremented and the keyboard
; scanned whenever a maskable interrupt occurs.
;
; FRAMES = 3 bytes variable.

; BYTES 1 & 2 FRAMES -> $5C78 and $5C79


; BYTE 3 FRAMES -> IY+40h = $5C7A
;
0038 MASK-INT:
PUSH AF ; Save the current values held in
PUSH HL ; these registers.
LD HL,($5C78) ; The lower two bytes of the

INC HL ; frame counter are incremented (FRAMES)


LD ($5C78),HL ; every 20 ms. (UK) -> INC BYTE_1_2(FRAMES)
LD A,H ; The highest byte of the frame counter is
OR L ; only incremented when the value
JR NZ,KEY-INT ; of the lower two bytes is zero
INC (IY+40h) ; INC BYTE_3(FRAMES) ($5C7A)
0048 KEY-INT:
PUSH BC ; Save the current values held
PUSH DE ; in these registers.
CALL KEYBOARD ; Now scan the keyboard. (CALL $02BF)
POP DE ; Restore the values.
POP BC
POP HL
POP AF
EI ; The maskable interrupt is en-
RET ; abled before returning.

Nótese cómo la ISR del modo 1 se ajusta a lo visto hasta ahora: se preserva cualquier registro que pueda
utilizarse dentro de la misma, se reduce el tamaño y tiempo de ejecución de la ISR en la medida de lo posible, y
se vuelve con un EI+RET.
La rutina actualiza el valor de la variable del sistema FRAMES (que viene a ser el equivalente de la variable
“abs_ticks” del ejemplo que veremos en el siguiente apartado) y llama a la rutina de la ROM “KEYBOARD”
($02BF) que es la encargada de chequear el estado del teclado y actualizar ciertas variables del sistema para que
el intérprete BASIC (o nuestros programas si corren en IM 1) pueda gestionar las pulsaciones de teclado
realizadas por el usuario. Si bien la rutina “KEYBOARD” a la que se llama desde la ISR no es todo lo
“pequeña” que se podría esperar de algo que se va a ejecutar en una ISR, sí que es cierto que es una de las
partes primordiales del intérprete BASIC y que es más óptimo y rápido obtener el estado del teclado en la ISR
(aunque la rutina sea larga e interrumpa 50 veces por segundo a nuestro programa con su ejecución) que tener
que realizar la lectura del teclado dentro del propio intérprete de forma continuada.

Nótese, como nos apunta metalbrain en los foros de Speccy.org, que FRAMES es una variable de 3 bytes y que
esta rutina ISR utiliza IY para acceder al tercer byte de esta variable cuando el incremento de los 2 bytes más
bajos requieren el incremente del tercer byte. Lo hace a través de IY+40h y esto explica porqué desde BASIC,
bajo IM1, no debemos utilizar código ASM que haga uso de IY, bajo riesgo de que este “INC” pueda realizarse
sobre un valor de IY que no sea el esperado y por tanto “corromper” un byte de código de nuestro programa o
de datos, pantalla, etc.

ISR de atención a la ULA en IM 2


La clave de este capítulo, y la principal utilidad del uso de interrupciones en nuestros programas es la de
aprovechar las interrupciones que la ULA genera 50 veces por segundo (60 en América) en el modo IM 2.

Antes de pasar al modo IM 2, nosotros somos los responsables de generar la tabla de vectores de interrupción
con los valores a los que el procesador debe de saltar en caso de recibir una interrupción de un dispositivo
externo. Para eso, generamos en memoria una tabla de 257 bytes y en ella introducimos las direcciones de salto
de las ISRs. En un Spectrum estándar sin dispositivos conectados al bus de expansión sólo recibiremos
interrupciones generadas por la ULA, con un device_id predictible de valor $FF (este device-id es también el
causante de que la tabla sea de 257 bytes y no de 256).

Veamos cómo instalar una rutina ISR que se ejecute cada vez que se recibe una interrupción de la ULA. Para
ello podemos generar nuestra tabla de vectores de interrupción a partir de una posición de memoria como, por
ejemplo, $FE00. Para ello asignamos a I el valor $FE, lo que implica que nuestra tabla de vectores de
interrupción estará localizada desde $FE00 hasta $FEFF+1 ($FF00).

Para atender a las interrupciones generadas por la ULA (device_id $FF), tendremos que realizar los siguientes
pasos:

• Crear una rutina de ISR correcta (preservar registros, salir con EI+RETI o EI+RET, etc).
• Colocar la dirección de nuestra ISR en las posiciones de memoria $FEFF y $FEFF+1 (que es de donde
leerá el Z80 la dirección de salto cuando reciba la interrupción con ID $FF).
• Asignar a I el valor $FE y saltar a IM 2 (de esta forma, le decimos al Z80 que la tabla de vectores de
interrupción empieza en $FE00).

El código resultante sería el siguiente:

; Instalando una ISR de atención a la ULA.

ORG 50000

; Instalamos la ISR:
LD HL, ISR_ASM_ROUTINE ; HL = direccion de la rutina de ISR
DI ; Deshabilitamos las interrupciones
LD ($FEFF), HL ; Guardamos en (65279 = $FEFF) la direccion
LD A, 254 ; de la rutina ISR_ASM_ROUTINE
LD I, A ; Colocamos en I el valor $FE
IM 2 ; Saltamos al modo de interrupciones 2
EI

(resto programa)

;--- Rutina de ISR. ---------------------------------------------


ISR_ASM_ROUTINE:
PUSH AF
PUSH HL

(código de la ISR)

POP HL
POP AF

EI
RETI

De esta forma, saltamos a IM 2 y el procesador se encargará de ejecutar la rutina ISR_ASM_ROUTINE 50


veces por segundo, por el siguiente proceso:

• La ULA provoca una señal de interrupción enmascarable INT.


• La ULA no coloca ningún device ID en el bus de datos; debido a las resistencias de pull-up, el bus de
datos toma el valor $FF.
• El procesador termina de ejecutar la instrucción en curso y, si las interrupciones están actualmente
habilitadas procesa la interrupción.
• El procesador lee del bus de datos el valor $FE y, al estar en modo IM 2, compone junto al registro una
dirección “$FEFF” (campo $FF dentro de la tabla de vectores de interrupción que empieza en $FE*256
= $FE00).
• El procesador lee la dirección de 16 bits que se compone con el contenido de las celdillas de memoria
$FEFF y $FEFF+1 ($FF00). Esta dirección de 16 bits es la que hemos cargado nosotros con “LD
($FEFF), HL” y que apunta a nuestra rutina ISR.
• El procesador salta a la dirección de 16 bits compuesta, que es la dirección de nuestra ISR.
• Se ejecuta la ISR, de la cual salimos con EI+RETI, provocando la continuación de la ejecución del
programa original hasta la siguiente interrupción.

Como hemos dicho en el apartado sobre ISRs, es crítico que la tabla de vectores de interrupción se ubique en
una página alta de la RAM, es decir; que no esté dentro del área comprendida entre los 16K y los 32K que el
procesador y la ULA comparten regularmente para que la ULA pueda actualizar la pantalla. En todos los
ejemplos que hemos visto y veremos, la dirección de la tabla de vectores de interrupción comienza a partir de
$FE00. Ubicarla a partir de $FF00 (que es la única página más alta que $FE00) no sería una elección apropiada
puesto que al necesitar una tabla de 257 bytes para la ULA (device ID=$FF), parte de la dirección de salto se
compondría con “$FFFF +1 = $0000” (la ROM).

Con la teoría descrita hasta ahora ya tenemos los mecanismos para realizar programas que dispongan de sus
propias ISRs de servicio, como los ejemplos que veremos a continuación.

Ejemplos y aplicaciones

Control de ticks, segundos y minutos

A continuación se muestra un ejemplo completo de ISR que gestiona una serie de variables en memoria:

• abs_ticks : Esta variable se incrementa en cada ejecución de la interrupción (es decir, 50 veces por
segundo), y al ser de 16 bits se resetea a 0 al superar el valor 65535. Puede ser muy útil como
controlador de tiempo entre 2 sucesos que duren menos de 21 minutos (65535/50/60).
• timer: Esta variable es igual que abs_ticks pero se decrementa en lugar de incrementarse. Existe para
ser utilizada por ciertas funciones útiles que veremos más adelante en este mismo capítulo.
• ticks : Esta variable se incrementa igual que abs_ticks en cada ejecución de la ISR (50 veces por
segundo), pero cuando su valor llega a 50 la seteamos a 0 y aprovechamos este cambio para incrementar
la variable segundos.
• seconds : Esta variable almacena segundos transcurridos. Sólo se incrementa cuando ticks vale 50, es
decir, cuando han pasado 50 ticks que son 1 segundo. Cuando la variable llega a 60, se resetea a cero y
se incrementa la variable minutes.
• pause : Esta variable nos permite que la ISR no incremente el tiempo cuando estamos en “modo pausa”.
• clock_changed : Esta variable cambia de 0 a 1 cuando nuestra ISR ha modificado el “reloj” interno
formado por las variables minutos y segundos. La utiliza el bucle principal del programa para saber
cuándo actualizar el reloj en pantalla.

Para ello generamos una ISR y la enlazamos con el modo 2 de interrupciones.

Tras esto, nos mantenemos en un bucle de programa infinito que detecta cuándo la variable clock_changed
cambia de 0 a 1 y que actualiza el valor en pantalla del reloj, volviendo a setear dicha variable a 0 hasta que la
ISR modifique de nuevo el reloj. Cuando clock_changed vale 0, el programa se mantiene en un simple bucle
que no realiza acciones salvo comprobar el estado de clock_changed continuamente. La ISR se ejecuta, por
tanto, “en paralelo” a nuestro programa cuando las interrupciones solicitan la atención del procesador, 50 veces
por segundo.

; Ejemplo de ISR que gestiona un contador de ticks, minutos y segundos.

ORG 50000

; Instalamos la ISR:
LD HL, CLOCK_ISR_ASM_ROUTINE
DI
LD (65279), HL ; Guardamos en (65279 = $FEFF) la direccion
LD A, 254 ; de la rutina CLOCK_ISR_ASM_ROUTINE
LD I, A
IM 2
EI

Bucle_entrada:
LD A, (clock_changed)
AND A
JR Z, Bucle_entrada ; Si clock_changed no vale 1, no hay
; que imprimir el mensaje -> loop

; Si estamos aqui es que clock_changed = 1... lo reseteamos


; e imprimimos por pantalla la información como MM:SS
XOR A
LD (clock_changed), A ; clock_changed = 0

CALL 0DAFh ; Llamamos a la rutina de la ROM que


; hace un CLS y pone el cursor en (0,0)

LD A, (minutes) ; Imprimimos minutos + ":" + segundos


CALL PrintInteger2Digits
LD A, ":"
RST 16
LD A, (seconds)
CALL PrintInteger2Digits

JR Bucle_entrada

clock_changed DB 0
ticks DB 0
seconds DB 0
minutes DB 0
pause DB 0
abs_ticks DW 0
timer DW 0

;-----------------------------------------------------------------------
; Rutina de ISR : incrementa ticks 50 veces por segundo, y el resto
; de las variables de acuerdo al valor de ticks.
;-----------------------------------------------------------------------
CLOCK_ISR_ASM_ROUTINE:
PUSH AF
PUSH HL

LD A, (pause)
OR A
JR NZ, clock_isr_fin ; Si pause==1, no continuamos la ISR

LD HL, (abs_ticks)
INC HL
LD (abs_ticks), HL ; Incrementamos abs_ticks (absolutos)

LD HL, (timer)
DEC HL
LD (timer), HL ; Decrementamos timer (ticks absolutos)

LD A, (ticks)
INC A
LD (ticks), A ; Incrementamos ticks (50 veces/seg)

CP 50
JR C, clock_isr_fin ; if ticks < 50, fin de la ISR
; si ticks >= 50, cambiar seg:min
XOR A
LD (ticks), A ; ticks = 0

LD A, 1
LD (clock_changed), A ; ha cambiado el numero de segundos

LD A, (seconds)
INC A
LD (seconds), A ; segundos = segundos +1

CP 60
JR C, clock_isr_fin ; si segundos < 60 -> salir de la ISR

XOR A ; si segundos == 60 -> inc minutos


LD (seconds), A ; segundos = 0

LD A, (minutes)
INC A
LD (minutes), A ; minutos = minutos + 1

CP 60
JR C, clock_isr_fin ; si minutos > 60 -> resetear minutos
XOR A
LD (minutes), A ; minutos = 0

clock_isr_fin:
POP HL
POP AF

EI
RETI

;-----------------------------------------------------------------------
; PrintInteger2Digits: Imprime en la pantalla un numero de 1 byte en
; base 10, pero solo los 2 primeros digitos (0-99).
; Para ello convierte el valor numerico en una cadena llamando
; a Byte2ASCII_2Dig y luego llama a RST 16 para imprimir cada
; caracter por separado.
;
; Entrada: A = valor a "imprimir" en 2 digitos de base 10.
;-----------------------------------------------------------------------
PrintInteger2Digits:
PUSH AF
PUSH DE
CALL Byte2ASCII_Dec2Digits ; Convertimos A en Cadena Dec 0-99
LD A, D
RST 16 ; Imprimimos primer valor HEX
LD A, E
RST 16 ; Imprimimos segundo valor HEX

POP DE
POP AF
RET

;-----------------------------------------------------------------------
; Byte2ASCII_Dec2Digits: Convierte el valor del registro H en una
; cadena de texto de max. 2 caracteres (0-99) decimales.
;
; IN: A = Numero a convertir
; OUT: DE = 2 bytes con los ASCIIs
;
; Basado en rutina dtoa2d de:
; http://99-bottles-of-beer.net/language-assembler-%28z80%29-813.html
;-----------------------------------------------------------------------
Byte2ASCII_Dec2Digits:
LD D, '0' ; Starting from ASCII '0'
DEC D ; Because we are inc'ing in the loop
LD E, 10 ; Want base 10 please
AND A ; Clear carry flag

dtoa2dloop:
INC D ; Increase the number of tens
SUB E ; Take away one unit of ten from A
JR NC, dtoa2dloop ; If A still hasn't gone negative, do another
ADD A, E ; Decreased it too much, put it back
ADD A, '0' ; Convert to ASCII
LD E, A ; Stick remainder in E
RET
;-----------------------------------------------------------------------

END 50000

La siguiente captura muestra la salida del anterior programa de ejemplo transcurridos 1 minuto y 5 segundos
desde el inicio de su ejecución:

El programa anterior nos muestra algunos detalles interesantes:


• CALL 0DAFh y RST 16 (CLS y PRINT-A): Podemos aprovechar las rutinas de la ROM (CLS, PLOT,
DRAW, PRINT-A) dentro de nuestros programas, evitando escribir más código del necesario cuando la
ROM ya provee de alguna rutina para ello. Por contra, esto hace nuestros programas no portables, ya
que las rutinas de la ROM del Spectrum no están presentes (al menos no en las mismas direcciones y
con los mismos parámetros de entrada y salida) en otros sistemas basados en Z80 como el Amstrad o el
MSX.

• Byte2ASCII_Dec2Digits : Esta rutina permite convertir un valor numérico de 8 bits de 0 a 99 en 2


caracteres ASCII para imprimir después con la rutina PrintInteger2Digits.

Algo tan básico como disponer de un reloj interno de ticks, segundos y minutos es sumamente importante para
los juegos, puesto que podemos:

• Temporizar el juego para proporcionar al jugador un tiempo límite o informarle de cuánto tiempo lleva
transcurrido. La variable “pause” permite que la ISR no cuente el tiempo cuando a nosotros nos interese
detener el contaje (juego pausado por el jugador, al mostrar escenas entre fase y fase o mensajes
modales en pantalla, etc).

• Utilizar la información de ticks (como “timer” o “abs_ticks” y/o otras variables “temporales” que
podemos agregar a la ISR) para que el tiempo afecte al juego. Esto permite, por ejemplo, para reducir el
nivel de vida de un personaje con el tiempo, etc.

• Actualizar regularmente el buffer de “notas” del chip AY de los modelos de 128K para reproducir
melodías AY en paralelo a la ejecución de nuestro programa.

• Llevar un control exacto de ticks para procesos de retardos con valores precisos. Es decir, si
necesitamos hacer una espera de N ticks, o de N segundos (sabiendo que 50 ticks son 1 segundo),
podemos utilizar la variable de 16 bits “timer” que se decrementa en cada ejecución de la ISR. Podemos
así generar una rutina WaitNTicks en la que establecer “timer” a un valor concreto y esperar a que valga
0 (ya que será decrementado por la ISR).

En este ejemplo en lugar de esperar a que timer valga 0, esperamos a que su byte alto valga FFh, en
previsión de utilizarla en otros bloques de código más largos en el que se nos pueda pasar el ciclo exacto
en que timer sea igual a cero. Comprobando que el byte alto de timer sea FFh, tenemos una rutina que
nos permite tiempos de espera desde 1 tick hasta 49151/50/60=16 minutos. Veamos el siguiente
ejemplo:

; Ejemplo de WaitNticks

ORG 50000

; Instalamos la ISR:
LD HL, CLOCK_ISR_ASM_ROUTINE
DI
LD (65279), HL ; Guardamos en (65279 = $FEFF) la direccion
LD A, 254 ; de la rutina CLOCK_ISR_ASM_ROUTINE
LD I, A
IM 2
EI

Bucle_entrada:
LD A, "."
RST 16
LD A, "5"
RST 16
LD A, " "
RST 16 ; Imprimimos por pantalla ".5 "
LD HL, 25 ; Esperamos 25 ticks (0.5 segundos)
CALL WaitNTicks
LD A, "3"
RST 16
LD A, " "
RST 16 ; Imprimimos por pantalla "3 "
LD HL, 3*50 ; Esperamos 150 ticks (3 segundos)
CALL WaitNTicks

JP Bucle_entrada

ticks DB 0
timer DW 0

;-----------------------------------------------------------------------
; WaitNTicks: Esperamos N ticks de procesador (1/50th) en un bucle.
;-----------------------------------------------------------------------
WaitNTicks:
LD (timer), HL ; seteamos "timer" con el tiempo de espera

Waitnticks_loop: ; bucle de espera, la ISR lo ira decrementando


LD HL, (timer)
LD A, H ; cuando (timer) valga 0 y lo decrementen, su
CP $FF ; byte alto pasara a valer FFh, lo que quiere
; decir que ha pasado el tiempo a esperar.
JR NZ, Waitnticks_loop ; si no, al bucle de nuevo.
RET

;-----------------------------------------------------------------------
; Rutina de ISR : incrementa ticks y decrementa timer 50 veces por seg.
;-----------------------------------------------------------------------
CLOCK_ISR_ASM_ROUTINE:
PUSH AF
PUSH HL

LD HL, (timer)
DEC HL
LD (timer), HL ; Decrementamos timer (absolutos)

LD A, (ticks)
INC A
LD (ticks), A ; Incrementamos ticks (50 veces/seg)

CP 50
JR C, clock_isr_fin ; if ticks < 50, fin de la ISR
XOR A ; si ticks >= 50, cambiar seg:min
LD (ticks), A ; y ticks = 0

clock_isr_fin:
POP HL
POP AF
EI
RETI

END 50000

Este ejemplo muestra por pantalla la cadena de texto “.5 ” (con un espacio al final) y después espera 25 ticks
(0.5 segundos). A continuación muestra la cadena “3 ” y espera 150 ticks (3 segundos). Este proceso se repite
en un bucle infinito.
• Aprovechando las rutinas anteriores, y que tenemos disponible al procesador para, por ejemplo,
chequear el teclado, podemos agregar a nuestro programa funciones como la siguiente, la cual mantiene
al procesador dentro de un bucle durante N segundos o bien hasta el usuario pulse una tecla,
permitiendo pantallas de presentación o de créditos donde no es necesario obligar al usuario a pulsar una
tecla para avanzar, aunque siga existiendo esta posibilidad.

;-----------------------------------------------------------------------
; WaitKeyOrTime: Esperamos N ticks de procesador o una tecla.
;-----------------------------------------------------------------------
WaitKeyOrTime:
LD (timer), HL ; seteamos "timer" con el tiempo de espera

Waitkeyticks_loop: ; bucle de espera, la ISR lo ira decrementando


XOR A
IN A,(254)
OR 224
INC A ; Comprobamos el estado del teclado
RET NZ ; Si hay tecla pulsada, salimos

LD HL, (timer)
LD A, H ; cuando (timer) valga 0 y lo decrementen, su
CP $FF ; byte alto pasara a valer FFh, lo que quiere
; decir que ha pasado el tiempo a esperar.
JR NZ, Waitkeyticks_loop ; si no, al bucle de nuevo.
RET

Compatibilidad de nuestra ISR en Timex Sinclair y con otros periféricos


Este apartado es extremadamente importante si pretendemos realizar programas totalmente funcionales en otros
modelos de Spectrum como los TIMEX SINCLAIR vendidos en el continente Americano, o bien en modelos
estándar con dispositivos conectados al bus de expansión.

Como ya hemos dicho, la ULA no está preparada para funcionar en IM 2 y por lo tanto no coloca ningún valor
en el bus de datos antes de generar una señal de interrupción. Debido a las resistencias pull-up, el valor “por
defecto” de este bus es $FFh (11111111b), que es el valor que hemos utilizado en nuestros anteriores ejemplos
para diseñar una ISR que se ejecute las 50 (ó 60) veces por segundo que interrumpe la ULA al Z80A. Es por
eso que en los ejemplos anteriores estamos escribiendo la dirección de nuestra ISR en ($FEFF y $FEFF+1).

Por desgracia, si tenemos conectado algún dispositivo hardware mal diseñado en el bus de expansión del
Spectrum, el valor del bus ya no tiene por qué ser $FFh sino que estos dispositivos externos pueden provocar
que dicho valor sea diferente. El mismo problema sucede en ciertos modelos de Timex Sinclair, como por
ejemplo el TS2068, el cual no tiene las resistencias de pull-up conectadas a las líneas del bus de datos por lo
que el valor que aparezca en dicho bus puede ser totalmente arbitrario o aleatorio. Los programas anteriores de
ejemplo, que ubicaba la ISR en $FEFF (asumiendo el device-id de $FF), no tendría asegurada la compatibilidad
con este modelo.

Con un valor aleatorio en el bus de datos, el procesador podría saltar a cualquiera de las direcciones de la tabla
de vectores de interrupción.

Una primera aproximación a solucionar este problema podría ser la de introducir la misma dirección de salto (la
de nuestra rutina ISR) en las 128 direcciones de salto de la tabla de vectores. De esta forma, fuera cual fuera el
valor en el bus de datos, el procesador siempre saltaría a nuestra ISR.

TABLA DE VECTORES DE INTERRUPCION

Posición - Valor
--------------------
($FE00) - $FE
($FE01) - $00
($FE02) - $FE
($FE03) - $00
(...) -
($FEFF) - $FE
($FF00) - $00

El problema es que tampoco podemos determinar si este valor aleatorio en el bus es par o impar, de forma que
si fuera par saltaría a la dirección correcta de uno de los vectores ($FE00), mientras que si fuera impar saltaría a
una dirección incorrecta compuesta por parte de la dirección de un device-id, y parte de la dirección del otro
($00FE), como ya vimos en un apartado anterior.

La forma de solucionar esta problemática es bastante curiosa y original: basta con ubicar nuestra ISR en una
dirección “capicúa”, donde coincidan la parte alta y la parte baja de la misma, y rellenar la tabla de vectores de
interrupción con este valor. Por ejemplo, en el compilador de C z88dk y la librería SPlib se utiliza para su ISR
la dirección $F1F1. De esta forma, la tabla de vectores de interrupción se llena con 257 valores “$F1”. Así, sea
cual sea el valor que tome el bus de datos cuando se recibe la interrupción (y sea par o impar), el procesador
siempre saltará a $F1F1, donde estará nuestra ISR.

TABLA DE VECTORES DE INTERRUPCION

Posición - Valor
--------------------
($FE00) - $F1
($FE01) - $F1
($FE02) - $F1
($FE03) - $F1
(...) - $F1
($FEFF) - $F1
($FF00) - $F1

La pega de este sistema es que convertimos al versátil modo IM 2 con posibilidad de ejecutar hasta 128 ISRs
diferentes que atiendan cada una a su periférico correspondiente en una evolución del IM 1 (donde siempre se
saltaba a $0038), pero en la cual la dirección de salto está fuera de la ROM y es personalizable. Este “IM 1
mejorado” lo que nos permite es funcionar como en IM1 pero con nuestra propia ISR única. De hecho, esta es
la forma más habitual de utilizar IM 2.

La única desventaja es que en esta ISR deberemos gestionar todas las interrupciones de cualquier periférico
basado en interrupciones que queramos que interactúe con nuestro programa, aunque en el 99% de los
programas o juegos (a excepción del uso del AMX mouse o similares) no se suele interactuar con los
periféricos mediante este sistema.

Resumamos lo que acabamos de ver y comprender de forma esquemática las ventajas de una tabla de 257 bytes
con el valor $F1 en cada elemento de la misma:
1. Por lo que hemos visto hasta ahora, en un Spectrum estándar sin dispositivos en el bus de expansión la
ULA se identifica al interrumpir el procesador como $FF, aunque no intencionadamente pues es el
resultado del valor por defecto que hay en el bus de datos cuando no se coloca ningún dato debido a las
resistencias de pull-up.
2. En nuestras anteriores rutinas guardábamos en FEFFh la dirección de la rutina de ISR que queríamos
que se ejecutara cuando la interrupcion se identificaba con ID $FF (la ULA).
3. Cargábamos I con 256 (FE) antes de saltar a IM2, de esta forma cuando la ULA producía una
interrupción con id $FFh se saltaba a la direccion que había en ($FEFF), que era la de nuestra ISR.
4. Por desgracia, en ciertos modelos Timex Sinclair o si tenemos dispositivos conectados al bus de
expansion puede que no encontremos $FF en el bus de datos, sino un valor arbitrario. Esto puede
producir que la interrupcion no llegue como “ID=FF” y que, por lo tanto, no se produzca el salto a
($FEFF) sino a otro de los elementos de la tabla de vectores de interrupcion.
5. Para evitar que esto ocurra, podemos generar una tabla de 257 bytes y llenarla con el valor “$F1”. De
esta forma, sea cual sea el valor leído en el bus de datos, se saltará a $F1F1 (ya sea par o impar el valor
del bus, las 2 partes de la dirección de salto en la tabla siempre sería $F1 + $F1).
6. Nuestra ISR deberá de estar pues en $F1F1 y ser la responsable de gestionar cualquier periferico que
pueda haber generado la interrupción.

A continuación podemos ver cómo sería el esqueleto del programa de ejemplo de reloj temporizador visto
anteriormente utilizando una ISR diseñada para funcionar aunque existan dispositivos conectados al bus de
expansión que modifiquen el valor del mismo cuando no haya datos en él.

; Ejemplo de ISR que gestiona un contador de ticks, minutos y segundos.


; Este sistema de definir la ISR y saltar a IM2 es el mas compatible
; con todos los modelos de Sinclair Spectrum y dispositivos conectados.

ORG 50000

; Generamos una tabla de 257 valores "$F1" desde $FE00 a $FF00


LD HL, $FE00
LD A, $F1
LD (HL), A ; Cargamos $F1 en $FE00
LD DE, $FE01 ; Apuntamos DE a $FE01
LD BC, 256 ; Realizamos 256 LDI para copiar $F1
LDIR ; en toda la tabla de vectores de int.

; Instalamos la ISR:
DI
LD A, 254 ; Definimos la tabla a partir de $FE00.
LD I, A
IM 2 ; Saltamos a IM2
EI

: ---------------------------------------------------------------
; (aqui insertamos el codigo del programa, incluidas subrutinas)
: ---------------------------------------------------------------

; A continuación la rutina ISR, ensamblada en $F1F1:


;
; Con el ORG $F1F1 nos aseguramos de que la ISR sera ensamblada
; por el ensamblador a partir de esta direccion, que es donde queremos
; que este ubicada para que el salto del procesador sea a la ISR.
; Asi pues, todo lo que siga a este ORG se ensamblara para cargarse
; a partir de la direccion $F1F1 de la RAM.

ORG $F1F1

;-----------------------------------------------------------------------
; Rutina de ISR : incrementa ticks 50 veces por segundo, y el resto
; de las variables de acuerdo al valor de ticks.
;-----------------------------------------------------------------------
CLOCK_ISR_ASM_ROUTINE:
PUSH AF
PUSH HL

; (... aqui insertamos el resto de la ISR...)

clock_isr_fin:
POP HL
POP AF

EI
RETI

; Si vamos a colocar mas codigo en el fichero ASM detra de la ISR; este


; sera ensamblado en direcciones a partir del final de la ISR en memoria
; (siguiendo a la misma). Como seguramente no queremos esto, es mejor ubicar
; la ISR con su ORG $F1F1 al final del listado, o bien enlazarla como
; un binario aparte junto al resto del programa, o bien colocar otro ORG
; tras las ISR y antes de la siguiente rutina a ensamblar.

END 50000

El “ORG $F1F1” indica al assembler (pasmo en este caso) que debe ensamblar todo lo que va detrás de esta
directiva a partir de la dirección de memoria indicada. En el ejemplo anterior hemos ubicado la rutina de ISR al
final del programa, puesto que si seguimos añadiendo código tras la ISR, esté será ensamblado en ($F1F1 +
TAMAÑO_ISR). Si no queremos que la ISR esté al final del listado fuente, podemos utilizar las siguientes
directivas de pasmo para continuar el ensamblado de más rutinas a partir de la dirección inmediatamente
anterior al ORG:

; Recuperando una posicion de ensamblado


ORG 50000

; Nuestro programa
; (...)

; Guardamos en una variable de preprocesador la posicion


; de este punto en el proceso de ensamblado ($)
PUNTO_ENSAMBLADO EQU $

;----------------------------------------------------
; Nuestra rutina de ISR ensamblada en $F1F1 debido
; a la directiva ORG $F1F1
;----------------------------------------------------
ORG $F1F1
Rutina_ISR:
; La rutina ISR

ORG PUNTO_ENSAMBLADO
;----------------------------------------------------
; El codigo continua pero no ensamblado tras $F1F1
; sino en la direccion anterior al ORG
;----------------------------------------------------
Mas_Rutinas:
; Resto del programa

El listado completo del ejemplo está disponible para su descarga al final del capítulo, pero es esencialmente
igual al primer ejemplo de reloj interno basado en ISR con ciertas excepciones:

• La rutina de ISR se ensambla en $F1F1 mediante una directiva del ensamblador ORG $F1F1 antes de la
misma, lo cual hace que en el programa resultante, dicho código se ubique a partir de $F1F1.
• Se genera en $FE00 una tabla de 257 bytes conteniendo el valor $F1, para que la dirección de salto de
una interrupción sea siempre $F1F1 independientemente de cuál sea el valor del device_id en el bus de
datos (sea también par o impar).
De esta forma nuestra ISR será compatible con los diferentes modelos de Sinclair Spectrum con o sin
periféricos conectados al bus de expansión.

Otros autores de libros sobre programación (y a su vez programadores), como David Webb, proponen la
utilización de $FDFD como vector de salto, y colocar en esta dirección un JP a la dirección de la rutina real.
Nótese que $FDFD está 3 bytes en memoria antes que $FE00, por lo que de esta forma se puede tener el salto a
la ISR junto a la tabla de vectores de interrupción, consecutivos en memoria. No obstante, esto añade 10 t-
estados adicionales a la ejecución de la ISR, los relativos al salto, y no nos es de especial utilidad dada la
posibilidad de los ensambladores cruzados de ubicar nuestra ISR en $F1F1 mediante la directiva ORG.

Curiosidades y consideraciones

Teclado y el cassette por interrupciones

Estando conectados los puertos de teclado y de unidad de cassette y disco a la ULA, el lector podría preguntarse
por qué las pulsaciones de teclado o la entrada de datos desde estos dispositivos de almacenamiento no se
gestionan mediante interrupciones.

Lamentablemente, en el caso del ZX Spectrum no se gestiona la pulsación de teclas ni la E/S del cassette
mediante interrupciones, sino que la ULA proporciona al procesador acceso al estado de estos componentes
directamente mediante operaciones de I/O (instrucciones Z80 IN/OUT).

Las instrucciones de acceso al cassette, por ejemplo, requieren tanta cantidad de tiempo de ejecución del
procesador que no sería factible tratarlo en una ISR de interrupción, sobre todo en un microprocesador con la
“escasa” potencia del Z80 y a 3.50 Mhz.

El bug de la ISR de NMI en la ROM

Resulta especialmente curioso el motivo por el cual las interrupciones NMI son generalmente de nula utilidad
en el Spectrum salvo para provocar un reset. Como ya hemos dicho, al recibirse una NMI se realiza un salto a
$0066 donde hay un bloque de código “ISR” especial de la ROM el cual, en teoría, estaba preparado para
permitir la ejecución de una subrutina propia cuya dirección ubicaramos en la dirección de memoria $5CB0, a
menos que el contenido de $5CB0 fuera 0, que provocaría un retorno de la NMI.

Por desgracia, un bug en esta subrutina acabó dejándola inservible salvo en el caso de ubicar un cero en esta
variable del sistema, con RESET como única consecuencia.

; THE 'NON-MASKABLE INTERRUPT' ROUTINE


; This routine is not used in the standard Spectrum but the code
; allows for a system reset to occur following activation of the
; NMI line. The system variable at 5CB0, named here NMIADD, has
; to have the value zero for the reset to occur.
0066 RESET: PUSH AF ; Save the current values held
PUSH HL ; in these registers.
LD HL,($5CB0) ; The two bytes of NMIADD
LD A,H ; must both be zero for the reset
OR L ; to occur.
JR NZ,$0070 ; Note: This should have been JR Z!
JP (HL) ; Jump to START.
0070 NO-RESET POP HL ; Restore the current values to
POP AF ; these registers and return.
RETN
La instrucción “JR NZ, $0070” debería haber sido un “JR Z, $0070” para permitir un “JP ($5CB0)” al recibir
una NMI.

Este bug estaba presente en la primera versión de la ROM del Spectrum y no fue corregido en futuras versiones,
entendemos que para preservar la compatibilidad hacia atrás y evitar que aparecieran dispositivos hardware que
no funcionara en revisiones del Spectrum con el bug en su ROM.

Modo IM 2 en Spectrum 16K

Como ya hemos comentado, la ULA y el procesador compiten en uso por la zona de memoria comprendida
entre los 16K y los 32K, por lo que es crítico ubicar el vector de interrupciones en un banco de memoria por
encima de los 32K (típicamente, en $FE00).

Lamentablemente, en los modelos de 16KB de memoria sólo tenemos disponible la famosa página de 16KB
entre $4000 y $7FFF que se ve afectada por las lecturas de memoria de la ULA.

Aunque no es habitual diseñar programas para los modelos de Spectrum de 16KB, Miguel A. Rodríguez Jódar
nos aporta una solución basada en apuntar el registro I a la ROM de tal forma que (I*256)+$FF proporcione un
valor de la ROM cuyo contenido sea una dirección de memoria física en RAM disponible para ubicar nuestra
ISR. Para poder realizar este pequeño truco es importante saber que el valor del bus de datos durante la
interrupción será $FF, es decir, saber que no hay dispositivos mal diseñados conectados al bus de expansión
que puedan alterar el valor del bus de datos:

Citando a Miguel A.:

La idea es poner un valor a I que esté comprendido entre 00h y 3Fh. Esto, claro está, hace que I apunte a la
ROM, y que por tanto la dirección final de salto tenga que estar en la ROM. ¿Y esto plantea alguna dificultad?
No, si encontramos algún valor de I tal que la posición I*256+255 contenga un valor de 16 bits (la dirección
final de la ISR) que esté entre 4000h y 7FFFh.

El siguiente programa en BASIC escanea la ROM probando todas las combinaciones de I posibles entre 0 y 63,
y muestra en pantalla la dirección en la que debería comenzar la ISR para los valores válidos que encuentre:

El resultado, con la ROM estándar del Spectrum 16/48K, es éste:


Así, por ejemplo, se puede escribir una rutina IM 2 para un programa de 16K, así:

LD I, 40
IM 2
... resto del programa...

ORG 32348
...aqui va la ISR...

Aunque se use este sistema, hay aún un “peligro” oculto, aunque considero que es de menor relevancia, al
menos hoy día. Es el hecho de que los periféricos “copiones” tipo Transtape y similares, usados en un
Spectrum real para crear un snapshot, no pueden saber si el micro está en modo IM1 o IM2. Pueden saber si
las interrupciones estaban habilitadas o no consultando el valor del flip-flop IFF2 al que se accede mediante el
bit de paridad del registro F tras ejecutar una instrucción LD A,I ó LD A,R , pero no se puede saber
directamente si el micro está en modo IM 0, IM1 ó IM 2.

En un emulador que cree snapshosts esto no es un problema, pero en un Spectrum real sí. Los “copiones” usan
un método puramente heurístico que consiste en ver el valor de I: si dicho valor es mayor que 63, entonces
asumen que el programa lo ha modificado con la intención de usar su propia ISR, por lo que graban el
snapshot indicando que el micro estaba en modo IM 2. En cualquier otro caso, asumen IM 1.

Habría que ver el código de la ROM de los copiones más populares para ver qué condición chequean, ya que
la otra opción es que asuman IM 1 sólamente cuando I valga 63 (el valor que la ROM pone por defecto) y
asuman IM 2 en cualquier otro caso. Si es así como lo hacen, un snapshot de 16K con interrupciones se
generará con la información correcta.
Paginación de memoria 128K
Si el bus de direcciones del Spectrum es de 16 bits y por lo tanto sólo puede acceder a posiciones de memoria
entre 0 y 65535… ¿cómo se las arreglan los modelos de 128KB para acceder a la memoria situada entre las
celdillas 65535 y 131071? La respuesta es un mecanismo tan simple como ingenioso: se utiliza una técnica
conocida como paginación de memoria.

En este artículo aprenderemos a aprovechar los 128K de memoria de que disponen los modelos de Spectrum
128K, +2, +2A y +3. Gracias a la paginación nos saltaremos la limitación de direccionamiento de 64K del
microprocesador Z80 para acceder a la totalidad de la memoria disponible.

Paginación
Los modelos de Spectrum 128K, +2, +2A, +2B y +3 disponen de 128KB de memoria, aunque no toda está
disponible simultáneamente. Al igual que en el modelo de 48KB, el microprocesador Z80 sólo puede
direccionar 64K-direcciones de memoria, por lo que para acceder a esta memoria “extra”, se divide en bloques
de 16KB y se “mapea” (pagina) sobre la dirección de memoria $c000.

¿Qué quiere decir esto? Que nuestro procesador, con un bus de direcciones de 16 bits, sólo puede acceder a la
memoria para leer las “celdillas” entre $0000 y $FFFF (0-65535). Para leer casillas de memoria superiores a
65535, harían falta más de 16 bits de direcciones, ya que 65535 es el mayor entero que se puede formar con 16
bits (1111111111111111b).

Así que … ¿cómo hacemos para utilizar más de 64KB de memoria si nuestro procesador sólo puede leer datos
de la celdilla 0, 1, 2, 3 … 65534 y 65535? La respuesta es: mediante la paginación.

Los 64KB de memoria del Spectrum, se dividen en 4 bloques de 16KB. El primer bloque ($0000 - $4000) está
mapeado sobre la ROM del Spectrum. Accediendo a los bytes desde $0000 a $3FFF de la memoria estamos
accediendo a los bytes $0000 a $3FFF del “chip (físico)” de 16K de memoria que almacena la ROM del
Spectrum. Se dice, pues, que la ROM está mapeada (o paginada) sobre $0000 en el mapa de memoria.

Dejando la ROM de lado (16KB), los 48KB de memoria restantes están formados por 3 bloques de 16KB. El
segundo bloque de 16K de memoria (primero de los 3 que estamos comentando), en el rango $4000 a $7FFF
está mapeado sobre el “chip de memoria” que almacena el área de pantalla del Spectrum. El tercero ($8000 a
$BFFFF) es memoria de propósito general (normalmente cargaremos nuestros programas aquí).

El bloque que nos interesa a nosotros es el cuarto. La zona final del área de memoria, desde $C000 a $FFFF, es
nuestra “ventana” hacia el resto de memoria del 128K. Dividiendo los 128KB de memoria en bloques de 16KB,
tendremos 8 bloques que podremos “montar” (o paginar) sobre el área $C000 a $FFFF.

Veamos una figura donde se muestra el estado del mapa de memoria del Spectrum:
Cualquiera de los 8 bloques de 16KB (128 KB / 16 KB = 8 bloques) puede ser “mapeado” en los 16Kb que van
desde $c000 a $ffff, y podremos cambiar este mapeo de un bloque a otro mediante instrucciones I/O concretas.

La última porción de 16KB de la memoria es, pues, una “ventana” que podemos deslizar para que nos dé
acceso a cualquiera de los 8 bloques disponibles. Esto nos permite “paginar” el bloque 0 y escribir o leer sobre
él, por ejemplo. El byte 0 del bloque 0 se accede a través de la posición de memoria $C000 una vez paginado,
el byte 1 desde $C0001, y así hasta el byte 16383, al que accedemos mediante $FFFF.

Si paginamos el bloque 1 en nuestra ventana $C000-$FFFF, cuando accedamos a este rango de memoria, ya no
accederíamos a los mismos datos que guardamos en el banco 0, sino a la zona de memoria “Banco 1”. Es
posible incluso mapear la zona de pantalla (Banco 5), de forma que las direcciones $4000 y $C000 serían
equivalentes: los 8 primeros píxeles de la pantalla.

El mapa de memoria del Spectrum con los bloques mapeables/paginables sobre $C000 es el siguiente:
Cambiando de banco
El puerto que controla la paginación en los modelos 128K es el $7FFD. En realidad, nuestro Spectrum sólo
decodifica los bits 1 y 15, por lo que cualquier combinación con los bits 1 y 15 a cero accederá a la gestión de
paginación. No obstante, se recomiente utilizar $7FFD por compatibilidad con otros sistemas.

La lectura del puerto $7FFD no devolverá ningún valor útil, pero sí podemos escribir en él un valor con cierto
formato:

Bits Significado
0-2 Página de la RAM (0-7) a mapear en el bloque $c000 - $ffff.
Visualizar la pantalla gráfica “normal” (0) o shadow (1).
La pantalla normal está en el banco 5, y la shadow en el 7.
Aunque cambiemos a la visualización de la pantalla shadow,
3
la pantalla “normal” RAM5 seguirá mapeada entre $4000 y $7fff.
No es necesario tener mapeada la pantalla shadow
para que pueda ser visualizado su contenido.
4 Selección de la ROM, entre (0) ROM BASIC 128K (Menú y editor), y (1) BASIC 48K.
Si lo activamos, se desactivará el paginado de memoria hasta que se resetee el
5
Spectrum. El hardware ignorará toda escritura al puerto $7FFD.

A la hora de cambiar de página de memoria hay que tener en cuenta lo siguiente:

• La pila (stack) debe de estar ubicada en una zona de memoria que no vaya a ser paginada (no puede
estar dentro de la zona que va a cambiar).
• Las interrupciones deben de estar deshabilitadas para realizar el cambio de banco.
• Si se va a ejecutar código con interrupciones (y no pueden estar deshabilitadas), entonces debemos
actualizar la variable del sistema $5B5C (23388d) con el último valor enviado al puerto $7FFD.
Un ejemplo de cambio de banco en ASM:

LD A, ($5b5c) ; Valor previo del puerto (variable del sistema)


AND $f8 ; Cambia sólo los bits que debas cambiar
OR 4 ; Seleccionar banco 4
LD BC, $7ffd ; Colocamos en BC el puerto a
DI ; Deshabilitamos las interrupciones
LD ($5b5c), A ; Actualizamos la variable del sistema
OUT (C), A ; Realizamos el paginado
EI

Podemos crearnos una rutina lista para usar con este código, como la que sigue:

;-----------------------------------------------------------------------
; SetRAMBank: Establece un banco de memoria sobre $c000
; Entrada : B = banco (0-7) a paginar entre $c000-$ffff
; Modifica : A, B, C
;-----------------------------------------------------------------------
SetRAMBank:
LD A, ($5b5c) ; Valor previo del puerto (variable del sistema)
AND $f8 ; Cambia sólo los bits que debas cambiar
OR B ; Seleccionar banco "B"
LD BC, $7ffd ; Colocamos en BC el puerto a
DI ; Deshabilitamos las interrupciones
LD ($5b5c), A ; Actualizamos la variable del sistema
OUT (C), A ; Realizamos el paginado
EI
RET

Un detalle apuntado por la documentación de World Of Spectrum es que los bancos 1, 3, 5 y 7 son “contended
memory”, lo que quiere decir que se reduce ligeramente la velocidad de acceso a estos bancos con respecto a
los otros bancos. Un apunte muy importante es que en el caso del +2A y +3, los bancos de contended-memory
ya no son el 1, 3, 5 y 7, sino los bloques 4, 5, 6 y 7. Al final de este capítulo veremos con más detalle qué es la
contended-memory y en qué puede afectar a nuestros programas.

Particularidades +2A/+3

En el caso del +2A y +3 hay que tener en cuenta una serie de detalles “extra” a lo visto anteriormente, y es que
estos 2 modelos tienen un modo de paginación especial, aunque siguen siendo compatible con el sistema de
paginación que hemos visto. Por eso estos detalles que veremos a continuación son opcionales, ya que podemos
utilizar el modo de paginación de la misma forma que en el +2 y 128K (paginación normal):

• Los bancos de contended-memory son los bloques 4, 5, 6 y 7 (no el 1, 3, 5 y 7).


• +2A y +3 tienen 4 ROMS en lugar de 2, por lo que el bit 4 del puerto $7FFD se convierte ahora en el bit
bajo de la ROM a seleccionar, mientras que el bit alto se toma del bit 2.
• +2A y +3 tienen funcionalidades extra de paginado, que se controlan con el puerto $1FFD.

Este puerto (el $1FFD) tiene el siguiente significado a la hora de escribir en él:

Bits Significado
0 Modo de paginado (0=normal, 1=especial)
1 Ignorado en el modo normal, usando en el modo especial.
En modo normal, bit alto de la selección de ROM.
2
Usado de forma diferente en el modo especial.
3-4 3=Motor del disco (1/0, ON/OFF), 4=Impresora
Cuando se activa el modo especial, el mapa de memoria cambia a una de estas configuraciones, según los
valores de los bits 1 y 2 del puerto $1FFD:

Por otra parte, las 4 ROMS mapeables del +2A y +3 son:

ROM Contenido
0 Editor 128K, Menú y programa de testeo
1 Chequeador de sintaxis 128K BASIC
2 +3DOS
3 BASIC 48K

De nuevo, al igual que en el caso del puerto genérico sobre paginación, es recomendable actualizar la variable
del sistema que almacena el “valor actual” de este puerto, en $5B67 (23399).

Ejemplo sencillo: alternando Bancos 0 y 1


El siguiente ejemplo muestra la paginación de la siguiente forma:

• Paginamos el bloque/banco 0 sobre el área $C000-$FFFF.


• Escribimos en memoria, en la posición $C000, el valor $AA.
• Paginamos el bloque/banco 1 sobre el área $C000-$FFFF.
• Escribimos en memoria, en la posición $C000, el valor $01.
• Volvemos a paginar el banco 0 sobre el área de paginación.
• Leemos el valor de la posición de memoria $C000 y rellenamos toda la pantalla con dicho valor.
• Volvemos a paginar el banco 1 sobre el área de paginación.
• Leemos el valor de la posición de memoria $C000 y rellenamos toda la pantalla con dicho valor.
Haciendo esto, guardamos 2 valores diferentes en 2 bancos diferentes, y posteriormente recuperamos dichos
bancos para verificar que, efectivamente, los valores siguen en las posiciones (0000) de los bancos y que la
paginación de una banco a otro funciona adecuadamente. Se han elegido los valores $AA y $01 porque se
muestra en pantalla como 2 tramas de pixeles bastante diferenciadas, siendo la primera un entramado de barras
verticales separadas por 1 pixel, y la segunda separados por 7 pixeles.

Para terminar de comprender el ejemplo, lo mejor es compilarlo y ejecutarlo:

;----------------------------------------------------------------------
; Bancos.asm
;
; Demostracion del uso de bancos / paginación en modo 128K
;----------------------------------------------------------------------

ORG 32000

LD HL, 0
ADD HL, SP ; Guardamos el valor actual de SP
EX DE, HL ; lo almacenamos en DE

LD SP, 24000 ; Pila fuera de $c000-$ffff

CALL Wait_For_Keys_Released
LD HL, $c000 ; Nuestro puntero

; Ahora paginamos el banco 0 sobre $c000 y guardamos un valor


; en el primer byte de sus 16K (en la direccion $c000):
LD B, 0
CALL SetRAMBank ; Banco 0

LD A, $AA
LD (HL), A ; ($c000) = $AA

; Ahora paginamos el banco 1 sobre $c000 y guardamos un valor


; en el primer byte de sus 16K (en la direccion $c000):
LD B, 1
CALL SetRAMBank ; Banco 1

LD A, $01
LD (HL), A ; ($C000) = $01

; Esperamos una pulsación de teclas antes de empezar:


CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released

; Ahora vamos a cambiar de nuevo al banco 0, leemos el valor que


; hay en $c000 y lo representamos en pantalla. Recordemos que
; acabamos de escribir $01 (00000001) antes de cambiar de banco,
; y que en su momento pusimos $AA (unos y ceros alternados):
LD B, 0
CALL SetRAMBank ; Banco 0
LD A, (HL) ; Leemos ($c000)
CALL ClearScreen ; Lo pintamos en pantalla

; Esperamos una pulsación de teclas:


CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released

; Ahora vamos a cambiar de nuevo al banco 1, leemos el valor que


; hay en $c000 y lo representamos en pantalla. Recordemos que
; acabamos de leer $A antes de cambiar de banco, y que en su
; momento pusimos $01:
LD B, 1
CALL SetRAMBank ; Banco 0
LD A, (HL) ; Leemos ($c000)
CALL ClearScreen ; Lo pintamos en pantalla

; Esperamos una pulsación de teclas:


CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released

EX DE, HL ; Recuperamos SP para poder volver


LD SP, HL ; a BASIC sin errores
RET

;-----------------------------------------------------------------------
; SetRAMBank: Establece un banco de memoria sobre $c000
; Entrada: B = banco (0-7) a paginar entre $c000-$ffff
;-----------------------------------------------------------------------
SetRAMBank:
LD A,($5b5c) ; Valor anterior del puerto
AND $f8 ; Sólo cambiamos los bits necesarios
OR B ; Elegir banco "B"
LD BC,$7ffd
DI
LD ($5b5c),A
OUT (C),A
EI
RET

;-----------------------------------------------------------------------
; ClearScreen: Limpia toda la pantalla con un patrón gráfico dado.
; Entrada: A = valor a "imprimir" en pantalla.
;-----------------------------------------------------------------------
ClearScreen:
PUSH HL
PUSH DE
LD HL, 16384
LD (HL), A
LD DE, 16385
LD BC, 6143
LDIR
POP DE
POP HL
RET

;-----------------------------------------------------------------------
; Rutinas para esperar la pulsación y liberación de todas las teclas:
;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
XOR A ; A = 0
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed
RET

Wait_For_Keys_Released:
XOR A
IN A, (254)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET

END 32000

El programa anterior, una vez ensamblado y ejecutado, esperará la pulsación de una tecla para mostrarnos en
pantalla el valor de la celdilla de memoria $c000 mapeando primero uno de los bancos, y luego el otro.
Contended Memory
En este capítulo hemos hablado de la Contended Memory (podríamos traducirlo por “contención de memoria” o
“memoria contenida”).

Esta peculiaridad de la memoria del Spectrum puede traernos de cabeza en circunstancias muy concretas, como
la programación de emuladores, o la creación de rutinas críticas donde el timming sea muy importante o donde
tengamos que sincronizarnos de una forma muy precisa con algún evento.

El efecto es el siguiente: algunas zonas de memoria, en determinadas circunstancias, son de acceso más lento
que otras en cuanto a ejecución de código que afecten a ellas (leer de esas zonas, escribir en esas zonas, ejecutar
código que está en esas zonas). La causa es sencilla: una misma celdilla de memoria no puede ser accedida por
2 dispositivos diferentes simultáneamente.

¿Acaso existen en el Spectrum otro dispositivo que acceda a la memoria además del microprocesador (cuando
lee, decodifica y ejecuta instrucciones, o cuando lee/escribe en memoria)? Sí, lo hay, y es la ULA (Uncommited
Logic Array).

La ULA es, digamos, “el chip gráfico” del Spectrum. Su labor no es como en los chips gráficos actuales o los
chips gráficos de otros ordenadores (y consolas) de 8 bits, la de apoyar al software con funciones extra, sino
que en el Spectrum la ULA se limita a leer la VIDEORAM (parte de la memoria que contiene la información
gráfica a representar en el monitor), interpretarla, y mandar al modulador de TV las señales adecuadas para la
visualización de dicha información en la TV. Sencillamente, es el chip que lee la VideoMemoria y la convierte
en los píxeles que vemos en la TV.

¿Cómo trabaja la ULA? Este pequeño chip fabricado por Ferranti recorre 50 veces por segundo la zona de
memoria que comienza en $4000 (16384) y transforma los datos numéricos en píxeles apagados o encendidos
con el color de la tinta y papel asociado a cada celdilla 8×1 (8×8 en realidad) que va leyendo byte a byte,
horizontalmente. Esto implica una sincronización con el haz de electrones del monitor de TV, que empieza en
la esquina superior-izquierda y avanza horizontalmente hasta llegar al final de cada línea para, como en una
máquina de escribir, pasar a la siguiente línea horizontal, y así hasta llegar hasta la esquina inferior derecha.

El problema es que mientras el haz de electrones del monitor avanza redibujando la imagen, el Spectrum no
puede interrumpirlo (de hecho, no puede controlarlo, sólo se sincroniza con él) y tiene que servirle todos los
datos necesarios para el retrazado de la imagen.

Esto implica que cuando la ULA está “redibujando” la pantalla (y recordemos que lo hace 50 veces por
segundo) y por tanto leyendo de las sucesivas posiciones de memoria comenzando en $4000, el procesador no
puede acceder a las celdillas exactas de memoria a las que accede la ULA hasta que ésta deja de hacer uso de
ellas. En otras palabras, a la hora de leer celdillas de memoria entre $4000 y $7FFF, la ULA tiene prioridad
sobre el procesador.

Por eso, los programas que corren en la zona de memoria entre $4000 y $7FFF (o pretenden acceder a la misma
justo cuando la ULA quiere leer algún dato gráfico de la VRAM) pueden ser ralentizados cuando la ULA está
leyendo la pantalla.

Como se detalla en la FAQ de comp.sys.sinclair alojada en World Of Spectrum, este efecto sólo se da cuando
se está dibujando la pantalla propiamente dicha, ya que para el trazado del borde la ULA proporciona al haz de
electrones el color a dibujar y no se accede a memoria, por lo que no se produce este retraso o delay.

Controlar exactamente los retrasos que se producen y cómo afectarán a la ejecución de nuestros programas es
un ejercicio bastante complejo que requiere conocimiento de los tiempos de ejecución de las diferentes
instrucciones, número de ciclo desde que comenzó el retrazado de la pantalla, etc. Por ejemplo, una misma
instrucción, NOP, que requiere 4 ciclos de reloj para ejecutarse en condiciones normales, puede ver aumentado
su tiempo de ejecución a 10 ciclos (4 de ejecución y 6 de delay) si el contador de programa (PC) está dentro de
una zona de memoria contenida, y dicho delay afectaría sólo al primer ciclo (lectura de la instrucción por parte
del procesador). Por contra, si en lugar de una instrucción NOP tenemos una instrucción LD que acceda a
memoria (también contenida), el delay puede ser mayor.

Como podéis imaginar, este es uno de los mayores quebraderos de cabeza para los programadores de
emuladores, y es la principal causa (junto con la Contended I/O, su equivalente en cuanto a acceso de puertos,
también producido por la ULA), de que hasta ahora no todos los juegos fueran correctamente emulados con
respecto a su funcionamiento en un Spectrum. También muchas demos con complejas sincronizaciones y
timmings dejaban de funcionar en emuladores de Spectrum que no implementaban estos “retrasos” y que, en su
emulación “perfecta del micro”, ejecutaban siempre todas las instrucciones a su velocidad “teórica”.

En nuestro caso, como programadores, la mejor manera de evitar problemas en la ejecución de nuestros
programas es la tratar de no situar código, siempre que sea posible, entre $4000 y $7FFF.

Si recordáis, en el capítulo dedicado a la gestión de la pila, ya obtuvimos la recomendación de no ubicar la pila


en el bloque de 16KB a partir de $4000 precisamente por este motivo.

Contended Memory + Paginación


¿Cómo afecta la contended-memory al sistema de paginación de los modelos 128K? Al igual que en el 48K
existe una “página” ($4000-$7FFF) a la que la ULA accede y por tanto afectada por sus lecturas, en el caso de
los 128K existen bancos de memoria completos (páginas) de memoria contenida. Como ya hemos visto, estos
bancos son:

• Modelos +2/128K : Bancos 1, 3, 5 y 7.


• Modelos +2A/+3: Bancos 4, 5, 6 y 7.

La afectación de velocidad de lectura de esta memoria es más importante de lo que parece. Según el manual del
+3:

The RAM banks are of two types: RAM pages 4 to 7 which are contended
(meaning that they share time with the video circuitry), and RAM pages
0 to 3 which are uncontended (where the processor has exclusive use).
Any machine code which has critical timing loops (such as music or
communications programs) should keep all such routines in uncontended
banks.

For example, executing NOPs in contended RAM will give an effective


clock frequency of 2.66Mhz as opposed to the normal 3.55MHz in
uncontended RAM.

This is a reduction in speed of about 25%.

Es decir, la velocidad de acceso a memoria (y por tanto, también de ejecución) cae a un 25% de promedio en un
banco con contended-memory con respecto a un banco que no lo sea.

El problema, que para el 128K y +2 las páginas que sufre una penalización son unas, y para el +2A y +3 otras
diferentes, por lo que parece que siempre tendremos que primar a uno de los modelos sobre otros: o usamos
números de bancos que no penalicen al +2A/+3, o lo hacemos para el 128K/+2.

Lo mejor es no situar en la zona paginada rutinas como las de vídeo o audio, o al menos, no hacerlo si éstas son
críticas. En cualquier caso, es probable que para el 90% de las rutinas o datos de un programa no existan
problemas derivados de correr o estar alojados en memoria contenida, pero puede ser un detalle a tener muy en
cuenta en rutinas que requieran un timming perfecto (efectos con el borde, efectos gráficos complejos, etc).
Paginación de memoria desde Z88DK (C)
Podemos paginar memoria también desde C usando Z88DK mediante un código como el siguiente:

//--- SetRAMBank ------------------------------------------------------


//
// Se mapea el banco (0-7) indicado sobre $C000.
//
// Ojo: aqui no se deshabilitan las interrupciones y ademas en lugar
// de usar el registro B, se usa un parametro tomado desde la pila.
// En caso de ser importante la velocidad, se puede usar "B" y no pasar
// el parametro en la pila, llamando SetRAMBank con un CALL.
//
void SetRAMBank( char banco )
{
#asm
.SetRAMBank
ld hl, 2
add hl, sp
ld a, (hl)

ld b, a
ld A, ($5B5C)
and F8h
or B
ld BC, $7FFD
ld ($5B5C), A
out (C), A
#endasm
}

Con el anterior código podemos mapear uno de los bancos de memoria de 16KB sobre la página que va desde
$C000 a $FFFF, pero debido al uso de memoria, variables y estructuras internas que hace Z88DK, debemos
seguir una serie de consideraciones.

• Todo el código en ejecución debe estar por debajo de $C000, para lo cual es recomendable definir los
gráficos al final del “binario”.
• Es importantísimo colocar la pila en la memoria baja, mediante la siguiente instrucción (o similar, según
la dirección en que queremos colocarla) al principio de nuestro programa:

/* Allocate space for the stack */


#pragma output STACKPTR=24500

La regla general es asegurarse de que no haya nada importante (para la ejecución de nuestro programa) en el
bloque $C000 a $FFFF cuando se haga el cambio: ni la pila, ni código al que debamos acceder. Tan sólo datos
que puedan ser intercambiandos de un banco a otro sin riesgo para la ejecución del mismo (por ejemplo, los
datos de un nivel de juego en el que ya no estamos).
En resumen
Comprendiendo el sistema de paginación de los modelos de 128K y aprendiendo a utilizarlo conseguimos una
gran cantidad de memoria adicional que ir paginando sobre el bloque $C000-$FFFF.

Así, podemos almacenar los datos de diferentes niveles en diferentes bloques, y cambiar de uno a otro mediante
paginación en el momento adecuado. Esto permite realizar cargas de datos desde cinta almacenando la totalidad
de los datos del juego o programa en bancos libres de memoria y convertir nuestro juego multicarga (con una
carga por fase) en un juego de carga única (con todos los elementos del juego almacenados en memoria),
evitando el tedioso sistema de rebobinar y volver a cargar la primera fase cuando el jugador muere.

Ahora bastará con que nuestro programa, una vez cargado en memoria y en ejecución, pagine un determinado
bloque, cargue 16K-datos sobre él, pagine otro bloque diferente, y realice otra carga de datos desde cinta, y así
sucesivamente con todos los bloques de datos del juego. Estas cargas de datos podemos hacerlas bien desde
nuestro programa “principal” una vez cargado y en memoria, o bien desde un mini-programa lanzado por el
cargador BASIC y previo a cargar el programa definitivo.

El resultado: 128KB de memoria a nuestro alcance, tanto para cargar múltiples datos gráficos o de mapeado
sobre ellos como para llenarlos internamente desde nuestro programa.
Gráficos (I): la videomemoria del Spectrum
En este capítulo vamos a ver la teoría relacionada con la generación de gráficos en el Spectrum: cómo se
almacena en memoria el estado de cada pixel de pantalla y el color de cada celdilla y cómo la ULA utiliza esta
información para regenerar las imágenes. Este capítulo es una preparación teórica y práctica básica e
imprescindible para los próximos capitulos del curso.

Cómo funciona un monitor CRT


Para comprender el funcionamiento de la videomemoria del Spectrum debemos empezar por comprender cómo
el monitor o TV CRT genera las imágenes.

Los monitores/televisiones CRT (de Cathode Ray Tube, o Tubo de Rayos Catódicos), que incluye tanto a los
televisores como a los monitores estándar que no sean de tecnología TFT/LED/PLASMA, funcionan mediante
un “bombardeo” de electrones que excitan los elementos fosforescentes de pantalla.

Simplificando el proceso, podría decirse que el monitor o pantalla es una matriz de elementos fosforescentes
(los píxeles) protegidos y delimitados por una máscara (que determina la “posición” de los mismos) y el CRT
un cañón de electrones capaz de “iluminar” el pixel al que apunta. Este pixel se mantiene “excitado” y por tanto
“encendido” un tiempo limitado, ya que se va apagando progresivamente cuando el haz de electrones deja de
excitarlo. Esto implica que hay que volver a bombardear dicho punto de nuevo para que se encienda durante
otro período de tiempo. Realizando esta operación suficientes veces por segundo (50 veces por segundo en
sistemas PAL y 60 en sistemas NTSC), dará la sensación óptica de que el pixel no se apaga nunca.

El CRT generando un pixel

Concretando un poco más, en el caso de los monitores de fósforo verde o de blanco y negro, cada pixel se
compone de un único elemento de la matriz (habitualmente de fósforo) excitable, que puede estar encendido o
apagado. En el caso de los monitores de color, cada pixel se compone de 3 sub-pixels muy cercanos de colores
Rojo, Azul y Verde (Red Green Blue) los cuales podemos ver si nos acercamos lo suficiente a un monitor CRT:

El haz de electrones activa 1, 2 ó los 3 subpíxeles (las componentes) que forman un pixel con una intensidad
mayor o menor según el color RGB y los valores de las componentes que forman el color real. Así, activando
con máxima intensidad las 3 componentes RGB (en 8 bits, R=255, G=255 y B=255), debido a la cercanía de los
subpíxeles, nuestro ojo apreciará desde la distancia de visión “normal” un único píxel de color blanco. Si el haz
de electrones excitara sólo el subpixel R y no el G y el B (R=valor, G=0, B=0), veríamos un pixel de color rojo
cuya tonalidad variaría en función del valor de la componente R.

La ULA tiene definidos los colores del Spectrum con unas componentes de color concretas que podemos ver
aproximadamente en la siguiente tabla y en la imagen donde se representan:

Valor Color Componentes RGB


0 Negro (0, 0, 0 )
1 Azul (0, 0, 192)
2 Rojo (192, 0, 0)
3 Magenta (192, 0, 192)
4 Verde (0, 192, 0)
5 Cian (0, 192, 192)
6 Amarillo (192, 192, 0)
7 Blanco (192, 192, 192)
8 Negro + Brillo (0, 0, 0)
9 Azul + Brillo (0, 0, 255)
10 Rojo + Brillo (255, 0, 0)
11 Magenta + Brillo (255, 0, 255)
12 Verde + Brillo (0, 255, 0)
13 Cian + Brillo (0, 255, 255)
14 Amarillo + Brillo (255, 255, 0)
15 Blanco + Brillo (255, 255, 255)
La gama de colores del Spectrum

Pero volvamos al retrazado de nuestra imagen: y es que no sólo hay que trazar y refrescar un único pixel: el
CRT debe de refrescar todos los píxeles de la pantalla. Para ello, el cañón de electrones del monitor (un triple
cañón realmente, para atacar a las 3 componentes de color) realiza un recorrido desde la esquina superior
izquierda hasta la inferior derecha refrescando todos los píxeles de la pantalla y volviendo de nuevo a la
posición inicial para repetir el proceso.

Como ya vimos en el capítulo dedicado a las interrupciones, el haz de electrones de una pantalla CRT comienza
su recorrido en la esquina superior izquierda del monitor y avanza horizontalmente hacia a la derecha
retrazando lo que se conoce como un “scanline” (una línea horizontal). Al llegar a la derecha del monitor y tras
haber trazado todos los píxeles de la primera línea, se desactiva el bombardeo de electrones y se produce un
retorno a la parte izquierda de la pantalla y un descenso al scanline inferior. Al llegar aquí, mediante la
sincronización con una señal HSYNC monitor-dispositivo, se “activa” de nuevo el trazado de imagen para
redibujar el nuevo scanline con la información que le suministra el dispositivo que está conectado al monitor.

El haz de electrones traza pues, scanline a scanline, toda la pantalla hasta llegar a la parte inferior derecha,
momento en el que el haz de electrones vuelve a la parte superior izquierda dejando de bombardear electrones
durante el retorno, sincronizándose con el dispositivo al que esté conectado (la ULA y el modulador de vídeo
del Spectrum en este caso) mediante una señal VSYNC.
Proceso de retrazado de la imagen

Este proceso se repite continuamente (a razón de 50 ó 60 veces por segundo según el sistema de televisión de
nuestra región) y no se puede interrumpir ni variar (ni el tiempo de avance de la señal de televisión en
horizontal ni el tiempo total que se tarda en retrazar un cuadro.

Es el dispositivo conectado a la televisión o monitor (el Spectrum en este caso) quien le debe de proporcionar
los datos gráficos que el monitor ha de retrazar, sincronizándose este dispositivo con el monitor mediante las
señales de HSYNC y VSYNC.

Cuando se produce un VSYNC y el monitor va a comenzar a trazar los datos del primer scanline, es la ULA en
el caso del Spectrum la encargada de alimentar el flujo de datos a dibujar con el timing correcto que necesita el
monitor conforme avanza por la pantalla. Mediante la señal de HSYNC se vuelven a sincronizar de forma que
la ULA pueda comenzar a surtir los datos del siguiente scanline, repitiendo el proceso hasta acabar el retrazado
de toda la imagen.

Así pues, sabemos que la televisión necesita retrazar continuamente la imagen que aparece en pantalla, por lo
que ésta debe de estar almacenada en algún lugar para que la ULA pueda leer estos datos y proporcionarselos al
monitor a través del cable de vídeo. Este almacen no es un área de memoria dentro de la ULA sino dentro de la
propia RAM de nuestro Spectrum. Hablamos de la videomemoria, videoram, o “fichero de imagen”.

La videomemoria del Spectrum


Cuando comenzamos nuestro curso de ensamblador vimos la organización del mapa de memoria del Spectrum,
con la ROM mapeada entre $0000 y $3FFFF, y los 16 o 48KB de memoria a continuación de la misma. A partir
de la dirección de memoria $4000 y hasta $7FFF nos encontramos un área de memoria etiquetada como
“videoram” o “videomemoria”.

Este área de aprox. 7 KB de memoria es donde podemos encontrar la representación digital de la imagen que
estamos viendo en el monitor y que la ULA lee regularmente para poder generar la señal de vídeo que requiere
el retrazar la imagen.

La videoram en el mapa de memoria del Spectrum

Las rutinas de la ROM o de BASIC que dibujan puntos, líneas, rectángulos o caracteres de texto, lo que
realmente hacen internamente es escribir datos en posiciones concretas y calculadas de la videoram ya que estos
datos escritos se convertirán en píxeles en el monitor cuando la ULA los recoja en su proceso de envío de datos
al monitor y éste los dibuje en la pantalla.

Algo tan sencillo como establecer a “1” el bit 7 de la posición de memoria $4000 provocará la aparición en el
monitor de un pixel activo en la posición (0,0) de la pantalla. Si en lugar de cambiar un único bit en esa
posición, cambiamos los bits apropiados en las posiciones apropiadas, podremos provocar el trazado de una
imagen, un carácter, etc.

Veamos un sencillo ejemplo de esto. Vamos a imprimir una letra A empezando en la posición (128,96) de la
pantalla. Definimos primero los píxeles que van a conformar esta letra mediante esta matriz de 8×8:

Pixel 76543210
--------------------
Scanline 0 --XXXX--
Scanline 1 -X----X-
Scanline 2 -X----X-
Scanline 3 -XXXXXX-
Scanline 4 -X----X-
Scanline 5 -X----X-
Scanline 6 -X----X-
Scanline 7 --------

Esta representación gráfica, convertida en bits a 1 (pixel activo) o a 0 (bit no activo) sería la siguiente:

Valor BIT 76543210 Decimal


--------------------------------
Scanline 0 00111100 = 60d
Scanline 1 01000010 = 66d
Scanline 2 01000010 = 66d
Scanline 3 01111110 = 126d
Scanline 4 01000010 = 66d
Scanline 5 01000010 = 66d
Scanline 6 01000010 = 66d
Scanline 7 00000000 = 0d

Habrá que escribir estos valores en posiciones concretas de la videomemoria que provoquen que los píxeles de
nuestra letra A aparezcan unos sobre otros y en la posición de pantalla elegida.

Así pues, ensamblamos y ejecutamos el siguiente programa:

; Ejemplo de escritura de un grafico con forma de A

ORG 50000

LD HL, 18514 ; Scanline 0 en Y=96


LD A, 60 ; 00111100b
LD (HL), A ; Escribir

LD HL, 18770 ; Scanline 1 en Y=97


LD A, 66 ; 01000010b
LD (HL), A ; Escribir

LD HL, 19026 ; Scanline 2 en Y=98


LD A, 66 ; 01000010b
LD (HL), A ; Escribir

LD HL, 19282 ; Scanline 3 en Y=99


LD A, 126 ; 01111110b
LD (HL), A ; Escribir

LD HL, 19538 ; Scanline 4 en Y=100


LD A, 66 ; 01000001b
LD (HL), A ; Escribir

LD HL, 19794 ; Scanline 5 en Y=101


LD A, 66 ; 01000001b
LD (HL), A ; Escribir

LD HL, 20050 ; Scanline 6 en Y=102


LD A, 66 ; 01000001b
LD (HL), A ; Escribir

LD HL, 20306 ; Scanline 7 en Y=103


LD A, 0 ; 00000000b
LD (HL), A ; Escribir

RET
END 50000

Lo que produce la siguiente imagen en pantalla:


Los valores de posiciones de memoria en que hemos escrito el estado de los píxeles han sido precalculadas
manualmente para que los valores que escribíamos en ella aparecieran en la posición exacta de pantalla en que
los vemos al ejecutar el programa.

Esto es una demostración de cómo alterar el contenido de la videoram es la forma real de generar gráficos en la
pantalla del Spectrum. Estos gráficos generados pueden ir desde un simple pixel de coordenadas (x,y) (cambio
de un bit en la dirección de memoria adecuada) hasta un sprite completo, una pantalla de carga o fuentes de
texto.

La resolución gráfica del Spectrum permite la activación o desactivación de 256 píxeles horizontales contra 192
píxeles verticales, es decir, la pantalla tiene una resolución de 256×192 píxeles que pueden estar, cada uno de
ellos, encendido o apagado.

Si nos olvidamos del color y pensamos en el Spectrum como en un sistema monocromo, se puede considerar
que 256×192 es una resolución de pantalla bastante respetable para la potencia de un microprocesador como el
Z80A, ya que a más resolución de pantalla, más operaciones de escritura y lectura de memoria necesitaremos
para generar los gráficos en nuestros juegos.

Por desgracia, la “alta” resolución del Spectrum se ve ligeramente empañada por el sistema de color en baja
resolución diseñado para poder reducir la cantidad de RAM necesaria para alojar la videomemoria.

A nivel de color, existe la posibilidad de definir color en baja resolución. Esto implica que podemos establecer
un color de tinta y otro de papel (así como brillo y parpadeo) en bloques de 8×8 píxeles con una resolución de
32×24 bloques. Se puede decir que la definición de los colores es, pues, a nivel de “carácter”.

Debido a esta mezcla de gráficos en alta definición y colorido en baja definición, la videomemoria del
Spectrum se divide en 2 áreas:

• El área de imagen: Es el área de memoria que va desde $4000 (16384) hasta $57FF (22527). Este área
de memoria de 6 KB almacena la información gráfica de 256×192 píxeles, donde cada byte (de 8 bits)
define el estado de 8 píxeles (en cada bit del byte se tiene el estado de un pixel, con 1=activo, 0=no
activo), de forma que se puede codificar cada línea de 256 pixeles con 256/8=32 bytes. Utilizando 32
bytes por línea, podemos almacenar el estado de una pantalla completa con 32*192 = 6144 bytes = 6
KB de memoria. Por ejemplo, la celdilla de memoria 16384 contiene el estado de los 8 primeros píxeles
de la línea 0 de la pantalla, desde (0,0) a (7,0).

• El área de atributos: Es el área de memoria comprendida entre $5800 (22528) y $5AFF (23295). Cada
uno de estos 768 bytes se denomina atributo y almacena los colores de pixel activo (tinta) y no activo
(papel) de un bloque de 8×8 de la pantalla. Por ejemplo, la celdilla de memoria 22528 almacena el
atributo de color del bloque (0,0) que se corresponde con los 64 píxeles desde las posiciones de pantalla
(0,0) hasta (7,7).

La ULA genera para el monitor una imagen utilizando los píxeles definidos en el área de imagen junto a los
colores que le corresponde a ese píxel según el valor del atributo del bloque en baja resolución al que
corresponda la posición del pixel.

Así, para generar el valor del punto de pantalla (6,0), la ULA utiliza el bit 1 de la posición de memoria 16384,
representando este pixel con el color de tinta (si el bit vale 1) o de papel (si vale 0) del atributo definido en
(22528), ya que el pixel (6,0) forma parte del primer bloque de baja resolución de pantalla.

En la siguiente imagen podemos ver un ejemplo simplificado de cómo se produce la generación de la imagen
como “superposición” de la información gráfica en alta resolución y la información de color en baja resolución:

Gráficos de 256×192 con color a 32×24

¿Cuál es el motivo de crear este sistema mixto de imagen de alta resolución y atributos de baja resolución? No
es otro que el ahorro de memoria. Si quisieramos disponer de un sistema de 256×192 píxeles donde cada pixel
pudiera disponer de su propio valor de color o de un índice en una paleta de colores, necesitaríamos la siguiente
cantidad de memoria para alojar la pantalla:
• Utilizando un sistema de 3 componentes RGB que vayan desde 0 a 255, necesitaríamos 3 bytes por cada
pixel, lo que implicaría la necesidad de 256x192x3 = 147456 bytes = 144KB sólo para almacenar la
imagen de pantalla actual. No sólo sería una enorme cantidad de memoria, sino que nuestro Z80A a
3.50Mhz a duras penas podría generar gráficos a pantalla completa con suficiente velocidad, ya que la
cantidad de operaciones de lectura y escritura serían enormes para su capacidad.

• Utilizando el sistema de paleta actual con 16 posibles colores (4 bits), codificando 2 píxeles en cada
byte (4 bits de índice de color en la paleta * 2 píxeles = 8 bits), obtendríamos un sistema de 4 bits por
píxel (2 píxeles por byte) que requeriría 256×192/2 bytes = 24576 = 24KB de memoria para alojar la
videomemoria. Esto representa la mitad exacta de toda la memoria RAM disponible del Spectrum y
8KB más de lo que disponía el modelo de 16KB que, no nos olvidemos, fue el Spectrum original.
Además, se perdería la posibilidad de hacer flash al no disponer de un bit a tal efecto.

Buscando una solución más económica (recordemos que Sir Clive Sinclair quería que los precios de sus
productos fueran realmente reducidos) se optó por un sistema de vídeo mixto (que fue incluso patentado) con
256×192 = 6144 bytes (6KB) dedicados al fichero de imagen y 32×24 = 768 bytes dedicados a los atributos de
bloques de color, resultando en un total de 6912 bytes. La videomemoria del Spectrum ocupaba así menos de 7
KB, permitiendo que el ZX Spectrum de 16KB de RAM todavía dispusiera de 9 KB de memoria de trabajo.

A cambio de este enorme ahorro de memoria, el color en el Spectrum implica realizar un cuidadoso diseño de
los gráficos y los mapeados para evitar lo que se conoce como “colour clash” o “attribute clash” (colisión de
atributos), que se produce cuando los gráficos pasan de un bloque de color en baja resolución a otro, con lo que
los colores que debía tener un determinado gráfico modifican los del fondo, los de otro gráfico, etc.

Para demostrar el efecto de la colisión de atributos podemos acudir a un sencillo programa en BASIC:

10 BORDER 1: PAPER 1: INK 7: CLS


20 FOR R = 10 TO 70 STEP 10 : CIRCLE 128, 96, R : NEXT R
30 PAUSE 0
40 INK 2 : PLOT 30, 30 : DRAW 220, 120

Lo primero que hace el programa es dibujar una serie de círculos concéntricos de color blanco (INK 7) sobre
papel azul (PAPER 1):

A continuación pulsamos una tecla y se ejecuta el “INK 2 + PLOT + DRAW” que traza una línea diagonal roja.
Como en una misma celdilla de 8×8 no pueden haber 2 colores de tinta diferentes, cada pixel rojo que dibuja la
rutina DRAW afecta a los 8×8 píxeles del recuadro al que corresponde. Cada nuevo pixel dibujado modifica los
atributos de su correspondiente bloque en baja resolución, por lo que se alteran también los colores de los
círculos allá donde coincidan con la línea:
Ampliando la zona central podemos ver el efecto del “attribute clash” con la alteración de los colores del
círculo debido al dibujado de los píxeles rojos de la línea:

En los juegos con colorido podemos apreciar el “attribute clash” fácilmente si es necesario gran cantidad de
colores en pantalla o el movimiento de los personajes debe de ser pixel a pixel sobre un fondo colorido. En el
siguiente ejemplo podemos ver una ampliación del sprite del juego Altered Beast donde el color de tanto las
botas como el cuerpo del personaje provocan el cambio de color de los píxeles del decorado que entran dentro
del mismo bloque de caracteres en baja resolución:

“Ligero” Attribute Clash en Altered Beast

Los programadores tienen diferentes técnicas para dotar a los juegos de color sorteando las limitaciones del
color en baja resolución y evitando el “attribute clash”. La más obvia y sencilla es de generar el juego en
formato monocolor, ya sea toda la pantalla o sólo el área de juego:
Area de juego monocolor en H.A.T.E.

De esta forma, todo el área donde se mueven los sprites es del mismo color por lo que no existen colisiones de
atributos entre ellos.

La forma más elaborada es la de realizar un diseño gráfico teniendo en mente el sistema de atributos del
Spectrum, de forma que se posicionen los elementos en pantalla de tal modo que no haya colisiones entre los
mismos. A continuación podemos ver un par de capturas que muestran un excelente colorido sin apenas
colisiones de atributos:

Excelente diseño gráfico que disimula la colisión de atributos

En este capítulo trataremos la organización de la zona de imagen y la zona de atributos de cara a tratar en el
próximo capítulo el cómo calcular las posiciones de memoria relativas a cada coordenada (x,y) de pixel o de
atributo en que deseemos escribir. Tras estos 2 capítulos sobre la videoram trabajaremos con sprites de baja o
alta resolución (movimiento de 32×24 vs 256×192 posiciones diferentes), fuentes de texto, etc.

A continuación veremos una descripción más detallada de cada una de estas 2 áreas de memoria.

Videomemoria: Área de Imagen


El área de imagen del Spectrum es el bloque de 6144 bytes (6KB) entre 16384 ($4000) y 22527 ($57FF). Cada
una de las posiciones de memoria de este área almacenan la información de imagen (estado de los píxeles) de 8
píxeles de pantalla consecutivos, donde un bit a 1 significa que el pixel está encendido y un valor de 0 que está
apagado.

Como veremos cuando hablemos del área de atributos, que los píxeles estén a ON o a OFF no implica que la
ULA sólo dibuje los píxeles activos. Si el pixel está activo (bit a 1), la ULA lo traza en pantalla utilizando el
color de tinta actual que corresponda a ese píxel mientras que un bit a 0 significa que el pixel no está encendido
y que la ULA debe de dibujarlo con el color de papel actual.

Así pues, en este área se codifica el estado de cada pixel a razón de 1 bit por píxel, lo que implica que cada byte
almacena la información de 8 píxeles consecutivos requiriendo la totalidad de la pantalla (256/8) * 192 = 32 *
192 = 6144 bytes.

Tomemos como ejemplo la primera celdilla de memoria del área de imagen, la $4000 o 16384. Los diferentes
bits de esta celdilla de memoria se corresponden con el estado de los píxeles desde (0,0) hasta (7,0):

Bits de (16384) 7 6 5 4 3 2 1 0
Pixel (0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0)

Podemos comprobar esto de una forma rápida ejecutando este sencillo programa en BASIC:

10 CLS
20 POKE 16384, 170
30 PAUSE 0

Con este programa escribimos el valor 170 (10101010 en binario) en la posición de memoria 16384, que
implica poner a ON (a 1) los píxeles (0,0), (2,0), (4,0) y (6,0), y poner a OFF (a 0) los píxeles (1,0), (3,0), (5,0)
y (7,0).

Si ejecutáis el programa en BASIC veréis aparecer en la esquina superior de la pantalla 4 píxeles activos,
alternándose con otros 4 píxeles no activos. Os mostramos una ampliación de la esquina superior de la pantalla
con el resultado de la ejecución:

Si avanzamos a la siguiente celdilla de memoria, la $4001 (o 16385), tendremos el estado de los siguientes
píxeles de la misma línea horizontal:

Bit de (16385) 7 6 5 4 3 2 1 0
Pixel (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (14,0) (15,0)
De nuevo, avanzando 1 byte más en memoria, avanzamos otros 8 píxeles horizontalmente:

Bit de (16386) 7 6 5 4 3 2 1 0
Pixel (16,0) (17,0) (18,0) (19,0) (20,0) (21,0) (22,0) (23,0)

Así, hasta que llegamos al byte número 32 desde 16384, es decir, a la celdilla 16415, donde:

Bit de (16415) 7 6 5 4 3 2 1 0
Pixel (248,0) (249,0) (250,0) (251,0) (252,0) (253,0) (254,0) (255,0)

Con este byte acabamos el primer “scanline” de 256 píxeles, que va desde (0,0) hasta (255,0). Comprobémoslo
con el siguiente programa en BASIC que guarda el valor 170 (10101010b) en las 32 posiciones de memoria
consecutivas a 16384:

10 CLS
20 FOR I=0 TO 31 : POKE 16384+I, 170 : NEXT I
30 PAUSE 0

En pantalla aparecerá lo siguiente:

Ahora la pregunta crucial es … ¿a qué pixel corresponderá el siguiente byte en videomemoria? Si aplicamos la
lógica, lo más intuitivo sería que la posición de memoria 16416 (16384+32) tuviera los datos de los píxeles
desde (0,1) hasta (7,1), es decir, los 8 primeros píxeles de la segunda línea (segundo scanline) de la pantalla.

Por desgracia, esto no es así, y los 32 bytes a partir de 16416 no hacen referencia a la segunda línea de pantalla
sino a la primera línea del segundo “bloque” de caracteres, es decir, a los píxeles desde (0,8) a (255,8), por lo
que realmente, los bits de 16416 representan:

Bit de (16416) 7 6 5 4 3 2 1 0
Pixel (0,8) (1,8) (2,8) (3,8) (4,8) (5,8) (6,8) (7,8)

Podemos comprobar esto mediante el siguiente programa en BASIC, que escribe el valor 170 en las primeras
64 posiciones de memoria de la VRAM:

10 CLS
20 FOR I=0 TO 63 : POKE 16384+I, 170 : NEXT I
30 PAUSE 0
Se podría esperar que al rellenar las primeras 2 posiciones de memoria se alteraran las 2 primeras líneas de la
pantalla, pero como hemos explicado, no es así sino que se escribe en la primera línea del primer carácter, y la
primera línea del segundo carácter en baja resolución.

En resumen, si avanzamos de 32 en 32 bytes, tenemos lo siguiente:

• La videomemoria empieza en 16384 y contiene “ristras” consecutivas de 32 bytes que almacenan en


estado de 256 píxeles.
• Los primeros 32 bytes definen la línea 0 del bloque en baja resolución Y=0 de pantalla.
• Los siguientes 32 bytes definen la línea 0 del bloque Y=1 de pantalla.
• Los siguientes 32 bytes definen la línea 0 del bloque Y=3 de pantalla.
• Los siguientes 32 bytes definen la línea 0 del bloque Y=4 de pantalla.
• Los siguientes 32 bytes definen la línea 0 del bloque Y=5 de pantalla.
• Los siguientes 32 bytes definen la línea 0 del bloque Y=6 de pantalla.
• Los siguientes 32 bytes definen la línea 0 del bloque Y=7 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=1 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=2 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=3 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=4 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=5 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=6 de pantalla.
• Los siguientes 32 bytes definen la línea 1 del bloque Y=7 de pantalla.
• Los siguientes 32 bytes definen la línea 2 del bloque Y=1 de pantalla.
• Los siguientes 32 bytes definen la línea 2 del bloque Y=2 de pantalla.
• (etc…)

Así hasta el byte 18331 o $47FF (cuando hemos avanzado 32*8*8 = 32 bytes por 8 líneas de cada una de las 8
filas de caracteres), que contiene el estado de los 8 píxeles del bloque de baja resolución (31,7) de la pantalla.
Organización del área de imagen de la VRAM

¿Qué quiere decir esto? Que los primeros 2KB de videoram (entre $4000 y $47FF) contienen la información de
todos los píxeles de los 8 primeros bloques en baja resolución de pantalla, de forma que primero vienen todas
las líneas horizontales 0 de cada bloque, luego todas las líneas horizontales 1 de cada bloque, líneas
horizontales 2 de cada bloque, etc, hasta que se rellenan las últimas líneas horizontales (líneas 7) de los 8
primeros caracteres. Esto produce que el rellenado de los 2 primeros KB de la videoram rellene un área de
pantalla entre (0,0) y (255,63), lo que se conoce como el primer tercio de la pantalla.

• Primer tercio: Los 2 primeros KB de la videoram (de $4000 a $47FF) cubren los datos gráficos de los
primeros 64 scanlines de la pantalla (líneas 0 a 7).
• Segundo tercio: Los siguientes 2KB de la videoram (de $4800 a $4FFF) cubren los datos gráficos de
los siguientes 64 scanlines de la pantalla (líneas 8 a 15).
• Tercer tercio: Los siguientes 2KB de la videoram (de $5000 a $57FF) cubren los datos gráficos de los
últimos 64 scanlines de la pantalla (líneas 16 a 23).

Y, resumiendo en un sólo párrafo la organización de cada tercio:


Cuando nos movemos dentro de la videomemoria que representa cada tercio de pantalla primero tenemos todas
las primeras líneas de cada “carácter 8×8”, después todas las segundas líneas de cada carácter, y así hasta las
octavas líneas de cada carácter, de tal forma que el último byte del “tércio” coincide con el pixel (7,7) del
carácter (31,7) de esa zona de la pantalla.

En la siguiente imagen podemos ver la ubicación de los 3 tercios y sus posiciones de inicio y final:

División de la pantalla en 3 tercios de 2KB de VRAM

Con el programa de ejemplo del apartado Explorando el área de imagen con un ejemplo podremos comprobar
experimentalmente la organización de la videomemoria y la división de la pantalla en tercios de 8 “caracteres”
de 8 scanlines cada uno.

Mientras tanto, sabiendo que entre $4000 y $57FF (6144 bytes) tenemos el área de imagen de la pantalla, donde
cada byte representa el estado de 8 píxeles, podemos realizar la siguiente rutina útil que sirve para rellenar toda
la pantalla con un patrón de píxeles determinado (CLS con patrón):

;-------------------------------------------------------
; Limpiar la pantalla con el patron de pixeles indicado.
; Entrada: A = patron a utilizar
;-------------------------------------------------------
ClearScreen:
PUSH HL
PUSH DE

LD HL, 16384 ; HL = Inicio de la videoram


LD (HL), A ; Escribimos el patron A en (HL)
LD DE, 16385 ; Apuntamos DE a 16385
LD BC, 192*32-1 ; Copiamos 192*32-1 veces (HL) en (DE)
LDIR ; e incrementamos HL y DL. Restamos 1
; porque ya hemos escrito en 16384.
POP DE
POP HL
RET

De esta forma, podemos llamar a nuestra rutina ClearScreen colocando en A el patron con el que rellenar la
pantalla, que puede ser 0 para “limpiarla” o 1 para activar todos los píxeles a 1.
Explorando el área de imagen con un ejemplo

Veamos un sencillo programa (vramtest.asm) que nos va a permitir verificar de forma experimental la teoría
sobre la videomemoria que hemos visto en los apartados anteriores.

Este programa carga HL con el inicio del área de imágen de la videomemoria (16384 o $4000), y escribe
bloques de 32 bytes (todo un scanline horizontal) con el valor 255 (11111111b o, lo que es lo mismo, los 8
píxeles de ese bloque activos).

Cada iteración del bucle interno escribe una línea de 256 píxeles en pantalla (32 bytes de valor 11111111b =
32*8 = 256 píxeles). Este bucle interno lo repetimos 192 veces para cubrir la totalidad de scanlines de la
pantalla.

Hemos añadido en cada iteración del bucle externo la necesidad de pulsar y liberar una tecla para permitir al
lector estudiar los efectos de cada escritura de 32 bytes en la pantalla.

Nótese como HL va a incrementarse de 32 en 32 bytes siempre, pero sin embargo, como ya sabemos por la
organización de la videoram, esto no se reflejará en pantalla con un avance línea a línea de nuestro “patrón” de
256 píxeles.

A continuación tenemos el código fuente del programa y 2 capturas de pantalla que ilustran lo que acabamos de
explicar.

; Mostrando la organizacion de la videomemoria

ORG 50000

; Pseudocodigo del programa:


;
; Limpiamos la pantalla
; Apuntamos HL a 16384
; Repetimos 192 veces:
; Esperamos pulsacion de una tecla
; Repetimos 32 veces:
; Escribir 255 en la direccion apuntada por HL
; Incrementar HL

Start:
LD A, 0
CALL ClearScreen ; Borramos la pantalla

LD HL, 16384 ; HL apunta a la VRAM


LD B, 192 ; Repetimos para 192 lineas

bucle_192_lineas:
LD D, B ; Nos guardamos el valor de D para el
; bucle exterior (usaremos B ahora en otro)
LD B, 32 ; B=32 para el bucle interior

; Esperamos que se pulse y libere tecla


CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released

LD A, 255 ; 255 = 11111111b = todos los pixeles

bucle_32_bytes:
LD (HL), A ; Almacenamos A en (HL) = 8 pixeles
INC HL ; siguiente byte (siguientes 8 pix.)
DJNZ bucle_32_bytes ; 32 veces = 32 bytes = 1 scanline

LD B, D ; Recuperamos el B del bucle exterior

DJNZ bucle_192_lineas ; Repetir 192 veces


JP Start ; Inicio del programa

;-----------------------------------------------------------------------
; Esta rutina espera a que haya alguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
XOR A
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed
RET

;-----------------------------------------------------------------------
; Esta rutina espera a que no haya ninguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
XOR A
IN A, (254)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET

;-----------------------------------------------------------------------
; Limpiar la pantalla con el patron de pixeles indicado.
; Entrada: A = patron a utilizar
;-----------------------------------------------------------------------
ClearScreen:
LD HL, 16384
LD (HL), A
LD DE, 16385
LD BC, 192*32-1
LDIR
RET

END 50000

La ejecución del programa tras realizar 6 pulsaciones de teclado mostraría el siguiente aspecto:

Comentemos esta captura de pantalla: como cabría esperar ahora que conocemos la organización del área de
imagen de la VRAM, aunque hemos escrito en la memoria de forma lineal (desde 16384 hasta 16384+(32*6)-
1), los scanlines en pantalla no son consecutivos, ya que no hemos cubierto los 6 primeros scanlines de la
pantalla sino el primer scanline de los 6 primeros bloques 8×8 del primer tercio.

Si continuamos realizando pulsaciones de teclado, agotaremos las líneas del primer tercio y pasaremos al
segundo, con una organización similar al del primero:
Recomendamos al lector que continue la ejecución del programa hasta recorrer toda la pantalla y que trate de
anticiparse mentalmente acerca de dónde se mostrará la siguiente línea antes de realizar la pulsación de teclado.

Es probable que la pauta de rellenado de la pantalla de nuestro ejemplo le resulte más que familiar al lector:
efectivamente, es el mismo orden de relleno que producen las pantallas de carga de los juegos cargadas a partir
de un LOAD “” SCREEN$. La carga de pantalla desde cinta con “LOAD ”“ SCREEN$” no es más que la
lectura desde cinta de los 6912 bytes de una pantalla completa (6144 bytes de imagen y 768 bytes de atributos)
y su almacenamiento lineal en $4000.

La lectura secuencial desde cinta y su escritura lineal en videomemoria resulta en la carga de los datos gráficos
en el mismo orden de scanlines en que nuestro programa de ejemplo ha rellenado la pantalla, seguida de la
carga de los atributos, que en un rápido avance (sólo 768 bytes a cargar desde cinta) dotaba a la pantalla de
carga de su color.

Del mismo modo, un simple SAVE “imagen” SCREEN$ o SAVE “imagen” CODE 16384, 6912 toma los 6912
bytes de la videoram y los almacena en cinta. El lector puede acudir al capítulo dedicado a Rutinas de SAVE y
LOAD para refrescar la información acerca de la carga de datos desde cinta e inclusión de pantallas gráficas
completas en sus programas.

Muchos programas comerciales trataban de evitar la carga de la pantalla visible scanline a scanline, para lo que
cargaban los datos de SCREEN$ en un área de memoria libre y después transferían rápidamente esta pantalla a
videoram con instrucciones LDIR.

Este concepto, el de Pantalla Virtual, resulta muy interesante: podemos utilizar un área de memoria alta para
simular que es la pantalla completa o una zona (la de juego) de la misma. Esto permitía dibujar los sprites y
gráficos sobre ella (sin que el jugador viera nada de estos dibujados, puesto que dicha zona de RAM no es
videoram), y volcarla regularmente sobre videoram tras un HALT. De esta forma se evita que el jugador pueda
ver parpadeos en el dibujado de los sprites o la construcción de la pantalla “a trozos”. La utilización de una
pantalla virtual implicará el consumo de casi 7KB de memoria para almacenar nuestra “vscreen”, por lo que lo
normal sería sólo replicar el área de juego (evitando marcadores y demás) si pensamos utilizar esta técnica.

Motivaciones de la organización del área de imágenes

Una vez ejecutado el programa anterior en todas sus iteraciones el lector podría preguntarse: ¿qué utilidad tiene
esta caprichosa organización de la videomemoria en lugar de una organización lineal y continua donde cada
nuevo bloque de 32 bytes se correspondiera con el siguiente scanline de pantalla?

Esta organización de memoria tiene como objetivo el facilitar las rutinas de impresión de texto, algo que
podemos ver en las posiciones de inicio de las diferentes líneas de un mismo carácter:
Scanline del carácter Dirección de memoria
0 $4000
1 $4100
2 $4200
3 $4300
4 $4400
5 $4500
6 $4600
7 $4700

Tal y como está organizada la videoram, basta con calcular la dirección de inicio del bloque en baja resolución
donde queremos trazar un carácter, imprimir los 8 píxeles que forman su scanline (con la escritura de un único
byte en videomemoria), y saltar a la siguiente posición de videomemoria donde escribir. Como se puede
apreciar en la tabla anterior, este salto a la siguiente línea se realiza con un simple INC del byte alto de la
direccion (INC H en el caso de que estemos usando HL para escribir). De esta forma se simplifican las rutinas
de trazado de caracteres y UDGs de la ROM.

Pensemos que los antecesores del ZX Spectrum (ZX80 y ZX81) tenían una videomemoria orientada al texto en
baja resolución, y con la visión del software de la época y la potencia de los microprocesadores existentes lo
normal era pensar en el Spectrum como un microordenador orientado a programar en BASIC y realizar
programas “de gestión”, más que pensar en él como una máquina de juegos. En este contexto, potenciar la
velocidad de ejecución del trazado de texto era crucial.

Videomemoria: Área de atributos


El área de atributos es el bloque de 768 bytes entre $5800 (22528) y $5AFF (23295), ambas celdillas de
memoria incluídas. Cada una de las posiciones de memoria de este área almacenan la información de color
(color de tinta, color de papel, brillo y flash) de un bloque de 8×8 píxeles en la pantalla.

El tamaño de 768 bytes de este área viene determinado por la resolución del sistema de color del Spectrum:
Hemos dicho que el sistema gráfico dispone de una resolución de 256×192, pero el sistema de color divide la
pantalla en bloques de 8×8 píxeles, lo que nos da una resolución de color de 256/8 x 192/8 = 32×24 bloques.
Como la información de color de cada bloque se codifica en un único byte, para almacenar la información de
color de toda una pantalla se requieren 32 x 24 x 1 = 768 bytes.

Sabemos ya pues que hay una correspondencia directa entre los 32×24 bloques de 8×8 píxeles de la pantalla y
cada byte individual del área de atributos, pero ¿cómo se estructura esta información?

La organización lógica del área de atributos es más sencilla y directa que la del área de imagen. Aquí, los 32
primeros bytes del área de atributos se corresponden con los 32 primeros bloques horizontales de la pantalla. Es
decir, la celdilla 22528 se corresponde con el bloque (0,0), la 22529 se corresponde con (1,0), la 22530 con
(2,0), y así hasta llegar a la celdilla 22559 en (31,0). La siguiente celdilla en memoria, 22560, se corresponde
con el siguiente bloque en pantalla, el primero de la segunda línea, (0,1), y así de forma sucesiva.
Atributos: correspondencia entre memoria y pantalla

Se puede decir que el área de atributos es totalmente lineal; consta de 768 bytes que se corresponden de forma
consecutiva con el estado de cada bloque y de cada fila horizontal de bloques de pantalla: Los primeros 32
bytes del área se corresponden con la primera fila horizontal de bloques, los siguientes 32 bytes con la segunda,
los siguientes 32 bytes con la tercera, hasta los últimos 32 bytes, que se corresponden con los de la línea 23. El
byte alojado en la última posición ($5AFF) se corresponde con el atributo del bloque (31,23).

A continuación podemos ver una tabla que muestra los inicios y fin de cada línea de atributos en pantalla:

Línea Inicio (carácter 0,N) Fin (carácter 31,N)


0 $5800 $581F
1 $5820 $583F
2 $5860 $585F
3 $5840 $587F
4 $5880 $589F
5 $58A0 $58BF
6 $58C0 $58DF
7 $58E0 $58FF
8 $5900 $591F
9 $5920 $593F
10 $5940 $595F
11 $5960 $597F
12 $5980 $599F
13 $59A0 $59BF
14 $59C0 $59DF
15 $59E0 $59FF
Línea Inicio (carácter 0,N) Fin (carácter 31,N)
16 $5A00 $5A1F
17 $5A20 $5A3F
18 $5A40 $5A5F
19 $5A60 $5A7F
20 $5A80 $5A9F
21 $5AA0 $5ABF
22 $5AC0 $5ADF
23 $5AE0 $5AFF

Esta organización del área de atributos es muy sencilla y permite un cálculo muy sencillo de la posición de
memoria del atributo de un bloque concreto de pantalla. Es decir, podemos encontrar fácilmente la posición de
memoria que almacena el atributo que corresponde a un bloque concreto en baja resolución de pantalla
mediante:

Direccion_Atributo(x_bloque,y_bloque) = 22528 + (y_bloque*32) + x_bloque

O, con desplazamientos:

Direccion_Atributo(x_bloque,y_bloque) = 22528 + (y_bloque<<5) + x_bloque

Si en vez de una posición de bloque tenemos una posición de pixel, podemos convertirla primero a bloque
dividiendo por 8:

Direccion_Atributo(x_pixel,y_pixel) = 22528 + ((y_pixel/8)*32) + (x_pixel/8)

Con desplazamientos:

Direccion_Atributo(x_pixel,y_pixel) = 22528 + ((y_pixel>>3)<<5) + (x_pixel>>3)

La información en cada byte de este área se codifica de la siguiente manera:

Bit 7 6 5-4-32-1-0
Valor FLASH BRIGHT PAPER INK

Es decir, utilizamos:

• Los bits 0, 1 y 2 para almacenar el color de tinta, es decir, el color que la ULA utilizará para trazar los
píxeles que estén activos (=1) del recuadro 8×8 al que referencia este atributo. Nótese que con 3 bits
podemos almacenar un valor numérico entre 0 y 7, que son los 8 colores básicos del Spectrum.
• Los bits 3, 4 y 5 para almacenar el color de tinta, es decir, el color que la ULA utilizará para trazar los
píxeles que estén activos (=1) del recuadro 8×8 al que referencia este atributo. De nuevo, se utiliza un
valor de 0-7.
• El bit 6 para indicar si está activado el modo brillo de color o no.
• El bit 7 para indicar si el bloque 8×8 al que referencia este atributo debe parpadear o no. El parpadeo,
para la ULA, consiste en el intercambio de las señales de color de tinta y color de papel que envía al
monitor, alternándolas cada aproximadamente medio segundo.

Recordemos que estos colores básicos son:


Valor Color
0 Negro
1 Azul
2 Rojo
3 Magenta
4 Verde
5 Cian
6 Amarillo
7 Blanco

A estos colores se les puede activar el bit de brillo para obtener una tonalidad más cercana a la intensidad
máxima de dicho color. Examinando de nuevo la captura que veíamos al principio del artículo:

La relación de los colores con su “identificador numérico” está basada en el estado de 4 bits: BRILLO,
COMPONENTE_R, COMPONENTE_G y COMPONENTE_B:

Valor Bits “BRILLO R G B” Color Componentes RGB


0 0000b Negro (0, 0, 0 )
1 0001b Azul (0, 0, 192)
2 0010b Rojo (192, 0, 0)
3 0011b Magenta (192, 0, 192)
4 0100b Verde (0, 192, 0)
5 0101b Cian (0, 192, 192)
6 0110b Amarillo (192, 192, 0)
7 0111b Blanco (192, 192, 192)
8 1000b Negro + Brillo (0, 0, 0)
9 1001b Azul + Brillo (0, 0, 255)
10 1010b Rojo + Brillo (255, 0, 0)
11 1011b Magenta + Brillo (255, 0, 255)
12 1100b Verde + Brillo (0, 255, 0)
13 1101b Cian + Brillo (0, 255, 255)
14 1110b Amarillo + Brillo (255, 255, 0)
15 1111b Blanco + Brillo (255, 255, 255)

Todos los colores se componen a través del estado de las componentes R, G y B (entre 0 y 1), así como de
mezclas de dichas componentes (Ej: Cian = 5 = 101b = R+B). Sería perfectamente posible separar la memoria
de atributos en 3 pantallas alojando el estado de las 3 componentes de color (o incluso de combinaciones de
ellas) extrayendo la información de los bits correspondientes.
Pero volvamos a cada atributo individual: Si tuvieramos que codificar mediante una operación matemática un
color directamente como atributo, y sabiendo que las multiplicaciones por potencias de dos equivalen a
desplazamientos, se podría realizar de la siguiente forma:

Atributo = (Flash*128) + (Bright*64) + (Paper*8) + Ink

O lo que es lo mismo:

Atributo = (Flash<<7) + (Bright<<6) + (Paper<<3) + Ink

A continuación veremos un ejemplo similar al del capítulo anterior para estudiar la organización de la memoria
de atributos.

Explorando el área de atributos con un ejemplo

A continuación tenemos el código de otro un sencillo programa (attrtest.asm) que muestra la total linealidad del
área de atributos con respecto a los bloques de baja resolución de la pantalla.

Este programa carga HL con el inicio del área de atributos de la videomemoria (22528), y escribe bloques de 32
bytes (los atributos de 32 bloques de 8×8) con un valor que cambia entre 8 y 15 (o lo que es lo mismo, variando
entre 0 y 7 los 3 bits de PAPEL del atributo, bits del 3 al 5).

Al igual que en el ejemplo anterior. hemos añadido de nuevo en cada iteración del bucle externo la necesidad de
pulsar y liberar una tecla para permitir al lector estudiar los efectos de cada escritura de 32 bytes en la pantalla,
y poder tratar de predecir el efecto de la escritura de los siguientes 32 bytes.

; Mostrando la organizacion de la videomemoria (atributos)

ORG 50000

; Pseudocodigo del programa:


;
; Borramos la pantalla
; Apuntamos HL a 22528
; Repetimos 24 veces:
; Esperamos pulsacion de una tecla
; Repetimos 32 veces:
; Escribir un valor de PAPEL 0-7 en la direccion apuntada por HL
; Incrementar HL

Start:
LD A, 0
CALL ClearScreen ; Borramos la pantalla

LD HL, 22528 ; HL apunta a la VRAM


LD B, 24 ; Repetimos para 192 lineas

bucle_lineas:
LD D, B ; Nos guardamos el valor de D para el
; bucle exterior (usaremos B ahora en otro)
LD B, 32 ; B=32 para el bucle interior

; Esperamos que se pulse y libere tecla


CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released

LD A, (papel) ; Cogemos el valor del papel


INC A ; Lo incrementamos
LD (papel), A ; Lo guardamos de nuevo
CP 8 ; Si es == 8 (>7), resetear
JR NZ,no_resetear_papel
LD A, 255
LD (papel), A ; Lo hemos reseteado: lo guardamos
XOR A ; A=0

no_resetear_papel:

SLA A ; Desplazamos A 3 veces a la izquierda


SLA A ; para colocar el valor 0-7 en los bits
SLA A ; donde se debe ubicar PAPER (bits 3-5).

bucle_32_bytes:
LD (HL), A ; Almacenamos A en (HL) = attrib de 8x8
INC HL ; siguiente byte (siguientes 8x8 pixeles.)
DJNZ bucle_32_bytes ; 32 veces = 32 bytes = 1 scanline de bloques

LD B, D ; Recuperamos el B del bucle exterior

DJNZ bucle_lineas ; Repetir 24 veces

JP Start ; Inicio del programa

papel defb 255 ; Valor del papel

;-----------------------------------------------------------------------
; Esta rutina espera a que haya alguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
XOR A
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed
RET

;-----------------------------------------------------------------------
; Esta rutina espera a que no haya ninguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
XOR A
IN A, (254)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET

;-----------------------------------------------------------------------
; Limpiar la pantalla con el patron de pixeles indicado.
; Entrada: A = patron a utilizar
;-----------------------------------------------------------------------
ClearScreen:
LD HL, 16384
LD (HL), A
LD DE, 16385
LD BC, 192*32-1
LDIR
RET

END 50000

Si ensamblamos y ejecutamos el programa veremos lo siguiente tras algunos ciclos de iteración del bucle
externo:
Nótese que no estamos trazando ningún pixel sino cambiando el color de PAPEL de cada bloque de 8×8 lo que
provoca el cambio de color de los 64 píxeles del bloque que no estén activos (que son todos pues hemos
borrado el contenido del área gráfica al principio del programa).

Con cada pulsación de teclado escribimos 32 bytes más en la zona de atributos, los cuales se corresponden con
la siguiente fila de bloques de pantalla en baja resolución.

La memoria de atributos difiere de la de imagen en cuanto a que es totalmente lineal y que cada byte representa
al bloque inmediatamente siguiente. Al llegar a la esquina derecha de la pantalla, el siguiente byte se
corresponde con el primero de la siguiente línea.

Con esta información, nos podemos crear la siguiente rutina para establecer el valor de atributos de toda la
pantalla:

;-------------------------------------------------------
; Establecer los colores de la pantalla con el byte de
; atributos indicado.
; Entrada: A = atributo a utilizar
;-------------------------------------------------------
ClearAttributes:
PUSH HL
PUSH DE

LD HL, 22528 ; HL = Inicio del area de atributos


LD (HL), A ; Escribimos el patron A en (HL)
LD DE, 22529 ; Apuntamos DE a 22528
LD BC, 24*32-1 ; Copiamos 767 veces (HL) en (DE)
LDIR ; e incrementamos HL y DL. Restamos 1
; porque ya hemos escrito en 22528.
POP DE
POP HL
RET

El color del borde de la pantalla


El área gráfica de 256×192 píxeles está centrada en el centro de la pantalla o monitor, dejando alrededor de ella
un marco denominado BORDE. Este borde tiene 64 píxeles en las franjas horizontales y 48 píxeles en las
verticales.

El borde tiene un color único que la ULA utiliza para retrazar todos y cada uno de los píxeles de este marco.
Podemos cambiar este color accediendo en el Z80 al puerto de la ULA que controla el borde.
El conocido comando de BASIC “BORDER” llama a la rutina de la ROM BORDER en $2294, la cual realiza el
cambio del color del borde mediante el acceso a la ULA y además actualiza la variable del sistema BORDCR
en 23624d.

Concretamente, basta con escribir un valor en el rango 0-7 en el puerto $FE (254) para que la ULA utilice ese
valor desde ese instante como color del borde. Las típicas líneas “de carga” en el borde que podemos ver
durante las rutinas de LOAD y SAVE son cambios del color del borde realizados rápidamente como
indicadores de la carga mientras la ULA está dibujando el cuadro actual. Si se cambia el borde con la suficiente
rapidez, la ULA cambiará el color con que lo está dibujando cuando todavía no ha acabado la generación del
cuadro de imagen actual. El valor 0-7 representa el identificador de color a utilizar de la paleta de 8 colores de
la ULA, y este valor lo almacena internamente la ULA (no el Z80), ya que requiere de acceso instantáneo a él
durante la generación del vídeo.

En el capítulo dedicado a los Puertos de Entrada / Salida pudimos ya observar un ejemplo de cambio de color
del borde, que ahora vamos a modificar para separar el OUT en una función SetBorder propia:

; Cambio del color del borde al pulsar espacio


ORG 50000

LD B, 6 ; 6 iteraciones, color inicial borde

start:

bucle:
LD A, $7F ; Semifila B a ESPACIO
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0 (ESPACIO)
JR NZ, bucle ; Si esta a 1 (no pulsado), esperar

LD A, B ; A = B
CALL SetBorder ; Cambiamos el color del borde

suelta_tecla: ; Ahora esperamos a que se suelte la tecla


LD A, $7F ; Semifila B a ESPACIO
IN A, ($FE) ; Leemos el puerto
BIT 0, A ; Testeamos el bit 0
JR Z, suelta_tecla ; Saltamos hasta que se suelte

DJNZ bucle ; Repetimos "B" veces


LD B, 7
JP start ; Y repetir

salir:
RET

;------------------------------------------------------------
; SetBorder: Cambio del color del borde al del registro A
;------------------------------------------------------------
SetBorder:
OUT (254), A
RET

END 50000 ; Ejecucion en 50000

La ejecución del programa anterior cambiará el color del borde con cada pulsación de la tecla ESPACIO:
Si por algún motivo necesitaramos actualizar la variable del sistema BORDCR (porque vayamos a llamar a
rutinas de la ROM que lo puedan manipular), bastará con modificar SetBorder para que almacene el valor del
borde en la posición de memoria (23624) colocando primero el valor 0-7 en la posición de bits de PAPEL y
estableciendo la tinta a negro si el brillo está activo:

;------------------------------------------------------------
; SetBorder: Cambio del color del borde al del registro A
; Se establece BORDCR tal cual lo requiere BASIC.
;------------------------------------------------------------
SetBorder:
OUT (254), A ; Cambiamos el color del borde
RLCA
RLCA
RLCA ; A = A*8 (colocar en bits PAPER)
BIT 5, A ; Mirar si es un color BRIGHT
JR NZ, SetBorder_fin ; No es bright -> guardarlo
; Si es bright
XOR 7 ; -> cambiar la tinta a 0

SetBorder_fin:
LD (23624), A ; Salvar el valor en BORDCR

RET

Mantener actualizado BORDCR puede ser útil si pretendemos llamar a la rutina de la ROM BEEPER (en
$03B65), ya que el puerto que se utiliza para controlar el altavoz es el mismo que el del borde (salvo que se
utiliza el bit 4 del valor que se envía con OUT $FE). La rutina BEEPER carga el valor de BORDCR para,
además del manipular el bit 4 del puerto, cargar los bits 0, 1 y 2 con el borde actual para que éste no cambie. Si
no estuviera almacenado el valor del borde en BORDCR y BEEPER no lo incluyera en los bits 0-2 de su OUT,
lo establecería en negro (000) con cada cambio del estado del speaker.

Finalmente, recomendamos al lector que elimine del programa anterior la necesidad de pulsar y soltar una tecla.
De esta forma podrá verificar qué sucede cuando se cambia el color del borde mientras la ULA lo está
dibujando:

; Cambio del color del borde mientras la ULA dibuja


ORG 50000

LD B, 6 ; 6 iteraciones, color inicial borde

start:

bucle:
LD A, B ; A = B
CALL SetBorder ; Cambiamos el color del borde

DJNZ bucle ; Repetimos "B" veces


LD B, 7
JP start ; Y repetir

salir:
RET

El resultado de la ejecución es el siguiente:

Como curiosidad al respecto de la diferencia de velocidad entre BASIC y ensamblador, pruebe a ejecutar el
siguiente programa en su intérprete BASIC:

10 FOR I=0 TO 7 : BORDER I : NEXT I : GOTO 10

La ejecución del anterior programa sólo es capaz de establecer 2 (3 a lo sumo) bordes diferentes en un mismo
cuadro de imagen mientras que la versión ASM puede cambiar el color del borde más de 35 veces por cuadro:

Cabe destacar que en esta ocasión BASIC es todavía más rápido de lo normal pues la ejecución de BORDER I
acaba resultando en la llamada a la función de la ROM “BORDER” que apenas tiene 12 instrucciones (parecida
a nuestra SetBorder), lo que deja todavía más en evidencia la velocidad de lo que es el intérprete de BASIC en
sí.

El "atributo actual" ATTR-T


Ahora que conocemos el formato de una celdilla de atributo podemos hablar de la variable del sistema ATTR-T
(dirección de memoria 23695), la cual almacena el atributo actual que las rutinas de la ROM del Spectrum
como nuestra conocida RST 16.
A continuación tenemos un ejemplo que imprime cadenas con diferentes atributos de color. Para ello se ha
creado una rutina PrintString basada en imprimir caracteres mediante RST 16, que utiliza el valor de ATTR-T.

; Mostrando la organizacion de la videomemoria (atributos)

ORG 50000

; Pseudocodigo del programa:


;
; Borramos la pantalla
; Apuntamos HL a 22528
; Repetimos 24 veces:
; Esperamos pulsacion de una tecla
; Repetimos 32 veces:
; Escribir un valor de PAPEL 0-7 en la direccion apuntada por HL
; Incrementar HL

Start:

LD A, 1 ; Borde azul
CALL SetBorder
LD A, 0
CALL ClearScreen ; Borramos la pantalla
LD A, 8+4 ; Atributos: rojo sobre azul
CALL ClearAttributes

LD HL, linea1
CALL PrintString

LD A, 12 ; Atributos: verde sobre azul


LD (23695), A

LD HL, linea2
CALL PrintString

LD A, 64+2+9 ; Atributos: magenta sobre cyan + brillo.


LD (23695), A

LD HL, linea2
CALL PrintString

; Esperamos que se pulse y libere tecla


CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released

RET ; Fin del programa

;-----------------------------------------------------------------------
; Esta rutina espera a que haya alguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
XOR A
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed
RET

;-----------------------------------------------------------------------
; Esta rutina espera a que no haya ninguna tecla pulsada para volver.
;-----------------------------------------------------------------------
Wait_For_Keys_Released:
XOR A
IN A, (254)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET
;-----------------------------------------------------------------------
; Limpiar la pantalla con el patron de pixeles indicado.
; Entrada: A = patron a utilizar
;-----------------------------------------------------------------------
ClearScreen:
LD HL, 16384 ; HL = Inicio del area de imagen
LD (HL), A ; Escribimos el valor de A en (HL)
LD DE, 16385 ; Apuntamos DE a 16385
LD BC, 192*32-1 ; Copiamos 6142 veces (HL) en (DE)
LDIR
RET

;-------------------------------------------------------------------------
; Establecer los colores de la pantalla con el byte de atributos indicado.
; Entrada: A = atributo a utilizar
;-------------------------------------------------------------------------
ClearAttributes:

LD HL, 22528 ; HL = Inicio del area de atributos


LD (HL), A ; Escribimos el patron A en (HL)
LD DE, 22529 ; Apuntamos DE a 22529
LD BC, 24*32-1 ; Copiamos 767 veces (HL) en (DE)
LDIR ; e incrementamos HL y DL. Restamos 1
; porque ya hemos escrito en 22528.
RET

;-------------------------------------------------------------------------
; SetBorder: Cambio del color del borde al del registro A
; Se establece BORDCR tal cual lo requiere BASIC.
;-------------------------------------------------------------------------
SetBorder:
OUT (254), A ; Cambiamos el color del borde
RLCA
RLCA
RLCA ; A = A*8 (colocar en bits PAPER)
BIT 5, A ; Mirar si es un color BRIGHT
JR NZ, SetBorder_fin ; No es bright -> guardarlo
; Si es bright
XOR 7 ; -> cambiar la tinta a 0

SetBorder_fin:
LD (23624), A ; Salvar el valor en BORDCR
RET

;-------------------------------------------------------------------------
; PrintString: imprime una cadena acabada en valor cero (no caracter 0).
; HL = direccion de la cadena de texto a imprimir.
;-------------------------------------------------------------------------
PrintString:

printstrloop:
LD A, (HL) ; Obtener el siguiente caracter a imprimir
CP 0 ; Comprobar si es un 0 (fin de linea)
RET Z ; Si es cero, fin de la rutina
RST 16 ; No es cero, imprimir caracter o codigo
; de control (13=enter, etc).
INC HL ; Avanzar al siguiente caracter
JP printstrloop ; Repetir bucle
ret

;-------------------------------------------------------------------------
; Datos
;-------------------------------------------------------------------------
linea1: defb 'Impreso con ATTR-T actual', 13, 13, 0
linea2: defb 'Esto es una prueba',13,'cambiando los atributos', 13, 13, 0
END 50000

Con nuestra nueva rutina de PrintString trazaremos en pantalla 1 línea con los atributos actuales seguida de 2
líneas con diferentes atributos. Nótese como RST 16 entiende e interpreta en las cadenas los códigos de control
como por ejemplo 13 (retorno de carro).

Nótese que dado lo habitual que puede ser llamar a ClearScreen y ClearAttributes, podemos desarrollar una
función ClearScreenAttributes que realice ambas funciones en una misma llamada:

;-----------------------------------------------------------------------
; Limpiar la pantalla con el patron de pixeles y atributos indicado.
; Entrada: H = atributo, L = patron
;-----------------------------------------------------------------------
ClearScreenAttrib:
PUSH DE
PUSH BC

LD A, H ; A = el atributo
EX AF, AF' ; Nos guardamos el atributo en A'
LD A, L ; Cargamos en A el patron
LD HL, 16384 ; HL = Inicio del area de imagen
LD (HL), A ; Escribimos el valor de A en (HL)
LD DE, 16385 ; Apuntamos DE a 16385
LD BC, 192*32-1 ; Copiamos 6142 veces (HL) en (DE)
LDIR

EX AF, AF' ; Recuperamos A (atributo) de A'


INC HL ; Incrementamos HL y DE
INC DE ; para entrar en area de atributos
LD (HL), A ; Almacenamos el atributo
LD BC, 24*32-1 ; Ahora copiamos 767 bytes
LDIR

POP BC
POP DE
RET

Por otra parte, cuando tratemos las fuentes de texto como sprites de carácteres en baja resolución utilizaremos
rutinas de impresión de cadenas más rápidas (y con juegos de caracteres personalizados) al no tener que
interpretar éstas los diferentes códigos de control que se pueden insertar en las mismas.
Efectos sobre la imagen y los atributos
Ahora ya conocemos la organización de la zona de imagen y atributos y sabemos (del capítulo sobre rutinas de
SAVE/LOAD) cargar en ella datos gráficos desde cinta o incluir los datos gráficos en nuestro propio programa
y volcarlos con instrucciones LDIR. Estamos pues en disposición de realizar pequeñas y sencillas rutinas de
borrado de pantalla o de aparición de los datos en la misma de diferentes formas, como por ejemplo:

• Efectos de fundido de los atributos de pantalla a negro.


• Efectos de aparición de imagen como establecer todos los atributos a negro, copiar los datos gráficos y
hacer aparecer la imagen realizando una copia de los atributos desde una zona de atributos virtual con
algún tipo de efecto (circular, desde los laterales, como un recuadro, etc).
• Desaparición de la imagen manipulando los bits de pantalla (de izquierda a derecha, de arriba a abajo,
reduciendo estos bits a cero, etc).
• Inversión horizontal, vertical o de estado de los bits de los datos gráficos de pantalla.
• Zoom o reducción de alguna zona de pantalla.
• (etc…).

Por ejemplo, la siguiente rutina vacía el contenido de una pantalla (preferentemente monocolor) haciendo una
rotación de los píxeles de cada bloque de pantalla. Los bloques 0-15 verán sus píxeles rotados a la izquierda y
los bloques 16-31 a la derecha:

; Fundido de los pixeles a cero con una cortinilla

ORG 50000

Start:

; Rellenamos la VRAM de pixeles copiando 6 KB de la ROM

LD HL, 0
LD DE, 16384
LD BC, 6144
LDIR

CALL Wait_For_Keys_Pressed
CALL Wait_For_Keys_Released
CALL FadeScreen

RET

;-----------------------------------------------------------------------
Wait_For_Keys_Pressed:
XOR A
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed
RET

;-----------------------------------------------------------------------
Wait_For_Keys_Released:
XOR A
IN A, (254)
OR 224
INC A
JR NZ, Wait_For_Keys_Released
RET

;-----------------------------------------------------------------------
; Fundido de pantalla decrementando los pixeles de pantalla
;-----------------------------------------------------------------------
FadeScreen:
PUSH AF
PUSH BC
PUSH DE
PUSH HL ; Preservamos los registros

LD B, 9 ; Repetiremos el bucle 9 veces


LD C, 0 ; Nuestro contador de columna

fadegfx_loop1:
LD HL, 16384 ; Apuntamos HL a la zona de atributos
LD DE, 6144 ; Iteraciones bucle

fadegfx_loop2:
LD A, (HL) ; Cogemos el grupo de 8 pixeles

;-- Actuamos sobre el valor de los pixeles --


CP 0 ;
JR Z, fadegfx_save ; Si ya es cero, no hacemos nada

EX AF, AF' ; Nos guardamos el dato en A'

LD A, C ; Pasamos el contador a A
CP 15 ; Comparamos A con 15
JR NC, fadegfx_mayor15 ; Si es mayor, saltamos

EX AF, AF' ; Recuperamos los pixeles desde A'


RLA ; Rotamos A a la izquierda
JR fadegfx_save ; Y guardamos el dato

fadegfx_mayor15:
EX AF, AF' ; Recuperamos los pixeles desde A'
SRL A ; Rotamos A a la derecha

;-- Fin actuacion sobre el valor de los pixeles --

fadegfx_save:

LD (HL), A ; Almacenamos el atributo modificado


INC HL ; Avanzamos puntero de memoria

; Incrementamos el contador y comprobamos si hay que resetearlo


INC C
LD A, C
CP 32
JR NZ, fadegfx_continue

LD C, 0

fadegfx_continue:

DEC DE
LD A, D
OR E
JP NZ, fadegfx_loop2 ; Hasta que DE == 0

DJNZ fadegfx_loop1 ; Repeticion 9 veces

POP HL
POP DE
POP BC
POP AF ; Restauramos registros
RET

END 50000

El efecto sobre píxeles aleatorios en pantalla es el siguiente:


Podemos cambiar la rutina para que realice diferentes efectos sobre los píxeles modificando el núcleo de la
misma, identificado con el comentario Actuamos sobre el valor de los píxeles.

A continuación podemos ver la rutina de degradación de atributos que vimos como un ejemplo en el capítulo
dedicado a la pila. Este efecto aplicado sobre una pantalla gráfica puede utilizarse como “fundido a negro” de la
misma. Podemos utilizar el esqueleto del programa anterior como base para llamar a esta rutina:

;-----------------------------------------------------------------------
; Fundido de pantalla decrementando tinta y papel en los atributos.
;-----------------------------------------------------------------------
FadeAttributes:
PUSH AF
PUSH BC
PUSH DE
PUSH HL ; Preservamos los registros

LD B, 9 ; Repetiremos el bucle 9 veces

fadescreen_loop1:
LD HL, 16384+6144 ; Apuntamos HL a la zona de atributos
LD DE, 768 ; Iteraciones bucle

HALT
HALT ; Ralentizamos el efecto

fadescreen_loop2:
LD A, (HL) ; Cogemos el atributo
AND 127 ; Eliminamos el bit de flash
LD C, A

AND 7 ; Extraemos la tinta (AND 00000111b)


JR Z, fadescreen_ink_zero ; Si la tinta ya es cero, no hacemos nada

DEC A ; Si no es cero, decrementamos su valor

fadescreen_ink_zero:

EX AF, AF' ; Nos hacemos una copia de la tinta en A'


LD A, C ; Recuperamos el atributo
SRA A
SRA A ; Pasamos los bits de paper a 0-2
SRA A ; con 3 instrucciones de desplazamiento >>

AND 7 ; Eliminamos el resto de bits


JR Z, fadescreen_paper_zero ; Si ya es cero, no lo decrementamos

DEC A ; Lo decrementamos

fadescreen_paper_zero:
SLA A
SLA A ; Volvemos a color paper en bits 3-5
SLA A ; Con 3 instrucciones de desplazamiento <<

LD C, A ; Guardamos el papel decrementado en A


EX AF, AF' ; Recuperamos A'
OR C ; A = A OR C = PAPEL OR TINTA

LD (HL), A ; Almacenamos el atributo modificado


INC HL ; Avanzamos puntero de memoria

DEC DE
LD A, D
OR E
JP NZ, fadescreen_loop2 ; Hasta que DE == 0

DJNZ fadescreen_loop1 ; Repeticion 9 veces

POP HL
POP DE
POP BC
POP AF ; Restauramos registros
RET

Rutinas más complejas pueden producir cortinillas y efectos mucho más vistosos. En la revista Microhobby se
publicaron muchos de estos efectos de zoom, desaparición de pantalla o inversión, dentro de la sección Trucos.

La Shadow VRAM de los modelos de 128K


En el capítulo dedicado a la paginación de memoria en los modelos de 128KB se habló de la paginación de
bloques de 16KB sobre el área entre $C000 y $FFFF. El bloque de 16KB que almacena la videoram (el bloque
5, o, como se le conoce técnicamente, RAM5) está normalmente mapeado sobre $4000.
Paginación 128K

En los modelos de 128K, existe un segundo bloque de 16KB que podemos utilizar como VideoRAM (Shadow
VRAM). El Z80 y la ULA nos permiten mapear RAM7 sobre $C000-$FFFF, dejando la VideoRAM original
sobre $4000. Más interesante todavía, la ULA puede visualizar el contenido de RAM7 en lugar del de RAM5
aunque no hayamos mapeado RAM7 en ningún sitio. Y recordemos que también podemos mapear la VRAM
estándar (RAM5) sobre $C000, accediendo a ella a través de $C000 además de mediante $4000.

El poder visualizar una VRAM aunque no esté mapeada y el poder mapear tanto RAM5 como RAM7 sobre
$C000 nos permite organizar el código de nuestro programa para que siempre escriba sobre $C000, teniendo
mapeada en $C000 la pantalla que actualmente no esté visible.

La utilidad principal de esta funcionalidad es la de poder generar un cuadro de imagen o animación en una
“pantalla virtual” (la pantalla shadow) que no es visible, cambiando la visualización a esta pantalla una vez
compuesta la imagen actual. De esta forma es posible trabajar con una pantalla completa sin que nos alcance el
haz de electrones durante su dibujado, especialmente en juegos que realicen scrolles de todo el área de imagen.

En el tiempo disponible tras un pulso VSYNC no hay tiempo material para actualizar los 6KB de una pantalla
completa sin que el haz de electrones alcance a nuestro programa conforme manipula la memoria, por lo que
esta técnica permitiría realizar ese tipo de acciones con el siguiente proceso:

• Mapeamos RAM7 sobre $C000.


• Visualizamos RAM5 (RAM7 no es visible).
• Trabajamos sobre $C000 (sobre RAM7). Los cambios en nuestra pantalla shadow no son visibles.
• Esperamos una interrupción (mediante HALT o mediante coordinación con la ISR de la ULA).
• Cambiamos la visualización a RAM7 (RAM5 deja de ser visible).
• Mapeamos ahora RAM5 sobre $C000.
• Trabajamos sobre $C000 (sobre RAM5). Los cambios en nuestra pantalla shadow no son visibles.
• Repetimos el proceso.
Con este mecanismo siempre trabajamos sobre $C000 pero los cambios que realizamos sobre esta pantalla
virtual no son perceptibles por el usuario. Cambiando la visualización de la VRAM a nuestra pantalla actual
tras una interrupción hacemos los cambios visibles de forma inmediata, sin que el haz de electrones afecte a
nuestro scroll o al dibujado de sprites. La tasa de fotogramas por segundo ya no sería de 50 (no podríamos
generar 1 cuadro de imagen por interrupción) pero se evitaría un posible molesto efecto de parpadeo o
cortinilla.

La desventaja de este sistema es que utilizamos $C000-$FFFF como pantalla virtual con lo que perdemos 16KB
efectivos de RAM así como la posibilidad de paginar sobre $C000. Nos quedan así 16KB de memoria (entre
$8000 y $BFFF) para alojar el código de nuestro programa, los datos gráficos, textos, etc. Esto puede ser una
enorme limitación según el tipo de juego o programa que estemos realizando.

En realidad, si diseñamos adecuadamente nuestro programa, podemos aprovechar más de 16KB, puesto que
sólo necesitamos mapear RAM5 ó RAM7 en $C000 durante la generación de la pantalla virtual. Esto obliga a
que los gráficos, fuentes, sprites y mapeados del juego deban estar disponibles en $8000-$BFFF, pero una vez
finalizada la generación de la pantalla podemos volver a mapear RAM0 sobre $C000, volviendo a la lógica del
juego que podría estar ubicada en ese bloque, junto al resto de variables, imágenes o textos usados en los
menúes, efectos sonoros, músicas, etc.

Como véis, se necesita tener muy controlada la ubicación de las diferentes rutinas y variables y diseñar el juego
para que mapee la página adecuada en cada momento y salte a una rutina concreta sólo cuando la rutina a la que
hace referencia un CALL esté contenida en la página mapeada.

Se reseñó también, en el apartado Particularidades del +2A/+3 la existencia de unos modos extendidos de
paginación que permitirían ubicar la segunda VideoRAM (el bloque 7, o RAM7) sobre $4000, permitiendo el
alternar entre la visualización de RAM5 o de RAM7 sin perder la memoria $C000-$FFFF como “Pantalla
Virtual”:

Modos de paginación especial del +2A/+3


Como puede verse en la figura anterior, los modos Bit2 = 0, Bit1 = 1 (Bancos 4-5-6-3) y Bit2 = 1, Bit 1=1
(Bancos 4-7-6-3) del puerto $1FFD permiten paginar cualquiera de las 2 videorams (RAM5 o RAM7) sobre
$4000.

Pese a las posibilidades de “animación sin parpadeo” que proporcionan estas técnicas, la utilización de
cualquiera de las dos tiene una desventaja clara además de la “pérdida” (durante el dibujado de la pantalla
shadow) de los 16KB $C000-$FFFF, y es la incompatibilidad con modelos de 48K, requiriendo un modelo de
128Kb para paginar RAM7 o incluso de un +2A/+3 para el uso de la paginación extendida. Si a los 16KB de
RAM5 le restamos los 7KB de pantalla nos quedan otros 9KB adicionales, pero con la particularidad de que ese
bloque de memoria está “compartido” con la ULA por lo que la velocidad de lectura, escritura y ejecución
efectiva de este bloque se puede ver reducida hasta en un 25%.
Gráficos (y II): Cálculo de direcciones y
coordenadas
En el anterior capítulo exploramos la organización interna de los 6912 bytes de la videomemoria del Spectrum,
separándo esta videomemoria en un área de 6144 bytes de archivo de imagen comenzando en la dirección
16384 y otros 768 bytes de archivo de atributos comenzando en la dirección 22528.

La manipulación de dichas áreas de memoria nos permite el trazado de gráficos en pantalla y la manipulación
de los colores que tienen dichos gráficos en el monitor. Esto es así porque este área de memoria es leída por el
chip de la ULA 50 veces por segundo en sistemas de televisión PAL (Europa y Australia) y 60 veces por
segundo en sistemas NTSC (América y Asia) para enviar la señal que el televisor convierte en una imagen para
nuestros ojos.

Es necesario que la ULA refresque la pantalla de forma continuada y regular ya que debe reflejar los cambios
que los programas hagan en la videomemoria, así cómo para refrescar el estado de los píxeles del monitor
(necesario por el funcionamiento de la tecnología CRT).

Sabemos por el capítulo anterior que escribir en la videomemoria nos permite trazar gráficos en pantalla. Hasta
ahora hemos visto efectos globales aplicados a toda la vram (borrados, fundidos, etc), pero nuestro interés
principal será, seguramente, el trazar gráficos con precisión de bloque o de pixel en la pantalla.

Para poder realizar esta tarea necesitamos relacionar las posiciones de memoria de la videoram con las
coordenadas (x,y) de pantalla cuya información gráfica representan. Necesitaremos pues programar rutinas de
cálculo de direcciones en función de coordenadas de alta y baja resolución. Sabemos cómo dibujar, pero no
cómo calcular la dirección de memoria donde hacerlo. Este es precisamente nuestro objetivo en esta sección.

Para empezar, estableceremos una terminología unánime a la que haremos referencia a lo largo de todo el
capítulo, con las siguientes definiciones:

• Resolución del área gráfica: El Spectrum dispone de una resolución de 256×192 píxeles cuyo estado
se define en cada byte del área de imagen de la videomemoria.
• Estado de un pixel: Cada byte del área de imagen contiene el estado de 8 píxeles, de tal forma que cada
uno de los bits de dicho byte pueden estar a 1 (pixel encendido, se traza con el color de tinta) o a 0
(apagado, se traza con el color del papel).
• Resolución del área de atributos: El Spectrum tiene una resolución de color de 32×24 puntos, que se
mapean sobre la pantalla de forma que cada grupo de 8×8 píxeles del área gráfica tiene una
correspondencia con un atributo del área de atributos.
• Atributo: Un atributo define en los 8 bits de un byte el color de tinta y papel y el estado de brillo y
parpadeo de un bloque concreto de la pantalla.
• Bloque o carácter: Si dividimos la pantalla de 256×192 en 32×24 bloques de color, nos quedan bloques
de 8×8 píxeles que mantienen el mismo atributo de pantalla. Estamos acostumbrados a trabajar con
bloques ya que el intérprete BASIC del Spectrum utiliza la fuente de la ROM de 8×8 en una rejilla de
bloques que coincide con la resolución de atributos. Podemos pensar en los bloques como “posiciones
de carácter”.
• Scanline: Un scanline es una línea normalmente horizontal de datos gráficos. Por ejemplo, el scanline 0
de pantalla es la línea gráfica que va desde (0,0) a (255,0), y que definen los 32 bytes de videomemoria
que van desde 16384 hasta 16415. También se puede hablar del scanline de un sprite o de un carácter
cuando nos referimos a una línea concreta de esa porción de gráfico.
• Coordenadas (x,y): Se utiliza la nomenclatura (x,y) para definir la posición de un píxel en pantalla en
función de su posición horizontal y vertical siendo (0,0) la esquina superior izquierda de la misma y
(255,191) la esquina inferior derecha. Son, pues, “coordenadas en alta resolución”.
• Coordenadas (c,f): Se utiliza la nomenclatura (c,f), de (columna,fila), para hacer referencia a la
posición de un bloque 8×8 en pantalla en función de su posición horizontal y vertical siendo (0,0) la
esquina superior izquierda y (31,23) la esquina inferior derecha. Se conocen como “coordenadas en baja
resolución” o “coordenadas de bloque” o “de carácter”.
• Conversión (c,f) a (x,y): Como cada bloque es de 8×8 píxeles, podemos convertir una coordenada en
baja resolución a coordenadas de pixel como (x,y) = (8*c,8*f). Asímismo, (c,f) = (x/8,y/8).
• Tercio de pantalla: El área gráfica del Spectrum se divide en 3 áreas de 2KB de videoram que
almacenan la información de 256×64 píxeles. Estas áreas son comunmente denominadas “tercios”.
• Offset o Desplazamiento: Llamaremos offset o desplazamiento a la cantidad de bytes que tenemos que
avanzar desde una base (normalmente el inicio de la propia memoria o un punto de la misma) para
llegar a una posición de memoria. Así, un offset de 32 bytes desde 16384 referenciará a los 8 píxeles
desde (0,1) a (7,1). En las rutinas que veremos, el offset estará calculado con $0000 como la base, es
decir, serán offsets absolutos (posiciones de memoria).

Con estas definiciones, podemos hacer las siguientes afirmaciones:

• La pantalla del Spectrum tiene 192 scanlines horizontales de 256 píxeles cada uno.
• La pantalla del Spectrum se divide en 32×24 bloques o posiciones de caracteres.
• Un bloque o carácter tiene 8 scanlines de 8 píxeles cada uno (8×8).
• Cada byte de la videoram almacena el estado de 8 píxeles, por lo que un bloque se almacena en 8×8/8 =
8 bytes.
• Cada posición de bloque / carácter de la pantalla tiene asociado un atributo del área de atributos.
• Cada uno de los 3 tercios de la pantalla tiene 8 líneas de 32 caracteres.

Para aprovechar la información que trataremos en este capítulo es imprescindible comprender a la perfección la
organización interna de la videomemoria que se detalló en el anterior capítulo.

A modo de resumen, la estructura interna de estas 2 áreas de memoria es la siguiente:

• Área de imagen:
o El área de imagen se divide en 3 tercios de pantalla de 2KB de memoria cada uno, que van de
$4000 a $47FF (tercio superior), de $4800 a $4FFF (tercio central) y de $5000 a $57FF (tercio
inferior).
o Cada uno de los tercios comprende 8 líneas de 32 bloques horizontales (256×64 píxeles). Dentro
de cada uno de esos 2KB, tenemos, de forma lineal, 64 bloques de 32 bytes (256 píxeles) de
información que representan cada scanline de esos 8 bloques.
o Los primeros 32 bytes de dicho bloque contienen la información del scanline 0 del bloque 0.
Avanzando de 32 en 32 bytes tenemos los datos del scanline 0 del bloque 1, el scanline 0 del
bloque 2, el scanline 0 del bloque 3, etc, hasta que llegamos al scanline 7 del bloque 0. Los
siguientes 32 bytes repiten el proceso pero con el scanline 1 de cada bloque.
o Tras los últimos 32 bytes de un tercio, vienen los primeros 32 bytes del siguiente tercio, con la
misma organización, pero afectando a otra porción de la pantalla.
• Area de atributos:
o El área de atributos se encuentra en memoria inmediatamente después del área de imagen, por lo
que empieza en la posición de memoria 16384+6144 = 22528 ($5800).
o Cada byte del área de atributos se denomina atributo y define el valor de color de tinta, papel,
brillo y flash de un carácter / bloque de la pantalla. Esto implica que el área de atributos ocupa
32x24x1 = 768 bytes en memoria, por lo que empieza en 22528 ($5800) y acaba en 23295
($5AFF).
o Los diferentes bits de un atributo de carácter son: Bit 7 = FLASH, Bit 6 = BRIGHT, Bits 5-3 =
PAPER, Bits 2-0 = INK.
o Los valores de tinta y papel son un valor de 0-7 que junto al brillo como bit más significativo
componen un índice (B-I-I-I) contra una paleta de colores interna definida en la ULA, donde el 0
es el color negro y el 15 el blanco de brillo máximo.
o La organización interna del área de atributos es lineal: Los primeros 32 bytes desde $5800 se
corresponden con los atributos de la primera fila de bloques de la pantalla. Los segundos 32
bytes, con la segunda fila, y así sucesivamente hasta los últimos 32 bytes que se corresponden
con los atributos de la fila 23. La organización de la zona de atributos no se ve pues relacionada
con los tercios de pantalla, tan sólo con la columna y fila (c,f) del bloque.

Nuestro capítulo de hoy tiene los siguientes objetivos prioritarios:

• Cálculo de posiciones de atributo: Saber calcular la posición en memoria del atributo de una posición
de carácter (c,f) o de un pixel (x,y).
• Cálculo de posiciones de carácter (baja resolución): Saber calcular la posición en memoria en que
comienzan los datos gráficos (pixel 0,0 del carácter) de un carácter o bloque de 8×8 píxeles referenciado
como (c,f) o (x,y), asumiendo una resolución de 32×24 bloques en pantalla coincidiendo con las
posiciones de carácter de texto estándar.
• Cálculo de posiciones de pixel (alta resolución): Saber calcular la posición en memoria de un pixel
referenciado por (x,y).
• Cálculo de posiciones diferenciales: Dada una dirección de memoria de un atributo, carácter o pixel,
ser capaz de modificar esta dirección para acceder a los elementos de la izquierda, derecha, arriba o
abajo.
Utilizaremos las rutinas que veremos a continuación para el posicionamiento en pantalla de los elementos de
nuestros juegos y programas. En los próximos capítulos trabajaremos ya con sprites en baja y alta resolución,
fuentes de texto, mapeados por bloques, etc.

Cálculo de posiciones de atributo


Durante el desarrollo de un programa gráfico o un juego necesitaremos (ya sea como funciones independientes
o dentro de rutinas de sprites/gráficos más amplias) alguna de las siguientes rutinas:

• Get_Attribute_Offset_LR(c,f) : Dadas las coordenadas en baja resolución (columna,fila) de un bloque


/ carácter, debe devolver la dirección de memoria del atributo de dicho bloque.

• Get_Attribute_Offset_HR(x,y) : Dadas las coordenadas en alta resolución (x,y) contenida en un


bloque / carácter, debe devolver la dirección de memoria del atributo de dicho bloque.

• Get_Attribute_Coordinates_LR(offset): Dada una dirección de memoria dentro del área de atributos,


debe devolver las coordenadas (c,f) en baja resolución del bloque al que está asociado.

• Get_Attribute_Coordinates_HR(offset): Dada una dirección de memoria dentro del área de atributos,


debe devolver las coordenadas (x,y) en alta resolución del pixel superior izquierdo del bloque al que
está asociado.

Es importante comprobar antes de llamar a nuestras rutinas si estas modifican algún registro o flag que
necesitemos preservar. Podemos modificar las rutinas para que realicen PUSH y POP de los registros
necesarios o hacer nosotros estos PUSH/POP en la rutina llamadora.

Comencemos con las rutinas:

Get_Attribute_Offset
Una primera aproximación a la obtención de la dirección en memoria de un atributo concreto (columna,fila)
podría ser la utilización de una tabla de 24 valores de 16 bits que alojara las direcciones de inicio en memoria
de los atributos del primer carácter de cada fila.

De esta forma bastaría con utilizar el número de fila como índice en la tabla y sumar el número de columna
para obtener la dirección de memoria de la celdilla de atributos de (c,f):

Línea f Dirección en Hexadecimal En Decimal En Binario


0 $5800 22528 0101100000000000b
1 $5820 22560 0101100000100000b
2 $5840 22592 0101100001000000b
3 $5860 22624 0101100001100000b
4 $5880 22656 0101100010000000b
5 $58A0 22688 0101100010100000b
6 $58C0 22720 0101100011000000b
7 $58E0 22752 0101100011100000b
Línea f Dirección en Hexadecimal En Decimal En Binario
8 $5900 22784 0101100100000000b
9 $5920 22816 0101100100100000b
10 $5940 22848 0101100101000000b
11 $5960 22880 0101100101100000b
12 $5980 22912 0101100110000000b
13 $59A0 22944 0101100110100000b
14 $59C0 22976 0101100111000000b
15 $59E0 23008 0101100111100000b
16 $5A00 23040 0101101000000000b
17 $5A20 23072 0101101000100000b
18 $5A40 23104 0101101001000000b
19 $5A60 23136 0101101001100000b
20 $5A80 23168 0101101010000000b
21 $5AA0 23200 0101101010100000b
22 $5AC0 23232 0101101011000000b
23 $5AE0 23264 0101101011100000b

Direcciones del atributo en el carácter (0,f)

Así pues, podríamos tener una tabla de 16 bytes para indexarla con el número de fila, que permitiría calcular la
dirección de memoria como:

dirección_atributo(c,f) = tabla_offsetY_LR[ f ] + c

No obstante, existe una opción mucho más aconsejable en el caso de los atributos como es el realizar el cálculo
de la dirección destino en lugar de un lookup en una tabla.

Como ya vimos en el capítulo anterior, la dirección de un atributo concreto se puede calcular mediante la
siguiente fórmula:

Direccion_Atributo(x_bloque,y_bloque) = 22528 + (f*32) + c

Desde el inicio del área de atributos, avanzamos 32 bytes por fila hasta posicionarnos en el bloque de 32 bytes
que referencia a nuestro bloque, y sumamos el número de columna.

Implementando este cálculo en código máquina, obtendríamos la siguiente rutina (de la cual no haremos uso, ya
que diseñaremos una versión mucho más óptima):

;-------------------------------------------------------------
; Obtener la direccion de memoria del atributo del caracter
; (c,f) especificado mediante multiplicacion por 32.
;
; Entrada: B = FILA, C = COLUMNA
; Salida: HL = Direccion del atributo
;-------------------------------------------------------------
Get_Attribute_Offset_LR_SLOW:
; calcular dir_atributo como "inicio_attr + (32*f) + c"
LD H, 0
LD L, B ; HL = "fila"
ADD HL, HL ; HL = HL*2
ADD HL, HL ; HL = HL*4
ADD HL, HL ; HL = HL*8
ADD HL, HL ; HL = HL*16
ADD HL, HL ; HL = HL*32
LD D, 0
LD E, C ; DE = "columna"
ADD HL, DE ; HL = fila*32 + columna
LD DE, 22528 ; Direccion de inicio de atributos
ADD HL, DE ; HL = 22528 + fila*32 + columna
RET

El código que acabamos de ver es perfectamente funcional pero tiene ciertas desventajas:

• Hace uso de prácticamente todo el juego de registros, DE incluído (lo que nos implicaría realizar
PUSHes y POPs en nuestra rutina externa o dentro de la misma).
• Tiene un coste de ejecución de 112 t-estados.

Veamos cómo podemos mejorar esta rutina: Si nos fijamos en la representación en binario de la anterior tabla
de direcciones, veremos que todas ellas siguen un patrón común:

Linea f Dirección en Hexadecimal En Decimal En Binario


0 $5800 22528 0101100000000000b
1 $5820 22560 0101100000100000b
2 $5840 22592 0101100001000000b
3 $5860 22624 0101100001100000b
4 $5880 22656 0101100010000000b
(…) (…) (…) (…)
21 $5AA0 23200 0101101010100000b
22 $5AC0 23232 0101101011000000b
23 $5AE0 23264 0101101011100000b

• Los 6 bits más significativos de la dirección son 010110, que es la parte de la dirección que provoca que
todas las posiciones estén entre $5800 y $5AFF.
• Los bits 5, 6, 7 y 8 se corresponden con la fila que queremos consultar.
• Los bits 0, 1, 2, 3 y 4 los utilizaremos para acceder a a la columna deseada. En la tabla anterior son
siempre 0 porque estamos mostrando las direcciones de inicio de cada fila, es decir, de (0,f), por lo que
estos bits 0-4 son 0.

La formación de la dirección destino queda pues así:

Cálculo de la dirección de atributo (c,f)

La rutina de cálculo de la dirección del atributo a partir de coordenadas de baja resolución se podría
implementar, pues, de la siguiente forma:
;-------------------------------------------------------------
; Get_Attribute_Offset_LR:
; Obtener la direccion de memoria del atributo del caracter
; (c,f) especificado. Por David Webb.
;
; Entrada: B = FILA, C = COLUMNA
; Salida: HL = Direccion del atributo
;-------------------------------------------------------------
Get_Attribute_Offset_LR:
LD A, B ; Ponemos en A la fila (000FFFFFb)
RRCA
RRCA
RRCA ; Desplazamos A 3 veces (A=A>>3)
AND 3 ; A = A AND 00000011 = los 2 bits mas
; altos de FILA (000FFFFFb -> 000000FFb)
ADD A, $58 ; Ponemos los bits 15-10 como 010110b
LD H, A ; Lo cargamos en el byte alto de HL
LD A, B ; Recuperamos de nuevo en A la FILA
AND 7 ; Nos quedamos con los 3 bits que faltan
RRCA
RRCA ; Los rotamos para colocarlos en su
RRCA ; ubicacion final (<<5 = >>3)
ADD A, C ; Sumamos el numero de columna
LD L, A ; Lo colocamos en L
RET ; HL = 010110FFFFFCCCCCb

La rutina realiza operaciones de bits para ubicar los datos de FILA, COLUMNA y 010011b en las posiciones
que requiere la dirección destino final. Aconsejamos al lector revisar el capítulo dedicado a Desplazamientos de
memoria, bits y operaciones lógicas para recordar el efecto de los desplazamientos realizados con operaciones
como RRCA, SRA, SLA, RLC, etc.

El coste de ejecución de esta rutina es de (RET aparte) 70 t-estados y no hace uso de DE, lo que es un ahorro
sustancial tanto en tiempo de ejecución como en preservación de un registro muy utilizado.

La salida de esta rutina se puede utilizar directamente para almacenar en (HL) el atributo del caracter (c,f) cuya
direccion hemos solicitado:

LD B, 10
LD C, 12
CALL Get_Attribute_Offset_LR

LD A, 85 ; Brillo + Magenta sobre Cyan


LD (HL), A ; Establecemos el atributo de (12,10)

La rutina no hace ningún tipo de comprobación del rango de COLUMNA y FILA, por lo que si
proporcionamos valores menores de cero o mayores de 31 o 23 respectivamente se devolverá una dirección de
memoria fuera del área de atributos.

La versión para coordenadas en alta resolución de la anterior rutina (Get_Attribute_Offset_HR(x,y)) se


implementa fácilmente mediante la conversión de las coordenadas (x,y) en coordenadas (c,f) dividiendo x e y
entre 8 para obtener las coordenadas de baja resolución que corresponden al pixel que estamos considerando.

Para eso, las primeras líneas de la rutina deberían ser:

SRL B
SRL B
SRL B ; B = B/8 -> Ahora B es FILA

SRL C
SRL C
SRL C ; C = C/8 -> Ahora C es COLUMNA

Una vez obtenido (c,f), el desarrollo de la rutina es el mismo que en el caso de Get_Attribute_Offset_LR(c,f):
;-------------------------------------------------------------
; Get_Attribute_Offset_HR:
; Obtener la direccion de memoria del atributo del caracter al
; que corresponde el pixel (x,y) especificado.
;
; Entrada: B = Y, C = X
; Salida: HL = Direccion del atributo
;-------------------------------------------------------------
Get_Attribute_Offset_HR:
SRL B
SRL B
SRL B ; B = B/8 -> Ahora B es FILA

SRL C
SRL C
SRL C ; C = C/8 -> Ahora C es COLUMNA

LD A, B
RRCA
RRCA
RRCA ; Desplazamos A 3 veces (A=A>>3)
AND 3 ; A = A AND 00000011 = los 2 bits mas
; altos de FILA (000FFFFFb -> 000000FFb)

ADD A, $58 ; Ponemos los bits 15-10 como 010110b


LD H, A ; Lo cargamos en el byte alto de HL
LD A, B ; Recuperamos de nuevo en A la FILA
AND 7 ; Nos quedamos con los 3 bits que faltan
RRCA
RRCA ; Los rotamos para colocarlos en su
RRCA ; ubicacion final (<<5 = >>3)
ADD A, C ; Sumamos el numero de columna
LD L, A ; Lo colocamos en L
RET ; HL = 010110FFFFFCCCCCb

Hemos utilizado las instrucciones de desplazamiento SRL sobre los registros B y C para dividir sus valores por
8 y convertir la dirección (x,y) en una dirección (c,f), pudiendo aplicar así el algoritmo de cálculo de dirección
que ya conocemos.

Get_Attribute_Coordinates
La siguiente rutina nos proporciona, dada una dirección de memoria apuntada por HL y dentro de la zona de
atributos, la posición (c,f) que corresponde a dicho carácter. Se basa en la descomposición de HL en los campos
que componen la dirección del atributo:

;-------------------------------------------------------------
; Get_Attribute_Coordinates_LR
; Obtener las coordenadas de caracter que se corresponden a
; una direccion de memoria de atributo.
;
; Entrada: HL = Direccion del atributo
; Salida: B = FILA, C = COLUMNA
;-------------------------------------------------------------
Get_Attribute_Coordinates_LR:
; Descomponemos HL = 010110FF FFFCCCCCb
LD A, H ; A = 010110FFb
AND 3 ; A = bits 0, 1 de HL = 2 bits altos de F, CF=0
RLCA
RLCA
RLCA ; Rotacion a izquierda 000000FFb -> 000FF000b
LD B, A ; B = 000FF000b

LD A, L
AND 224 ; Nos quedamos con los 3 bits mas altos
RLCA
RLCA
RLCA ; Rotacion a izquierda FFF00000b -> 00000FFFb
OR B ; A = A + B = 000FFFFFb
LD B, A ; B = FILA

LD A, L
AND 31 ; Nos quedamos con los 5 bits mas bajos
LD C, A ; C = COLUMNA

RET

De nuevo, el código no incluye ningún tipo de control sobre la dirección que se le proporciona, que podría estar
fuera de la zona de atributos y le haría devolver valores en el rango 0-255 para B y para C que, obviamente, no
corresponden con la dirección entrada en HL.

La rutina para trabajar con coordenadas en alta resolución (Get_Attribute_Coordinates_HR(x,y)) es


esencialmente idéntica a su versión en baja resolución, salvo que finaliza multiplicando B y C por 8 (mediante
instrucciones de desplazamiento a izquierda) para convertir las coordenadas (c,f) en (x,y). Los valores (x,y)
resultantes se corresponderán con el pixel superior izquierdo del bloque apuntado por (c,f).

;-------------------------------------------------------------
; Get_Attribute_Coordinates_HR
; Obtener las coordenadas de pixel que se corresponden a
; una direccion de memoria de atributo.
;
; Entrada: HL = Direccion del atributo
; Salida: B = y, C = x
;-------------------------------------------------------------
Get_Attribute_Coordinates_HR:
; Descomponemos HL = 010110FF FFFCCCCCb
LD A, H ; A = 010110FFb
AND 3 ; A = bits 0, 1 de HL = 2 bits altos de F, CF=0
RLCA
RLCA
RLCA ; Rotacion a izquierda 000000FFb -> 000FF000b
LD B, A ; B = 000FF000b

LD A, L
AND 224 ; Nos quedamos con los 3 bits mas altos
RLCA
RLCA
RLCA ; Rotacion a izquierda FFF00000b -> 00000FFFb
OR B ; A = A + B = 000FFFFFb
LD B, A ; B = FILA

LD A, L
AND 31 ; Nos quedamos con los 5 bits mas bajos
LD C, A ; C = COLUMNA

SLA C
SLA C
SLA C ; C = C*8

SLA B
SLA B
SLA B ; B = B*8

RET

Cálculo de posiciones diferenciales de atributo


Una vez calculada la posición de memoria de un atributo, puede interesarnos (por ejemplo, en una rutina de
impresión de Sprites) el conocer la dirección de memoria del bloque inferior, superior, izquierdo o derecho sin
necesidad de recalcular HL a partir de las coordenadas. Por ejemplo, esto sería útil para imprimir los atributos
de un sprite de Ancho X Alto caracteres sin recalcular la dirección de memoria para cada atribuo.

Asumiendo que HL contiene una dirección de atributo válida y que tenemos verificado que nuestro sprite no
tiene ninguno de sus caracteres fuera del área de pantalla, podemos modificar HL para movernos a cualquiera
de los atributos de alrededor. Para eso aprovecharemos la linealidad del área de atributos incrementando o
decrementando HL para movernos a izquierda o derecha y sumando o restando 32 a HL para bajar o subir una
línea:

Atributo_derecha:
INC HL ; HL = HL + 1

Atributo_izquierda:
DEC HL ; HL = HL - 1

Atributo_abajo:
LD DE, 32
ADD HL, DE ; HL = HL + 32

Atributo_arriba:
LD DE, -32
ADD HL, DE ; HL = HL - 32

Si tenemos la necesidad de preservar el valor del registro DE y el utilizarlo para sumar o restar 32 nos supone
hacer un PUSH y POP del mismo a la pila y queremos evitar esto, podemos sumar la parte baja y después
incrementar la parte alta si ha habido acarreo:

Atributo_abajo_sin_usar_DE_2:
LD A, L ; A = L
ADD 32 ; Sumamos A = A + 32 . El Carry Flag se ve afectado.
LD L, A ; Guardamos en L (L = L+32)
JR NC, attrab_noinc
INC H
attrab_noinc: ; Ahora HL = (H+CF)*256 + (L+32) = HL + 32

Atributo_arriba_sin_usar_DE:
LD A, L ; A = L
SUB 32 ; Restamos A = A - 32 . El Carry Flag se ve afectado.
LD L, A ; Guardamos en L (L = L-32)
JR NC, attrab_nodec
DEC H
attrab_nodec: ; Ahora HL = (H+CF)*256 + (L+32) = HL + 32

Nótese que, como nos apunta Jaime Tejedor en los foros de Speccy.org, el código con salto…

JR NC, attrab_noinc
INC H
attrab_noinc:

… es más rápido que la combinación de ADD y ADC para sumar 32 al byte bajo de HL y 0 + Acarreo al byte
alto de HL:

LD A, 0 ; Ponemos A a cero, no podemos usar un "XOR A"


; o un "OR A" porque afectariamos al Carry Flag.
ADC H ; A = H + CarryFlag
LD H, A ; H = H + CarryFlag
; Ahora HL = (H+CF)*256 + (L+32) = HL + 32

Este código no utiliza DE pero se apoya en el registro A para los cálculos. Si necesitamos preservar su valor,
siempre podemos realizar un EX AF, AF antes y después de la ejecución de la rutina.
Cálculo de posiciones de caracteres
Nuestro siguiente objetivo es el de conocer el mecanismo para trabajar con gráficos de baja resolución o
gráficos de bloque / carácter. Esto nos permitirá dibujar gráficos de 8×8 píxeles (o de múltiplos de ese tamaño)
comenzando en posiciones de memoria de carácter, en nuestra pantalla de 32×24 bloques de baja resolución.

Para ello necesitamos calcular la dirección de inicio en videomemoria de la dirección de inicio del bloque.

Las rutinas que tenemos que implementar son:

• Get_Char_Offset_LR(c,f) : Dadas las coordenadas en baja resolución (columna,fila) de un bloque /


carácter, debe devolver la dirección de memoria de los 8 pixeles del scanline 0 de dicho bloque.

• Get_Char_Offset_HR(x,y) : Dadas las coordenadas en alta resolución (x,y) de un bloque / carácter,


debe devolver la dirección de memoria de los 8 pixeles del scanline 0 de dicho bloque.

• Get_Char_Coordinates_LR(offset): Dada una dirección de memoria dentro del área de imagen, debe
devolver las coordenadas (c,f) en baja resolución del bloque al que está asociada.

• Get_Char_Coordinates_HR(offset): Dada una dirección de memoria dentro del área de imagen, debe
devolver las coordenadas (x,y) en alta resolución del pixel superior izquierdo del bloque al que está
asociada.

Nótese que podemos realizar las 2 primeras rutinas de forma que devuelvan el offset calculado bien en el
registro DE o bien en el registro HL. Según utilicemos los registros en el código que llama a la rutina, puede
sernos más conveniente recibir el valor en uno u otro registro. Si resulta necesario, podemos adaptar el código
de las rutinas para que funcionen con uno u otro registro, o utilizar al final de la misma (o tras el CALL) un EX
HL, DE que devuelva el resultado en el registro que más nos interese.

Cálculo de posiciones de caracteres por composición

Utilizando técnicas de composición desde los bits de las coordenadas vamos a calcular la dirección de inicio de
cada “primera línea” de fila de caracteres de la pantalla, es decir, el scanline 0 de cada fila de bloques en baja
resolución. Conociendo la posición inicial de dicha línea podemos sumar el número de columna y
posicionarnos en el inicio del carácter (c,f) deseado, para trazar en él texto o un sprite de 8×8 (o múltiplos).

Al igual que en el caso de las direcciones de atributo, es posible componer la dirección de memoria de este
“pixel 0” del “scanline 0” de la fila f mediante descomposición de los bits de las coordenadas y su
recomposición en una dirección en memoria.

Para encontrar la relación coordenadas/dirección comencemos viendo una tabla con las direcciones de pantalla
buscadas ya precalculadas:

Linea f Direccion (0,f) (HEX) (Decimal) (Binario) Tercio (0-2) Fila dentro del tercio
0 $4000 16384 0100000000000000b 0 (00b) 0
Linea f Direccion (0,f) (HEX) (Decimal) (Binario) Tercio (0-2) Fila dentro del tercio
1 $4020 16416 0100000000100000b 0 (00b) 1
2 $4040 16448 0100000001000000b 0 (00b) 2
3 $4060 16480 0100000001100000b 0 (00b) 3
4 $4080 16512 0100000010000000b 0 (00b) 4
5 $40A0 16544 0100000010100000b 0 (00b) 5
6 $40C0 16576 0100000011000000b 0 (00b) 6
7 $40E0 16608 0100000011100000b 0 (00b) 7
8 $4800 18432 0100100000000000b 1 (01b) 0
9 $4820 18464 0100100000100000b 1 (01b) 1
10 $4840 18496 0100100001000000b 1 (01b) 2
11 $4860 18528 0100100001100000b 1 (01b) 3
12 $4880 18560 0100100010000000b 1 (01b) 4
13 $48A0 18592 0100100010100000b 1 (01b) 5
14 $48C0 18624 0100100011000000b 1 (01b) 6
15 $48E0 18656 0100100011100000b 1 (01b) 7
16 $5000 20480 0101000000000000b 2 (10b) 0
17 $5020 20512 0101000000100000b 2 (10b) 1
18 $5040 20544 0101000001000000b 2 (10b) 2
19 $5060 20576 0101000001100000b 2 (10b) 3
20 $5080 20608 0101000010000000b 2 (10b) 4
21 $50A0 20640 0101000010100000b 2 (10b) 5
22 $50C0 20672 0101000011000000b 2 (10b) 6
23 $50E0 20704 0101000011100000b 2 (10b) 7

Examinemos (y marquemos) los bits de la representación binaria de la dirección para una selección de
elementos de la tabla:

Linea f Direccion (0,f) (HEX) (Decimal) (Binario) Tercio (0-2) Fila dentro del tercio
0 $4000 16384 0100000000000000b 0 (00b) 0
1 $4020 16416 0100000000100000b 0 (00b) 1
2 $4040 16448 0100000001000000b 0 (00b) 2
3 $4060 16480 0100000001100000b 0 (00b) 3
(…) (…) (…) (…) (…) (…)
8 $4800 18432 0100100000000000b 1 (01b) 0
9 $4820 18464 0100100000100000b 1 (01b) 1
10 $4840 18496 0100100001000000b 1 (01b) 2
(…) (…) (…) (…) (…) (…)
23 $50E0 20704 0101000011100000b 2 (10b) 7

Lo primero que puede llamarnos la atención es lo siguiente:

• Hay una relación directa entre el byte alto de la dirección y el tercio en que está posicionada la línea.
Esta relación está marcada por los bits 3 y 4 del byte superior:
o Tercio superior (0, 00b) → Byte alto = $40 → Bits 3 y 4 = 00.
o Tercio central (1, 01b) → Byte alto = $48 → Bits 3 y 4 = 01.
o Tercio inferior (2, 10b) → Byte alto = $50 → Bits 3 y 4 = 10.
o Conclusión: el número de tercio se corresponde con los 2 bits superiores de la coordenada Y, de
tal forma que las fila (0,7) están en el tercio 00b, las filas 8-15 en el tercio 01b, y las 16-23 en el
10b.
• Hay una relación directa entre el número de fila dentro de cada tercio (0-7) y los 3 bits superiores (5-7)
del byte bajo de la dirección.
• Los 3 bytes más significativos de la dirección son siempre 010b. Esta es la parte de la composición de la
dirección que ubica el offset en memoria en el rango de direcciones del área de imagen de la videoram
($4000 a $57FF).
• Los 5 bytes menos significativos de la dirección son siempre cero en la tabla. En realidad, representan a
la columna (posición c de carácter dentro de los 32 bytes de datos horizontales) pero al estar calculando
direcciones de inicio de línea (c = 0 = 00000b), en nuestro caso son siempre cero.

Así pues, podemos componer la dirección en memoria del pixel (0,0) de un carácter (0,f) de la pantalla como:

Cálculo de la dirección del scanline 0 de (0,f)

A partir del anterior diagrama, se desarrollan las siguientes subrutinas:

Get_Line_Offset
Esta rutina devuelve en HL la dirección de memoria de una fila en baja resolución. Esa dirección apunta a los 8
píxeles (0-7) del scanline 0 de la fila solicitada.

;-------------------------------------------------------------
; Get_Line_Offset_LR(f)
; Obtener la direccion de memoria de inicio de la fila f.
;
; Entrada: B = FILA
; Salida: HL = Direccion de memoria del caracter (0,f)
;-------------------------------------------------------------
Get_Line_Offset_LR:
LD A, B ; A = B, para extraer los bits de tercio
AND $18 ; A = A AND 00011000b
; A = estado de bits de TERCIO desde FILA
ADD A, $40 ; Sumamos $40 (2 bits superiores = 010)
LD H, A ; Ya tenemos la parte alta calculada
; H = 010TT000
LD A, B ; Ahora calculamos la parte baja
AND 7 ; Nos quedamos con los bits más bajos de FILA
; que coinciden con FT (Fila dentro del tercio)
RRCA ; Ahora A = 00000NNNb (donde N=FT)
RRCA ; Desplazamos A 3 veces
RRCA ; A = NNN00000b
LD L, A ; Lo cargamos en la parte baja de la direccion
RET ; HL = 010TT000NNN00000b

Get_Char_Offset
Como ya sabemos, la posición horizontal de un pixel dentro de una fila sí que es lineal, a razón de 8 píxeles por
columna, por lo que:

OFFSET(c,f) = Direccion_Inicio(f) + c

y también:

OFFSET(x,y) = Direccion_Inicio(y/8) + (x/8)

Así, una vez calculado el inicio de línea, basta sumar la columna para obtener la dirección de memoria del
scanline 0 del carácter en baja resolución (c,f):

Cálculo de la dirección del scanline 0 de (c,f)

El código con la columna añadida quedaría así:

;-------------------------------------------------------------
; Get_Char_Offset_LR(c,f)
; Obtener la direccion de memoria del caracter (c,f) indicado.
;
; Entrada: B = FILA, C = COLUMNA
; Salida: HL = Direccion de memoria del caracter (c,f)
;-------------------------------------------------------------
Get_Char_Offset_LR:
LD A, B ; A = B, para extraer los bits de tercio
AND $18 ; A = A AND 00011000b
; A = estado de bits de TERCIO desde FILA
ADD A, $40 ; Sumamos $40 (2 bits superiores = 010)
LD H, A ; Ya tenemos la parte alta calculada
; H = 010TT000
LD A, B ; Ahora calculamos la parte baja
AND 7 ; Nos quedamos con los bits más bajos de FILA
; que coinciden con FT (Fila dentro del tercio)
RRCA ; Ahora A = 00000NNNb (N=FT)
RRCA ; Desplazamos A 3 veces a la derecha
RRCA ; A = NNN00000b
ADD A, C ; Sumamos COLUMNA -> A = NNNCCCCCb
LD L, A ; Lo cargamos en la parte baja de la direccion
RET ; HL = 010TT000NNNCCCCCb

Una rutina que deba trabajar con direcciones en alta resolución pero que devuelva el offset del inicio del bloque
que contiene el punto (x,y) deberá dividir B y C entre 8 en el punto de entrada de la rutina:

;-------------------------------------------------------------
; Get_Char_Offset_HR(x,y)
; Obtener la direccion de memoria del caracter que contiene
; el pixel (x,y) indicado.
;
; Entrada: B = Y, C = X
; Salida: HL = Direccion de memoria del caracter con (x,y)
;-------------------------------------------------------------
Get_Char_Offset_HR:
SRL B
SRL B
SRL B ; B = B/8 -> Ahora B es FILA

SRL C
SRL C
SRL C ; C = C/8 -> Ahora C es COLUMNA

(...) ; Resto de la rutina Get_Char_Offset_LR


RET

Get_Char_Coordinates
Nuestra siguiente subrutina tiene como objetivo el calcular la posición (c,f) en baja resolución de un carácter
dado un offset en memoria que almacene alguno de los 64 pixeles del mismo. Llamar a esta función con la
dirección de cualquiera de las 8 líneas de un carácter devolvería el mismo par de coordenadas (c,f):

;-------------------------------------------------------------
; Get_Char_Coordinates_LR(offset)
; Obtener las coordenadas (c,f) que corresponden a una
; direccion de memoria de imagen en baja resolucion.
;
; Entrada: HL = Direccion de memoria del caracter (c,f)
; Salida: B = FILA, C = COLUMNA
;-------------------------------------------------------------
Get_Char_Coordinates_LR:

; HL = 010TT000 NNNCCCCCb ->


; Fila = 000TTNNNb y Columna = 000CCCCCb
; Calculo de la fila:
LD A, H ; A = H, para extraer los bits de tercio
AND $18 ; A = 000TT000b
LD B, A ; B = A = 000TT000b

LD A, L ; A = L, para extraer los bits de N (FT)


AND $E0 ; A = A AND 11100000b = NNN00000b
RLC A ; Rotamos A 3 veces a la izquierda
RLC A
RLC A ; A = 00000NNNb
OR B ; A = A OR B = 000TTNNNb
LD B, A ; B = A = 000TTNNNb

; Calculo de la columna:
LD A, L ; A = L, para extraer los bits de columna
AND $1F ; Nos quedamos con los ultimos 5 bits de L
LD C, A ; C = Columna
RET ; HL = 010TT000NNNCCCCCb

Adaptar esta rutina a alta resolución (Get_Char_Coordinates_HR(x,y)) implicaría el multiplicar las


coordenadas X e Y por 8, añadiendo el siguiente código inmediatamente antes del RET:

SLA C
SLA C
SLA C ; C = C*8

SLA B
SLA B
SLA B ; B = B*8

Si no queremos tener una rutina específica para esta operación, podemos llamar a la rutina en baja resolución y
realizar los desplazamientos (*8) a la salida de la misma.

Cálculo de posiciones diferenciales de carácter

Recorrer los 8 scanlines de un bloque

Dada en HL la dirección del primer scanline de un bloque, podemos avanzar a lo largo de los 7 scanlines del
mismo bloque sumando “256” a dicha dirección. Como sumar 256 equivale a incrementar la parte alta de la
dirección, podemos subir y bajar al scanline anterior y siguiente de los 8 que componen el carácter mediante
simples DEC H e INC H:

Scanline_Arriba_HL:
DEC H ; H = H - 1 (HL = HL-255)

Scanline_Abajo_HL:
INC H ; H = H + 1 (HL = HL-255)

Este salto de 256 bytes será válido sólo dentro de los 8 scanlines de un mismo carácter.

Offset del carácter de la izquierda/derecha/arriba/abajo

Dentro de las rutinas de impresión de sprites de más de un carácter es probable que necesitemos movernos a los
carácteres de alrededor de uno dado (normalmente hacia la derecha y hacia abajo).

Las siguientes rutinas no realizan control de la posición, por lo que moverse en una dirección cuando estamos
en el límite del eje vertical u horizontal tendrá resultados diferentes de los esperados.

Moverse un carácter a derecha o izquierda es sencillo dada la disposición lineal de las filas de caracteres.
Estando en el scanline 0 de un carácter, bastará con incrementar o decrementar la posición de memoria actual:

Caracter_Derecha_HL:
INC HL ; HL = HL + 1

Caracter_Izquierda_HL:
DEC HL ; HL = HL - 1

Moverse un carácter arriba o abajo es más laborioso ya que tenemos que tener en cuenta los cambios de
tercios. Para ello, basta con que recordemos la disposición de los bits de la dirección:

Bits = Dirección VRAM Bits de Tercio Bits de scanline Bits de Carácter-Y Bits de Columna
Bits = Dirección VRAM Bits de Tercio Bits de scanline Bits de Carácter-Y Bits de Columna
HL = 010 TT SSS NNN CCCCC

Así, para saltar al siguiente carácter tenemos que incrementar los 3 bits más altos de L (sumando 32). Esto
provocará el avance de bloque en bloque, pero debemos tener en cuenta el momento en que realizamos un salto
del bloque 7 al 8, y del 15 al 16, ya que entonces tenemos que cambiar de tercio y poner NNN a 0.

Podemos detectar fácilmente el paso de la fila 7 a la 8 y de la 15 a la 16 ya que en ambos casos la “Fila dentro
del Tercio” (NNN, bits 7, 6 y 5 de la dirección) pasaría de 111b a 1000b, lo que provocaría que estos 3 bits se
quedaran a 0 y se activara el bit de CARRY.

Es decir, cuando tenemos TT = 00b y NNN = 111b y queremos avanzar al siguiente scanline, sumamos 32
(00100000b) con lo que provocamos NNN = 000b y Carry=1. Teniendo la variable “Fila dentro del Tercio” a 1,
basta con que incrementemos TT sumando 00001000b (8) a la parte alta, lo que sumaría 01b a los 2 bits de
tercio TT:

El código sería el siguiente:

LD A, L ; Cargamos A en L y le sumamos 32 para


ADD A, 32 ; incrementar "Bloque dentro del tercio"
LD L, A ; L = A
JR NC, no_ajustar_H_abajob ; Si esta suma produce acarreo, ajustar
LD A, H ; la parte alta sumando 8 a H (TT = TT + 1).
ADD A, 8 ; Ahora NNN=000b y TT se ha incrementado.
LD H, A ; H = A
no_ajustar_H_abajob
; Ahora HL apunta al bloque de debajo.

El procedimiento para subir un carácter es similar:

LD A, L ; Cargamos L en A
AND 224 ; A = A AND 11100000b
JR NZ, no_ajustar_h_arribab ; Si no es cero, no retrocedemos tercio
LD A, H ; Si es cero, ajustamos tercio (-1)
SUB 8 ; Decrementamos TT
LD H, A
no_ajustar_h_arribab:
LD A, L ; Decrementar NNN
SUB 32
LD L, A ; NNN = NNN-1

En forma de rutina:

Caracter_Abajo_HL:
LD A, L ; Cargamos A en L y le sumamos 32 para
ADD A, 32 ; incrementar "Bloque dentro del tercio"
LD L, A ; L = A
RET NC ; Si esta suma no produce acarreo, fin
LD A, H ; la parte alta sumando 8 a H (TT = TT + 1).
ADD A, 8 ; Ahora NNN=000b y TT se ha incrementado.
LD H, A ; H = A
RET

Caracter_Arriba_HL:
LD A, L ; Cargamos L en A
AND 224 ; A = A AND 11100000b
JR NZ, nofix_h_arribab ; Si no es cero, no retrocedemos tercio
LD A, H ; Si es cero, ajustamos tercio (-1)
SUB 8 ; Decrementamos TT
LD H, A
nofix_h_arribab:
LD A, L ; Decrementar NNN
SUB 32
LD L, A ; NNN = NNN-1
RET

Con estas 2 subrutinas podemos subir y bajar carácter a carácter sin tener que recalcular la dirección destino y
haciendo uso sólo de A, H y L. Hay que tener en cuenta, no obstante, que se no comprueban los límites de la
pantalla, por lo que no nos avisarán si pretendemos “subir” más arriba de la línea 0 o “bajar” más abajo de la
23.

Cálculo de posiciones de píxeles


Finalmente, en cuanto a coordenación, vamos a estudiar el cálculo de la dirección de memoria de un pixel (x,y)
en alta resolución. La dirección en memoria obtenida tendrá la información gráfica de 8 píxeles (pues cada byte
almacena el estado de 8 píxeles horizontales consecutivos). Debido a esto nuestra rutina no sólo deberá
devolver el offset en memoria sino un valor de posición relativa 0-7 que nos permita alterar el pixel concreto
solicitado.

Nuestra rutina de cálculo de offset puede ser implementada mediante 2 aproximaciones:

1. Mediante cálculo de la posición de memoria a partir de las coordenadas (x,y), utilizando operaciones de
descomposición y rotación de bits, como ya hemos visto en los apartados anteriores.
2. Mediante una tabla precalculada de posiciones de memoria que almacene la dirección de inicio de cada
línea de pantalla, a la cual sumaremos el número de columna (x/8), obteniendo así el offset de nuestro
pixel.

Vamos a ver las 2 técnicas por separado con rutinas aplicadas a cada uno de los métodos. Cada sistema, como
veremos, tiene sus ventajas e inconvenientes, resultando siempre ambos un balance entre el tiempo de ejecución
de una rutina y la ocupación en bytes en memoria entre código y datos de la misma.

Las rutinas que tenemos que implementar son:

• Get_Pixel_Offset(x,y) : Dadas las coordenadas en alta resolución (x,y) de un pixel, debe devolver la
dirección de memoria que aloja el pixel y un indicador de la posición del pixel dentro de dicho byte
(recordemos que cada dirección de memoria contiene los datos de 8 píxeles lineales consecutivos),
utilizando descomposición y composición de bits.

• Get_Pixel_Coordinates(offset): Dada una dirección de memoria dentro del área de imagen, debe
devolver las coordenadas (x,y) del pixel al que está asociada.

• Get_Pixel_Offset_LUT(x,y) : Dadas las coordenadas en alta resolución (x,y) de un pixel, debe


devolver la dirección de memoria que aloja dicho pixel mediante la utilización de tablas de precálculo
(Look Up Table, o LUT).

Cálculo de posiciones de pixeles mediante composición


Hasta ahora hemos visto rutinas que nos proporcionan la posición en memoria de un bloque en baja resolución,
pero en el caso que veremos ahora tenemos una coordenada Y que se mueve de 0 a 191, por lo que la posición
en memoria puede corresponder a cualquiera de los 8 scanlines de un bloque dado. Además, la coordenada X
tampoco es un carácter por lo que el pixel resultante es el estado de un bit concreto de la dirección obtenida.

Así pues, ¿cómo podemos calcular la dirección destino del pixel cuando tratamos con coordenadas en alta
resolución? Recuperemos para ello parte de nuestra tabla de direcciones de memoria en baja resolución:

Línea Línea Direccion (0,f) Tercio (0- Fila en el


(Decimal) (Binario)
LowRes HiRes (HEX) 2) tercio
0 0 $4000 16384 0100000000000000b 0 (00b) 0
1 8 $4020 16416 0100000000100000b 0 (00b) 1
2 16 $4040 16448 0100000001000000b 0 (00b) 2
3 24 $4060 16480 0100000001100000b 0 (00b) 3
(…) (…) (…) (…) (…) (…) (…)

Añadamos ahora las direcciones en alta resolución y veamos el estado de los diferentes bits de la coordenada Y
y de la dirección de videomemoria que le corresponde:

Coord. Coord. Coord. Y Direccion (0,y) Tercio (0- Fila en el


(Binario)
F Y (Binario) (HEX) 2) tercio
0 0 00000000b $4100 0100000000000000b 0 (00b) 0
0 1 00000001b $4200 0100000100000000b 0 (00b) 0
0 2 00000010b $4300 0100001000000000b 0 (00b) 0
0 3 00000011b $4400 0100001100000000b 0 (00b) 0
0 4 00000100b $4500 0100010000000000b 0 (00b) 0
0 5 00000101b $4600 0100010100000000b 0 (00b) 0
0 6 00000110b $4700 0100011000000000b 0 (00b) 0
0 7 00000111b $4800 0100011100000000b 0 (00b) 0
1 8 00001000b $4020 0100000000100000b 0 (00b) 1
1 9 00001001b $4120 0100000100100000b 0 (00b) 1
1 10 00001010b $4220 0100001000100000b 0 (00b) 1
1 11 00001011b $4320 0100001100100000b 0 (00b) 1
1 12 00001100b $4420 0100010000100000b 0 (00b) 1
(…) (…) (…) (…) (…) (…) (…)

Como puede verse, la diferencia entre la composición de baja resolución y la de alta resolución es la
modificación de los 3 bits menos significativos de la parte alta de la dirección, que son un reflejo de los 3 bits
bajos de la coordenada Y.

Si examinamos en binario la coordenada Y, vemos que ésta se puede descomponer en 2 bits de Tercio de
Pantalla, 3 bits de Fila Dentro del Tercio (FT o N en los ejemplos) y 3 bits de Scanline Dentro Del Carácter (S):
Por otra parte, ya sabemos que C es X / 8, por lo que ya tenemos todos los componentes para realizar nuestra
rutina de cálculo de dirección de memoria.

Así pues, la composición final de la dirección de memoria del pixel (x,y) se define de la siguiente forma:

Cálculo de la dirección del pixel (x,y)

No obstante, recordemos que esta dirección de memoria obtenida hace referencia a 8 píxeles, por lo que
necesitamos obtener además la información del número de bit con el que se corresponde nuestro pixel, que
podemos extraer del resto de la división entre 8 de la coordenada X (P = X AND 7).

La rutina resultante es similar a la vista en baja resolución con la descomposición de la coordenada Y en el


“número de scanline” (0-7) y la “fila dentro del tercio (0-7)”:
;-------------------------------------------------------------
; Get_Pixel_Offset_HR(x,y)
; Obtener la direccion de memoria del pixel (x,y).
;
; Entrada: B = Y, C = X
; Salida: HL = Direccion de memoria del caracter con (x,y)
; A = Posicion del pixel (0-7) en el byte.
;-------------------------------------------------------------
Get_Pixel_Offset_HR:

; Calculo de la parte alta de la direccion:


LD A, B
AND 7 ; A = 00000SSSb
LD H, A ; Lo guardamos en H
LD A, B ; Recuperamos de nuevo Y
RRA
RRA
RRA ; Rotamos para asi obtener el tercio
AND 24 ; con un AND 00011000b -> 000TT000b
OR H ; H = H OR A = 00000SSSb OR 000TT000b
OR 64 ; Mezclamos H con 01000000b (vram)
LD H, A ; Establecemos el "H" definitivo

; Calculo de la parte baja de la direccion:


LD A, C ; A = coordenada X
RRA
RRA
RRA ; Rotamos para obtener CCCCCb
AND 31 ; A = A AND 31 = 000CCCCCb
LD L, A ; L = 000CCCCCb
LD A, B ; Recuperamos de nuevo Y
RLA ; Rotamos para obtener NNN
RLA
AND 224 ; A = A AND 11100000b
OR L ; L = NNNCCCCC
LD L, A ; Establecemos el "L" definitivo

; Finalmente, calcular posicion relativa del pixel:


LD A, C ; Recuperamos la coordenada X
AND 7 ; AND 00000111 para obtener pixel
; A = 00000PPP
RET

Esta rutina de 118 t-estados nos devuelve el valor de la dirección calculado en HL y la posición relativa del
pixel dentro del byte:

Valor de A 7 6 5 4 3 2 1 0
Posición del pixel
+7 +6 +5 +4 +3 +2 +1 +0
desde la izquierda
Posición del pixel
0 1 2 3 4 5 6 7
dentro del byte (Bit)

Esta posición relativa del pixel nos sirve para 2 cosas:

Por una parte, cuando realicemos rutinas de impresión de sprites con movimiento pixel a pixel, este valor nos
puede servir para tratar los sprites (rotarlos) de cara a su impresión en posiciones de byte.

Por otra parte, si necesitamos activar (PLOT, bit=1), desactivar (UNPLOT, b=0) o testear el estado del pixel
(x,y), podremos utilizar este valor “posición del pixel” para generar una máscara de pixel.

Obtención y uso de la Máscara de Pixel


Nuestra rutina de coordenación nos devuelve en HL la dirección de memoria que contiene el pixel (x,y) y en A
la posición relativa del pixel dentro de dicha dirección.

Para poder modificar el pixel exacto al que hacen referencia la pareja de datos HL y A resulta necesario
convertir A en una “máscara de pixel” que nos permita manipular la memoria con sencillas operaciones
lógicas sin afectar al estado de los demás píxeles.

Esta “máscara de pixel” tiene activo el bit 8-pixel ya que el pixel 0 es el pixel de más a la izquierda de los 8, es
decir, el bit 7 de la dirección:

Bit activo 7 6 5 4 3 2 1 0
Pixel 01234567

La máscara que debemos generar, en función del valor de A, es:

Valor de A Máscara de pixel


0 10000000b
1 01000000b
2 00100000b
3 00010000b
4 00001000b
5 00000100b
6 00000010b
7 00000001b

La porción de código que hace esta conversión es la siguiente:

LD B, A ; Cargamos A (posicion de pixel) en B


INC B ; Incrementamos B (para pasadas del bucle)
XOR A ; A = 0
SCF ; Set Carry Flag (A=0, CF=1)
pix_rotate_bit:
RRA ; Rotamos A a la derecha B veces
DJNZ pix_rotate_bit

La rutina pone A a cero y establece el Carry Flag a 1, por lo que la primera ejecución de RRA (que siempre se
realizará) ubica el 1 del CF en el bit 7 de A. A continuación el DJNZ que se realiza “B” veces mueve ese bit a 1
a la derecha (también “B” veces) dejando A con el valor adecuado según la tabla que acabamos de ver.
En formato de rutina:

;--------------------------------------------------------
: Relative_to_Mask: Convierte una posicion de pixel
; relativa en una mascara de pixel.
; IN: A = Valor relativo del pixel (0,7)
; OUT: A = Pixel Mask (128-1)
; CHANGES: B, F
;--------------------------------------------------------
Relative_to_Mask:
LD B, A ; Cargamos A (posicion de pixel) en B
INC B ; Incrementamos B (para pasadas del bucle)
XOR A ; A = 0
SCF ; Set Carry Flag (A=0, CF=1)
pix_rotate_bit:
RRA ; Rotamos A a la derecha B veces
DJNZ pix_rotate_bit
RET

Mediante esa máscara podemos activar (PLOT), desactivar (UNPLOT) y testear (TEST) el estado del pixel en
cuestión:

; Activar el pixel apuntado por HL usando la máscara A


Plot_Pixel_HL:
OR (HL)
LD (HL), A
RET

; Desactivar el pixel apuntado por HL usando la máscara A


Unplot_Pixel_HL:
CPL A
AND (HL)
LD (HL), A
RET

; Testear el pixel apuntado por HL usando la máscara A


Test_Pixel_HL:
AND (HL)
RET

La anterior rutina de PLOT funciona realizando un OR entre la máscara de pixel y el estado de actual de la
memoria, y luego escribiendo el resultado de dicho OR en la videoram. De esta forma, sólo alteramos el pixel
sobre el que queremos escribir.

Explicándolo con un ejemplo, supongamos que queremos escribir en el pixel (3,0) de la pantalla y ya hay
píxeles activos en (0,0) y (7,0):

Pixeles activos en (16384) = 10000001


Máscara de pixel = 00010000

Si ejecutaramos un simple “LD (HL), A”, el resultado de la operación eliminaría los 2 píxeles activos que ya
teníamos en memoria:

Pixeles activos en (16384) = 10000001


Máscara de pixel A = 00010000
OPERACION (HL)=A = LD (HL), A
Resultado en (16384) = 00010000

Mediante el OR entre la máscara de pixel y la videomemoria conseguimos alterar el estado de (3,0) sin
modificar los píxeles ya existentes:

Pixeles activos en (16384) = 10000001


Máscara de pixel A = 00010000
OPERACION A = A OR (HL) = OR (HL)
Resultado en A = 10010001
OPERACION (HL)=A = LD (HL), A
Resultado en (16384) = 10010001

Si en lugar de un OR hubieramos complementado A y hubieramos hecho un AND, habríamos puesto a 0 el bit


(y por tanto el pixel):

Pixeles activos en (16384) = 10000001


Máscara de pixel A = 00010000
OPERACION A = CPL(A) = 11101111
OPERACION A = A OR (HL) = AND (HL)
Resultado en A = 10000001
OPERACION (HL)=A = LD (HL), A
Resultado en (16384) = 10000001

El cálculo de memoria y la escritura de un pixel quedaría pues de la siguiente forma:

LD C, 127 ; X = 127
LD B, 95 ; Y = 95
CALL Get_Pixel_Offset_HR ; Calculamos HL y A
OR (HL) ; OR de A y (HL)
LD (HL), A ; Activamos pixel

La primera pregunta que nos planteamos es, si es imprescindible disponer de una máscara de pixel para dibujar
o borrar píxeles, ¿por qué no incluir este código de rotación de A directamente en la rutina de coordenación? La
respuesta es, “depende de para qué vayamos a utilizar la rutina”.

Si la rutina va a ser utilizada principalmente para trazar píxeles, resultará conveniente incorporar al final de
Get_Pixel_Offset_HR() el cálculo de la máscara, y devolver en A dicha máscara en lugar de la posición relativa
del pixel.

Pero lo normal en el desarrollo de programas y juegos es que utilicemos la rutina de coordenación para obtener
la posición inicial en la que comenzar a trazar sprites, bloques (del mapeado), fuentes de texto, marcadores. En
ese caso es absurdo emplear “ciclos de reloj” adicionales para el cálculo de una máscara que sólo se utiliza en el
trazado de puntos. En esas circunstancias resulta mucho más útil disponer de la posición relativa del pixel, para,
como ya hemos comentado, conocer la cantidad de bits que necesitamos rotar estos datos gráficos antes de su
trazado.

Por ese motivo, no hemos agregado esta pequeña porción de código a la rutina de Get_Pixel_Offset, siendo el
programador quien debe decidir en qué formato quiere obtener la salida de la rutina.

La rutina de la ROM PIXEL-ADDRESS

Curiosamente, los usuarios de Spectrum tenemos disponible en la memoria ROM una rutina parecida, llamada
PIXEL-ADDRESS (o PIXEL-ADD), utilizada por las rutinas POINT y PLOT de la ROM (y de BASIC). La
rutina está ubicada en $22AA y su código es el siguiente:

; THE 'PIXEL ADDRESS' SUBROUTINE


; This subroutine is called by the POINT subroutine and by the PLOT
; command routine. Is is entered with the co-ordinates of a pixel in
; the BC register pair and returns with HL holding the address of the
; display file byte which contains that pixel and A pointing to the
; position of the pixel within the byte.
;
; IN: (C,B) = (X,Y)
; OUT: HL = address, A = pixel relative position in (HL)

$22AA PIXEL-ADD
LD A,$AF ; Test that the y co-ordinate (in
SUB B ; B) is not greater than 175.
JP C,24F9,REPORT-B
LD B,A ; B now contains 175 minus y.

$22B1 PIXEL_ADDRESS_B: ; Entramos aqui para saltarnos la limitacion


; hacia las 2 ultimas lineas de pantalla.

AND A ; A holds b7b6b5b4b3b2b1b0,


RRA ; the bite of B. And now
; 0b7b6b5b4b3b2b1.
SCF
RRA ; Now 10b7b6b5b4b3b2.
AND A
RRA ; Now 010b7b6b5b4b3.
XOR B
AND $F8 ; Finally 010b7b6b2b1b0, so that
XOR B ; H becomes 64 + 8*INT (B/64) +
LD H,A ; B (mod 8), the high byte of the
LD A,C ; pixel address. C contains X.
RLCA ; A starts as c7c6c5c4c3c2c1c0.
RLCA
RLCA ; And is now c2c1c0c7c6c5c4c3.
XOR B
AND $C7
XOR B ; Now c2c1b5b4b3c5c4c3.
RLCA
RLCA ; Finally b5b4b3c7c6c5c4c3, so
LD L,A ; that L becomes 32*INT (B(mod
LD A,C ; 64)/8) + INT(x/8), the low byte.
AND $07 ; A holds x(mod 8): so the pixel
RET ; is bit (A - 7) within the byte.

Esta rutina tiene una serie de ventajas: Entrando por $22B1 tenemos 23 instrucciones (107 t-estados) que
realizan el cálculo de la dirección de memoria además de la posición del pixel dentro del byte al que apunta
dicha dirección. La rutina está ubicada en ROM, por lo que ahorramos esta pequeña porción de espacio en
nuestro programa. Además, no usa la pila, no usa registros adicionales a B, C, HL y A, y no altera los valores
de B y C durante el cálculo.

Nótese que aunque la rutina está ubicada en $22AA y se entra con los valores (x,y) en C y B, el principio de la
rutina está diseñado para evitar que PLOT y POINT puedan acceder a las 2 últimas filas (16 últimos píxeles) de
la pantalla. Para saltarnos esta limitación entramos saltando con un CALL a $22B1 con la coordenada X en el
registro C y la coordenada Y en los registros A y B:

LD A, (coord_x)
LD C, A
LD A, (coord_y)
LD B, A
CALL $22B1

De esta forma no sólo nos saltamos la limitación de acceso a las 2 últimas líneas de la pantalla sino que
podemos especificar las coordenadas empezando (0,0) en la esquina superior izquierda, con el sistema
tradicional de coordenadas, en contraposición al PLOT de BASIC (y de la ROM), donde se comienza a contar
la altura como Y = 0 en la parte inferior de la pantalla (empezando en 191-16=175).

Veamos un ejemplo de uso de la rutina de coordenación de la ROM:

; Ejemplo de uso de pixel-address (ROM)


ORG 50000

PIXEL_ADDRESS EQU $22B1

entrada:

; Imprimimos un solo pixel en (0,0)


LD C, 0 ; X = 0
LD B, 0 ; Y = 0
LD A, B ; A = Y = 0
CALL PIXEL_ADDRESS ; HL = direccion (0,0)
LD A, 128 ; A = 10000000b (1 pixel).
LD (HL), A ; Imprimimos el pixel

; Imprimimos 8 pixeles en (255,191)


LD C, 255 ; X = 255
LD B, 191 ; Y = 191
LD A, B ; A = Y = 191
CALL PIXEL_ADDRESS
LD A, 255 ; A = 11111111b (8 pixeles)
LD (HL), A

; Imprimimos 4 pixeles en el centro de la pantalla


LD C, 127 ; X = 127
LD B, 95 ; Y = 95
LD A, B ; A = Y = 95
CALL PIXEL_ADDRESS
LD A, 170 ; A = 10101010b (4 pixeles)
LD (HL), A

loop: ; Bucle para no volver a BASIC y que


jr loop ; no se borren la 2 ultimas lineas

END 50000

La ejecución del anterior programa nos dejará la siguiente información gráfica en pantalla:

Nótese que la rutina de la ROM nos devuelve en A la posición relativa del pixel cuyas coordenadas hemos
proporcionado, por lo que podemos convertir A en una máscara de pixel a la salida de la rutina encapsulando
PIXEL-ADDRESS en una rutina “propia” que haga ambas operaciones, a cambio de 2 instrucciones extras (un
CALL y un RET adicionales):

PIXEL_ADDRESS EQU $22B1

;----------------------------------------------------------
; Rutina que encapsula a PIXEL_ADDRESS calculando pix-mask.
; IN: (C,B) = (X,Y)
; OUT: HL = address, A = pixel mask
;----------------------------------------------------------
PIXEL_ADDRESS_MASK:
CALL PIXEL_ADDRESS ; Llamamos a la rutina de la ROM
LD B, A ; Cargamos A (posicion de pixel) en B
INC B ; Incrementamos B (para pasadas del bucle)
XOR A ; A = 0
SCF ; Set Carry Flag (A=0, CF=1)
pix_rotate_bit:
RRA ; Rotamos A a la derecha B veces
DJNZ pix_rotate_bit
RET

Cálculo de posiciones de pixeles mediante tabla

Hasta ahora hemos visto cómo calcular la dirección de memoria de un pixel (x,y) mediante descomposición de
las coordenadas y composición de la dirección destino utilizando operaciones lógicas y de desplazamiento.

La alternativa a este método es la utilización de una Look Up Table (LUT), una tabla de valores precalculados
mediante la cual obtener la dirección destino dada una variable concreta.

En nuestro caso, crearíamos una Lookup Table (LUT) que se indexaría mediante la coordenada Y, de tal modo
que la dirección destino de un pixel X,Y sería:

DIRECCION_DESTINO = Tabla_Offsets_Linea[Y] + (X/8)


PIXEL_EN_DIRECCION = Resto(X/8) = X AND 7

La tabla de offsets de cada inicio de línea tendría 192 elementos de 2 bytes (tamaño de una dirección), por lo
que ocuparía en memoria 384 bytes. A cambio de esta “elevada” ocupación en memoria, podemos obtener
rutinas más rápidas que las de composición de las coordenadas.

A continuación se muestra la tabla de offsets precalculados:

Scanline_Offsets:
DW 16384, 16640, 16896, 17152, 17408, 17664, 17920, 18176
DW 16416, 16672, 16928, 17184, 17440, 17696, 17952, 18208
DW 16448, 16704, 16960, 17216, 17472, 17728, 17984, 18240
DW 16480, 16736, 16992, 17248, 17504, 17760, 18016, 18272
DW 16512, 16768, 17024, 17280, 17536, 17792, 18048, 18304
DW 16544, 16800, 17056, 17312, 17568, 17824, 18080, 18336
DW 16576, 16832, 17088, 17344, 17600, 17856, 18112, 18368
DW 16608, 16864, 17120, 17376, 17632, 17888, 18144, 18400
DW 18432, 18688, 18944, 19200, 19456, 19712, 19968, 20224
DW 18464, 18720, 18976, 19232, 19488, 19744, 20000, 20256
DW 18496, 18752, 19008, 19264, 19520, 19776, 20032, 20288
DW 18528, 18784, 19040, 19296, 19552, 19808, 20064, 20320
DW 18560, 18816, 19072, 19328, 19584, 19840, 20096, 20352
DW 18592, 18848, 19104, 19360, 19616, 19872, 20128, 20384
DW 18624, 18880, 19136, 19392, 19648, 19904, 20160, 20416
DW 18656, 18912, 19168, 19424, 19680, 19936, 20192, 20448
DW 20480, 20736, 20992, 21248, 21504, 21760, 22016, 22272
DW 20512, 20768, 21024, 21280, 21536, 21792, 22048, 22304
DW 20544, 20800, 21056, 21312, 21568, 21824, 22080, 22336
DW 20576, 20832, 21088, 21344, 21600, 21856, 22112, 22368
DW 20608, 20864, 21120, 21376, 21632, 21888, 22144, 22400
DW 20640, 20896, 21152, 21408, 21664, 21920, 22176, 22432
DW 20672, 20928, 21184, 21440, 21696, 21952, 22208, 22464
DW 20704, 20960, 21216, 21472, 21728, 21984, 22240, 22496

En hexadecimal (para ver la relación entre los aumentos de líneas y el de scanlines y bloques):

Scanline_Offsets:
DW $4000, $4100, $4200, $4300, $4400, $4500, $4600, $4700
DW $4020, $4120, $4220, $4320, $4420, $4520, $4620, $4720
DW $4040, $4140, $4240, $4340, $4440, $4540, $4640, $4740
DW $4060, $4160, $4260, $4360, $4460, $4560, $4660, $4760
DW $4080, $4180, $4280, $4380, $4480, $4580, $4680, $4780
DW $40A0, $41A0, $42A0, $43A0, $44A0, $45A0, $46A0, $47A0
DW $40C0, $41C0, $42C0, $43C0, $44C0, $45C0, $46C0, $47C0
DW $40E0, $41E0, $42E0, $43E0, $44E0, $45E0, $46E0, $47E0
DW $4800, $4900, $4A00, $4B00, $4C00, $4D00, $4E00, $4F00
DW $4820, $4920, $4A20, $4B20, $4C20, $4D20, $4E20, $4F20
DW $4840, $4940, $4A40, $4B40, $4C40, $4D40, $4E40, $4F40
DW $4860, $4960, $4A60, $4B60, $4C60, $4D60, $4E60, $4F60
DW $4880, $4980, $4A80, $4B80, $4C80, $4D80, $4E80, $4F80
DW $48A0, $49A0, $4AA0, $4BA0, $4CA0, $4DA0, $4EA0, $4FA0
DW $48C0, $49C0, $4AC0, $4BC0, $4CC0, $4DC0, $4EC0, $4FC0
DW $48E0, $49E0, $4AE0, $4BE0, $4CE0, $4DE0, $4EE0, $4FE0
DW $5000, $5100, $5200, $5300, $5400, $5500, $5600, $5700
DW $5020, $5120, $5220, $5320, $5420, $5520, $5620, $5720
DW $5040, $5140, $5240, $5340, $5440, $5540, $5640, $5740
DW $5060, $5160, $5260, $5360, $5460, $5560, $5660, $5760
DW $5080, $5180, $5280, $5380, $5480, $5580, $5680, $5780
DW $50A0, $51A0, $52A0, $53A0, $54A0, $55A0, $56A0, $57A0
DW $50C0, $51C0, $52C0, $53C0, $54C0, $55C0, $56C0, $57C0
DW $50E0, $51E0, $52E0, $53E0, $54E0, $55E0, $56E0, $57E0

La tabla ha sido generada mediante el siguiente script en python:

$ cat specrows.py
#!/usr/bin/python

print "Scanline_Offsets:"
for tercio in range(0,3):
for caracter in range(0,8):
print " DW",
for scanline in range(0,8):
# Componer direccion como 010TTSSSNNN00000
base = 16384
direccion = base + (tercio * 2048)
direccion = direccion + (scanline * 256)
direccion = direccion + (caracter * 32)
print str(direccion),
if scanline!=7: print ",",
print

La tabla de valores DW estaría incorporada en nuestro programa y por tanto pasaría a formar parte del “binario
final”, incluyendo en este aspecto la necesidad de carga desde cinta.

Si por algún motivo no queremos incluir la tabla en el listado, podemos generarla en el arranque de nuestro
programa en alguna posición de memoria libre o designada a tal efecto mediante la siguiente rutina:

;--------------------------------------------------------
; Generar LookUp Table de scanlines en memoria.
; Rutina por Derek M. Smith (2005).
;--------------------------------------------------------

Scanline_Offsets EQU $F900

Generate_Scanline_Table:
LD DE, $4000
LD HL, Scanline_Offsets
LD B, 192

genscan_loop:
LD (HL), E
INC L
LD (HL), D ; Guardamos en (HL) (tabla)
INC HL ; el valor de DE (offset)

; Recorremos los scanlines y bloques en un bucle generando las


; sucesivas direccione en DE para almacenarlas en la tabla.
; Cuando se cambia de caracter, scanline o tercio, se ajusta:
INC D
LD A, D
AND 7
JR NZ, genscan_nextline
LD A, E
ADD A, 32
LD E, A
JR C, genscan_nextline
LD A, D
SUB 8
LD D, A

genscan_nextline:
DJNZ genscan_loop
RET

La anterior rutina ubicará en memoria una tabla con el mismo contenido que las ya vistas en formato DW.

Get_Pixel_Offset_LUT
Una vez tenemos generada la tabla (ya sea en memoria o pregenerada en el código de nuestro programa),
podemos indexar dicha tabla mediante la coordenada Y y sumar la coordenada X convertida en columna para
obtener la dirección de memoria del pixel solicitado, y la posición relativa del mismo en el byte:

;-------------------------------------------------------------
; Get_Pixel_Offset_LUT_HR(x,y):
;
; Entrada: B = Y, C = X
; Salida: HL = Direccion de memoria del pixel (x,y)
; A = Posicion relativa del pixel en el byte
;-------------------------------------------------------------
Get_Pixel_Offset_LUT_HR:
LD DE, Scanline_Offsets ; Direccion de nuestra LUT
LD L, B ; L = Y
LD H, 0
ADD HL, HL ; HL = HL * 2 = Y * 2
ADD HL, DE ; HL = (Y*2) + ScanLine_Offset
; Ahora Offset = [HL]
LD A, (HL) ; Cogemos el valor bajo de la direccion en A
INC L
LD H, (HL) ; Cogemos el valor alto de la direccion en H
LD L, A ; HL es ahora Direccion(0,Y)
; Ahora sumamos la X, para lo cual calculamos CCCCC
LD A, C ; Calculamos columna
RRA
RRA
RRA ; A = A>>3 = ???CCCCCb
AND 31 ; A = 000CCCCB
ADD A, L ; HL = HL + C
LD L, A
LD A, C ; Recuperamos la coordenada (X)
AND 7 ; A = Posicion relativa del pixel
RET ; HL = direccion destino

Veamos un ejemplo de utilización de las anteriores rutinas:

; Ejemplo de uso de LUT


ORG 50000

entrada:

CALL Generate_Scanline_Table
LD B, 191

loop_draw:
PUSH BC ; Preservamos B (por el bucle)
LD C, 127 ; X = 127, Y = B

CALL Get_Pixel_Offset_LUT_HR

LD A, 128
LD (HL), A ; Imprimimos el pixel

POP BC
DJNZ loop_draw

loop: ; Bucle para no volver a BASIC y que


JR loop ; no se borren la 2 ultimas lineas

Y su salida en pantalla:

Optimizando la lectura a través de tablas

El coste de ejecución de la rutina Get_Pixel_Offset_LUT_HR es de 117 t-estados, demasiado elevada por


culpa de las costosas (en términos temporales) instrucciones de 16 bits, sobre todo teniendo en cuenta que
hemos empleado 384 bytes de memoria en nuestra tabla.

Una ingeniosa solución a este problema consiste en dividir la tabla de 192 direcciones de 16 bits en 2 tablas de
192 bytes cada una que almacenen la parte alta de la dirección en la primera de las tablas y la parte baja de la
dirección en la segunda, de tal forma que:

H = Tabla_Parte_Alta[Y]
L = Tabla_Parte_Baja[Y]

Si realizamos esta división y colocamos en memoria las tablas de forma que estén alineadas en una dirección
múltiplo de 256, el mecanismo de acceso a la tabla será mucho más rápido.

La ubicación de las tablas en memoria en una dirección X múltiplo de 256 tendría el siguiente aspecto:

Sea una Direccion_XX divisible por 256:

Direccion_XX hasta Direccion_XX+191


-> Partes bajas de las direcciones de pantalla

Direccion_XX+256 hasta Direccion_XX+447


-> Partes altas de las direcciones de pantalla.

El paso de una tabla a otra se realizará incrementando o decrementando la parte alta del registro de 16 bits (INC
H o DEC H), gracias al hecho de que son 2 tablas múltiplos de 256 y consecutivas en memoria.
Veamos primero la rutina para generar la tabla separando las partes altas y bajas y alineando ambas a una
dirección múltiplo de 256:

;----------------------------------------------------------
; Generar LookUp Table de scanlines en memoria en 2 tablas.
;
; En Scanline_Offsets (divisible por 256)
; -> 192 bytes de las partes bajas de la direccion.
;
; En Scanline_Offsets + 256 (divisible por 256)
; -> 192 bytes de las partes altas de la direccion.
;
; Rutina por Derek M. Smith (2005).
;----------------------------------------------------------

Scanline_Offsets EQU 64000 ; Divisible por 256

Generate_Scanline_Table_Aligned:
LD DE, $4000
LD HL, Scanline_Offsets
LD B, 192

genscan_loop:
LD (HL), E ; Escribimos parte baja
INC H ; Saltamos a tabla de partes altas
LD (HL), D ; Escribimos parte alta
DEC H ; Volvemos a tabla de partes bajas
INC L ; Siguiente valor

; Recorremos los scanlines y bloques en un bucle generando las


; sucesivas direccione en DE para almacenarlas en la tabla.
; Cuando se cambia de caracter, scanline o tercio, se ajusta:
INC D
LD A, D
AND 7
JR NZ, genscan_nextline
LD A, E
ADD A, 32
LD E, A
JR C, genscan_nextline
LD A, D
SUB 8
LD D, A

genscan_nextline:
DJNZ genscan_loop
RET

En el ejemplo anterior, tendremos entre 64000 y 64191 los 192 valores bajos de la dirección, y entre 64256 y
64447 los 192 valores altos de la dirección. Entre ambas tablas hay un hueco de 256-192=64 bytes sin usar que
debemos saltar para poder alinear la segunda tabla en un múltiplo de 256.

Estos 64 bytes no se utilizan en la rutina de generación ni (como veremos a continuación) en la de cálculo, por
lo que podemos aprovecharlos para ubicar variables de nuestro programa, tablas temporales, etc, y así no
desperdiciarlos.

Si necesitaramos reservar espacio en nuestro programa para después generar la tabla sobre él, podemos hacerlo
mediante las directivas de preprocesado del ensamblador ORG (Origen) y DS (Define Space). Las siguientes
líneas (ubicadas al final del fichero de código) reservan en nuestro programa un array de 448 bytes de longitud
y tamaño cero alineado en una posición múltiplo de 256:

ORG 64000

Scanline_Offsets:
DS 448, 0
También podemos incluir las 2 tablas “inline” en nuestro programa, y además sin necesidad de conocer la
dirección de memoria en que están (por ejemplo, embedidas dentro del código del programa en la siguiente
dirección múltiplo de 256 disponible) aprovechando el soporte de macros del ensamblador PASMO:

; Macro de alineacion para PASMO


align macro value
if $ mod value
ds value - ($ mod value)
endif
endm

align 256

Scanline_Offsets:
LUT_Scanlines_LO:
DB $00, $00, $00, $00, $00, $00, $00, $00, $20, $20, $20, $20
DB $20, $20, $20, $20, $40, $40, $40, $40, $40, $40, $40, $40
DB $60, $60, $60, $60, $60, $60, $60, $60, $80, $80, $80, $80
DB $80, $80, $80, $80, $A0, $A0, $A0, $A0, $A0, $A0, $A0, $A0
DB $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0, $E0, $E0, $E0, $E0
DB $E0, $E0, $E0, $E0, $00, $00, $00, $00, $00, $00, $00, $00
DB $20, $20, $20, $20, $20, $20, $20, $20, $40, $40, $40, $40
DB $40, $40, $40, $40, $60, $60, $60, $60, $60, $60, $60, $60
DB $80, $80, $80, $80, $80, $80, $80, $80, $A0, $A0, $A0, $A0
DB $A0, $A0, $A0, $A0, $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
DB $E0, $E0, $E0, $E0, $E0, $E0, $E0, $E0, $00, $00, $00, $00
DB $00, $00, $00, $00, $20, $20, $20, $20, $20, $20, $20, $20
DB $40, $40, $40, $40, $40, $40, $40, $40, $60, $60, $60, $60
DB $60, $60, $60, $60, $80, $80, $80, $80, $80, $80, $80, $80
DB $A0, $A0, $A0, $A0, $A0, $A0, $A0, $A0, $C0, $C0, $C0, $C0
DB $C0, $C0, $C0, $C0, $E0, $E0, $E0, $E0, $E0, $E0, $E0, $E0

Free_64_Bytes:
DB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DB 0, 0, 0, 0

LUT_Scanlines_HI:
DB $40, $41, $42, $43, $44, $45, $46, $47, $40, $41, $42, $43
DB $44, $45, $46, $47, $40, $41, $42, $43, $44, $45, $46, $47
DB $40, $41, $42, $43, $44, $45, $46, $47, $40, $41, $42, $43
DB $44, $45, $46, $47, $40, $41, $42, $43, $44, $45, $46, $47
DB $40, $41, $42, $43, $44, $45, $46, $47, $40, $41, $42, $43
DB $44, $45, $46, $47, $48, $49, $4A, $4B, $4C, $4D, $4E, $4F
DB $48, $49, $4A, $4B, $4C, $4D, $4E, $4F, $48, $49, $4A, $4B
DB $4C, $4D, $4E, $4F, $48, $49, $4A, $4B, $4C, $4D, $4E, $4F
DB $48, $49, $4A, $4B, $4C, $4D, $4E, $4F, $48, $49, $4A, $4B
DB $4C, $4D, $4E, $4F, $48, $49, $4A, $4B, $4C, $4D, $4E, $4F
DB $48, $49, $4A, $4B, $4C, $4D, $4E, $4F, $50, $51, $52, $53
DB $54, $55, $56, $57, $50, $51, $52, $53, $54, $55, $56, $57
DB $50, $51, $52, $53, $54, $55, $56, $57, $50, $51, $52, $53
DB $54, $55, $56, $57, $50, $51, $52, $53, $54, $55, $56, $57
DB $50, $51, $52, $53, $54, $55, $56, $57, $50, $51, $52, $53
DB $54, $55, $56, $57, $50, $51, $52, $53, $54, $55, $56, $57

Finalmente, veamos si ha merecido la pena el cambio a 2 tablas analizando la nueva rutina de cálculo de
dirección:

;-------------------------------------------------------------
; Get_Pixel_Offset_LUT_2(x,y):
;
; Entrada: B = Y, C = X
; Salida: HL = Direccion de memoria del pixel (x,y)
; A = Posicion relativa del pixel en el byte
;-------------------------------------------------------------
Get_Pixel_Offset_LUT_2:
LD A, C ; Ponemos en A la X
RRA
RRA
RRA ; A = ???CCCCC
AND 31 ; A = 000CCCCCb
LD L, B ; B = coordenada Y
LD H, Scanline_Offsets/256 ; Parte alta de la dir de tabla
ADD A, (HL) ; A = columna + tabla_baja[linea]
INC H ; saltamos a la siguiente tabla
LD H, (HL) ; cargamos en H la parte alta
LD L, A ; cargamos en L la parte baja
LD A, C ; Recuperamos la coordenada (X)
AND 7 ; A = Posicion relativa del pixel
RET

El coste de ejecución de esta rutina es de 77 t-estados, incluyendo el RET, la conversión de “X” en “Columna”
y la obtención de la posición relativa del pixel.

Cálculos contra Tablas

Como casi siempre en código máquina, nos vemos forzados a elegir entre velocidad y tamaño: las rutinas
basadas en tablas son generalmente más rápidas al evitar muchos cálculos, pero a cambio necesitamos ubicar en
memoria dichas tablas. Por contra, las rutinas basadas en cálculos ocupan mucho menos tamaño en memoria
pero requieren más tiempo de ejecución.

Debemos elegir uno u otro sistema en función de las necesidades y requerimientos de nuestro programa: si
disponemos de poca memoria libre y el tiempo de cálculo individual es suficiente, optaremos por la rutina de
composición. Si, por contra, la cantidad de memoria libre no es un problema y sí que lo es el tiempo de cálculo,
usaremos las rutinas basadas en tablas.

Los programadores debemos muchas veces determinar si una rutina es crítica o no según la cantidad de veces
que se ejecute en el “bucle principal” y el porcentaje de tiempo que su ejecución supone en el programa.

Por ejemplo, supongamos una rutina de impresión de sprites de 3×3 bloques: aunque el tiempo de dibujado de
los sprites en sí de un juego sea crítico, el posicionado en pantalla para cada sprite sólo se realiza una vez (para
su esquina superior izquierda) frente a toda la porción de código que debe imprimir los 9 caracteres (9*8 bytes
en pantalla) más sus atributos, con sus correspondientes rotaciones si el movimiento es pixel a pixel. El
movimiento entre los diferentes bloques del sprite se realiza normalmente de forma diferencial. Probablemente,
invertir “tiempo” para optimizar o “memoria” para tener tablas de precalculo sea más aconsejable en el cuerpo
de la rutina de sprites o en tablas de sprites pre-rotados que en la coordenación en sí misma.

La diferencia entre rutinas de tablas y de cálculos se resume en la siguiente tabla:

Rutina Tiempo de ejecución Bytes rutina Bytes adicionales Tamaño Total


Cálculo 118 t-estados 32 Ninguno 32
448 (384 si aprovechamos
Tablas 77 t-estados 17 GetOffset + 32 GenLUT 443 - 487 bytes
los 64 entre tablas)

Cálculo de posiciones de posiciones diferenciales de pixel

Los cálculos de las posiciones de píxeles en alta resolución son “costosos” por lo que a la hora de dibujar
sprites, líneas, círculos o cualquier otra primitiva gráfica, lo normal es realizar el cálculo de una posición inicial
y moverse diferencialmente respecto a la misma.
Para eso se utilizan rutinas de posicionamiento diferencial como las que ya vimos en los atributos o en baja
resolución que nos permitan movernos a cualquiera de los 8 píxeles de alrededor de la dirección HL y posición
de pixel que estamos considerando.

Offset del pixel de la izquierda/derecha

En el caso de la coordenación por caracteres (baja resolución), nos bastaba con decrementar o incrementar HL
para ir al carácter siguiente o anterior. En este caso, debemos tener en cuenta que cada byte contiene 8 píxeles,
por lo que se hace necesaria una máscara de pixel para referenciar a uno u otro bit dentro del byte apuntado por
HL.

Teniendo una máscara de pixel en A, las rutinas de cálculo del pixel a la izquierda y a la derecha del pixel
actual se basarían en la ROTACIÓN de dicha máscara comprobando las situaciones especiales en las 2
situaciones especiales que se pueden presentar:

• El pixel se encuentra en 10000000b y queremos acceder al pixel de la izquierda.


• El pixel se encuentra en 00000001b y queremos acceder al pixel de la derecha.

En esos casos se podría utilizar el incremento y decremento de la posición de HL:

; HL = Direccion de pantalla base


; A = Mascara de pixeles

Pixel_Izquierda_HL_Mask:
RLC A ; Rotamos A a la izquierda
RET NC ; Si no se activa el carry flag, volvemos
DEC L ; Si se activa, hemos pasado de 10000000b
RET ; a 00000001b y ya podemos alterar HL

Pixel_Derecha_HL_Mask:
RRC A ; Rotamos A a la derecha
RET NC ; Si no se activa el carry flag, volvemos
INC L ; Si se activa, hemos pasado de 00000001b
RET ; a 10000000b y ya podemos alterar HL

Son apenas 4 instrucciones, lo que resulta en un cálculo significativamente más rápido que volver a llamar a la
rutina de coordenación original.

Nótese cómo en lugar de utilizar DEC HL o INC HL (6 t-estados), realizamos un DEC L o INC L (4 t-
estados), ya que dentro de un mismo scanline de pantalla no hay posibilidad de, moviendo a derecha o
izquierda, variar el valor del byte alto de la dirección (siempre y cuando no excedamos los límites de la pantalla
por la izquierda o por la derecha). De esta forma ahorramos 2 valiosos ciclos de reloj en una operación que
suele realizarse en el bucle más interno de las rutinas de impresión de sprites.

Si en lugar de una máscara de pixel tenemos en A la posición relativa (0-7), podemos utilizar el siguiente
código:

; HL = Direccion de pantalla base


; A = Posicion relativa del pixel (0=Pixel de la izquierda)

Pixel_Derecha_HL_Rel:
INC A ; Incrementamos A
AND 7 ; Si A=8 -> A=0
RET NZ ; Si no es cero, hemos acabado
INC L ; Si se activa, hemos pasado al byte
RET ; siguiente -> alterar HL

Pixel_Izquierda_HL_Rel:
DEC A ; Decrementamos A
RET P ; Si no hay overflow (A de 0 a 255), fin
AND 7 ; 11111111b -> 00000111b
DEC L ; Hemos pasado al byte siguiente ->
RET ; alteramos HL

Recordemos que ninguna de estas rutinas contempla los líneas izquierdo y derecho de la pantalla.

Offset del pixel del scanline de arriba/abajo

Moverse un scanline arriba o abajo requiere código adicional, como ya vimos en el apartado de coordenadas de
caracteres, para detectar tanto los cambios de tercios como los cambios de caracteres.

Bits = Dirección VRAM Bits de Tercio Bits de scanline Bits de Carácter-Y Bits de Columna
HL = 010 TT SSS NNN CCCCC

Primero debemos detectar si estamos en el último scanline del carácter, ya que avanzar 1 scanline implicaría
poner SSS a 000 e incrementar NNN (carácter dentro del tercio). Al incrementar NNN (carácter dentro del
tercio) tenemos que verificar también si NNN pasa de 111 a 1000 lo que supone un cambio de tercio:

El código para incrementar HL hacia el siguiente scanline horizontal detectando los saltos de tercio y de
carácter sería el siguiente:

; Avanzamos HL 1 scanline:
INC H ; Incrementamos HL en 256 (siguiente scanline)
LD A, H ; Cargamos H en A
AND 7 ; Si despues del INC H los 3 bits son 0,
; es porque era 111b y ahora 1000b.
JR NZ, nofix_abajop ; Si no es cero, hemos acabado (solo INC H).
LD A, L ; Es cero, hemos pasado del scanline 7 de un
; caracter al 0 del siguiente: ajustar NNN
ADD A, 32 ; Ajustamos NNN (caracter dentro de tercio += 1)
LD L, A ; Ahora hacemos la comprobacion de salto de tercio
JR C, nofix_abajop ; Si esta suma produce acarreo, habria que ajustar
; tercio, pero ya lo hizo el INC H (111b -> 1000b)
LD A, H ; Si no produce acarreo, no hay que ajustar
SUB 8 ; tercio, por lo que restamos el bit TT que sumo
LD H, A ; el INC H inicial.

nofix_abajop:
; HL contiene ahora la direccion del siguiente scanline
; ya sea del mismo caracter o el scanline 0 del siguiente.

Y para retroceder HL en 1 scanline, usando la misma técnica:

LD A, H
AND 7 ; Comprobamos scanline
JR Z, Anterior_SL_DEC ; Si es cero, hay salto de caracter
DEC H ; No es cero, basta HL = HL - 256
RET ; Hemos acabado (solo INC H).
Anterior_SL_DEC: ; Hay que ir de caracter 000b a 111b
DEC H ; Decrementamos H
LD A, L ; Ajustamos NNN (caracter en tercio -=1)
SUB 32
LD L, A
RET C ; Si se produjo carry, no hay que ajustar
LD A, H ; Se produjo carry, ajustamos el tercio
ADD A, 8 ; por el DEC H inicial.
LD H, A

Veamos este mismo código en forma de subrutina, aprovechando con RET la posibilidad de evitar los saltos
hacia el final de las rutinas:
;-------------------------------------------------------------
; Siguiente_Scanline_HL:
; Obtener la direccion de memoria del siguiente scanline dada
; en HL la direccion del scanline actual, teniendo en cuenta
; saltos de caracter y de tercio.
;
; Entrada: HL = Direccion del scanline actual
; Salida: HL = Direccion del siguiente scanline
;-------------------------------------------------------------
Siguiente_Scanline_HL:
INC H
LD A, H
AND 7
RET NZ
LD A, L
ADD A, 32
LD L, A
RET C
LD A, H
SUB 8
LD H, A
RET ; Devolvemos en HL el valor ajustado

La rutina para retroceder al scanline superior es de similar factura, con una pequeña reorganización del código
para evitar el salto con JR:

;-------------------------------------------------------------
; Anterior_Scanline_HL:
; Obtener la direccion de memoria del anterior scanline dada
; en HL la direccion del scanline actual, teniendo en cuenta
; saltos de caracter y de tercio.
;
; Entrada: HL = Direccion del scanline actual
; Salida: HL = Direccion del anterior scanline
;-------------------------------------------------------------
Anterior_Scanline_HL:
LD A, H
DEC H
AND 7
RET NZ
LD A, 8
ADD A, H
LD H, A
LD A, L
SUB 32
LD L, A
RET NC
LD A, H
SUB 8
LD H, A
RET ; Devolvemos en HL el valor ajustado

Siguiente_Scanline_HL será especialmente útil en el desarrollo de rutinas de impresión de Sprites, aunque lo


normal es que incluyamos el código “inline” dentro de dichas rutinas, para ahorrar los ciclos de reloj usandos
en un CALL+RET.
Otras rutinas

Rutinas cruzadas y de propósito general

En el libro “Lenguaje Máquina Avanzado para ZX Spectrum” de David Webb encontramos 3 rutinas útiles de
propósito general para obtener el offset en el área de imagen dada una dirección de atributo o viceversa.

La primera subrutina obtiene la dirección del atributo que corresponde a una dirección de pantalla especificada:

;-------------------------------------------------------------
; Attr_Offset_From_Image (DF-ATT):
;
; Entrada: HL = Direccion de memoria de imagen.
; Salida: DE = Direccion de atributo correspondiente a HL.
;-------------------------------------------------------------
Attr_Offset_From_Image:
LD A, H
RRCA
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L
RET

La segunda realiza el proceso inverso: obtiene la dirección del archivo de imagen dada una dirección de
atributo. Esta dirección se corresponde con la dirección de los 8 píxeles del primer scanline del carácter.

;-------------------------------------------------------------
; Image_Offset_From_Attr (ATT_DF):
;
; Entrada: HL = Direccion de memoria de atributo.
; Salida: DE = Direccion de imagen correspondiente a HL.
;-------------------------------------------------------------
Image_Offset_From_Attr:
LD A, H
AND 3
RLCA
RLCA
RLCA
OR $40
LD D, A
LD E, L
RET
La tercera es una combinación de localización de offset de imagen, de atributo, y el valor del atributo:

;-------------------------------------------------------------
; Get_Char_Data(c,f) (LOCATE):
;
; Entrada: B = FILA, C = COLUMNA
; Salida: DE = Direccion de atributo.
; HL = Direccion de imagen.
; A = Atributo de (C,F)
;-------------------------------------------------------------
Get_Char_Data:
LD A, B
AND $18
LD H, A
SET 6, H
RRCA
RRCA
RRCA
OR $58
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD L, A
LD E, A
LD A, (DE)
RET

Llamando a la anterior rutina con unas coordenadas (c,f) en C y B obtenemos la dirección de memoria de
imagen (HL) y de atributo (DE) de dicho carácter, así como el valor del atributo en sí mismo (A).
Cálculo de direcciones de pantalla a partir de
coordenadas
Cuando tenemos la coordenada de cualquier sprite almacenada en registros o variables, necesitaremos saber
donde hay que empezar a ponerlo cuando vayamos a pintarlo. Esto, dada la extraña disposición de la memoria
de la pantalla en el Spectrum, no es tan sencillo como pueda serlo en otros sistemas (por ejemplo en el modo
VGA 13h del PC, donde el cálculo consiste en hacer (320*y)+x ). Así pues, vamos a dedicar este artículo a ver
varias formas distintas de realizar esta tarea, comentando sus ventajas e inconvenientes.

Para poder comprender las distintas rutinas hay que tener muy en cuenta la disposición de la memoria de video,
así que vamos a recordarla por si a alguien se le ha olvidado.

Básicamente, el aparente caos que supone el orden de la pantalla se entiende una vez que se descomponen las
direcciones en campos de bits bien diferenciados:

byte alto byte bajo


010 tt yyy YYY XXXXX

Donde:

tt tercio de la pantalla, de 00 a 10.


YYY fila dentro del tercio (en caracteres).
yyy fila dentro del caracter (en pixels).
XXXXX columna (en caracteres).

Los datos de los que partimos son un byte con la coordenada Y en el formato: ttYYYyyy , y otro byte para la
coordenada X en el formato: 000XXXXX.

Así pues nuestro cometido principal consiste en reordenar todos los campos de la coordenada Y al insertarlos
en la dirección, para terminar añadiendo al resultado la coordenada X, lo cual es bastante trivial.

Para realizar el calculo de la coordenada Y tenemos dos posibilidades básicas: hacer todo el cálculo por nuestra
cuenta, o usar una tabla que almacene todos los resultados. También tenemos la opción intermedia de usar las
tablas sólo para la parte mas compleja y calcular el resto.

Realizando el cálculo, la primera rutina que se me ocurrió fue ésta:

; entrada x=L, y=A; salida en DE


CALC1: LD D,2 ; D = 00000010 2 / 7 2 / 7
RLCA ; A = tYYYyyyt 1 / 4 3 / 11
RL D ; D = 0000010t 2 / 8 5 / 19
RLCA ; A = YYYyyytt 1 / 4 6 / 23
RL D ; D = 000010tt 2 / 8 8 / 31
PUSH AF ; 1 /11 9 / 42
AND 224 ; A = YYY00000 2 / 7 11 / 49
ADD A,L ; A = YYYXXXXX 1 / 4 12 / 53
LD E,A ; E = YYY00000 1 / 4 13 / 57
POP AF ; A = YYYyyytt 1 /11 14 / 68
RLCA ; A = YYyyyttY 1 / 4 15 / 72
RLCA ; A = YyyyttYY 1 / 4 16 / 76
RLCA ; A = yyyttYYY 1 / 4 17 / 80
RLCA ; A = yyttYYYy 1 / 4 18 / 84
RL D ; D = 00010tty 2 / 8 20 / 92
RLCA ; A = yttYYYyy 1 / 4 21 / 96
RL D ; D = 0010ttyy 2 / 8 23 / 104
RLCA ; A = ttYYYyyy 1 / 4 24 / 108
RL D ; D = 010ttyyy 2 / 8 26 / 116
RET ; 1 /10 27 / 126

En los comentarios podeis ver el resultado de la última operación (para que se entienda lo que va haciendo el
programa), los bytes y estados de la instrucción actual y por último la suma de bytes / estados hasta el
momento. En los siguientes listados prescindiré de algunos de estos campos.

En total esta primera rutina ocupa 27 bytes, y tarda 126 estados en realizar el cálculo.

¿Es mucho, es poco? Veamos si podemos mejorarla.

Si nos damos cuenta, existen algunas instrucciones que se repiten, por lo que podemos ahorrar algunos bytes
haciendo:

; entrada x=L, y=A; salida en DE


CALC2: LD D,74 ; 2 / 7 2 / 7
ROT1: RLCA ; 1 / 4,4 3 / 15
RL D ; 2 / 8,8 5 / 31
JR NC, ROT1 ; 2 / 12,7 7 / 50
PUSH AF ; 1 / 11 8 / 61
AND 224 ; 2 / 7 10 / 68
ADD A,L ; 1 / 4 11 / 72
LD E,A ; 1 / 4 12 / 76
POP AF ; 1 / 11 13 / 87
RLCA ; 1 / 4 14 / 91
RLCA ; 1 / 4 15 / 95
RLCA ; 1 / 4 16 / 99
ROT2: RLCA ; 1 / 4,4,4 17 / 111
RL D ; 2 / 8,8,8 19 / 135
JR NC, ROT2 ; 2 / 12,12,7 21 / 166
RET ; 1 / 10 22 / 176

Como podéis observar, el algoritmo es identico al caso anterior, pero hemos introducido un par de bits extra en
el registro D que actúan de bits marcadores (los bit markers que vimos en el artículo anterior) para salir de los
bucles.

A cambio de ahorrar 5 bytes, estamos tardando 50 estados más en realizar el cálculo.

Si nos fijamos en la primera rutina, vemos que estamos haciendo una tontería con el campo yyy, ya que
estamos rotando el byte 8 veces, para que el campo vuelva otra vez al mismo sitio al final. Además, el uso de
instrucciones PUSH AF y POP AF resulta costoso en estados, así que podemos refinar la rutina un poquito:

; entrada x=L, y=D; salida en HL


CALC3: LD A,D ; A = ttYYYyyy 1 / 4 1 / 4
LD H,2 ; H = 00000010 2 / 7 3 / 11
RLCA ; A = tYYYyyyt 1 / 4 4 / 15
RL H ; H = 0000010t 2 / 8 6 / 23
RLCA ; A = YYYyyytt 1 / 4 7 / 27
RL H ; H = 000010tt 2 / 8 9 / 35
AND 224 ; A = YYY00000 2 / 7 11 / 42
ADD A,L ; A = YYYXXXXX 1 / 4 12 / 46
LD L,A ; L = YYYXXXXX 1 / 4 13 / 50
LD A,D ; A = ttYYYyyy 1 / 4 14 / 54
AND 7 ; A = 00000yyy 2 / 7 16 / 61
RLC H ; D = 00010tt0 2 / 8 18 / 69
RLC H ; D = 0010tt00 2 / 8 20 / 77
RLC H ; D = 010tt000 2 / 8 22 / 85
ADD A,H ; A = 010ttyyy 1 / 4 23 / 89
LD H,A ; H = 010ttyyy 1 / 4 24 / 93
RET ; 1 /10 25 / 103
Ahora ocupa 25 bytes, y tarda sólo 103 estados en realizar el cálculo.

Haciendo lo mismo de antes, la versión pequeña saldría así:

; entrada x=L, y=D; salida en HL


CALC4: LD A,D ; 1 / 4 1 / 4
LD H,74 ; 2 / 7 3 / 11
ROT1: RLCA ; 1 / 4,4 4 / 19
RL H ; 2 / 8,8 6 / 35
JR NC,ROT1 ; 2 / 12,7 8 / 54
AND 224 ; 2 / 7 10 / 61
ADD A,L ; 1 / 4 11 / 65
LD L,A ; 1 / 4 12 / 69
LD A,D ; 1 / 4 13 / 73
AND 7 ; 2 / 7 15 / 80
ROT2: RLC H ; 2 / 8,8,8 17 / 104
JR NC,ROT2 ; 2 / 12,12,7 19 / 135
ADD A,H ; 1 / 4 20 / 139
LD H,A ; 1 / 4 21 / 143
RET ; 1 /10 22 / 153

De nuevo perdemos 50 estados, pero en esta ocasión tan solo ganamos 3 bytes en lugar de 5, por lo que no
bajamos de los 22 bytes.

Parece como si no pudiésemos hacer el cálculo en menos de 100 estados, pero sí que es posible si volvemos a
cambiar de método.

En lugar de utilizar 5 rotaciones para poner el campo del tercio en su sitio, podemos detectar en que tercio
estamos al efectuar las dos primeras rotaciones (las que ajustan la posición del campo YYY) gracias al bit de
acarreo, y en caso de que sea necesario, “inyectar” el bit directamente.

; entrada x=C, y=B; salida en HL


CALC5: LD A,B ; A = ttYYYyyy 1 / 4
AND A,7 ; A = 00000yyy 2 / 7
OR 64 ; A = 01000yyy 2 / 7
LD H,A ; H = 01000yyy 1 / 4
LD A,B ; A = ttYYYyyy 1 / 4
AND 248 ; A = ttYYY000 2 / 7
ADD A,A ; A = tYYY0000 1 / 4
JR NC,noter_3 ; 2 / 7,12
SET 4,H ; H = 01010yyy 2 / 8
noter_3: ADD A,A ; A = YYY00000 1 / 4
JR NC,noter_2 ; 2 / 7,12
SET 3,H ; H = 01001yyy 2 / 8
noter_2: ADD A,C ; A = YYYXXXXX 1 / 4
LD L,A ; L = YYYXXXXX 1 / 4
RET ; 1 / 10

El cálculo para la parte alta ya está terminado en la cuarta instrucción en el caso de que estemos en el primer
tercio. Los otros dos tercios necesitan uno de los SETs para añadir el bit que les falta. Esta rutina también ocupa
22 bytes, y tarda 83 estados en el primer tercio y 86 para los otros dos.

Parece que hemos llegado a la conclusión de que no es posible hacerlo más pequeño, pero es posible hacerlo
más rápido si tenemos en cuenta que no existe el caso 11 para el campo tt, por lo que si detectamos el tercer
tercio, no es necesario probar si estamos en el segundo. Así que separando los caminos un poquito y repitiendo
por tanto algo de código obtenemos:

; entrada x=C, y=B; salida en HL


CALC6: LD A,B ; A = ttYYYyyy 1 / 4
AND 7 ; A = 00000yyy 2 / 7
OR 64 ; A = 01000yyy 2 / 7
LD H,A ; H = 01000yyy 1 / 4
LD A,B ; A = ttYYYyyy 1 / 4
AND 248 ; A = ttYYY000 2 / 7
ADD A,A ; A = tYYY0000 1 / 4
JR C,ter_3 ; 2 / 7,12
ADD A,A ; A = YYY00000 1 / 4
JR NC,ter_1 ; 2 / 7,12
SET 3,H ; H = 01001yyy 2 / 8
ter_1: ADD A,C ; A = YYYXXXXX 1 / 4
LD L,A ; L = YYYXXXXX 1 / 4
RET ; 1 / 10
ter_3: SET 4,H ; H = 01010yyy 2 / 8
ADD A,A ; A = YYY00000 1 / 4
ADD A,C ; A = YYYXXXXX 1 / 4
LD L,A ; L = YYYXXXXX 1 / 4
RET ; 1 / 10

26 bytes, los tiempos para cada tercio son respectivamente: 78, 81, 79. Aparte de ganar 7 estados para el tercer
tercio, hemos ganado otros 5 para los otros dos casos porque el primer salto que antes se daba ahora no se
produce (y los saltos condicionales son mas rápidos cuando no se producen (7 estados) que cuando se producen
(12)).

Si el segundo salto lo hacemos de forma que salte en caso de acarreo hacia otra zona donde se realiza el proceso
del segundo tercio, separando el código un poco más, optimizamos el camino del primer tercio a costa del
camino del segundo, con lo que obtendríamos 73, 86, 79 (y ocuparía algunos bytes mas).

Y eso es todo en cuanto a los métodos de cálculo directo. Vamos a ver ahora los métodos que utilizan tablas.

Las tablas no son más que listas de resultados de ciertos cálculos que dejamos apuntados en memoria para no
tener que recalcularlos cada vez que los necesitamos. Por lo tanto permiten ganar tiempo a cambio de usar
memoria.

En un principio, para cada posible valor de Y, necesitaremos dos valores en las tablas, uno para el byte alto y
otro para la primera mitad del byte bajo, a la cual le sumaremos el campo X para obtener la dirección completa.
Como tenemos 192 posibles valores de Y, obtenemos un total de 192*2=384 bytes de datos.

Tan solo necesitamos que la rutina lea los datos de la tabla. Eso se puede hacer por ejemplo así:

; entrada x=A, y=L; salida en DE


TABLA1: LD DE,TABLA ; 3 / 10 3 / 10
LD H,0 ; 2 / 7 5 / 17
ADD HL,HL ; 1 / 11 6 / 28
ADD HL,DE ; 1 / 11 7 / 39
ADD A,(HL) ; 1 / 7 8 / 46
LD E,A ; 1 / 4 9 / 50
INC HL ; 1 / 6 10 / 56
LD D,(HL) ; 1 / 7 11 / 63
RET ; 1 / 10 12 / 73

La tabla se construye pasando ttYYYyyy → YYY00000, 010ttyyy para cada valor del 0 al 191, y puede estar
situada en cualquier parte de la memoria.

En un principio parece que no ganamos mucho sólo por el hecho de utilizar una tabla, ya que tenemos que
calcular la posición de los datos que buscamos, y para esto tenemos que usar instrucciones de 16 bits que
resultan muy costosas en tiempo. Para mejorar esto tenemos que colocar alguna restricción en la tabla, y
aprovecharla para tener que realizar menos cálculos. Por ejemplo, si hacemos que la tabla comience en una
dirección fija que sea múltiplo de 512, en lugar de un 0 colocamos en H el valor TABLA/512, con lo cual al
hacer la suma ADD HL,HL tenemos ya la dirección de la tabla calculada por completo, y no necesitamos
utilizar para nada el registro DE.

; entrada x=A, y=L; salida en HL


TABLA2: LD H,TABLA/512 ; 2 / 7 2 / 7
ADD HL,HL ; 1 / 11 3 / 18
ADD A,(HL) ; 1 / 7 4 / 25
INC L ; 1 / 4 5 / 29
LD H,(HL) ; 1 / 7 6 / 36
LD L,A ; 1 / 4 7 / 40
RET ; 1 / 10 8 / 50

Ahora podemos utilizar INC L en lugar de INC HL porque el ADD HL,HL dejó un valor par, y por lo tanto al
incrementar L nunca va a incrementarse H, y así nos ahorramos 2 estados mas. En el caso anterior, cabía la
posibilidad de que la etiqueta TABLA cayera en un lugar impar.

Esto ya tiene mejor pinta, ¿verdad?. Pues la cosa puede mejorar mas aún. Si en lugar de una tabla con los 384
valores juntos creamos dos tablas de 192 bytes separadas entre si 256 bytes (osea, dejando un hueco de 64),
podemos pasar de un valor al siguiente usando INC H en lugar de INC L (o INC HL), y nos podemos ahorrar la
otra suma de 16 bits.

Sin restricción en la colocación de la pareja de tablas quedaría:

; entrada x=A, y=L; salida en DE


TABLA3: LD DE,TABLA1 ; 3 / 10
LD H,0 ; 2 / 7
ADD HL,DE ; 1 / 11
ADD A,(HL) ; 1 / 7
LD E,A ; 1 / 4
INC H ; 1 / 4
LD D,(HL) ; 1 / 7
RET ; 1 / 10 11+384 = 395 / 60

Ahora se hacen dos tablas ttYYYyyy → YYY00000 por una parte y ttYYYyyy → 010ttyyy por otra, para cada
valor del 0 al 191.

Y juntando los dos trucos (aunque ahora la pareja de tablas basta con alinearlas a un múltiplo de 256, no hace
falta 512), obtenemos la versión más rápida:

; entrada x=A, y=L; salida en HL


TABLA4: LD H,TABLA/256 ; 2 / 7
ADD A,(HL) ; 1 / 7
INC H ; 1 / 4
LD H,(HL) ; 1 / 7
LD L,A ; 1 / 4
RET ; 1 / 10 7+384 = 391 / 39

Como podemos ver, las tablas pueden ayudarnos a reducir el tiempo a la mitad, lo cual resulta muy útil cuando
tenemos que llamar a una rutina muy a menudo.

Por último vamos a ver un par de ejemplos de rutinas “híbridas”, en el sentido de que realizan parte del cálculo
y miran otra parte en alguna tabla, obteniendose mayor velocidad que con el cálculo pero ocupando menos
espacio que las soluciones con tabla que hemos visto hasta ahora.

La primera que vamos a ver se aprovecha del hecho de que el cálculo de la parte baja es muy sencillo, así que
tan solo usamos una tabla de 192 bytes para la parte alta. La versión directamente optimizada (con la tabla
alineada a un múltiplo de 256) podría ser algo así:

; entrada x=B, y=A; salida en HL


HIBRI1: LD H,TABLA/256 ; 2 / 7
LD L,A ; 1 / 4
LD H,(HL) ; 1 / 7
RLCA ; 1 / 4
RLCA ; 1 / 4
AND 224 ; 2 / 7
ADD A,B ; 1 / 4
LD L,A ; 1 / 4
RET ; 1 / 10 11+192=203 / 51
En este caso tan solo necesitamos una tabla recorriendo ttYYYyyy → 010ttyyy de 0 a 191.

Para la última rutina que vamos a ver nos aprovecharemos del hecho de que los valores que aparecen en la tabla
de 192 bytes se repiten 8 veces, dado que el campo YYY no pinta nada en esta tabla. Así pues rotamos el
campo Y para acumular los bits que nos interesan en las posiciones mas bajas y borramos los de las posiciones
altas antes de mirar en la tabla. La tabla resultante es de 32 bytes, de los cuales 8 no se usan (dado que el tercio
11 no existe), por lo que son 24 bytes ocupando un espacio de 31.

; entrada x=B, y=A; salida en HL


HIBRI2: LD H,TABLITA/256 ; 2 / 7
RLCA ; 1 / 4
RLCA ; 1 / 4
LD C,A ; 1 / 4
AND 31 ; 2 / 7
LD L,A ; 1 / 4
LD H,(HL) ; 1 / 7
LD A,C ; 1 / 4
AND 224 ; 2 / 7
ADD A,B ; 1 / 4
LD L,A ; 1 / 4
RET ; 1 / 10 15+24 = 39 / 66

La tablita en este caso se construye recorriendo 000yyytt → 010ttyyy de 0 a 30 (ignorando los casos con tt =
11).

Visto todo esto, ¿cual es la mejor rutina? Si nos quedamos sólo con las mejores rutinas, podemos escoger la
relación velocidad/espacio que mejor convenga a nuestro programa, entre las siguientes:

Rutina Bytes Estados


TABLA4: 391 39
HIBRI1: 203 51
HIBRI2: 39(*) 66
CALC6: 26 81
CALC5: 22 86

(* > 46 si no aprovechásemos los huecos.)

¿Pueden optimizarse más? Pues depende. Como rutinas de uso general, creo que no dan más de si (aparte de la
obviedad que supone usarlas en línea y evitar por lo tanto el RET), pero una vez aplicadas a un juego
determinado, las características del juego pueden favorecer nuevas optimizaciones. Por ejemplo, si un juego tan
solo usa los dos primeros tercios, dejando el tercio de abajo para puntuaciones, barras de energía, etc…
podemos reducir los tamaños de las tablas a 256,128 o 16 bytes (en esta ocasión seguidos) respectivamente, y
las rutinas de cálculo quedarían como una sóla de 18 bytes, tardando 71/74 estados.

Con esto concluye el artículo, si tenéis dudas o queréis proponer temas para futuros artículos, podeis notificarlo
en los foros de Speccy.org.

La mayoría de las rutinas vistas en este artículo ya fueron discutidas hace tiempo en dicha lista, y quiero
agradecer especialmente la colaboración de Truevideo (que planteó su rutina usando tabla) y Z80user (autor
original de CALC6, de la cual se deriva fácilmente CALC5).
Gráficos (y III): Sprites y Gráficos en baja
resolución (bloques)
En este capítulo crearemos rutinas específica de impresión de sprites de 8×8 píxeles en posiciones exáctas de
carácter (gráficos de bloques) y la extenderemos a la impresión de sprites de 16×16 píxeles (2×2 bloques).
Además de estas rutinas de tamaños específicos, analizaremos una rutina más genérica que permita imprimir
sprites de tamaños múltiples del anterior (16×16, 24×16, etc).

El capítulo hará uso intensivo de los algoritmos de cálculo de direcciones de memoria a partir de coordenadas y
del movimiento relativo descritos en 2 anteriores capítulos, aunque las rutinas mostradas a continuación podrían
ser directamente utilizadas incluso sin conocimientos sobre la organización de la videomemoria del Spectrum.

Estudiaremos también los métodos de impresión sobre pantalla: transferencia directa, operaciones lógicas y uso
de máscaras, y la generación de los datos gráficos y de atributos a partir de la imagen formada en un editor de
sprites.

Teoría sobre el trazado de sprites

Comencemos con las definiciones básicas de la terminología que se usará en este capítulo.

Sprite: Se utiliza el término anglosajón sprite (traducido del inglés: “duendecillo”) para designar en un juego o
programa a cualquier gráfico que tenga movimiento. En el caso del Spectrum, que no tiene como otros sistemas
hardware dedicado a la impresión de Sprites, aplicamos el término a cualquier mapa de bits (del inglés bitmap)
que podamos utilizar en nuestros programas: el gráfico del personaje protagonista, los de los enemigos, los
gráficos de cualquier item o incluso las propias fuentes de texto de los marcadores.

Editor de Sprites: Los sprites se diseñan en editores de sprites, que son aplicaciones diseñadas para crear
Sprites teniendo en cuenta la máquina destino para la que se crean. Por ejemplo, un editor de Sprites para
Spectrum tomará en cuenta, a la hora de aplicar colores, el sistema de tinta/papel en bloques de 8×8 píxeles y
no nos permitirá dibujar colores saltándonos dicha limitación que después existirá de forma efectiva en el
hardware destino.
Estos programas gráficos tratan los sprites como pequeños rectángulos (con o sin zonas transparentes en ellos)
del ancho y alto deseado y se convierten en mapas de bits (una matriz de píxeles activos o no activos
agrupados) que se almacenan en los programas como simples ristras de bytes, preparados para ser volcados en
la pantalla con rutinas de impresión de sprites.

Sprite en editor de sprites, su bitmap, y su conversión a datos binarios.


Rutinas de impresión de Sprites: son rutinas que reciben como parámetro la dirección en memoria del Sprite
y sus Atributos y las coordenadas (x,y) destino, y vuelcan esta información gráfica en la pantalla.

Sprite Set: Normalmente todos los Sprites de un juego se agrupan en un “sprite set” (o “tile set”), que es una
imagen rectangular o un array lineal que almacena todos los datos gráficos del juego de forma que la rutina de
impresión de Sprites pueda volcar uno de ellos mediante unas coordenadas origen y un ancho y alto (caso del
tileset rectangular) o mediante un identificador dentro del array de sprites (caso del tileset lineal).

El sistema de sprites en formato rectangular suele ser utilizado en sistemas más potentes que el Spectrum,
permitiendo además sprites de diferentes tamaños en el mismo tileset. Las rutinas que imprimen estos sprites a
lo largo del juego requieren como parámetros, además de la posición (x,y) de destino, una posición (x,y) de
origen y un ancho y alto para “extraer” cada sprite de su “pantalla origen” y volcarlo a la pantalla destino.

Sprite set de Pacman -© NAMCO- coloreado por Simon Owen


para la versión SAM. Gráficos en formato rectangular.
Las rutinas de impresión requieren
coordenadas (xorg,yorg).

En el caso del Spectrum, nos interesa mucho más el sistema de almacenamiento lineal dentro de un “vector” de
datos, ya que normalmente agruparemos todos los sprites de un mismo tamaño en un mismo array. Podremos
disponer de diferentes arrays para elementos de diferentes tamaños. Cuando queramos hacer referencia a uno de
los sprites de dicho array, lo haremos con un identificador numérico (0-N) que indicará el número de sprite que
queremos dibujar comenzando desde arriba y designando al primero como 0.
Parte del sprite set de Sokoban: gráficos en formato
lineal vertical. Las rutinas de impresión
requieren un identificador de sprite (0-NumSprites).

En un juego donde todos los sprites son de 16×16 y los fondos están formados por sprites o tiles de 8×8, se
podría tener un “array” para los sprites, otro para los fondos, y otro para las fuentes de texto. Durante el
desarrollo del bucle del programa llamaremos a la rutina de impresión de sprites pasando como parámetro el
array de sprites, el ancho y alto del sprite, y el identificador del sprite que queremos dibujar.

Frame (fotograma): El “sprite set” no sólo suele alojar los diferentes gráficos de cada personaje o enemigo de
un juego, sino que además se suelen alojar todos los frames (fotogramas) de animación de cada personaje. En
sistemas más modernos se suele tener un frameset (un array de frames) por cada personaje, y cada objeto del
juego tiene asociado su frameset y su estado actual de animación y es capaz de dibujar el frame que le
corresponde.
En el Spectrum, por limitaciones de memoria y código, lo normal es tener todo en un mismo spriteset, y tener
almacenados los identificadores de animación de un personaje en lugar de su frameset. Así, sabremos que
nuestro personaje andando hacia la derecha tiene una animación que consta de los frames (por ejemplo) 10, 11
y 12 dentro del spriteset.

Tiles: Algunos bitmaps, en lugar de ser llamados “sprites”, reciben el nombre de tiles (“bloques”).
Normalmente esto sucede con bitmaps que no van a tener movimiento, que se dibujan en posiciones exáctas de
carácter, y/o que no tienen transparencia. Un ejemplo de tiles son los “bloques” que forman los escenarios y
fondos de las pantallas cuando son utilizados para componer un mapa de juego en base a la repetición de los
mismos. Los tiles pueden ser impresos con las mismas rutinas de impresión de Sprites (puesto que son
bitmaps), aunque normalmente se diseñan rutinas específicas para trazar este tipo de bitmaps aprovechando sus
características (no móviles, posición de carácter, no transparencia), con lo que dichas rutinas se pueden
optimizar. Como veremos en el próximo capítulo, los tiles se utilizan normalmente para componer el área de
juego mediante un tilemap (mapa de tiles):
Tilemap: componiendo un mapa en pantalla
a partir de tiles de un tileset/spriteset + mapa.

Máscaras de Sprites: Finalmente, cabe hablar de las máscaras de sprites, que son bitmaps que contienen un
contorno del sprite de forma que se define qué parte del Sprite original debe sobreescribir el fondo y qué parte
del mismo debe de ser transparente.

Un Sprite y su máscara aplicados sobre el fondo.


Las máscaras son necesarias para saber qué partes del sprite son transparentes: sin ellas habría que testear el
estado de cada bit para saber si hay que “dibujar” ese pixel del sprite o no. Gracias a la máscara, basta un AND
entre la máscara y el fondo y un OR del sprite para dibujar de una sóla vez 8 píxeles sin realizar testeos
individuales de bits.

Diseño de una rutina de impresión de sprites


En microordenadores como el Spectrum existe un vínculo especial entre los “programadores” y los
“diseñadores gráficos”, ya que estos últimos deben diseñar los sprites teniendo en cuenta las limitaciones del
Spectrum y a veces hacerlo tal y como los programadores los necesitan para su rutina de impresión de Sprites o
para salvar las limitaciones de color del Spectrum o evitar colisiones de atributos entre personajes y fondos.

El diseño gráfico del Sprite

El diseñador gráfico y el programador deben decidir el tamaño y características de los Sprites y el “formato
para el sprite origen” a la hora de exportar los bitmaps como “datos binarios” para su volcado en pantalla.

A la hora de crear una rutina de impresión de sprites tenemos que tener en cuenta el formato del Sprite de
Origen. Casi se podría decir que más bien, la rutina de impresión de sprites debemos escribirla o adaptarla al
formato de sprites que vayamos a utilizar en el juego.

Dicho formato puede ser:

• Sprite con atributos de color (multicolor) o sin atributos de color (monocolor).


• Si el sprite tiene atributos de color, los atributos pueden ir:
o En un array de atributos aparte del array de datos gráficos.
o En el mismo array de datos gráficos, pero detrás del último de los sprites (linealmente, igual que
los sprites), como: sprite0,sprite1,atributos0,atributos1.
o En el mismo array de datos gráficos, pero el atributo de un sprite va detrás de dicho sprite en el
vector, intercalado: sprite0,atributos0,sprite1,atributos1.
• Sprite que altere o no altere el fondo:
o Si no debe alterarlo, se tiene que decidir si será mediante impresión por operación lógica o si
será mediante máscaras (y dibujar y almacenar estas).

Además, hay que tener las herramientas para el dibujado y la conversión de los bitmaps o spritesets en código,
en el formato que hayamos decidido. Más adelante en el capítulo profundizaremos en ambos temas: la
organización en memoria del Sprite (o del Spriteset completo) y las herramientas de dibujo y conversión.

La creación de la rutina de impresión

Dadas las limitaciones en velocidad de nuestro querido procesador Z80A, el realizar una rutina de impresión de
sprites en alta resolución rápida es una tarea de complejidad media/alta que puede marcar la diferencia entre un
juego bueno y un juego malo, especialmente conforme aumenta el número de sprites en pantalla y por tanto, el
parpadeo de los mismos si la rutina no es suficientemente buena.
La complejidad de las rutinas que veremos concretamente en este capítulo será de un nivel más asequible
puesto que vamos a trabajar con posiciones de carácter en baja resolución y además crearemos varias rutinas
específicas y una genérica.

Para crear estas rutinas necesitamos conocer la teoría relacionada con:

• El cálculo de posición en memoria de las coordenadas (c,f) en las que vamos a dibujar el Sprite.
• El dibujado de cada scanline del sprite en pantalla, ya sea con LD/LDIR o con operaciones lógicas tipo
OR/XOR.
• El avance a través del sprite para acceder a otros scanlines del mismo.
• El avance diferencial en pantalla para movernos hacia la derecha (por cada bloque de anchura del
sprite), y hacia abajo (por cada scanline de cada bloque y por cada bloque de altura del sprite).
• El cálculo de posición en memoria de atributos del bloque (0,0) del sprite.
• El avance diferencial en la zona de atributos para imprimir los atributos de los sprites de más de 1×1
bloques.

Gracias a los 2 últimos capítulos del curso y a nuestros conocimientos en ensamblador, ya tenemos los
mecanismos para dar forma a la rutina completa.

Diseñaremos rutinas de impresión de sprites en baja resolución de 1×1 bloques (8×8 píxeles), 2×2 bloques
(16×16 píxeles) y NxM bloques. Las 2 primeras rutinas, específicas para un tamaño concreto, serán más
óptimas y eficientes que la última, que tendrá que adecuarse a cualquier tamaño de sprite y por lo tanto no
podrá realizar optimizaciones basadas en el conocimiento previo de ciertos datos.

Por ejemplo, cuando sea necesario multiplicar algún registro por el valor del ancho del sprite, en el caso de la
rutina de 1×1 no será necesario multiplicar y en el caso de la rutina de 2×2 podremos hacer uso de 1
desplazamiento a izquierda, pero la rutina de propósito general tendrá que realizar la multiplicación por medio
de un bucle de sumas. Así, imprimir un sprite de 2×2 con su rutina específica será mucho más rápido que
imprimir el mismo sprite con la genérica.

Aunque trataremos de optimizar las rutinas en la medida de lo posible, se va a intentar no realizar


optimizaciones que hagan la rutina ilegible para el lector. Las rutinas genéricas que veremos hoy serán rápidas
pero siempre podrán optimizarse más mediante trucos y técnicas al alcance de los programadores con más
experiencia. Es labor del programador avanzado el adaptar estas rutinas a cada juego para optimizarlas al
máximo en la medida de lo posible.

En este sentido, en alguna de las rutinas utilizaremos variables en memoria para alojar datos de entrada o datos
temporales o intermedios. Aunque acceder a la memoria es “lenta” comparada con tener los datos guardados en
registros, cuando comenzamos a manejar muchos parámetros de entrada (y de trabajo) en una rutina y además
hay que realizar cálculos con ellos, es habitual que agotemos los registros disponibles, más todavía teniendo en
cuenta la necesidad de realizar dichos cálculos. En muchas ocasiones se acaba realizando uso de la pila con
continuos PUSHes y POPs destinados a guardar valores y recuperarlos posteriormente a realizar los cálculos o
en ciertos puntos de la rutina.

Las instrucciones PUSH y POP toman 11 y 10 t-estados respectivamente, mientras que escribir o leer un valor
de 8 bits en memoria (LD (NN), A y LD A, (NN)) requiere 13 t-estados y escribir o leer un valor de 16 bits
toma 20 t-estados (LD (NN), rr y LD rr, (NN)) con la excepción de LD (NN), HL que cuesta 16 t-estados.
Instrucción Tiempo en t-estados
PUSH rr 11
PUSH IX o PUSH IY 16
POP rr 10
POP IX o POP IY 14
LD (NN), A 13
LD A, (NN) 13
LD rr, (NN) 20
LD (NN), rr 20
LD (NN), HL 16

Aunque es una diferencia apreciable, no siempre podemos obtener una “linealidad” de uso de la pila que
requiera 1 POP por cada PUSH, por lo que en ocasiones se hace realmente cómodo y útil el aprovechar
variables de memoria para diseñar las rutinas.

En nuestro caso utilizaremos algunas variables de memoria para facilitar la lectura de las rutinas: serán más
sencillas de seguir y más intuitivas a costa de algunos ciclos de reloj. No deja de ser cierto también que los
programadores en ocasiones nos obsesionamos por utilizar sólo registros y acabamos realizando combinaciones
de intercambios de valores en registros y PUSHes/POPs que acaban teniendo más coste que la utilización de
variables de memoria.

El programador profesional tendrá que adaptar cada rutina a cada caso específico de su programa y en este
proceso de optimización podrá (o no) sustituir dichas variables de memoria por combinaciones de código que
eviten su uso, aunque no siempre es posible dado el reducido juego de registros del Z80A.

Finalmente, recordar que las rutinas que veremos en este capítulo pueden ser ubicadas en memoria y llamadas
desde BASIC. Una vez ensambladas y POKEadas en memoria, podemos hacer uso de ellas utilizando POKE
para establecer los parámetros de llamada y RANDOMIZE USR DIR_RUTINA para ejecutarlas. A lo largo de
la vida de revistas como Microhobby se publicaron varios paquetes de rutinas de gestión de Sprites en
ensamblador que utilizan este método y que estaban pensadas para ser utilizadas tanto desde código máquina
como desde BASIC.

Organización de los sprites en memoria


Como ya hemos visto, una vez diseñados los diferentes sprites de nuestro juego hay que agruparlos en un
formato que después, convertidos a datos binarios, pueda interpretar nuestra rutina de impresión.

Hay 4 decisiones principales que tomar al respecto:

• Formato de organización del tileset (lineal o en forma de matriz/imagen).


• Formato de almacenamiento de cada tile (por bloques, por scanlines).
• Formato de almacenamiento de los atributos (después de los sprites, intercalados con ellos).
• Formato de almacenamiento de las máscaras de los sprites si las hubiera.
El formato de organización del tileset no debería requerir mucho tiempo de decisión: la organización del
tileset en formato lineal es mucho más eficiente para las rutinas de impresión de sprites que el almacenamiento
en una “imagen” rectangular. Teniendo todos los sprites (o tiles) en un único vector, podemos hacer referencia
a cualquier bloque, tile, sprite o cuadro de animación mediante un identificador numérico.

De esta forma, el “bloque 0” puede ser un bloque vacío, el bloque “1” el primer fotograma de animación de
nuestro personaje, etc.

Donde sí debemos tomar decisiones importantes directamente relacionadas con el diseño de la rutina de
impresión de Sprites es en la organización en memoria de los datos gráficos del sprite y sus atributos (y la
máscara si la hubiera). El formato de almacenamiento de los tiles, los atributos y los datos de máscara
definen la exportación de los datos desde el editor de Sprites y cómo debe de trabajar la rutina de impresión.

Veamos con un ejemplo práctico las distintas opciones de que disponemos. Para ello vamos a definir un
ejemplo basado en 2 sprites de 16×8 pixeles (2 bloques de ancho y 1 de alto, para simplificar). Marcaremos
cada scanline de cada bloque con una letra que representa el valor de 8 bits con el estado de los 8 píxeles, de
forma que podamos estudiar las posibilidades existentes.

Así, los 16 píxeles de la línea superior del sprite (2 bytes), los vamos a identificar como “A1” y “B1”. Los
siguientes 16 píxeles (scanline 2 del sprite y de cada uno de los 2 bloques), serán los bytes “C1” y “D1”, y así
sucesivamente.

Datos gráficos Sprite 1:


| A1 | B1 |
| C1 | D1 |
| E1 | F1 |
| G1 | H1 |
| I1 | J1 |
| K1 | L1 |
| M1 | N1 |
| O1 | P1 |

Atributos Sprite 1:
| S1_Attr1 | S1_Attr2 |

Y:

Datos gráficos Sprite 2:


| A2 | B2 |
| C2 | D2 |
| E2 | F2 |
| G2 | H2 |
| I2 | J2 |
| K2 | L2 |
| M2 | N2 |
| O2 | P2 |

Atributos Sprite 1:
| S2_Attr1 | S2_Attr2 |

Al organizar los datos gráficos y de atributos en disco, podemos hacerlo de 2 formas:

• Utilizando 2 arrays: uno con los datos gráficos y otro con los atributos, organizando la información
horizontal por scanlines del sprite. Todos los datos gráficos o de atributo de un mismo sprite son
consecutivos en memoria y el “salto” se hace al acabar cada scanline completo del sprite (no de cada
bloque). La rutina de impresión recibe como parámetro la dirección de inicio de ambas tablas y traza
primero los gráficos y después los atributos.
Tabla_Sprites:
DB A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1
DB L1, M1, N1, O1, P1, A2, B2, C2, D2, E2, F2
DB G2, H2, I2, J2, K2, L2, M2, N2, O2, P2

Tabla_Atributos:
DB S1_Attr1, S1_Attr2, S2_Attr1, S2_Attr2

• Utilizando un único array: Se intercalan los atributos dentro del array de gráficos, detrás de cada
Sprite. La rutina de impresión calculará en el array el inicio del sprite a dibujar y encontrará todos los
datos gráficos de dicho sprite seguidos a partir de este punto. Al acabar de trazar los datos gráficos, nos
encontramos directamente en el vector con los datos de atributo del sprite que estamos tratando.

Tabla_Sprites:
DB A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1
DB L1, M1, N1, O1, P1, S1_Attr1, S1_Attr2, A2
DB B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, L2
DB M2, N2, O2, P2, S2_Attr1, S2_Attr2

Finalmente, no debemos olvidarnos de que si utilizamos máscaras de sprite también deberemos incluirlas en
nuestro “array de datos” (o sprite set). El dónde ubicar cada scanline de la máscara depende, de nuevo, de
nuestra rutina de impresión. Una primera aproximación sería ubicar cada byte de máscara antes o después de
cada dato del sprite, para que podamos realizar las pertinentes operaciones lógicas entre la máscara, el fondo y
el sprite.

Si denominamos “XX” a los datos de la máscara del sprite 1 y “YY” a los datos de máscara del sprite 2, nuestra
tabla de datos en memoria quedaría de la siguiente forma:

; Formato: Una única tabla:


Tabla_Sprites:
DB XX, A1, XX, B1, XX, C1, XX, D1, XX, E1, XX, F1, XX, G1
DB XX, H1, XX, I1, XX, J1, XX, K1, XX, L1, XX, M1, XX, N1
DB XX, O1, XX, P1, S1_Attr1, S1_Attr2
DB YY, A2, YY, B2, YY, C2, YY, D2, YY, E2, YY, F2, YY, G2
DB YY, H2, YY, I2, YY, J2, YY, K2, YY, L2, YY, M2, YY, N2
DB YY, O2, YY, P2, S2_Attr1, S2_Attr2

; Formato: Dos tablas:


Tabla_Sprites:
DB XX, A1, XX, B1, XX, C1, XX, D1, XX, E1, XX, F1, XX, G1
DB XX, H1, XX, I1, XX, J1, XX, K1, XX, L1, XX, M1, XX, N1
DB XX, O1, XX, P1, YY, A2, YY, B2, YY, C2, YY, D2, YY, E2
DB YY, F2, YY, G2, YY, H2, YY, I2, YY, J2, YY, K2, YY, L2
DB YY, M2, YY, N2, YY, O2, YY, P2

Tabla_Atributos:
DB S1_Attr1, S1_Attr2, S2_Attr1, S2_Attr2

Para las rutinas que crearemos como ejemplo utilizaremos el formato lineal horizontal mediante 2 tablas, una
con los gráficos y otra con los atributos de dichos gráficos. En las rutinas con máscara, intercalaremos los datos
de máscara antes de cada dato del sprite, como acabamos de ver. Es el formato más sencillo para la generación
de los gráficos y para los cálculos en las rutinas, y por tanto el elegido para mostrar rutinas comprensibles por el
lector.

Sería posible también almacenar la información del sprite por columnas (formato lineal vertical), lo cual
requeriría rutinas diferentes de las que vamos a ver en este capítulo.
A continuación hablaremos sobre el editor de Sprites SevenuP y veremos de una forma gráfica el formato de
organización lineal-horizontal de datos en memoria, y cómo un gráfico de ejemplo se traduce de forma efectiva
en un array de datos con el formato deseado.

Conversion de datos graficos a códigos dibujables


Para diseñar los sprites de nuestro juego necesitaremos utilizar un editor de Sprites. Existen editores de sprites
nativos en el Spectrum, pero esa opción nos podría resultar realmente incómoda por usabilidad y gestión de los
datos (se tendría que trabajar en un emulador o la máquina real y los datos sólo se podrían exportar a cinta o a
TAP/TZX).

Lo ideal es utilizar un Editor de Sprites nativo de nuestra plataforma cruzada de desarrollo que permita el
dibujado en un entorno cómodo y la exportación de los datos a código “.asm” (ristras de DBs) que incluir
directamente en nuestro ensamblador.

Nuestra elección principal para esta tarea es SevenuP, de metalbrain. Nos decantamos por SevenuP por su clara
orientación al dibujo “al pixel” y sus opciones para el programador, especialmente el sistema de exportación de
datos a C y ASM y la gestión de máscaras y frames de animación. Además, SevenuP funciona bajo múltiples
Sistemas Operativos, siendo la versión para Microsoft Windows emulable también en implementaciones como
WINE de GNU/Linux.

Para el propósito de este capítulo (y, en general durante el proceso de creación de un juego), dibujaremos en
SevenuP nuestro spriteset con los sprites distribuídos verticalmente (cada sprite debajo del anterior). Crearemos
un nuevo “sprite” con File → New e indicaremos el ancho de nuestro sprite en pixels y un valor para la altura
que nos permita alojar suficientes sprites en nuestro tileset.
Por ejemplo, para guardar la información de 10 sprites de 16×16 crearíamos un nuevo sprite de 16×160 píxeles.
Si nos vemos en la necesidad de ampliar el sprite para alojar más sprites podremos “cortar” los datos gráficos,
crear una imagen nueva con un tamaño superior y posteriormente pegar los datos gráficos cortados. La
documentación de SevenUp explica cómo copiar y pegar:

Modo de Selección 1:
====================

Set Pixel/Reset Pixel


El botón izquierdo pone los pixels a 1.
El botón derecho pone los pixels a 0.
Atajo de teclado: 1

Modo de Selección 2:
====================

Toggle Pixel/Select Zone


El botón izquierdo cambia el valor de los pixels entre 0 y 1.
El botón derecho controla la selección. Para seleccionar una zona,
se hace click-derecho en una esquina, click-derecho en la opuesta y ya
tenemos una porción seleccionada. La zona seleccionada será algo mas
brillante que la no seleccionada y las rejillas (si están presentes)
se verán azules. Ahora los efectos solo afectarán a la zona seleccionada,
y se puede copiar esta zona para pegarla donde sea o para usarla como
patrón en el relleno con textura. Un tercer click-derecho quita la
selección. Atajo de teclado: 2

Copy
Copia la zona seleccionada (o el gráfico completo si no hay zona seleccionada)
a la memoria intermedia para ser pegada (en el mismo gráfico o en otro) o para
ser usada como textura de relleno. Atajo de teclado: CTRL+C.

Paste
Activa/desactiva el modo de pegado, que pega el gráico de la memoria intermedia
a la posición actual del ratón al pulsar el botón izquierdo. Los atributos solo
se pegan si el pixel de destino tiene la misma posición dentro del carácter que
la fuente de la copia. Con el botón derecho se cancela el modo de pegado. Atajo
de teclado: CTRL+V.

Otra opción es trabajar con un fichero .sev por cada sprite del juego, aprovechando así el soporte para
“fotogramas” de SevenuP. No obstante, suele resultar más cómodo mantener todos los sprites en un único
fichero con el que trabajar ya que podemos exportar todo con una única operación y nos evita tener que
“mezclar” las múltiples exportaciones de cada fichero individual.

Mediante el ratón (estando en modo 1) podemos activar y desactivar píxeles y cambiar el valor de tinta y papel
de cada recuadro del Sprite. El menú de máscara nos permite definir la máscara de nuestros sprites, alternando
entre la visualización del sprite y la de la máscara.

El menú de efectos nos permite ciertas operaciones básicas con el sprite como la inversión, rotación, efecto
espejo horizontal o vertical, rellenado, etc.

Es importante que guardemos el fichero en formato .SEV pues es el que nos permitirá realizar modificaciones
en los gráficos del programa y una re-exportación a ASM si fuera necesario.

Antes de exportar los datos a ASM, debemos definir las opciones de exportación en File → Output Options:
Este menú permite especificar diferentes opciones de exportación:

• Data outputted: Permite seleccionar si queremos exportar sólo los gráficos, sólo los atributos, o los
dos, primero gráficos y luego atributos o primero atributos y luego gráficos.
• Mask Before Graph: Si activamos esta opción, cada byte del sprite irá precedido en el array por su
byte de máscara correspondiente.
• Sort priority: Esta importantísima opción determina el orden de la exportación, indicando a SevenuP
qué orden / prioridad debe de seguir al recorrer el gráfico para exportar los valores. Las diferentes
opciones para priorizar son:
o X Char: Coordenada X de bloque. Prioriza el recorrer el sprite horizontalmente aunque pasemos
a otro bloque del mismo.
o Char line: Scanline horizontal de bloque. Prioriza acabar los datos del bloque actual
horizontalmente antes de pasar al siguiente elemento.
o Y Char: Coordenada Y de bloque. Prioriza el recorrer el bloque actual verticalmente antes de
bajar al siguiente.
o Mask: Prioriza el valor de máscara del byte actual sobre el resto de elementos.
• Interleave: Permite definir la forma en que se intercalan gráficos y atributos en el sprite.

Veamos las opciones que debemos especificar de forma predeterminada para exportar los datos de nuestro set
de sprites en el formato adecuado para las rutinas utilizadas en este capítulo:

• Múltiples sprites en formato vertical sin máscara y sin atributos en 1 array:


o Sort Priorities: X char, Char line, Y char
o Data Outputted: Gfx
o Mask: No
• Múltiples sprites en formato vertical sin máscara y con atributos en 1 array:
o Sort Priorities: X char, Char line, Y char
o Data Outputted: Gfx+Attr
o Mask: No
• Múltiples sprites en formato vertical sin máscara y con atributos en 2 arrays:
o Sort Priorities: X char, Char line, Y char
o Data Outputted: Primero exportamos Gfx y luego Attr
o Mask: No
• Múltiples sprites en formato vertical con máscara intercalada y con atributos:
o Sort Priorities: Mask, X char, Char line, Y char
o Data Outputted: Gfx+Attr
o Mask: Yes, before graphic
• Múltiples sprites en formato vertical con máscara intercalada y con atributos en 2 arrays:
o Sort Priorities: Mask, X char, Char line, Y char
o Data Outputted: Primero exportamos Gfx y luego Attr
o Mask: Yes, before graphic

Tras establecer las opciones adecuadas para el gráfico en cuestión, seleccionamos File → Export Data: para
generar un fichero de texto de extensión .asm con los datos en el formato elegido.

Veamos un ejemplo bastante claro de las posibilidades de exportación utilizando dos sprites de 2×2 bloques
(16×16) con valores binarios fácilmente identificables para cada uno de los 8 bloques que forman estos 2 sprites
de 16×16:

El spriteset es, pues, de 16×32 píxeles, o lo que es lo mismo, 2 sprites de 2×2 bloques colocados verticalmente.

Hemos rellenado cada “bloque” del spriteset con un patrón de píxeles diferente que sea claramente identificable
en su conversión a valor numérico, de forma que los 8 scanlines de cada bloque tienen el mismo valor, pero que
a su vez es diferente de los valores de los demás bloques:

------------
| 1 | 128 | <-- Bloques 1 y 2 de Sprite 1
------------
| 2 | 64 | <-- Bloques 3 y 4 de Sprite 1
------------
| 4 | 32 | <-- Bloques 1 y 2 de Sprite 2
------------
| 8 | 16 | <-- Bloques 3 y 4 de Sprite 2
------------

Veamos el resultado de la exportación del Sprite con diferentes opciones:

Multiples sprites en formato vertical sin máscara y sin atributos:

Marcamos en “Data Outputted” la opción “Gfx”, de forma que no se exporten los atributos. Asímismo,
definimos como “Sort Priority” el orden “X char, Char line, Y Char”. Esto da prioridad a los scanlines
completos (Char Line) sobre la coordenada Y de cada carácter (Y Char), por lo que tendremos un scanline de
cada carácter en nuestro export, avanzando hacia abajo en nuestros 2 sprites:

;Sort Priorities: X char, Char line, Y char


;Data Outputted: Gfx
;Interleave: Sprite
;Mask: No

Sprite_Sin_Atributos:
DEFB 1,128, 1,128, 1,128, 1,128
DEFB 1,128, 1,128, 1,128, 1,128
DEFB 2, 64, 2, 64, 2, 64, 2, 64
DEFB 2, 64, 2, 64, 2, 64, 2, 64
DEFB 4, 32, 4, 32, 4, 32, 4, 32
DEFB 4, 32, 4, 32, 4, 32, 4, 32
DEFB 8, 16, 8, 16, 8, 16, 8, 16
DEFB 8, 16, 8, 16, 8, 16, 8, 16

Multiples sprites en formato vertical sin máscara y con atributos al final:

Si marcamos la opción “Data Outputted” = “Gfx + Attr”, obtendremos el siguiente export:

;Sort Priorities: X char, Char line, Y char


;Data Outputted: Gfx+Attr
;Interleave: Sprite
;Mask: No

Sprite_con_atributos:
DEFB 1,128, 1,128, 1,128, 1,128
DEFB 1,128, 1,128, 1,128, 1,128
DEFB 2, 64, 2, 64, 2, 64, 2, 64
DEFB 2, 64, 2, 64, 2, 64, 2, 64
DEFB 4, 32, 4, 32, 4, 32, 4, 32
DEFB 4, 32, 4, 32, 4, 32, 4, 32
DEFB 8, 16, 8, 16, 8, 16, 8, 16
DEFB 8, 16, 8, 16, 8, 16, 8, 16
DEFB 57, 58, 59, 60, 61, 62, 57, 56

El array de datos resultante es esencialmente igual al anterior, salvo que se añaden los 8 bytes de atributos (2
sprites de 16×16 = 2×2 bytes por cada sprite = 8 bytes de atributo).

Multiples sprites en formato vertical con máscara y con atributos al final:


Con las mismas opciones que en el caso anterior, pero activando la opción “Mask Before Graph” y subiendo la
prioridad de la máscara al máximo obtenemos el siguiente array de datos:

;Sort Priorities: Mask, X char, Char line, Y char


;Data Outputted: Gfx+Attr
;Interleave: Sprite
;Mask: Yes, before graphic

Sprite_con_mascara_y_atributos:
DEFB 254, 1,127,128,254, 1,127,128
DEFB 254, 1,127,128,254, 1,127,128
DEFB 254, 1,127,128,254, 1,127,128
DEFB 254, 1,127,128,254, 1,127,128
DEFB 253, 2,191, 64,253, 2,191, 64
DEFB 253, 2,191, 64,253, 2,191, 64
DEFB 253, 2,191, 64,253, 2,191, 64
DEFB 253, 2,191, 64,253, 2,191, 64
DEFB 251, 4,223, 32,251, 4,223, 32
DEFB 251, 4,223, 32,251, 4,223, 32
DEFB 251, 4,223, 32,251, 4,223, 32
DEFB 251, 4,223, 32,251, 4,223, 32
DEFB 247, 8,239, 16,247, 8,239, 16
DEFB 247, 8,239, 16,247, 8,239, 16
DEFB 247, 8,239, 16,247, 8,239, 16
DEFB 247, 8,239, 16,247, 8,239, 16
DEFB 57, 58, 59, 60, 61, 62, 57, 56

En este export, hemos habilitado el uso de máscaras y el byte de máscara de cada scanline aparece justo antes
del byte de datos del mismo. Los atributos permanecen al final del array.

Multiples sprites en formato vertical separando GFX y ATTR en 2 tablas:

Podemos obtener la información del Sprite en 2 tablas separadas realizando 2 exportaciones con las anteriores
configuraciones que hemos visto: bastará con realizar la primera como Outputted Data = Gfx y la segunda
como Outputted Data = Attr, marcando la opción de Append Data para no sobreescribir el fichero destino en el
segundo export.

De esta forma, utilizando como ejemplo una exportación de datos sin máscara, el fichero resultante tendrá el
siguiente contenido:

;Sort Priorities: X char, Char line, Y char


;Data Outputted: Gfx
;Interleave: Sprite
;Mask: No

Sprites:
DEFB 1,128, 1,128, 1,128, 1,128
DEFB 1,128, 1,128, 1,128, 1,128
DEFB 2, 64, 2, 64, 2, 64, 2, 64
DEFB 2, 64, 2, 64, 2, 64, 2, 64
DEFB 4, 32, 4, 32, 4, 32, 4, 32
DEFB 4, 32, 4, 32, 4, 32, 4, 32
DEFB 8, 16, 8, 16, 8, 16, 8, 16
DEFB 8, 16, 8, 16, 8, 16, 8, 16

;Sort Priorities: X char, Char line, Y char


;Data Outputted: Attr
;Interleave: Sprite
;Mask: No

Atributos:
DEFB 57, 58, 59, 60, 61, 62, 57, 56
Como puede verse, SevenuP nos permite realizar la exportación tal y como la necesitemos en nuestras rutinas
de impresión. Nosotros utilizaremos principalmente los 3 formatos que acabamos de ver, pero otras rutinas
pueden necesitar de otra organización diferente, la cual podemos lograr alterando estos parámetros.

Aconsejamos al lector la lectura del fichero README de SevenuP para conocer todas sus funciones y la forma
de utilizarlas.

Por otra parte, si no nos resultase cómodo trabajar con SevenuP como editor, podemos utilizarlo simplemente
como exportador de datos. Basta con utilizar un programa de dibujo clásico (xpaint, kpaint, MSPaint,
PaintShop Pro, o incluso The GIMP o Photoshop) y dibujar nuestros sprites en formato tileset vertical.
Debemos utilizar los colores de la paleta del Spectrum y las mismas restricciones que nos encontraríamos con
el sistema de atributos del Spectrum (celdillas de 8×8, 16 colores, etc.). El fichero final debe guardarse
preferentemente como PNG sin compresión.

Después, utilizaremos la opción de importación de SevenuP (File → Import) para convertir la imagen de color
“real” a un mapa de bits ya editable en SevenuP. Con la imagen importada en SevenuP, y tras realizar los
retoques que consideremos oportunos, realizamos una exportación a código ASM con los parámetros de
configuración de exportación correctos para nuestra rutina de impresión de Sprites.

Paso de parámetros a las rutinas


Al programar esta rutina, y cualquiera de las que veremos en este capítulo, debemos decidir cómo pasar los
parámetros de entrada a la misma, ya que en estas rutinas manejaremos bastantes parámetros y en alguno de los
casos no tendremos suficientes registros libres para establecerlos antes del CALL.

En este sentido, podemos utilizar para el paso de los parámetros, las siguientes posibilidades:

• Registros de 8 y 16 bits, allá donde sea posible, especialmente en rutinas con pocos parámetros de
entrada y que sean llamadas en el programa en momentos críticos o gran cantidad de veces.
• La Pila: realizando PUSH de los parámetros de entrada en un orden concreto. La rutina, en su punto
inicial, hará POP de dichos valores en los registros adecuados. Tiene la ventaja de que podemos ir
recuperando los valores conforme los vayamos necesitando y tras haber realizado cálculos con los
parámetros anteriores que nos hayan dejado libres registros para los siguientes cálculos.
• El Stack del Calculador: Este método permite que programa BASIC puedan llamar a nuestras subrutinas
en ensamblador mediante DEF FN. Su principal desventaja es la lentitud y la no portabilidad a otros
Sistemas.
• Variables de memoria: podemos establecer los valores de entrada en variables de memoria con LD y
recuperarlos dentro de la rutina también con instrucciones de carga LD. Esta técnica tiene una
desventaja: la “lentitud” del acceso a memoria para lectura y escritura, pero también tiene sustanciales
ventajas:
o Los registros quedan libres para realizar todo tipo de operaciones.
o No tenemos que preservar los valores de los parámetros de entrada al realizar operaciones con
los registros, y podemos recuperarlos en cualquier otro punto de la rutina para realizar nuevos
cálculos.
o Nos permite llamar a las rutinas desde BASIC, estableciendo los parámetros con POKE y
después realizando el RANDOMIZE USR direccion_rutina.
Lo normal en rutinas de este tipo sería utilizar en la medida de lo posible el paso mediante registros (programa
en ASM puro) o mediante la pila (programas en C y ASM), pero en nuestros ejemplos utilizaremos el último
método: paso de variables en direcciones de memoria, con el objetivo de que las rutinas puedan llamarse desde
BASIC y para que sean lo suficientemente sencillas de leer para que cada programador pueda adaptar la entrada
de una rutina concreta a las necesidades de su programa utilizando otro de los métodos de paso de parámetros
descrito.

Es habitual incluso que en los programas ni siquiera se le pase a las rutinas las direcciones origen de Sprites y
Atributos sino que dichas direcciones estén “hardcodeadas” dentro del código de la rutina, utilizando un único
array de tiles y otro de atributos con unas direcciones concretas y exactas. Este podría ser un paso básico de
optimización que evitaría carga y paso de parámetros y nos permitiría recuperar direcciones de origen de los
arrays en cualquier punto de la rutina.

Por otra parte, el paso de parámetros es sólo un pequeño porcentaje del tiempo total de la rutina: el interés de
optimización residirá en la impresión del sprite en sí, ya que esta recogida de parámetros se realiza una sola vez
por sprite independientemente del número de bloques que lo compongan (en cuyo dibujado será donde
realicemos el mayor gasto de tiempo).

Trazado de Sprites de 8x8


Comencemos con la primera rutina de impresión de sprites: trazaremos un sprite de 8×8 píxeles (1 bloque) en
una dirección de baja resolución (c,f) de la pantalla. Este tipo de Sprite es parecido a lo que en BASIC se
denomina “UDG” (User Defined Graphic), pero sin las limitaciones en el número de UDGs posibles a definir.

El sprite a utilizar como “modelo” para desarrollar la rutina y para un posterior ejemplo de aplicación de la
misma será el siguiente:

El bitmap con el que se corresponde este sprite y los valores binarios de los scanlines son los siguientes:

Pixeles Binario Decimal


--------------------------------
---***-- = 00011100 = 28
--*****- = 00111110 = 62
-**-*-** = 01101011 = 107
-******* = 01111111 = 127
-*-***-* = 01011101 = 93
-**---** = 01100011 = 99
--*****- = 00111110 = 62
---***-- = 00011100 = 28

Efectivamente, si exportamos el sprite en SevenuP con los parámetros “X Char, Char line, Y Char, Mask”, y
exportando por un lado el gráfico y por otro los atributos, obtenemos el siguiente fichero fuente:

; SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain

;GRAPHIC DATA:
;Pixel Size: ( 8, 8) - (1, 1)
;Sort Priorities: Char line

cara_gfx:
DEFB 28, 62,107,127, 93, 99, 62, 28

cara_attrib:
DEFB 56

Sobreescribiendo el fondo (impresión con LD)

La primera de las rutinas que veremos realizará transferencias directas de datos entre el origen (el sprite) y el
destino (la pantalla).

Para nuestra rutina utilizaremos el siguiente esquema de paso de parámetros:

Dirección Parámetro
50000 Dirección de la tabla de Sprites
50002 Dirección de la tabla de Atributos
50004 Coordenada X en baja resolución
50005 Coordenada Y en baja resolución
50006 Numero de sprite a dibujar (0-N)

El pseudocódigo de la rutina es el siguiente:

; Recoger parametros de entrada


; Calcular posicion origen (array sprites) en DE como
; direccion = base_sprites + (NUM_SPRITE*8)
; Calcular posicion destino (pantalla) en DE con X e Y

; Repetir 8 veces:
; Dibujar scanline (DE) -> (HL)
; Incrementar scanline del sprite (DE).
; Bajar a siguiente scanline en pantalla (HL).

; Si base_atributos == 0 -> RET


; Calcular posicion origen de los atributos array_attr+NUM_SPRITE en HL.
; Calcular posicion destino en area de atributos en DE.
; Copiar (HL) en (DE)

La rutina debe de comenzar calculando la direcciones origen y destino para las transferencias de datos.
La dirección origen es el primer scanline del sprite a dibujar. Dada una tabla de sprites 8×8 en formato vertical,
y teniendo en cuenta que cada sprite 8×8 ocupa 8 bytes (1 byte por cada scanline, 8 scanlines), nos
posicionaremos en el sprite correcto “saltando” 8 bytes por cada sprite que haya antes del sprite que buscamos:

DIR_MEMORIA_GFX = DIR_BASE_SPRITES + (NUM_SPRITE_A_DIBUJAR * 8)

La dirección destino en pantalla la calculamos a partir de las coordenadas C, F (x/8,y/8) con las técnicas que
vimos en el capítulo anterior; en este caso, mediante coordenación en baja resolución.

Una vez tenemos la dirección origen y destino para los gráficos, realizamos un bucle de 8 iteraciones (por haber
8 scanlines) que haga lo siguiente:

• Copiamos el byte apuntado por DE (scanline de sprite) a la dirección apuntada por HL (pantalla).
• Incrementamos DE (siguiente scanline del sprite).
• Cambiamos HL para que apunte en pantalla al scanline de debajo del actual (sumando 1 a la parte alta).

Tras las 8 iteraciones tendremos el gráfico completo dibujado en pantalla, y faltará el procesado de los
atributos.

La misma rutina que vamos a crear servirá para dibujar gráficos con atributos o sin atributos. Nos puede
interesar el dibujado de gráficos sin atributos en juegos monocolor donde los atributos de fondo ya están
establecidos en pantalla y no sea necesario re-escribirlos, ahorrando ciclos de reloj al no realizar esta tarea sin
efectos en pantalla. Nótese que aunque no dibujemos los atributos de un sprite, esto no quiere decir que no
tendrá color en pantalla: si no modificamos los atributos de una posición (c,f), el sprite que dibujemos en esas
coordenadas adoptará los colores de tinta y papel que ya tenía esa posición de pantalla.

En lugar de crear 2 rutinas diferentes, una que imprima un sprite con atributos y otra que lo haga sin ellos,
vamos a utilizar en este capítulo una única rutina indicando en el parámetro de la dirección de atributos si
queremos dibujarlos o no. La rutina comprobará si la dirección de atributos es 0 (basta con comprobar su parte
alta) y si es así, saldrá con un RET sin realizar los cálculos de atributos (dirección origen, destino, y dibujado).

Utilizaremos este sistema de comprobación (DIR_ATTR==0) para evitar la necesidad de crear 2 rutinas
diferentes, aunque en un juego lo normal será que adaptemos la rutina al caso concreto y exacto (SIN o CON
atributos) para evitar la comprobación de DIR_ATTR=0 y cualquier otro código no necesario.

Continuemos: Si DIR_ATRIBUTOS (50002) no es cero, se utilizará dicha dirección y el número del sprite para
calcular la posición del único atributo que tenemos que trazar en pantalla (sprite 8×8 = 1 único atributo). Como
cada sprite tiene un atributo de 1 byte, esta dirección origen será:

DIR_MEMORIA_ATTR = DIR_BASE_ATTRIBS + NUM_SPRITE_A_DIBUJAR

Es decir, saltamos 1 byte (un atributo) por cada sprite 8×8 (1 bloque=1 atributo) para posicionarnos en el
atributo del sprite DS_NUMSPR.

La dirección destino en el área de atributos se calcula con las rutinas que vimos en el capítulo anterior, y la
transferencia destino se realiza con instrucciones de carga.

Los cálculos y la transferencia de atributos los haremos usando HL como puntero origen y DE como puntero
destino, ya que necesitamos realizar ADDs de 16 bits para calcular la dirección origen del atributo y el único
registo que soporta esta operación como destino es HL.

Hemos asumido que nuestra rutina trabaja con una tabla de sprites en formato vertical, pero en nuestro ejemplo
tenemos un único sprite. Esto no importa porque tener un único sprite es un subcaso de la tabla de sprites donde
NUM_SPRITE=0, por lo que las mismas rutinas nos sirven si les solicitamos imprimir el sprite número “0”.
;-------------------------------------------------------------
; DrawSprite_8x8_LD:
; Imprime un sprite de 8x8 pixeles con o sin atributos.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
;-------------------------------------------------------------
DrawSprite_8x8_LD:

; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X


LD BC, (DS_COORD_X)

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A ; Ya tenemos la parte alta calculada (010TT000)
LD A, B ; Ahora calculamos la parte baja
AND 7
RRCA
RRCA
RRCA ; A = NNN00000b
ADD A, C ; Sumamos COLUMNA -> A = NNNCCCCCb
LD E, A ; Lo cargamos en la parte baja de la direccion
; DE contiene ahora la direccion destino.

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*8)

LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD H, 0
LD L, A ; HL = DS_NUMSPR
ADD HL, HL ; HL = HL * 2
ADD HL, HL ; HL = HL * 4
ADD HL, HL ; HL = HL * 8 = DS_NUMSPR * 8
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 8)
; HL contiene la direccion de inicio en el sprite

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; Dibujar 8 scanlines (DE) -> (HL) y bajar scanline


;;; Incrementar scanline del sprite (DE)

LD B, 8 ; 8 scanlines -> 8 iteraciones

drawsp8x8_loopLD:
LD A, (DE) ; Tomamos el dato del sprite
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en sprite
INC H ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawsp8x8_loopLD

;;; En este punto, los 8 scanlines del sprite estan dibujados.


LD A, H
SUB 8 ; Recuperamos la posicion de memoria del
LD B, A ; scanline inicial donde empezamos a dibujar
LD C, L ; BC = HL - 8

;;; Considerar el dibujado de los atributos (Si DS_ATTRIBS=0 -> RET)


LD HL, (DS_ATTRIBS)

XOR A ; A = 0
ADD A, H ; A = 0 + H = H
RET Z ; Si H = 0, volver (no dibujar atributos)

;;; Calcular posicion destino en area de atributos en DE.


LD A, B ; Codigo de Get_Attr_Offset_From_Image
RRCA ; Obtenemos dir de atributo a partir de
RRCA ; dir de zona de imagen.
RRCA ; Nos evita volver a obtener X e Y
AND 3 ; y hacer el calculo completo de la
OR $58 ; direccion en zona de atributos
LD D, A
LD E, C ; DE tiene el offset del attr de HL

LD A, (DS_NUMSPR) ; Cogemos el numero de sprite a dibujar


LD C, A
LD B, 0
ADD HL, BC ; HL = HL+DS_NUMSPR = Origen de atributo

;;; Copiar (HL) en (DE) -> Copiar atributo de sprite a pantalla


LD A, (HL)
LD (DE), A ; Mas rapido que LDI (7+7 vs 16 t-estados)
RET ; porque no necesitamos incrementar HL y DE

Al respecto del código de la rutina, caben destacar las siguientes consideraciones:

• Nótese que en la rutina se emplean las subrutinas Get_Char_Offset_LR y Attr_Offset_From_Image con


el código de las mismas embebido dentro de la rutina principal. Esto se hace con el objetivo de evitar los
correspondientes CALLs y RET y para poder personalizarlas (en este caso, están modificadas para
devolver la dirección calculada en DE en lugar de en HL, por requerimientos del código de
DrawSprite_8x8).

• Para realizar la transferencia de datos entre el sprite (apuntado por DE) y la pantalla (apuntada por HL)
hemos utilizado 2 instrucciones de transferencia LD usando A como registro intermedio en lugar de
utilizar una instrucción LDI. Más adelante veremos el por qué de esta elección.

• Como ya vimos en el capítulo anterior, para avanzar o retroceder el puntero HL en pantalla, en lugar de
utilizar DEC HL o INC HL (6 t-estados), realizamos un DEC L o INC L (4 t-estados). Esto es posible
porque dentro de un mismo scanline de pantalla no varía el valor del byte alto de la dirección. Esta
pequeña optimización no podemos realizarla con el puntero de datos del Sprite porque no tenemos la
certeza de que esté dentro de una página de 256 bytes y que, por lo tanto, alguno de los incrementos del
puntero deba modificar la parte alta del mismo.

• La rutina que hemos visto, por simplicar el código, utiliza un bucle de 8 iteraciones para dibujar los 8
scanlines. Esto ahorra espacio (ocupación de la rutina) pero implica un testeo del contador y salto por
cada iteración (excepto en la última). En una rutina crítica, si tenemos suficiente espacio libre, y si
conocemos de antemano el número de iteraciones exacto de un bucle, lo óptimo sería desenrollar el
bucle, es decir, repetir 8 veces el código de impresión. De este modo evitamos el LD B, 8 y el DJNZ
bucle.

En el caso de nuestra rutina de ejemplo, cambiaríamos…

LD B, 8 ; 8 scanlines

drawsp8x8_loopLD:
LD A, (DE) ; Tomamos el dato del sprite
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en sprite
INC H ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawsp8x8_loopLD

… por:
LD A, (DE) ; Scanline 0
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 1
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 2
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 3
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 4
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 5
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 6
LD (HL), A
INC DE
INC H

LD A, (DE) ; Scanline 7
LD (HL), A
INC DE
;;;INC H ; no es necesario el ultimo INC H

Nótese cómo al desenrollar el bucle ya no es necesario el último INC H para avanzar al siguiente scanline de
pantalla. El INC DE sí que es necesario ya que tenemos que avanzar en el sprite al primero de los atributos
(aunque este INC también podría realizarse después del código de comprobación de la dirección de atributo,
evitando hacerlo si no queremos imprimirlos).

Al no ser necesario el INC H, en la versión desenrollada del bucle tenemos que cambiar la resta de HL - 8 por
HL - 7:

;;; En este punto, los 8 scanlines del sprite estan dibujados.


LD A, H
SUB 7 ; Recuperamos la posicion de memoria del
LD B, A ; scanline inicial donde empezamos a dibujar
LD C, L ; BC = HL - 7

Lo normal es desenrollar sólo aquellas rutinas lo suficiente críticas e importantes como para compensar el
mayor espacio en memoria con un menor tiempo de ejecución. En esta rutina evitaríamos la pérdida de ciclos
de reloj en el establecimiento del contador, en el testeo de condición de salida y en el salto, a cambio de una
mayor ocupación de espacio tras el ensamblado.

Una rutina de impresión de sprites de tamaños fijos (8×8, 16×16, 8×16, etc) en la que conocemos el número de
iteraciones verticales y horizontales para la impresión es uno de los casos típicos en los que usaremos esta
técnica. Si la rutina fuera para sprites de tamaño variable (NxM), no podríamos aplicarla porque necesitamos
los bucles de N y M iteraciones que no podemos sustituir de antemano.
El programa de ejemplo que veremos a continuación utiliza la rutina anterior (no incluída en el listado) para
imprimir el sprite de ejemplo 8×8 que hemos visto:

; Ejemplo impresion sprites 8x8


ORG 32768

DS_SPRITES EQU 50000


DS_ATTRIBS EQU 50002
DS_COORD_X EQU 50004
DS_COORD_Y EQU 50005
DS_NUMSPR EQU 50006

CALL ClearScreen_Pattern

; Establecemos los parametros de entrada a la rutina


; Los 2 primeros se pueden establecer una unica vez
LD HL, cara_gfx
LD (DS_SPRITES), HL
LD HL, cara_attrib
LD (DS_ATTRIBS), HL
LD A, 15
LD (DS_COORD_X), A
LD A, 8
LD (DS_COORD_Y), A
XOR A
LD (DS_NUMSPR), A

CALL DrawSprite_8x8_LD

loop:
JR loop
RET

;--------------------------------------------------------------------
; ClearScreen_Pattern
; Limpia la pantalla con patrones de pixeles alternados
;--------------------------------------------------------------------
ClearScreen_Pattern:
LD B, 191 ; Numero de lineas a rellenar

cs_line_loop:
LD C, 0
LD A, B
LD B, A
CALL $22B1 ; ROM (Pixel-Address)

LD A, B
AND 1
JR Z, cs_es_par
LD A, 170
JR cs_pintar

cs_es_par:
LD A, 85

cs_pintar:
LD D, B ; Salvar el contador del bucle
LD B, 32 ; Imprimir 32 bytes

cs_x_loop:
LD (HL), A
INC HL
DJNZ cs_x_loop

LD B, D ; Recuperamos el contador externo


DJNZ cs_line_loop ; Repetimos 192 veces
RET
;--------------------------------------------------------------------
;SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain
;GRAPHIC DATA:
;Pixel Size: ( 8, 8) - (1, 1)
;Sort Priorities: Char line
;--------------------------------------------------------------------

cara_gfx:
DEFB 28, 62,107,127, 93, 99, 62, 28

cara_attrib:
DEFB 56

;--------------------------------------------------------------------
DrawSprite_8x8_LD:
;;; codigo de la rutina...

El resultado de la ejecución del anterior programa es el siguiente:

Transferencia por LDI vs LD+INC

En nuestra rutina de impresión hemos utilizado instrucciones de carga entre memoria para transferir bytes desde
la dirección apuntada por DE (el origen; el Sprite) a la dirección apuntada por HL (el destino; la pantalla).

Para trazar los píxeles en pantalla podríamos haber utilizado la instrucción LDI, que con 16 t-estados realiza
una transferencia de 1 byte entre la dirección de memoria apuntada por HL (origen) y la apuntada por DE
(destino), y además incrementa HL y DE.

El bucle principal de impresión de nuestro programa es el siguiente:

drawsp8x8_loop:
LD A, (DE) ; A = (DE) = leer dato del sprite
LD (HL), A ; (HL) = A = escribir dato a la pantalla
INC DE ; Incrementamos DE (puntero sprite)
INC H ; Incrementamos scanline HL (HL+=256)
DJNZ drawsp8x8_loop

Las instrucciones antes del DJNZ tienen un coste de ejecución de 7, 7, 6 y 4 t-estados respectivamente
(empezando por el LD A, (DE) y acabando por el INC H). Esto suma un total de 24 t-estados por cada byte
transferido.

Si invertimos el uso de los punteros y utilizamos HL como puntero al Sprite (origen) y DE como puntero a
pantalla (destino), el bucle anterior podría haberse reescrito de la siguiente forma:
drawsp8x8_loop:
LDI ; Copia (HL) en (DE) y HL++ DE++
INC D ; Sumamos 256 (+1=257)
DEC E ; Restamos 1 (+=256)
DJNZ drawsp8x8_loop

Aunque es un formato más compacto, el coste de ejecución es el mismo (16+4+4 = 24 t-estados).

Entre las 2 posibles formas de realizar la impresión (LD+INC vs LDI), utilizaremos la primera porque, como
veremos a continuación, la impresión de sprites mediante operaciones lógicas o mediante máscaras no permite
el uso de LDI y utilizar la primera técnica hace todas las rutinas muy similares entre sí y por lo tanto podremos
aplicar en todas cualquier mejora u optimización de una forma más rápida y sencilla.

Además, LDI decrementa el registro BC tras las transferencia, por lo que si lo utilizamos en un bucle tenemos
que tener en cuenta que cada LDI puede alterar el valor de BC y por tanto del contador de iteraciones del bucle,
lo cual es otro motivo para elegir LD+INC vs LDI.

Respetando el fondo (impresión con OR)

Veamos una ampliación del Sprite de 8×8 del ejemplo anterior impreso sobre un fondo no plano:

Nótese cómo la impresión del sprite no ha respetado el fondo en los píxeles a cero del mismo: al establecer el
valor del byte en pantalla con un LD, hemos establecido a cero en pantalla los bits que estaban a cero en el
sprite sin respetar el valor que hubiera en videoram para dichos bits.

Ejemplo:

Valor en Videomemoria (HL): 10101010


Valor en el sprite - reg. A: 00111110
Operación: LD (HL), A
Resultado en VRAM: 00111110

La primera de las soluciones a este problema es la de escribir los píxeles con una operación lógica OR entre el
scanline y el valor actual en memoria. Con la operación OR mezclaremos los bits de ambos elementos:

Ejemplo:

Valor en Videomemoria (HL): 10101010


Valor en el sprite - reg. A: 00111110
Operación: LD + OR/XOR
Resultado en VRAM: 10111110

La operación OR nos permitirá respetar el fondo en aquellos juegos en que los sprites no tengan zonas a 0
dentro del contorno del mismo (que, como veremos más adelante, no es el caso de nuestro pequeño sprite).
Para modificar la rutina de impresión de Sprites de forma que utilice OR (u otra operación lógica con otras
aplicaciones como XOR), sólo necesitamos añadir la correspondiente instrucción lógica entre A y el contenido
de la memoria:

Cambiamos el bucle de impresión…

LD B, 8 ; 8 scanlines -> 8 iteraciones

drawsp8x8_loopLD:
LD A, (DE) ; Tomamos el dato del sprite
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en sprite
INC H ; Incrementamos puntero en pantalla
DJNZ drawsp8x8_loopLD

por:

LD B, 8 ; 8 scanlines -> 8 iteraciones

drawsp8x8_loop_or:
LD A, (DE) ; Tomamos el dato del sprite
OR (HL) ; NUEVO: Hacemos un OR del scanline con el fondo
LD (HL), A ; Establecemos el valor del OR en videomemoria
INC DE ; Incrementamos puntero en sprite (DE+=1)
INC H ; Incrementamos puntero en pantalla (HL+=256)
DJNZ drawsp8x8_loop_or

A continuación, el código fuente completo de la rutina de impresión de 8×8 con operación lógica OR:

;-------------------------------------------------------------
; DrawSprite_8x8_OR:
; Imprime un sprite de 8x8 pixeles con o sin atributos.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
;-------------------------------------------------------------
DrawSprite_8x8_OR:

; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X


LD BC, (DS_COORD_X)

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A ; Ya tenemos la parte alta calculada (010TT000)
LD A, B ; Ahora calculamos la parte baja
AND 7
RRCA
RRCA
RRCA ; A = NNN00000b
ADD A, C ; Sumamos COLUMNA -> A = NNNCCCCCb
LD E, A ; Lo cargamos en la parte baja de la direccion
; DE contiene ahora la direccion destino.

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*8)

LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD H, 0
LD L, A ; HL = DS_NUMSPR
ADD HL, HL ; HL = HL * 2
ADD HL, HL ; HL = HL * 4
ADD HL, HL ; HL = HL * 8 = DS_NUMSPR * 8
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 8)
; HL contiene la direccion de inicio en el sprite

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; Dibujar 8 scanlines (DE) -> (HL) y bajar scanline


;;; Incrementar scanline del sprite (DE)

LD B, 8 ; 8 scanlines -> 8 iteraciones

drawsp8x8_loop_or:
LD A, (DE) ; Tomamos el dato del sprite
OR (HL) ; NUEVO: Hacemos un OR del scanline con el fondo
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en sprite
INC H ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawsp8x8_loop_or

;;; En este punto, los 8 scanlines del sprite estan dibujados.


LD A, H
SUB 8 ; Recuperamos la posicion de memoria del
LD B, A ; scanline inicial donde empezamos a dibujar
LD C, L ; BC = HL - 8

;;; Considerar el dibujado de los atributos (Si DS_ATTRIBS=0 -> RET)


LD HL, (DS_ATTRIBS)

XOR A ; A = 0
ADD A, H ; A = 0 + H = H
RET Z ; Si H = 0, volver (no dibujar atributos)

;;; Calcular posicion destino en area de atributos en DE.


LD A, B ; Codigo de Get_Attr_Offset_From_Image
RRCA ; Obtenemos dir de atributo a partir de
RRCA ; dir de zona de imagen.
RRCA ; Nos evita volver a obtener X e Y
AND 3 ; y hacer el calculo completo de la
OR $58 ; direccion en zona de atributos
LD D, A
LD E, C ; DE tiene el offset del attr de HL

LD A, (DS_NUMSPR) ; Cogemos el numero de sprite a dibujar


LD C, A
LD B, 0
ADD HL, BC ; HL = HL+DS_NUMSPR = Origen de atributo

;;; Copiar (HL) en (DE) -> Copiar atributo de sprite a pantalla


LD A, (HL)
LD (DE), A ; Mas rapido que LDI (7+7 vs 16 t-estados)
RET ; porque no necesitamos incrementar HL y DE

(Nótese que el bucle de impresión de scanlines también puede, y debería, ser desenrollado).

Veamos qué ha ocurrido con los píxeles del fondo y la operación lógica OR:
Ampliando el sprite…

¿Qué ha ocurrido con el sprite? ¿Por qué le faltan los “ojos” y hay un pixel activo en medio de la “boca”?
Sencillamente, porque mediante el OR hemos impreso el sprite respetando el valor a 1 de los píxeles del fondo
cuando el mismo pixel estaba a 0 en nuestro sprite. Eso ha hecho que alrededor de nuestro “personaje” no se
haya borrado el fondo, ya que los píxeles a cero de nuestro sprite se convierten en “transparentes”. Por
desgracia, eso también hace que los ojos del personaje sean transparentes en lugar de estar a cero. En el caso del
ejemplo anterior, los “ojos” del personaje coinciden con 2 píxeles de pantalla activos por lo que la operación
OR los deja a 1. Lo mismo ocurre con el pixel en el centro de la boca, que se corresponde con un pixel activo
en la pantalla.

En tal caso, ¿qué hacemos para imprimir nuestro sprite respetando el fondo pero que a su vez podamos disponer
de zonas que no sean transparentes?

La respuesta es: mediante máscaras.

Impresión 8x8 usándo máscaras

Como hemos visto, las operaciones con OR nos permiten respetar el fondo pero a su vez provocan zonas
transparentes en nuestro sprite. En sistemas más modernos se utiliza un “color transparente” (que suele ser unas
componentes concretas de color con un tono específico de rosa fucsia). En el caso del Spectrum, el color reside
en los atributos y no en los sprites en sí, por lo que no podemos utilizar un “color transparencia”.

La solución para respetar el fondo y sólo tener transparencias en los sprites en las zonas que nosotros deseemos
es la utilización de máscaras.
La máscara es un bitmap del mismo tamaño que el sprite al que está asociada. Tiene un contorno similar al del
sprite, donde colocamos a 1 todos los pixeles del fondo que necesitamos respetar (zonas transparentes) y a 0
todos los píxeles que se transferirán desde el sprite (píxeles opacos o píxeles del sprite) y que deben de ser
borrados del fondo.

A la hora de dibujar el sprite en pantalla, se realiza un AND entre el valor del pixel y el valor del fondo (de los
8 píxeles, en el caso del Spectrum), con lo cual “borramos” del fondo todos aquellos píxeles a cero en la
máscara. Después se realiza un OR del Sprite en el resultado del anterior AND, activando los píxeles del Sprite.

De esta forma, podemos respetar todos los píxeles del fondo alrededor de la figura del personaje, así como
algunas zonas de mismo que podamos querer que sean transparentes, mientras que borramos todos aquellos
píxeles de pantalla que deben de ser reemplazados por el sprite.

Apliquemos una máscara a nuestro ejemplo anterior. Al hacer el AND entre la máscara y el fondo (paso 1.-),
eliminamos el entramado de pixeles de pantalla, con lo que al imprimir nuestro sprite con el OR (paso 2.-), los
ojos y el centro de la boca serán de nuevo píxeles no activos:

El resultado es que los ojos y la boca del sprite, que en la máscara están a 0, son borrados del fondo y por lo
tanto no se produce el efecto de “transparencia” que presentaba el dibujado con OR.

No obstante, nuestro sprite sigue teniendo un problema relacionado con el sistema de atributos del Spectrum, y
es que los píxeles de nuestro personaje se “confunden” con los del fondo en los contornos del sprite, ya que
todos tendrán idéntico color si están dentro de una misma celda 8×8. Para evitar esto, lo ideal sería disponer de
un “borde” alrededor del mismo.

Podemos aprovechar la máscara (no en nuestro sprite de 8×8, pero sí en sprites de mayores dimensiones), para
dotar de un “reborde” a nuestra figura y que los píxeles del sprite no se confundan con los del fondo. Basta con
hacer el contorno en la máscara más grande que el contorno del sprite, de esta forma, los bytes de contorno
“extra” de la máscara borrarán fondo alrededor del Sprite, con lo que no se producirá la “confusión” con el
fondo.

La siguiente imagen ilustra lo que acabamos de comentar:


Así pues, con la máscara podemos conseguir 2 cosas:

• Evitar transparencias en el interior de nuestro Sprite: bastará con que la máscara sea una copia del
contorno del sprite, pero “relleno”, por lo que todos los pixeles del sprite serán transferidos dentro de
dicho contorno.

• Conseguir un contorno de pixeles “a cero” alrededor de nuestro Sprite, un borde que lo haga
visualmente más identificable y no mezcle sus píxeles con los de la pantalla. Para eso, basta con que el
contorno del sprite en la máscara sea ligeramente más grande que el del sprite.

Por contra, el uso de máscaras tiene también desventajas:

• El uso de máscaras requiere utilizar el doble de memoria para el spriteset gráfico (no para el de
atributos), ya que por cada byte del sprite necesitamos el correspondiente byte de máscara.

• Las rutinas de impresión con máscaras son más lentas que sin ellas, porque tienen que recoger datos de
la máscara, realizar operaciones lógicas entre los datos de la misma y el fondo, y realizar incrementos
adicionales del puntero al sprite (DE).

La rutina de impresión de máscaras tiene ahora que recoger un dato extra por cada byte del sprite: la máscara
que se corresponde con ese byte. Para no utilizar un puntero de memoria adicional en un array de máscara, lo
ideal es exportar cada byte de máscara junto al byte del sprite correspondiente, de forma que podamos recoger
ambos valores usando el mismo puntero (DE en nuestro caso). Para eso habría que exportar el Sprite como
Mask, X Char, Char line, Y Char y activando “Mask before graphic”.

El pseudocódigo de la nueva rutina DrawSprite_8x8_MASK sería el siguiente:


Pseudocódigo de la rutina:

; Recoger parametros
; Calcular posicion destino en HL con X e Y
; Calcular posicion origen en DE base_sprites + (frame*8*2) (*2 -> por la mascara)

; Repetir 8 veces:
; Coger dato de mascara
; AND del byte de mascara con el byte actual de fondo.
; Coger dato de scanline del sprite (DE++)
; Dibujar scanline en pantalla como un OR sobre el resultado del AND.
; Incrementar scanline sprite/mascara en DE.
; Bajar a siguiente scanline en pantalla.

La rutina completa sería:

;-------------------------------------------------------------
; DrawSprite_8x8_MASK:
; Imprime un sprite de 8x8 pixeles + mascara con o sin atributos.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
;-------------------------------------------------------------
DrawSprite_8x8_MASK:

; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X


LD BC, (DS_COORD_X)

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A ; Ya tenemos la parte alta calculada (010TT000)
LD A, B ; Ahora calculamos la parte baja
AND 7
RRCA
RRCA
RRCA ; A = NNN00000b
ADD A, C ; Sumamos COLUMNA -> A = NNNCCCCCb
LD E, A ; Lo cargamos en la parte baja de la direccion
; DE contiene ahora la direccion destino.

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*16)

LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD H, 0
LD L, A ; HL = DS_NUMSPR
ADD HL, HL ; HL = HL * 2
ADD HL, HL ; HL = HL * 4
ADD HL, HL ; HL = HL * 8
ADD HL, HL ; HL = HL * 16 = DS_NUMSPR * 16
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 16)
; HL contiene la direccion de inicio en el sprite

EX DE, HL ; Intercambiamos DE y HL para el OR

;;; Dibujar 8 scanlines (DE) -> (HL) + bajar scanline y avanzar en SPR
LD B, 8

drawspr8x8m_loop:
LD A, (DE) ; Obtenemos un byte del sprite (el byte de mascara)
AND (HL) ; A = A AND (HL)
LD C, A ; Nos guardamos el valor del AND
INC DE ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE) ; Obtenemos el byte grafico
OR C ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A ; Imprimimos el dato tras aplicar operaciones logicas
INC DE ; Avanzamos al siguiente dato del sprite
INC H ; Incrementamos puntero en pantalla (siguiente scanline)
DJNZ drawspr8x8m_loop

;;; En este punto, los 8 scanlines del sprite estan dibujados.


LD A, H
SUB 8 ; Recuperamos la posicion de memoria del
LD B, A ; scanline inicial donde empezamos a dibujar
LD C, L ; BC = HL - 8

;;; Considerar el dibujado de los atributos (Si DS_ATTRIBS=0 -> RET)


LD HL, (DS_ATTRIBS)

XOR A ; A = 0
ADD A, H ; A = 0 + H = H
RET Z ; Si H = 0, volver (no dibujar atributos)

;;; Calcular posicion destino en area de atributos en DE.


LD A, B ; Codigo de Get_Attr_Offset_From_Image
RRCA ; Obtenemos dir de atributo a partir de
RRCA ; dir de zona de imagen.
RRCA ; Nos evita volver a obtener X e Y
AND 3 ; y hacer el calculo completo de la
OR $58 ; direccion en zona de atributos
LD D, A
LD E, C ; DE tiene el offset del attr de HL

LD A, (DS_NUMSPR) ; Cogemos el numero de sprite a dibujar


LD C, A
LD B, 0
ADD HL, BC ; HL = HL+DS_NUMSPR = Origen de atributo

;;; Copiar (HL) en (DE) -> Copiar atributo de sprite a pantalla


LD A, (HL)
LD (DE), A ; Mas rapido que LDI (7+7 vs 16 t-estados)
RET ; porque no necesitamos incrementar HL y DE

La rutina que acabamos de ver presenta los siguientes cambios respecto a las rutinas con LD y OR:

• La primera modificación es el cálculo de la dirección de origen. Antes cada sprite ocupaba 8 bytes por
lo que teníamos que calcular la dirección origen en el array de Sprites así:

DIR_MEMORIA_GFX = DIR_BASE_SPRITES + (NUM_SPRITE_A_DIBUJAR * 8)

Pero ahora cada sprite tiene por cada byte gráfico un byte de máscara, ocupando 16 bytes en lugar de 8. El
cálculo debe adaptarse pues a:

DIR_MEMORIA_GFX = DIR_BASE_SPRITES + (NUM_SPRITE_A_DIBUJAR * 16)

El código para el cálculo agrega un “ADD HL, HL” adicional para esta tarea:

LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD H, 0
LD L, A ; HL = DS_NUMSPR
ADD HL, HL ; HL = HL * 2
ADD HL, HL ; HL = HL * 4
ADD HL, HL ; HL = HL * 8
ADD HL, HL ; HL = HL * 16 = DS_NUMSPR * 16
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 16)
; HL contiene la direccion de inicio en el sprite

• La segunda modificación principal en la rutina es la impresión de cada dato del sprite en sí. En nuestros
sprites con máscara cada bloque de 8 píxeles (1 byte) viene precedido del byte de la máscara, por lo que
debemos recoger ésta, hacer el AND de la máscara con el fondo, incrementar DE, recoger el dato
gráfico y hacer el OR de éste con el resultado anterior:

LD B, 8
drawspr8x8m_loop:
LD A, (DE) ; Obtenemos un byte del sprite (el byte de mascara)
AND (HL) ; A = A AND (HL)
LD C, A ; Nos guardamos el valor del AND
INC DE ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE) ; Obtenemos el byte grafico
OR C ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A ; Imprimimos el dato tras aplicar operaciones logicas
INC DE ; Avanzamos al siguiente dato del sprite
INC H ; Incrementamos puntero en pantalla (siguiente scanline)
DJNZ drawspr8x8m_loop

No olvidemos que el bucle de operación lógica de la máscara e impresión del scanline puede, como en los
anteriores casos, ser desenrollado.

Veamos su aplicación en el ejemplo de nuestro pequeño sprite sonriente. En SevenuP, activamos la máscara en
Mask → Use Mask y cambiamos al modo “Ver máscara” con Mask → View Mask. Podemos volver al modo de
visualización de sprite de nuevo con Mask → View Mask.

En el módo de visualización de máscara, podemos generar la máscara de nuestro sprite produciendo una
versión invertida del mismo. Para ayudarnos en esa tarea, SevenuP nos muestra con diferentes colores el estado
de los píxeles del sprite, e incluso tiene una opción de “AutoMask” que generará una máscara básica como
inversión del sprite para comenzar a trabajar con ella:

La máscara de nuestro pequeño Sprite en SevenuP

En la anterior imagen, tenemos en “negro” los píxeles a respetar en el fondo, y en blanco y amarillo (dentro de
la imagen) los píxeles a borrar. SevenuP nos marca en diferente color los píxeles a cero de la máscara que
coinciden con los del sprite.
Exportando a ASM este sprite con los parámetros de Prioridad: Mask, X Char, Char line, Y Char y activando
“Mask before graphic” obtenemos los siguientes array de datos (con 2 exportaciones por separado de Gfx y
Attr):

;GRAPHIC DATA:
;Sort Priorities: Mask, X Char, Char line, Y Char
;Con ancho=1, aparece: Mask, Char line
;Data Outputted: Gfx+Attr
;Interleave: Sprite
;Mask: Yes, before graphic

cara_gfx:
DEFB 227, 28, 193, 62, 128, 107, 128, 127
DEFB 128, 93, 128, 99, 193, 62, 227, 28

cara_attrib:
DEFB 56

Veamos el resultado del mismo ejemplo que hemos usado hasta ahora (fondo con patrón de píxeles alternados),
pero con la rutina de impresión con máscaras. El código es igual a los 2 ejemplos anteriores, pero llamando a
DrawSprite_8x8_MASK:

Ampliando la anterior captura de pantalla en la zona del sprite, podemos apreciar que los ojos y la boca de
nuestro personaje ya se visualizan correctamente:
Nuestro pequeño sprite se vería mucho mejor con un reborde vacío alrededor, ya que hay píxeles del fondo
pegados a nuestro personaje. Por desgracia, en un sprite de 8×8 como el nuestro apenas nos queda espacio para
este reborde en la máscara, pero se podría haber aplicado si el sprite fuera de mayores dimensiones.

Un apunte final sobre los ejemplos que hemos visto: nótese que estamos llamando a todas las rutinas asignando
DS_NUMSPR=0, para imprimir el “primer” (y único sprite en nuestro caso) del spriteset. El Spriteset podría
tener hasta 255 sprites dispuestos verticalmente (con sus máscaras intercaladas) y esta misma rutina, sin
modificaciones, nos serviría para dibujar cualquiera de ellos variando DS_NUMSPR.

Trazado de Sprites de 16x16

Impresión 16x16 con Transferencia por LD

La impresión de sprites de 16×16 sin máscaras es esencialmente idéntica a la de 8×8, ajustando las funciones
que hemos visto hasta ahora a las nuevas dimensiones del sprite:

• El cálculo de la dirección origen en el sprite cambia, ya que ahora cada scanline ocupa 2 bytes y no 1, y
tenemos 2 bloques de altura en el sprite y no uno. Antes calculábamos la dirección origen como
BASE+(DS_NUMSPR*8), pero ahora tendremos que avanzar 8*2*2=32 bytes por cada sprite en los
sprites sin máscara. El cálculo quedaría como BASE+(DS_NUMSPR*32).
• Para multiplicar DS_NUMSPR por 32 vamos a utilizar desplazamientos a la derecha de un pseudo-
registro de 16 bits formado por A y L en lugar de utilizar sumas sucesivas ADD HL, HL. Esta técnica
requiere menos ciclos de reloj para su ejecución.
• La impresión de datos debe imprimir todo un scanline horizontal del Sprite (2 bytes) antes de avanzar al
siguiente scanline de pantalla.
• El avance al siguiente scanline cambia ligeramente, ya que necesitamos incrementar HL
(concretamente, L) para poder imprimir el segundo byte horizontal.
• El bucle vertical es de 16 iteraciones en lugar de 8, ya que el sprite tiene 2 caracteres verticales.
• El cálculo de la dirección origen en los atributos cambia, ya que ahora tenemos 4 bytes de atributos por
cada sprite. Así, la posición ya no se calcula como BASE+(DS_NUMSPR*1) sino como
BASE+(DS_NUMSPR*4).
• La impresión de los atributos también cambia, ya que ahora hay que imprimir 4 atributos (2×2 bloques)
en lugar de sólo 1.

El pseudocódigo de la rutina que tenemos que programar sería el siguiente:

; Recoger parametros de entrada


; Calcular posicion origen (array sprites) en DE como
; direccion = base_sprites + (NUM_SPRITE*32)
; Calcular posicion destino (pantalla) en HL con X e Y

; Repetir 8 veces:
; Dibujar byte (DE) -> (HL), trazando el scanline del 1er bloque
; Incrementar HL y DE
; Dibujar byte (DE) -> (HL), trazando el scanline del 2o bloque
; Incrementar DE
; Bajar a siguiente scanline en pantalla (HL), sumando 256 (INC H/DEC L).

; Avanzar puntero de pantalla (HL) a la posicion de la segunda


; fila de bloques a trazar

; Repetir 8 veces:
; Dibujar byte (DE) -> (HL), trazando el scanline del 1er bloque
; Incrementar HL y DE
; Dibujar byte (DE) -> (HL), trazando el scanline del 2o bloque
; Incrementar DE
; Bajar a siguiente scanline en pantalla (HL), sumando 256 (INC H/DEC L).

; Si base_atributos == 0 -> RET


; Calcular posicion origen de los atributos array_attr+(NUM_SPRITE*4) en HL.
; Calcular posicion destino en area de atributos en DE.

; Dibujar los atributos (2 filas de 2 atributos, 4 bytes en total):


; Copiar (HL) en (DE)
; Incrementar HL y DE
; Copiar (HL) en (DE)
; Incrementar HL
; Avanzar a la siguiente línea de atributos en pantalla (DE+=32)
; Copiar (HL) en (DE)
; Incrementar HL y DE
; Copiar (HL) en (DE)

El código completo de la rutina de impresión en 2×2 quedaría, pues, como sigue:

;-------------------------------------------------------------
; DrawSprite_16x16_LD:
; Imprime un sprite de 16x16 pixeles con o sin atributos.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
;-------------------------------------------------------------
DrawSprite_16x16_LD:

; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X


LD BC, (DS_COORD_X)

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A

PUSH DE ; Lo guardamos para luego, lo usaremos para


; calcular la direccion del atributo

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*32)
;;; Multiplicamos con desplazamientos, ver los comentarios.
LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD L, 0 ; AL = DS_NUMSPR*256
SRL A ; Desplazamos a la derecha para dividir por dos
RR L ; AL = DS_NUMSPR*128
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*64
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*32
LD H, A ; HL = DS_NUMSPR*32
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 32)
; HL contiene la direccion de inicio en el sprite

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; Repetir 8 veces (primeros 2 bloques horizontales):


LD B, 8

drawsp16x16_loop1:
LD A, (DE) ; Bloque 1: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla

LD A, (DE) ; Bloque 2: Leemos dato del sprite


LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite

INC H ; Hay que sumar 256 para ir al siguiente scanline


DEC L ; pero hay que restar el INC L que hicimos.
DJNZ drawsp16x16_loop1

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


; desde el septimo scanline de la fila Y+1 al primero de la Y+2

;;;INC H ; No hay que hacer INC H, lo hizo en el bucle


;;;LD A, H ; No hay que hacer esta prueba, sabemos que
;;;AND 7 ; no hay salto (es un cambio de bloque)
;;;JR NZ, drawsp16_nofix_abajop
LD A, L
ADD A, 32
LD L, A
JR C, drawsp16_nofix_abajop
LD A, H
SUB 8
LD H, A

drawsp16_nofix_abajop:

;;; Repetir 8 veces (segundos 2 bloques horizontales):


LD B, 8

drawsp16x16_loop2:
LD A, (DE) ; Bloque 1: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla

LD A, (DE) ; Bloque 2: Leemos dato del sprite


LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite

INC H ; Hay que sumar 256 para ir al siguiente scanline


DEC L ; pero hay que restar el INC L que hicimos.
DJNZ drawsp16x16_loop2

;;; En este punto, los 16 scanlines del sprite estan dibujados.

POP BC ; Recuperamos el offset del primer scanline

;;; Considerar el dibujado de los atributos (Si DS_ATTRIBS=0 -> RET)


LD HL, (DS_ATTRIBS)

XOR A ; A = 0
ADD A, H ; A = 0 + H = H
RET Z ; Si H = 0, volver (no dibujar atributos)

;;; Calcular posicion destino en area de atributos en DE.


LD A, B ; Codigo de Get_Attr_Offset_From_Image
RRCA ; Obtenemos dir de atributo a partir de
RRCA ; dir de zona de imagen.
RRCA ; Nos evita volver a obtener X e Y
AND 3 ; y hacer el calculo completo de la
OR $58 ; direccion en zona de atributos
LD D, A
LD E, C ; DE tiene el offset del attr de HL

LD A, (DS_NUMSPR) ; Cogemos el numero de sprite a dibujar


LD C, A
LD B, 0
ADD HL, BC ; HL = HL+DS_NUMSPR
ADD HL, BC ; HL = HL+DS_NUMSPR*2
ADD HL, BC ; HL = HL+DS_NUMSPR*3
ADD HL, BC ; HL = HL+HL=(DS_NUMSPR*4) = Origen de atributo

LDI
LDI ; Imprimimos las 2 primeras filas de atributo

;;; Avance diferencial a la siguiente linea de atributos


LD A, E ; A = L
ADD A, 30 ; Sumamos A = A + 30 mas los 2 INCs de LDI.
LD E, A ; Guardamos en L (L = L+30 + 2 por LDI=L+32)
JR NC, drawsp16x16_attrab_noinc
INC D
drawsp16x16_attrab_noinc:
LDI
LDI
RET ; porque no necesitamos incrementar HL y DE

Lo primero que nos llama la atención de la rutina es la forma de multiplicar por 32 el valor de DS_NUMSPR.
Una primera aproximación de multiplicación de HL = NUM_SPR * 32 podría ser aumentar el número de sumas
ADD HL, HL tal y como se realizan en las rutinas de 8×8:

;;; Multiplicar DS_SPRITES por 32 con sumas


LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD H, 0 ; H = 0
LD L, A ; HL = DS_NUMSPR
ADD HL, HL ; HL = HL * 2
ADD HL, HL ; HL = HL * 4
ADD HL, HL ; HL = HL * 8
ADD HL, HL ; HL = HL * 16
ADD HL, HL ; HL = HL * 32
ADD HL, BC ; HL = DS_SPRITES + (DS_NUMSPR * 32)

Esta porción de código tarda 11 t-estados por cada ADD de 16 bits, más 7 t-estados de LD H, 0, más 4 de LD
L, A, lo que da un total de 77 t-estados para realizar la multiplicación.

La técnica empleada en el listado, proporcionada por metalbrain, implica cargar el valor de DS_NUMSPR en
la parte alta de un registro de 16 bits (con lo que el registro tendría el valor de DS_NUMSPR*256, como si lo
hubieramos desplazado 8 veces a la izquierda), y después realizar desplazamientos de 16 bits a la derecha,
dividiendo este valor por 2 en cada desplazamiento. Con un desplazamiento, obtenemos en el registro el valor
DS_NUMSPR * 128, con otro desplazamiento DS_NUMSPR * 64, y con otro más DS_NUMSPR * 32.

Los desplazamientos los tenemos que realizar con el registro A (mas rápidos), por lo que utilizaremos el par de
registros A + L para realizar la operación y finalmente cargar el resultado en HL:

;;; Multiplicar DS_SPRITES por 32 con desplazamientos >>


LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD L, 0 ; AL = DS_NUMSPR*256
SRL A ; Desplazamos a la derecha para dividir por dos
RR L ; AL = DS_NUMSPR*128
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*64
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*32
LD H, A ; HL = DS_NUMSPR*32
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 32)
; HL contiene la direccion de inicio en el sprite

Esta porción de código tiene un coste de 11 (ADD) + 8 + 8 + 8 (RR) + 4 + 4 + 4 (RRA) + 7 + 4 (LD) = 58 t-


estados, 29 ciclos de reloj menos que la rutina con ADD.

Si tuvieramos que multiplicar por 64 (realizar un RRA/RL menos), el coste sería todavía menor: 12 t-estados
menos con un total de 46 t-estados. En el caso de las rutinas de 8×8, resultaba más rápido realizar la
multiplicación por medio de sumas que por desplazamientos, con un coste total de 55 t-estados.

Otra parte interesante de la rutina de dibujado de sprites está en la impresión de los datos gráficos. En esta
ocasión hay que imprimir 2 bytes horizontales en cada scanline, y sumar 256 para avanzar a la siguiente línea
de pantalla. Esto nos obliga a decrementar HL en 1 unidad (con DEC L) para compensar el avance horizontal
utilizado para posicionarnos en el lugar de dibujado del segundo bloque del sprite. Tras esto, ya podemos hacer
el avance de scanline con un simple INC H (HL=HL+256).

Una vez finalizado el bucle de 8 iteraciones que imprime los datos de los 2 bloques de la fila 1 del sprite,
debemos avanzar al siguiente scanline de pantalla (8 más abajo de la posicion Y inicial) para trazar los 2
bloques restantes (los bloques “de abajo”). Para ello se ha insertado el código de “Siguiente_Scanline_HL”
dentro de la rutina (evitando el CALL y el RET). La instrucción inicial INC H de la rutina que vimos en el
capítulo anterior no es necesaria porque la ejecuta la última iteración del bucle anterior.

Tras ajustar HL tenemos que dibujar los 2 últimos bloques del sprite, con un bucle similar al que dibujó los 2
primeros.

Nótese que hemos dividido la impresión de los 16 scanlines en 2 bucles (1 para cada fila de caracteres). Cada
una de las 2 filas a dibujar está dentro de una posición de carácter y para avanzar un scanline basta con
incrementar la parte alta de la dirección, pero para pasar de un bloque al siguiente el código es diferente.
El bucle podría haber sido de 16 iteraciones utilizando la rutina genérica de “Avanzar HL en 1 scanline” (que
funciona tanto para avanzar dentro de un carácter como para avanzar del fin de un carácter al siguiente), y
habría tenido el siguiente aspecto:

LD B, 16 ; 16 iteraciones

drawsp16x16_loop:
LD A, (DE) ; Bloque 1: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla
LD A, (DE) ; Bloque 2: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
DEC L ; Decrementamos el avance realizado

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


;;;INC H ; No hay que hacer INC H, lo hizo en el bucle
;;;LD A, H ; No hay que hacer esta prueba, sabemos que
;;;AND 7 ; no hay salto (es un cambio de bloque)
;;;JR NZ, drawsp16_nofix_abajop
LD A, L
ADD A, 32
LD L, A
JR C, drawsp16_nofix_abajop
LD A, H
SUB 8
LD H, A
drawsp16_nofix_abajop:

DJNZ drawsp16x16_loop

Este código es más pequeño en tamaño que el uso de 2 bucles, pero estamos efectuando un JR innecesario en
14 de las 16 iteraciones, ya que sólo se debe chequear el caracter/tercio en el salto de un bloque de pantalla al
siguiente, que sólo ocurre 1 vez en el caso de un sprite de 2×2 bloques impreso en posiciones de carácter.
Además, en la última iteración es innecesario incrementar y ajustar HL, por lo que son ciclos de reloj que se
malgastan.

Finalmente, a la hora de escribir los atributos cambia el cálculo de la posición origen (ahora es
DS_NUMSPR*4, cuya multiplicación realizamos en base a 4 sumas), así como la copia de los 4 bytes, ya que
hay que imprimir los 2 primeros (LDI + LDI), avanzar DE hasta la siguiente “fila de atributos”, y copiar los 2
siguientes (con otras 2 instrucciones LDI).

Veamos la ejecución de la rutina con un sencillo ejemplo… Primero dibujamos en SevenuP el pequeño
personaje de 2×2 bloques con el que abríamos este apartado y lo exportamos a ASM:

;-----------------------------------------------------------------------
;ASM source file created by SevenuP v1.20
;GRAPHIC DATA:
;Pixel Size: ( 16, 64)
;Char Size: ( 2, 8)
;Sort Priorities: X char, Char line, Y char
;Mask: No
;-----------------------------------------------------------------------

bicho_gfx:
DEFB 8,128, 4, 64, 0, 0, 7,224
DEFB 15, 80, 15, 80, 15,240, 7,224
DEFB 0, 0, 1, 64, 0, 32, 2, 0
DEFB 104, 22,112, 14, 56, 28, 0, 0

bicho_attrib:
DEFB 70, 71, 67, 3
A continuación, imprimimos el sprite mediante la siguiente porción de código:

LD HL, bicho_gfx
LD (DS_SPRITES), HL
LD HL, bicho_attrib
LD (DS_ATTRIBS), HL
LD A, 13
LD (DS_COORD_X), A
LD A, 8
LD (DS_COORD_Y), A
XOR A
LD (DS_NUMSPR), A
CALL DrawSprite_16x16_LD

Este es el aspecto del sprite impreso en pantalla sobre la trama de píxeles alternos que estamos utilizando hasta
el momento:

Ampliando la zona de pantalla con el sprite:

Impresión 16x16 usándo operaciones lógicas

Podemos convertir la anterior rutina fácilmente en una rutina de impresión con operaciones lógicas añadiendo
el OR (HL) (o el XOR) antes de la escritura del dato en pantalla.

Simplemente, reemplazaríamos las diferentes operaciones de transferencia cambiando:

LD A, (DE) ; Bloque 1: Leemos dato del sprite


LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla

por:

LD A, (DE) ; Bloque 1: Leemos dato del sprite


OR (HL) ; Realizamos operación lógica
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla

El resto de la rutina es esencialmente idéntico a la versión con transferencias de datos LD.

Impresión 16x16 usándo máscaras

La rutina para dibujar sprites de 16×16 (2×2) con transparencia usando máscaras también sería una mezcla
entre la rutina de 8×8 con máscara y a la de 16×16 sin máscara, con el siguiente cambio:

• El cálculo de la dirección origen en el sprite cambia, ya que ahora cada scanline ocupa 4 bytes (2 del
sprite y 2 de la máscara) y no 2, y tenemos 2 bloques de altura en el sprite y no uno. Como tenemos el
doble de datos gráficos por cada sprite, si antes habíamos calculado la dirección origen como
BASE+(DS_NUMSPR*32), ahora tendremos que calcularla como BASE+(DS_NUMSPR*64). Esta
circunstancia nos ahorra un desplazamiento del pseudoregistro “AL” hacia la derecha (evitamos un
RRA + RR L), lo que nos permite hacer la operación de multiplicación con sólo 46 t-estados:

El código quedaría de la siguiente forma:

;-------------------------------------------------------------
; DrawSprite_16x16_MASK:
; Imprime un sprite de 16x16 pixeles + mascara con o sin atributos.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
;-------------------------------------------------------------
DrawSprite_16x16_MASK:

; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X


LD BC, (DS_COORD_X)

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A

PUSH DE ; Lo guardamos para luego, lo usaremos para


; calcular la direccion del atributo

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*64)
;;; Multiplicamos con desplazamientos, ver los comentarios.
LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD L, 0 ; AL = DS_NUMSPR*256
SRL A ; Desplazamos a la derecha para dividir por dos
RR L ; AL = DS_NUMSPR*128
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*64
LD H, A ; HL = DS_NUMSPR*64
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 64)
; HL contiene la direccion de inicio en el sprite

EX DE, HL ; Intercambiamos DE y HL para las OP LOGICAS

;;; Dibujar 8 scanlines (DE) -> (HL) + bajar scanline y avanzar en SPR
LD B, 8

drawspr16m_loop1:
LD A, (DE) ; Obtenemos un byte del sprite (el byte de mascara)
AND (HL) ; A = A AND (HL)
LD C, A ; Nos guardamos el valor del AND
INC DE ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE) ; Obtenemos el byte grafico
OR C ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A ; Imprimimos el dato tras aplicar operaciones logicas
INC DE ; Avanzamos al siguiente dato del sprite
INC L ; Avanzamos al segundo bloque en pantalla

LD A, (DE) ; Obtenemos un byte del sprite (el byte de mascara)


AND (HL) ; A = A AND (HL)
LD C, A ; Nos guardamos el valor del AND
INC DE ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE) ; Obtenemos el byte grafico
OR C ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A ; Imprimimos el dato tras aplicar operaciones logicas
INC DE ; Avanzamos al siguiente dato del sprite

DEC L ; Volvemos atras del valor que incrementamos


INC H ; Incrementamos puntero en pantalla (siguiente scanline)
DJNZ drawspr16m_loop1

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


; desde el septimo scanline de la fila Y+1 al primero de la Y+2

;;;INC H ; No hay que hacer INC H, lo hizo en el bucle


;;;LD A, H ; No hay que hacer esta prueba, sabemos que
;;;AND 7 ; no hay salto (es un cambio de bloque)
;;;JR NZ, drawsp16_nofix_abajop
LD A, H
AND 7
JR NZ, drawsp16m_nofix_abajop
LD A, L
ADD A, 32
LD L, A
JR C, drawsp16m_nofix_abajop
LD A, H
SUB 8
LD H, A
drawsp16m_nofix_abajop:

;;; Repetir 8 veces (segundos 2 bloques horizontales):


LD B, 8

drawspr16m_loop2:
LD A, (DE) ; Obtenemos un byte del sprite (el byte de mascara)
AND (HL) ; A = A AND (HL)
LD C, A ; Nos guardamos el valor del AND
INC DE ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE) ; Obtenemos el byte grafico
OR C ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A ; Imprimimos el dato tras aplicar operaciones logicas
INC DE ; Avanzamos al siguiente dato del sprite
INC L ; Avanzamos al segundo bloque en pantalla

LD A, (DE) ; Obtenemos un byte del sprite (el byte de mascara)


AND (HL) ; A = A AND (HL)
LD C, A ; Nos guardamos el valor del AND
INC DE ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE) ; Obtenemos el byte grafico
OR C ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A ; Imprimimos el dato tras aplicar operaciones logicas
INC DE ; Avanzamos al siguiente dato del sprite

DEC L ; Volvemos atras del valor que incrementamos


INC H ; Incrementamos puntero en pantalla (siguiente scanline)
DJNZ drawspr16m_loop2

;;; En este punto, los 16 scanlines del sprite estan dibujados.

POP BC ; Recuperamos el offset del primer scanline

;;; Considerar el dibujado de los atributos (Si DS_ATTRIBS=0 -> RET)


LD HL, (DS_ATTRIBS)

XOR A ; A = 0
ADD A, H ; A = 0 + H = H
RET Z ; Si H = 0, volver (no dibujar atributos)

;;; Calcular posicion destino en area de atributos en DE.


LD A, B ; Codigo de Get_Attr_Offset_From_Image
RRCA ; Obtenemos dir de atributo a partir de
RRCA ; dir de zona de imagen.
RRCA ; Nos evita volver a obtener X e Y
AND 3 ; y hacer el calculo completo de la
OR $58 ; direccion en zona de atributos
LD D, A
LD E, C ; DE tiene el offset del attr de HL

LD A, (DS_NUMSPR) ; Cogemos el numero de sprite a dibujar


LD C, A
LD B, 0
ADD HL, BC ; HL = HL+DS_NUMSPR
ADD HL, BC ; HL = HL+DS_NUMSPR*2
ADD HL, BC ; HL = HL+DS_NUMSPR*3
ADD HL, BC ; HL = HL+HL=(DS_NUMSPR*4) = Origen de atributo

LDI
LDI ; Imprimimos las 2 primeras filas de atributo

;;; Avance diferencial a la siguiente linea de atributos


LD A, E ; A = L
ADD A, 30 ; Sumamos A = A + 30 mas los 2 INCs de LDI.
LD E, A ; Guardamos en L (L = L+30 + 2 por LDI=L+32)
JR NC, drawsp16m_attrab_noinc
INC D
drawsp16m_attrab_noinc:
LDI
LDI
RET ; porque no necesitamos incrementar HL y DE

Datos a destacar sobre el código:


• Al igual que en el caso de la rutina con LD y con OR, el bucle de 16 iteraciones de la rutina de
impresión con máscaras, se debería desenrollar si el trazado de sprites es una rutina prioritaria en
nuestro programa (que suele ser el caso, especialmente en juegos). No obstante, hay que tener en cuenta
que desenrollar estos 2 bucles supone añadir a la rutina 14 veces el tamaño de cada iteración (hay 16
iteraciones, en el código hay 2 de ellas repetidas 8 veces, por lo que en desenrollamiento añadiría 14
veces el código de impresión). Esto puede suponer un problema cuando los programas son grandes y
nos acercamos a los límites de la memoria del Spectrum.

Probemos la rutina con un spriteset de múltiples sprites de 2×2 bloques con su máscara:

Exportados con los parámetros adecuados, nos queda el siguiente resultado en forma de array:

;-----------------------------------------------------------------------
; ASM source file created by SevenuP v1.20
; SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain
;GRAPHIC DATA:
;Pixel Size: ( 16, 64)
;Char Size: ( 2, 8)
;Sort Priorities: Mask, X char, Char line, Y char
;Data Outputted: Gfx+Attr
;Mask: Yes, before graphic
;-----------------------------------------------------------------------

bicho_gfx:
DEFB 247, 8,127,128,251, 4,191, 64, 255, 0,255, 0,248, 7, 31,224
DEFB 240, 15, 15, 80,240, 15, 15, 80, 240, 15, 15,240,248, 7, 31,224
DEFB 255, 0,255, 0,254, 1,191, 64, 255, 0,223, 32,253, 2,255, 0
DEFB 151,104,233, 22,143,112,241, 14, 199, 56,227, 28,255, 0,255, 0
DEFB 251, 4,191, 64,255, 0,255, 0, 248, 7, 31,224,240, 15, 15, 80
DEFB 240, 15, 15, 80,240, 15, 15,240, 248, 7, 31,224,255, 0,255, 0
DEFB 255, 0,255, 0,254, 1,191, 64, 255, 0,255, 0,253, 2,223, 32
DEFB 247, 8,255, 0,241, 14,143,112, 248, 7,135,120,255, 0,255, 0
DEFB 253, 2,223, 32,255, 0,255, 0, 252, 3, 15,240,248, 7, 7,168
DEFB 248, 7, 7,168,248, 7, 7,248, 252, 3, 15,240,255, 0,255, 0
DEFB 255, 0,255, 0,254, 1,191, 64, 255, 0,255, 0,254, 1,191, 64
DEFB 255, 0,255, 0,253, 2, 31,224, 250, 5, 15,240,255, 0,255, 0
DEFB 254, 1,239, 16,253, 2,223, 32, 255, 0,255, 0,252, 3, 15,240
DEFB 248, 7, 7,168,248, 7, 7,168, 248, 7, 7,248,252, 3, 15,240
DEFB 255, 0,255, 0,254, 1,191, 64, 255, 0,223, 32,253, 2,255, 0
DEFB 151,104,233, 22,143,112,241, 14, 199, 56,227, 28,255, 0,255, 0

bicho_attrib:
DEFB 70, 71, 67, 3, 70, 71, 67, 3, 70, 71, 3, 67, 70, 71, 3, 67

La siguiente captura muestra la impresión de uno de los sprites de 2×2 bloques del tileset usando su
correspondiente máscara sobre nuestro fondo de píxeles alternos:

En ella, resulta evidente el problema del colour clash o colisión de atributos.

Por desgracia, debido a las características de color en baja resolución del Spectrum, el uso de máscaras con
sprites multicolor sobre fondos con patrones provoca este tipo de resultados. Los sprites multicolor con máscara
deben utilizarse en otras circunstancias / formatos de juego.

Por ejemplo, si el juego fuera monocolor y no imprimieramos los atributos del sprite, el resultado sería mucho
más agradable a la vista:
El resultado en esta ocasión es mucho mejor, aunque este sprite sigue necesitando claramente un reborde
generado vía máscara para una impresión mucho más agradable a la vista, aún en un fondo tan “desfavorable”
como es este patrón de píxeles alternos.

Y es que, como vamos a ver a continuación, cada técnica de impresión tiene una situación de aplicación válida.

Técnicas de impresión adecuadas a cada tipo de juego


Hemos visto en este capítulo 3 técnicas diferentes de impresión de Sprites, las cuales nos dan 4 posibilidades:

Impresión con transferencia (LD/LDI):

La impresión por transferencia directa de datos (sprite → pantalla) es adecuada para juegos con movimiento
“bloque a bloque” (en baja resolución), donde todos los sprites (y el mapa de juego) se mueven en la rejilla de
32×24 bloques del Spectrum (juegos de puzzle, rogue-likes, juegos basados en mapas de bloques, etc).
Tetris, Plotting, Maziacs y Boulder Dash

En estos juegos no suele ser necesario tener transparecia con el fondo (al ser plano) y no coinciden 2 sprites en
la misma cuadrícula (si lo hacen, es, por ejemplo, para recoger un objeto).

Impresión por operación lógica OR:

La impresión por OR es adecuada en juegos donde el personaje no tenga pixeles a cero dentro de su contorno,
de forma que no se vean píxeles del fondo en el interior de nuestro personaje, alterando el sprite.

La segunda opción (y la más habitual) es su uso en juegos donde (sea como sea el personaje del jugador) el
fondo sea plano (sin tramas) y el color del mismo sea idéntico al color de PAPER de los sprites del juego. De
esta forma nos podemos mover en el juego sin causar colisión de atributos con el fondo (ya que no tiene pixeles
activos y el PAPER del fondo es igual al PAPER de nuestro Sprite) y podemos ubicar en la pantalla objetos
multicolor en posiciones de carácter que no provoquen colisión de atributos con nuestro personaje o si la
provocan sea imperceptible (elemento muy ajustado a tamaño de carácter) o durante un período de tiempo
mínimo (recogida de un objeto).

Manic Miner, Dizzy, Dynamite Dan y Fred

Impresión por operación lógica XOR:


Más que juegos adecuados para esta técnica, podemos hablar más bien de sprites adecuados para ella. Nos
referimos a los cursores, que suelen ser dibujados con XOR para no confundirse con el fondo (permitiendo una
visión “inversa” del mismo sobre el sprite) y para poder ser borrados con otra operación XOR.

Impresión con máscaras:

Por norma general, las rutinas de impresión de sprites con máscara se utilizan principalmente en juegos
monocolor donde todo el área de juego (fondo incluído) y los personajes comparten el mismo atributo de
pantalla. De esta forma, la impresión del sprite no cambia el color de los píxeles del fondo que caen dentro de
su recuadro y además nos ahorramos la impresión de los atributos, puesto que se realiza rellenando los atributos
del área de juego una única vez durante el dibujado inicial de la pantalla.

Una segunda opción es que el fondo sea multicolor y nuestro personaje se imprima sin atributos, adaptandose el
personaje al color del fondo sin cambiar éste. En la captura de abajo tenemos al personaje principal de Target:
Renegade adoptando los atributos de las posiciones de pantalla donde es dibujado.

H.A.T.E., Arkanoid, Rick Dangerous y Target: Renegade

Así pues, a la hora de realizar un juego, deberemos elegir la técnica más adecuada al tipo de juego que vamos a
programar.
Rutina genérica trazado de Sprites multicarácter
Nuestro siguiente paso sería el de crear una rutina de sprites que sirva para cualquier resolución de bloques de
un sprite, a la que además de los parámetros anteriores se le tendría que añadir el ancho y alto del sprite.

Como no conocemos de antemano las dimensiones del sprite, todos los cálculos de dirección de origen de datos
gráficos y de atributos se deben de basar en multiplicaciones realizadas con bucles. El bucle basado en el
número de scanlines (ALTO_en_pixeles iteraciones) no se puede desenrollar y cada una de las transferencias de
datos tiene que realizarse también en un bucle de ANCHO_en_caracteres iteraciones. Además, como
necesitamos realizar varios bucles, utilizaremos más registros y por lo tanto nos veremos en la necesidad de
alojar valores en la pila o en variables temporales o incluso de recuperar parámetros más de una vez dentro de
la rutina.

La rutina resultante es mucho menos óptima y rápida que cualquier rutina específica de 8×8, 8×16, 16×8,
16×16, etc. Si tenemos predefinidos todos los tamaños de los sprites de nuestro juego es recomendable utilizar
estas rutinas específicas en lugar de una rutina genérica.

La rutina que crearemos permitirá la impresión de sprites de cualquier tamaño con la única restricción que
Ancho * Alto en caracteres no supere el valor de 255, por ejemplo, se podría imprimir un sprite de 15×16 o
16×15 bloques.

La rutina, al ser genérica, y para que pueda ser legible por el lector, no es especialmente eficiente: debido a la
necesidad de realizar multiplicaciones de 16 bits, se utilizan los registros HL, DE y BC y nos vemos obligados
a hacer continuos PUSHes y POPs de estos registros, así como a apoyarnos en variables de memoria. La rutina
podría ser optimizada en cuanto a algoritmo y/o en cuanto a reordenación de código y uso de shadow-registers
para tratar de ganar todos los t-estados posibles.

Incluso una opción mucho más recomendable sería crear tantas rutinas específicas como tamaños de sprites
tengamos en nuestro juego y hacer una rutina “maestra” (un wrapper) que llame a una u otra según el tamaño
del Sprite que estemos solicitando imprimir. Un par de comprobaciones sobre el ancho y el alto del Sprite y
saltos condicionales con un CALL final a la rutina adecuada resultará muchísimo más óptimo que la rutina que
vamos a examinar:

;-------------------------------------------------------------
; DrawSprite_MxN_LD:
; Imprime un sprite de MxN pixeles con o sin atributos.
; Maximo, 16x15 / 15x16 bloques de ancho x alto caracteres.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
; 50010 Ancho del sprite en caracteres
; 50011 Alto del sprite en caracteres
;-------------------------------------------------------------
DrawSprite_MxN_LD:

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*ANCHO*ALTO)

;;;; Multiplicamos ancho por alto (en bloques)


LD A, (DS_WIDTH)
LD C, A
LD A, (DS_HEIGHT)
RLCA ; Multiplicamos por 8, necesitamos
RLCA ; la altura en pixeles (FILAS*8)
RLCA ; Y la guardamos porque la necesitaremos:
LD (drawsp_height), A
;;; Multiplicamos Ancho_bloques * Alto_pixeles:
LD B, A
XOR A ; A = 0
drawsp_mul1:
ADD A, C ; A = A + C (B veces) = B*C
DJNZ drawsp_mul1 ; B veces -> A = A*C = Ancho * Alto
; Ahora A = Ancho*Alto (maximo 255!!!)

;;; Multiplicamos DS_NUMSPR por (Ancho_bloques*Alto_pixeles)


LD B, A ; Repetimos Ancho * Alto veces
LD HL, 0
LD D, H ; HL = 0
LD A, (DS_NUMSPR)
LD E, A ; DE = DS_NUMSPR
drawsp_mul2:
ADD HL, DE ; HL = HL+DS_NUMSPR
DJNZ drawsp_mul2 ; Sumamos HL+DE B veces = DS_NUMSPR*B

; guardamos el valor de ancho*alto_pixeles*NUMSPR


LD (drawsp_width_by_height), HL

;;; Calculamos direccion origen copia en el sprite


LD BC, (DS_SPRITES)
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR*ANCHO*ALTO)
; HL contiene la direccion de inicio en el sprite

;;; Calculamos las coordenadas destino de pantalla en DE:

LD BC, (DS_COORD_X) ; B = Y, C = X
LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A
PUSH DE ; Lo guardamos para luego, lo usaremos para
; calcular la direccion del atributo
EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; Bucle de impresión vertical


; Recogemos de nuevo la altura en pixeles
LD A, (drawsp_height)
LD B, A ; Contador del bucle exterior del bucle

drawsp_yloop:
LD C, B ; Nos guardamos el contador

;;; Bucle de impresion horizontal


LD A, (DS_WIDTH)
LD B, A

PUSH HL ; Guardamos en pila inicio de scanline


; para poder volver a el luego
drawsp_xloop:
LD A, (DE) ; Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla
DJNZ drawsp_xloop
POP HL ; Recuperamos de pila inicio de scanline

;;; Avanzamos al siguiente scanline de pantalla


INC H
LD A, H
AND 7
JR NZ, drawspNM_nofix
LD A, L
ADD A, 32
LD L, A
JR C, drawspNM_nofix
LD A, H
SUB 8
LD H, A
drawspNM_nofix:

LD B, C
DJNZ drawsp_yloop ; Repetimos "alto_en_pixeles" veces

;;; Aqui hemos dibujado todo el sprite, vamos a los attributos

POP BC ; Recuperamos el offset del primer scanline

;;; Considerar el dibujado de atributos (Si DS_ATTRIBS=0 -> RET)


LD A,[DS_ATTRIBS+1] ; para obtener la parte alta de la direccion
OR A ; para comprobar si es 0
RET Z ; Si H = 0, volver (no dibujar atributos)

;;; Calcular posicion destino en area de atributos en DE.


LD A, B ; Codigo de Get_Attr_Offset_From_Image
RRCA ; Obtenemos dir de atributo a partir de
RRCA ; dir de zona de imagen.
RRCA ; Nos evita volver a obtener X e Y
AND 3 ; y hacer el calculo completo de la
OR $58 ; direccion en zona de atributos
LD D, A
LD E, C ; DE tiene el offset del attr de HL
PUSH DE ; Guardamos una copia

; Recuperamos el valor de ancho_caracteres * alto_en_pixeles * NUMSPR


; para ahorrarnos repetir otra vez dos multiplicaciones:
LD HL, (drawsp_width_by_height)

;;; HL = ANCHO_BLOQUES*ALTO_PIXELES*NUMSPR
;;; El Alto lo necesitamos en BLOQUES, no en píxeles-> dividir /8
SRL H ; Desplazamos H a la derecha
RR L ; Rotamos L a la derecha introduciendo CF
SRL H ;
RR L ;
SRL H ;
RR L ; Resultado : HL = HL >> 3 = HL / 8

;;;; HL = ANCHO_BLOQUES*ALTO_BLOQUES*NUMSPR
LD C, L
LD B, H
LD HL, (DS_ATTRIBS)
ADD HL, BC ; HL = Base_Atributos + (DS_NUMSPR*ALTO*ANCHO)

POP DE ; Recuperamos direccion destino

LD A, (DS_HEIGHT)
LD B, A

;;; Bucle impresion vertical de atributos


drawsp_attyloop:
LD C, B

PUSH DE ; Guardamos inicio de linea de atributos


LD A, (DS_WIDTH)
LD B, A

;;; Bucle impresion horizontal de atributos


drawsp_attxloop:
LD A, (HL) ; Leer atributo del sprite
INC HL
LD (DE), A ; Escribir atributo
INC E
DJNZ drawsp_attxloop

POP DE ; Recuperamos inicio de linea de atributos

;;; Avance diferencial a la siguiente linea de atributos


LD A, E
ADD A, 32
LD E, A
JR NC, drawsp_attrab_noinc
INC D
drawsp_attrab_noinc:

LD B, C
DJNZ drawsp_attyloop

RET

drawsp_height DB 0
drawsp_width_by_height DW 0

Nótese que pese al uso extensivo de registros y memoria, todavía podemos realizar algunas optimizaciones,
como la de “reaprovechar” el cálculo de Ancho*Alto*NUMSPR realizado al principio de la rutina (gráficos)
para los cálculos del final (atributos). Como la multiplicación inicial tiene Alto en píxeles y lo necesitamos en
Caracteres, dividimos el registro HL por 8 mediante instrucciones de desplazamiento que afecten a su parte baja
y su parte alta.

La anterior rutina sería llamada de la siguiente forma para imprimir nuestro ya conocido sprite de 2×2 bloques:

ORG 32768

DS_SPRITES EQU 50000


DS_ATTRIBS EQU 50002
DS_COORD_X EQU 50004
DS_COORD_Y EQU 50005
DS_NUMSPR EQU 50006
DS_WIDTH EQU 50010
DS_HEIGHT EQU 50011

LD HL, bicho_gfx
LD (DS_SPRITES), HL
LD HL, bicho_attrib
LD (DS_ATTRIBS), HL
LD A, 13
LD (DS_COORD_X), A
LD A, 8
LD (DS_COORD_Y), A
LD A, 2
LD (DS_WIDTH), A
LD A, 2
LD (DS_HEIGHT), A
XOR A
LD (DS_NUMSPR), A

CALL DrawSprite_MxN_LD
RET

La rutina anterior trabaja mediante transferencia de datos sprite → pantalla. Para realizar una operación lógica,
bastará modificar el bucle horizontal de impresión y añadir el correspondiente OR / XOR contra (HL).
La rutina para trabajar con máscaras implicar modificar el bucle principal para gestionar los datos de la máscara
y modificar también las multiplicaciones iniciales de parámetros (*2 en el caso de los datos gráficos, sin
modificación en los atributos).

Borrado de Sprites y Restauración del fondo


Una vez vistas las rutinas de impresión, hay que tratar el tema del borrado de los sprites de pantalla. El proceso
de movimiento y animación implica el borrado del Sprite de su anterior posición (o en la actual si sólo cambia
el fotograma) y el redibujado del mismo.

Hay diferentes técnicas de borrado de sprites, en las que profundizaremos cuando tratemos las Animaciones,
pero podemos distinguir inicialmente las 4 siguientes:

Borrado de sprite por sobreimpresión:

Se utiliza con rutinas de impresión sin transparencia y se basa en imprimir otro sprite encima del que queremos
borrar. Ese otro sprite puede ser, en juegos de fondo plano, un tile totalmente vacío con los colores o patrones
del fondo (un “blanco”) de forma que eliminemos el sprite y recuperemos el fondo que había en esa posición
antes de imprimirlo.

En juegos de fondo monocolor (típicamente fondo negro) se suele reservar el bloque 0 del SpriteSet para alojar
un “bloque vacío” de fondo que sirva de tile con el que sobreescribir con LDs una posición de sprite para
borrarlo.

Borrado de sprite por borrado de atributos:

En juegos con fondo plano (ejemplo: fondo totalmente negro) y movimiento a bloques, podemos borrar un
sprite que está sobre el fondo simplemente cambiando los atributos de las celdillas de pantalla a borrar a tinta y
papel del color del fondo. Por ejemplo, en un fondo negro, se podría borrar un sprite de 16×16 pixeles con 4
simples operaciones de escritura de atributo de color tinta=papel=negros. Cuando otro sprite sea dibujado sobre
nuestro “sprite borrado”, si se hace con una operación de transferencia eliminará todo rastro gráfico del Sprite
anterior.

Si el juego no tiene movimiento por bloques, aún podemos aprovechar esta técnica mediante el sistema descrito
por TrueVideo en los foros de Speccy.org: utilizando una tabla de 32×24 bytes que represente el estado de
ocupación de las celdillas de pantalla.

Citando a TrueVideo:

El buffer al que me refiero puedes verlo como una segunda capa de atributos,
que es la forma más fácil de implementar. Es una zona de memoria de
32x24 = 768 bytes (como el area de atributos) en la que se marcan los caracteres
ocupados en cada momento.

Algunas pistas para implementarlo:

- cuando vayas a imprimir un caracter consulta primero su entrada en el buffer.


Si está libre puedes volcar el gráfico directamente con LD (HL),A... porque es
más rápido y sabes *seguro* que puedes machacar lo que haya en pantalla. Si por
el contrario está ocupado (o sea, hay otro sprite en esa posición) sabes que tienes
que imprimir mezclando para preservar el fondo (p.e. con XOR). De esta forma tu
rutina de impresión imprimirá siempre de la forma más rápida posible, excepto en
los caracteres que se superpongan.
- con este método puedes borrar pintando atributos en lugar de volcar blancos con
toda seguridad, sin riesgo a que se vea basura, porque la rutina de impresión de
caracteres sabe en todo momento cuándo mezclar y cuándo no.

- es importante elegir con cabeza la dirección en memoria del buffer. Teniendo en


cuenta que el área de atributos de la pantalla empieza en $5800, si el buffer lo
sitúas p.e. en $xx00 (donde xx es la dirección que tu quieras de la parte alta)
entonces puedes moverte de una zona a otra cambiando sólo la parte alta del
registro. En general es buena idea usar siempre la dirección de atributos como
base para todos tus cálculos: a partir de una dirección de atributos puedes calcular
fácilmente la dirección del archivo de pantalla.. y también enlazar con otros bufferes
propios de forma rápida. Por el contrario al trabajar con coordenadas (por decir algo)
los cálculos para referenciar los buffers (de atributos o propios) son más costosos.

La ventaja de todo esto es que, a cambio de mantener ese buffer, nos aseguramos de
que borramos siempre muy rápido e imprimimos "lento" sólo cuando es estrictamente
necesario.

Borrado (e impresión) con XOR

Cuando un sprite ha sido dibujado con la operación XOR, podemos borrarlo realizando otra impresión en la
misma posición mediante la misma operación. Esto es así porque la operación XOR es reversible.

Veamos la tabla de verdad de la operación XOR:

Bit Pantalla Bit Sprite XOR


0 0 0
0 1 1
1 0 1
1 1 0

A continuación tenemos un ejemplo de cómo restaurar el fondo gracias a XOR:

Impresión del sprite con XOR:


-----------------------------
Fondo: 01010101
Sprite: 00111000
Tras XOR: 01101101

Recuperación del fondo con XOR:


-------------------------------
Fondo: 01101101
Sprite: 00111000
Tras XOR: 01010101 <-- Valor original de la pantalla

La impresión de un sprite con XOR produce una especie de “negativo” del Sprite en la pantalla allá donde haya
píxeles de fondo activos, por lo que esto, unido a la facilidad de borrado, lo hace ideal para la impresión de
cursores y punteros.

Almacenamiento en buffer temporal

La última solución es la más costosa en términos de memoria y de instrucciones, pero nos permite restaurar el
estado del fondo sea cual sea el número de sprites impresos y las características de este.
La técnica consiste en almacenar el contenido de los datos gráficos y de atributo del fondo en un array de
memoria temporal antes de volcar el sprite sobre la pantalla.

Podríamos realizar una función específica para esta tarea (recuperar una porción de fondo, o lo que es lo
mismo, crear un sprite tomando los datos de la pantalla); sería una modificación de las rutinas de impresión
donde el origen sería la pantalla y el destino el array. No obstante, esto no es eficiente porque estaríamos
realizando las operaciones de cálculo de posiciones 2 veces: una al salvaguardar el contenido del fondio y otra,
más tarde, al llamar a la función de dibujado del Sprite.

Para evitar esto, podemos adaptar las rutinas de impresión para que salvaguarden el fondo en nuestro array
temporal antes de escribir los datos en pantalla.

Como tenemos ya utilizados DE y HL para la transferencia del Sprite, necesitamos un registro de 16 bits
adicional. En este caso utilizaremos el registro IX para apuntar al array temporal.

Podemos modificar cualquiera de las rutinas para recuperar el fondo antes de escribir el sprite. Tomemos como
ejemplo el esqueleto de la rutina de impresión de 8×8 y veamos las instrucciones que habría que añadir para
salvaguardar el fondo en un array temporal externo. No reproduciremos la totalidad de la rutina reducir la
extensión del listado:

;-------------------------------------------------------------
; DrawSprite_8x8_Restore:
; Imprime un sprite de 8x8 pixeles salvaguardando el fondo
; en la direccion de memoria indicada por (50008).
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; 50000 Direccion de la tabla de Sprites
; 50002 Direccion de la tabla de Atribs (0=no atributos)
; 50004 Coordenada X en baja resolucion
; 50005 Coordenada Y en baja resolucion
; 50006 Numero de sprite a dibujar (0-N)
; 50008 Direccion del array temporal para el fondo
;-------------------------------------------------------------
DS_TEMPBUF EQU 50008

;;; Recogida de datos y parametros


(...)
LD IX, (DS_TEMPBUF)
(...)

;;; Bucle de impresion del Sprite:


LD B, 8 ; 8 scanlines

drawsp8x8_loopLD:
LD A, (HL) ; NUEVO: Leemos el valor actual del fondo
LD (IX), A ; NUEVO: Lo almacenamos en el array temporal
INC IX ; NUEVO: Incrementamos IX (puntero array fondo)

; Ya podemos imprimir el sprite


LD A, (DE) ; Tomamos el dato del sprite
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en sprite
INC H ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawsp8x8_loopLD

(...)
;;; Impresion de atributos
LD A, (DE) ; NUEVO: Leemos el atributo actual
LD (IX), A ; NUEVO: Lo guardamos en el array temporal

LD A, (HL) ; Ya podemos imprimir el atributo


LD (DE), A
RET
El registro IX no es especialmente rápido (10 t-estados el INC IX y 19 t-estados el LD (IX), A o LD (IX+0),
A, pero es más rápido utilizarlo que escribir y ejecutar una rutina adicional para almacenar el fondo.

Cuando necesitemos borrar el sprite, bastará con recuperar el fondo que había antes de imprimirlo, es decir,
volcar el contenido del buffer temporal en la posición del sprite a borrar. Para realizar este volcado podemos
llamar a una de las funciones de volcado de Sprites basada en transferencias ya que el buffer del fondo en
memoria se corresponde con un spriteset de 1 sólo elemento (DS_NUMSPR=0), sin máscara. Las rutinas
impresión de 8×8, 16×16 o genéricas basadas en LD servirían para recuperar el contenido del fondo usando
como origen el buffer temporal donde lo hemos almacenado.

Si tenemos más de un Sprite y hemos almacenado el fondo de cada uno de ellos en un array diferente, debemos
restaurar todos los fondos en orden inverso al que se haya realizado la impresión. Veamos por qué:

Supongamos que imprimimos un sprite (y almacenamos el fondo). Al imprimir el segundo sprite en la misma
posición que el anterior o en una posición cercana, almacenaríamos como fondo de este segundo sprite parte del
anterior sprite impreso. Si recuperamos los fondos en orden inverso al impreso, nos aseguramos de que el
último fondo recuperado será el del sprite 1, que es el correcto.

No olvidemos que las operaciones de borrado y reimpresión deben de ser lo más rápidas posibles para evitar
parpadeos debidos a que el ojo humano pueda ver el momento del borrado si la ULA refresca la pantalla tras el
“restore” del fondo y antes del dibujado del nuevo sprite.

Optimizar las rutinas

Adaptación de las rutinas al juego

Las rutinas que hemos visto en este apartado y en los siguientes no son, con diferencia, las versiones más
óptimas posibles que un programador avanzado podría crear. Es imprescindible que adaptemos las rutinas al
juego que vamos a crear eliminando todas aquellas partes de la misma que no sean necesarias.

Por ejemplo, si nuestro juego va a imprimir sprites sin atributos sobre un fondo monocolor con los atributos ya
establecidos, lo más normal sería modificar las subrutinas y eliminar todo rastro de la gestión y cálculo de los
atributos, no requiriendo siquiera establecer la dirección de atributo a cero y el código que hace esta
comprobación. Con esto ahorramos espacio en memoria (de instrucciones eliminadas) y tiempo de ejecución
(de comprobaciones innecesarias).

Adaptación de los parámetros de entrada

Por otra parte, si no tenemos intención de llamar a estas funciones desde BASIC, lo más óptimo sería eliminar
el paso de parámetros por variables de memoria y utilizar registros y/o pila allá donde sea posible, puesto que
estas operaciones son más rápidas que los acceso a memoria.

No obstante, nótese que las 2 variables que más tiempo cuesta de establecer y de leer (DS_SPRITES y
DS_ATTRIBS) se pueden establecer como constantes (y no como variables) directamente dentro de la rutina de
impresión. Eso quiere decir que si tenemos un único tileset, podríamos cambiar partes de la rutina, como:

LD BC, (DS_SPRITES)

por:

LD BC, Tabla_Sprites
Establecer BC a un valor inmediato (LD BC, NN) tiene un coste de 10 t-estados, frente a los 20 t-estados que
cuesta establecerlo desde una posición de memoria (LD BC, (NN)). De esta forma evitamos cargar desde
memoria las direcciones de los tilesets y también tener que establecerlos al inicio del programa.

Deshabilitar las interrupciones

Si nuestra rutina de dibujado de interrupciones es crítica, puede que nos resulte necesario en algunas
circunstancias realizar un DI (Disable Interrupts) al principio de la misma y un EI (Enable Interrupts) al acabar,
para evitar que una interrupción del Z80 sea lanzada durante la ejecución de la misma.

Normalmente no deberíamos requerir de esta operación porque lo normal es estar sincronizado con las
interrupciones y realizar el redibujado de los sprites en la ISR o al volver de la ISR de atención a la ULA.

No obstante, si no estamos sincronizados con las interrupciones o si estamos usando IM1, es un dato interesante
que nos puede evitar la ejecución de código ISR en medio de la ejecución de la rutina.

Trataremos este tema con más detalle en el capítulo dedicado a Animaciones, en el apartado sobre Evitar
Flickering o Parpadeos.

Uso de la pila para acceso a datos

Una opción realmente ingeniosa para optimizar el acceso al sprite sería la de utilizar la pila para realizar
operaciones de lectura de 2 bytes del sprite con una única instrucción (POP).

Apuntando SP a nuestro sprite, un simple POP DE carga en E y D (en ese orden) los 2 bytes apuntados por SP
e incrementa la dirección contenida en DE en 2 unidades, por lo que realizamos 2 operaciones de lectura y 2
incrementos (de 16 bits) en 1 byte de código y 10 t-estados, frente a los 7+7+6+6 = 26 t-estados que cuestan las
operaciones LD+INC por separado. Eso implica leer en E y D dos bytes de datos del sprite o el byte de máscara
y un byte de datos.

Como desventaja, debemos deshabilitar las interrupciones con DI al principio del programa y habilitarlas de
nuevo antes del RET con EI ya que no podemos permitir la ejecución de una ISR estando SP apuntando al
Sprite. Un “RST” o “CALL” (así como un PUSH) ejecutado con SP apuntando al Sprite provocaría la
corrupción del mismo al introducirse en la pila la dirección de memoria del RET o RETI para la ISR.

Veamos el pseudocódigo de una rutina que use la pila de esta forma:

; DI
; Calcular dirección destino en pantalla
; Calcular dirección origen de sprite
; Guardar estado anterior pila
; Establecer direccion Sprite en SP en dir sprite

; Bucle de N scanlines:
; Recuperar datos del sprite con POP

; Calcular e imprimir atributos

; Recuperar puntero de pila


; EI
; RET

A continuación vamos a ver una versión “reducida” (sin impresión de atributos, y con un bucle de 16
iteraciones sin desenrollar) de DrawSprite_16x16 que utiliza la pila para recuperar los datos del sprite mediante
POP DE. Esto implica la lectura de 2 bytes en una sóla instrucción y el doble incremento de DE para apuntar a
los 2 siguientes elementos.
En la rutina guardaremos el valor de SP en una variable temporal, antes de modificarlo, para poder recuperarlo
antes del RET. Una vez calculada la dirección de origen del sprite a dibujar, realizaremos la copia de SP en
DS_TEMP_SP (variable de memoria) y cargaremos el valor de HL en SP:

;-------------------------------------------------------------
; DrawSprite_16x16_LD_STACK_no_attr:
; Imprime un sprite de 16x16 pixeles sin atributos
; usando la pila para obtener datos del sprite.
;-------------------------------------------------------------

DS_TEMP_SP DW 0 ; Variable temporal donde alojar SP

DrawSprite_16x16_LD_STACK_no_attr:

;;; Deshabilitar interrupciones (vamos a cambiar SP).


DI

; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X


LD BC, (DS_COORD_X)

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A

;;; Guardamos el valor actual y correcto de SP


LD (DS_TEMP_SP), SP

;;; Calcular posicion origen (array sprites) en HL:


LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD L, 0 ; AL = DS_NUMSPR*256
SRL A ; Desplazamos a la derecha para dividir por dos
RR L ; AL = DS_NUMSPR*128
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*64
RRA ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L ; AL = DS_NUMSPR*32
LD H, A ; HL = DS_NUMSPR*32
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 32)
; HL contiene la direccion de inicio en el sprite

;;; Establecemos el valor de SP apuntando al sprite


LD SP, HL

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; Repetir 8 veces (primeros 2 bloques horizontales):


LD B, 16

drawsp16x16_stack_loop:
POP DE ; Recuperamos 2 bytes del sprite
; Y ademas no hace falta sumar 2 a DE
LD (HL), E ; Ahora imprimimos los 2 bytes en pantalla
INC L
LD (HL), D
DEC L

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


; desde el septimo scanline de la fila Y+1 al primero de la Y+2
INC H ; Siguiente scanline
LD A, H ; Ajustar tercio/bloque si necesario
AND 7
JR NZ, drawsp16_nofix_abajop
LD A, L
ADD A, 32
LD L, A
JR C, drawsp16_nofix_abajop
LD A, H
SUB 8
LD H, A

drawsp16_nofix_abajop:
DJNZ drawsp16x16_stack_loop

;;; En este punto, los 16 scanlines del sprite estan dibujados.

;;; Recuperamos el valor de SP


LD HL, (DS_TEMP_SP)
LD SP, HL

EI ; Habilitar de nuevo interrupciones


RET

Al igual que en el caso de las rutinas anteriores, si desenrollamos el bucle de 16 iteraciones en 2 bucles de 8,
podemos utilizar INC H dentro de los bucles y el código de Scanline_Abajo_HL entre ambos. El bucle
convertido en dos quedaría de la siguiente forma:

drawsp16x16_stack_loop1:
POP DE ; Recuperamos 2 bytes del sprite
; Y ademas no hace falta sumar 2 a DE
LD (HL), E ; Ahora imprimimos los 2 bytes en pantalla
INC L ; de la parte superior del Sprite
LD (HL), D
DEC L
INC H
DJNZ drawsp16x16_stack_loop1

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


; desde el septimo scanline de la fila Y+1 al primero de la Y+2
; No hay que comprobar si (H & $07) == 0 porque sabemos que lo es
LD A, L
ADD A, 32
LD L, A
JR C, drawsp16_nofix_abajop
LD A, H
SUB 8
LD H, A
drawsp16_nofix_abajop:

LD B,8
drawsp16x16_stack_loop2:
POP DE ; Recuperamos 2 bytes del sprite
; Y ademas no hace falta sumar 2 a DE
LD (HL), E ; Ahora imprimimos los 2 bytes en pantalla
INC L ; de la parte inferior del Sprite
LD (HL), D
DEC L
INC H
DJNZ drawsp16x16_stack_loop2
Gráficos (y IV): Fuentes de texto
En prácticamente cualquier programa o juego nos encontraremos con la necesidad de imprimir en pantalla texto
y datos numéricos en diferentes posiciones de pantalla: los menúes, los nombres de los niveles, los marcadores
y puntuaciones, etc.

La impresión de fuentes de texto es una aplicación directa de las rutinas de impresión de sprites en baja
resolución 00103que creamos en el capítulo anterior. Cada carácter es un sprite de 8×8 píxeles a dibujar en
coordenadas (c,f) de baja resolución.

Crearemos una rutina de impresión de caracteres y, basada en esta, una rutina de impresión de cadenas, con la
posibilidad de añadir códigos de control y de formato, e impresión de datos numéricos o variables decimales,
hexadecimales, binarias y de cadena.

Aprovechando la rutina de escaneo de teclado de la ROM, analizaremos también una rutina de INPUT de datos
para nuestros programas en ensamblador basados en texto (aventuras, juegos de gestión, etc).

Finalmente, examinaremos una fuente de texto de 4×8 píxeles que nos permitirá una resolución en pantalla de
64 columnas de caracteres en pantalla.

Fuentes de texto 8x8


Si creamos un juego gráfico de caracteres como un tileset de sprites de 8×8 píxeles cada uno conteniendo una
letra del alfabeto, podemos utilizar las rutinas de impresión de sprites de 8×8 para trazar en pantalla cualquier
carácter (visto como un sprite de 1×1 bloques).
Parte de un tileset de caracteres en SevenuP

Ubicando las letras en el spriteset con el mismo orden que el código ASCII (empezando por el espacio, código
32, como sprite cero), podemos utilizar el código ASCII de la letra a imprimir como número de sprite dentro
del tileset.

Los 95 caracteres imprimibles (96 en el Spectrum) del código ASCII.


Fuente: Wikipedia

Podríamos diseñar una fuente de 256 caracteres en el editor de Sprites, pero se requerirían 256*8 = 4096 bytes
(4KB!) para almacenar los datos en memoria. En realidad, no es necesario crear 256 caracteres en el editor,
puesto que en el Spectrum:

• Los primeros 31 códigos ASCII son códigos de control.


• Los códigos ASCII del 128 al 143 son los UDGs predefinidos e imprimibles.
• Los códigos ASCII del 144 al 164 son los UDGs programables en modo 48K (del 144 al 162 en modo
128K)
• Los códigos ASCII del 165 al 255 son los códigos de control (tokens) de las instrucciones de BASIC en
modo 48K (del 163 al 255 en modo 128K).
Códigos ASCII en el Spectrum desde el 32 al 164
Fuente: Wikipedia

Como hay un total de 96 caracteres de texto imprimibles (desde el 32 al 127), para disponer de un juego de
caracteres suficiente con minúsculas, mayúsculas, signos de puntuación y dígitos nos bastaría con una fuente de
96 sprites de 8 bytes cada uno, es decir, un total de 768 bytes.

Podemos agregar “caracteres” adicionales para disponer de códigos de control que nos permitan imprimir
vocales con acentos, eñes, cedilla (ç), etc. Al definir los textos de nuestro programa habría que utilizar estos
códigos de control en formato numérico (DB) intercalados con el texto ASCII (“España” = DB “Espa”,
codigo_enye, “a”).

Si tenemos problemas de espacio en nuestro programa también podemos utilizar un set de caracteres más
reducido que acaben en el ASCII 'Z' (ASCII 90), lo que nos dejaría un charset con números, signos de
puntuación y letras mayúsculas (sin minúsculas). El espacio ocupado por este spriteset sería de 90-32=58
caracteres, es decir, 464 bytes.

Por otra parte, no es necesario exportar los atributos de la fuente ya que lo normal es utilizar un atributo
determinado para todos los caracteres de la misma, aunque nada nos impide dotar de diferente color a cada letra
si así lo deseamos y exportar también los datos de atributo. En este capítulo sólo exportaremos los gráficos
puesto que utilizaremos un atributo común para todos los elementos de la fuente, o cambios de formato (tinta y
papel) manuales allá donde sea necesario.

Sea cual sea el tamaño de nuestro SpriteSet (desde el juego reducido de 58 caracteres hasta el set completo de
256, pasando por el estándar de 96), podemos imprimir caracteres de dicha fuente con las rutinas de impresión
de 8×8 del capítulo anterior.
Para los ejemplos de este capítulo utilizaremos una fuente personalizada de texto de 64 caracteres (signos de
puntuación, dígitos numéricos y letras mayúsculas, incluídas 5 vocales con acentos) dibujada por Javier Vispe y
convertida a código con SevenuP:

Exportando los datos en SevenuP en formato X Char, Char Line, Y Char como Gfx only, obtenemos el
siguiente fichero .asm:

; ASM source file created by SevenuP v1.20


; SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain

;GRAPHIC DATA:
;Pixel Size: ( 8, 512)
;Char Size: ( 1, 64)
;Sort Priorities: Char X, Char line, Y char
;Data Outputted: Gfx
;Interleave: Sprite

charset1:
DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 24, 24, 24, 0, 24, 0
DEFB 0,108,108, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DEFB 56, 0, 76, 56,110,196,122, 0, 0, 12, 12, 8, 0, 0, 0, 0
DEFB 0, 24, 0, 48, 48, 48, 24, 0, 0, 24, 0, 12, 12, 12, 24, 0
DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0,126,126, 24, 24, 0
DEFB 0, 0, 0, 0, 0, 12, 12, 8, 0, 0, 0,126,126, 0, 0, 0
DEFB 0, 0, 0, 0, 0, 56, 56, 0, 0, 0, 6, 12, 24, 48, 96, 0
DEFB 124, 0,206,214,230,254,124, 0, 28, 0,124, 28, 28, 28, 28, 0
DEFB 124, 0,198, 28,112,254,254, 0, 124, 0,198, 12,198,254,124, 0
DEFB 12, 0, 60,108,254,254, 12, 0, 254, 0,192,252, 14,254,252, 0
DEFB 60, 0,224,252,198,254,124, 0, 254, 0, 14, 28, 28, 56, 56, 0
DEFB 124, 0,198,124,198,254,124, 0, 124, 0,198,126, 6,126,124, 0
DEFB 0, 0, 24, 24, 0, 24, 24, 0, 118, 6,192,254,254,198,198, 0
DEFB 246, 6,192,252,192,254,254, 0, 12, 12, 48, 56, 56, 56, 56, 0
DEFB 118, 6,192,198,198,254,124, 0, 198, 6,192,198,198,254,124, 0
DEFB 0, 24, 0, 24, 24, 24, 24, 0, 124, 0,198,254,254,198,198, 0
DEFB 252, 0,198,252,198,254,252, 0, 124, 0,198,192,198,254,124, 0
DEFB 252, 0,198,198,198,254,252, 0, 254, 0,192,252,192,254,254, 0
DEFB 254, 0,224,252,224,224,224, 0, 124, 0,192,206,198,254,124, 0
DEFB 198, 0,198,254,254,198,198, 0, 56, 0, 56, 56, 56, 56, 56, 0
DEFB 6, 0, 6, 6,198,254,124, 0, 198, 0,220,248,252,206,198, 0
DEFB 224, 0,224,224,224,254,254, 0, 198, 0,254,254,214,198,198, 0
DEFB 198, 0,246,254,222,206,198, 0, 124, 0,198,198,198,254,124, 0
DEFB 252, 0,198,254,252,192,192, 0, 124, 0,198,198,198,252,122, 0
DEFB 252, 0,198,254,252,206,198, 0, 126, 0,224,124, 6,254,252, 0
DEFB 254, 0, 56, 56, 56, 56, 56, 0, 198, 0,198,198,198,254,124, 0
DEFB 198, 0,198,198,238,124, 56, 0, 198, 0,198,198,214,254,108, 0
DEFB 198, 0,124, 56,124,238,198, 0, 198, 0,238,124, 56, 56, 56, 0
DEFB 254, 0, 28, 56,112,254,254, 0, 60,102,219,133,133,219,102, 60
DEFB 0, 0, 96, 48, 24, 12, 6, 0, 24, 0, 24, 48, 96,102, 60, 0
DEFB 60, 0, 70, 12, 24, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0,126

PrintChar_8x8

La rutina de impresión de caracteres debe de recoger el código ASCII a dibujar y realizar el cálculo para
posicionar un puntero “origen” dentro del tileset contra el ASCII correspondiente. También debe calcular la
dirección destino en la pantalla en base a las coordenadas en baja resolución. Una vez trazado el carácter,
establecerá el atributo del mismo con el valor contenido en FONT_ATTRIB.

;-------------------------------------------------------------
; PrintChar_8x8:
; Imprime un caracter de 8x8 pixeles de un charset.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X = Coordenada X en baja resolucion (0-31)
; FONT_Y = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB = Atributo a utilizar en la impresion.
; Registro A = ASCII del caracter a dibujar.
;-------------------------------------------------------------
PrintChar_8x8:

LD BC, (FONT_X) ; B = Y, C = X
EX AF, AF' ; Nos guardamos el caracter en A'

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A ; DE contiene ahora la direccion destino.

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*8)
EX AF, AF' ; Recuperamos el caracter a dibujar de A'
LD BC, (FONT_CHARSET)
LD H, 0
LD L, A
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, BC ; HL = BC + HL = FONT_CHARSET + (A * 8)

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; Dibujar 7 scanlines (DE) -> (HL) y bajar scanline (y DE++)


LD B, 7 ; 7 scanlines a dibujar

drawchar8_loop:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en caracter
INC H ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawchar8_loop

;;; La octava iteracion (8o scanline) aparte, para evitar los INCs
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria

LD A, H ; Recuperamos el valor inicial de HL


SUB 7 ; Restando los 7 "INC H"'s realizados

;;; Calcular posicion destino en area de atributos en DE.


; Tenemos A = H
RRCA ; Codigo de Get_Attr_Offset_From_Image
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L

;;; Escribir el atributo en memoria


LD A, (FONT_ATTRIB)
LD (DE), A ; Escribimos el atributo en memoria
RET

La rutina es muy parecida a las que ya vimos en el capítulo anterior para impresión de sprites 8×8, pero
eliminando el cálculo del atributo origen al establecerlo desde la variable FONT_ATTRIB.

Además, hemos semi-desenrollado el bucle de impresión para hacer 7 iteraciones y después escribir el último
scanline fuera del bucle. De esta forma evitamos el INC DE + INC H que se realizaría para el último scanline y
que es innecesario. De nuevo, desenrollar totalmente el bucle sería lo más óptimo, ya que evitaríamos el “LD B,
7” y el “DJNZ”.

La impresión se realiza por transferencia, pero podemos convertirla en impresión por operación lógica
cambiando los:

LD A, (DE) ; Tomamos el dato del caracter


LD (HL), A ; Establecemos el valor en videomemoria

Por:

LD A, (DE) ; Tomamos el dato del caracter


OR (HL) ; Hacemos OR entre dato y pantalla
LD (HL), A ; Establecemos el valor en videomemoria

Para llamar a nuestra nueva rutina establecemos los valores de impresión en las variables de memoria y
realizamos el CALL correspondiente. Aunque en esta rutina podríamos utilizar el paso por registros, vamos a
emplear paso de parámetros por variables de memoria por 2 motivos:

• Para que sean utilizables desde BASIC.


• Para crear un sistema de coordenadas de texto X, Y y de atributos que nos permita, como veremos más
adelante en el capítulo, gestionar la impresión de texto de una manera más cómoda.

Nótese que nuestro set de caracteres empieza en el ASCII 32 (espacio) el cual se corresponde con el sprite 0
(sprite en blanco). En teoría, la rutina debería restar 32 a cada código ASCII recibido en el registro A para
encontrar el identificador de sprite correcto en el tileset.

En lugar de realizar una resta en cada impresión, podemos establecer el valor de FONT_CHARSET a la
dirección del array menos 256 (32*8), con lo que cuando se calcule la dirección origen usando el ASCII real
estaremos accediendo al sprite correcto.

Así, asignando a FONT_CHARSET el valor de “charset1-256”, cuando solicitemos imprimir el ASCII 32,
estaremos accediendo realmente a charset1-256+(32*8) = charset1-256+256 = charset1. De esta forma no
necesitamos restar 32 a ningun carácter ASCII para que la rutina imprima el carácter correspondiente real a un
valor ASCII.
El código de asignación de variables iniciales y llamada a la función sería pues similar al siguiente:

;;; Establecimiento de valores iniciales

LD HL, charset1-256 ; Saltamos los 32 caracteres iniciales


LD (FONT_CHARSET), HL ; Establecemos el valor del charset
LD A, 6
LD (FONT_ATTRIB), A ; Color amarillo sobre negro

;;; Impresion de un caracter "X" en 15,8


LD A, 15
LD (FONT_X), A
LD A, 8
LD (FONT_Y), A
LD A, 'X'
CALL PrintChar_8x8 ; Imprimimos el caracter

Nótese que PrintChar_8x8 modifica registros y no los preserva, por lo que si tenemos que salvaguardar el valor
de algún registro, debemos realizar PUSH antes de llamar a la función y POP al volver de ella.

El siguiente ejemplo (donde se ha omitido el código de las funciones ya vistas hasta ahora) muestra el charset
8×8 de 64 caracteres en pantalla, en un bucle de 4 líneas de 16 caracteres cada una:

; Visualizacion del charset de ejemplo


ORG 32768

LD H, 0
LD L, A
CALL ClearScreenAttrib ; Borramos la pantalla

LD HL, charset1-256 ; Saltamos los 32 caracteres iniciales


LD (FONT_CHARSET), HL
XOR A
LD (FONT_X), A
INC A
LD (FONT_Y), A ; Empezamos en (0,1)
LD A, 6
LD (FONT_ATTRIB), A ; Color amarillo sobre negro

LD C, 32 ; Empezamos en caracter 32

;;; Bucle vertical


LD E, 4

bucle_y:

;;; Bucle horizontal


LD B, 16 ; Imprimimos 16 caracteres horiz

bucle_x:
LD A, (FONT_X)
INC A
LD (FONT_X), A ; X = X + 1

LD A, C
PUSH BC
PUSH DE ; Preservamos registros
CALL PrintChar_8x8 ; Imprimimos el caracter "C"
POP DE
POP BC

INC C ; Siguiente caracter


DJNZ bucle_x ; Repetir 16 veces

LD A, (FONT_Y) ; Siguiente linea:


INC A
INC A ; Y = Y + 2
LD (FONT_Y), A
XOR A
LD (FONT_X), A ; X = 0

DEC E
JR NZ, bucle_y ; Repetir 4 veces (16*4=64 caracteres)

loop:
JR loop
RET

;-------------------------------------------------------------
FONT_CHARSET DW 0
FONT_ATTRIB DB 56 ; Negro sobre gris
FONT_X DB 0
FONT_Y DB 0
;-------------------------------------------------------------

PrintString_8x8

Nuestro siguiente objetivo es el de poder agrupar diferentes caracteres en una cadena y diseñar una función que
permita imprimir toda la cadena mediante llamadas consecutivas a PrintChar_8x8.

Definiremos las cadenas de texto como ristras de códigos ASCII acabadas en un byte 0 (valor 0, no ASCII '0'):

cadena DB "EJEMPLO DE CADENA", 0

El pseudocódigo de la rutina sería el siguiente:

Pseudocódigo de la rutina:

; Recoger parametros

; Repetir:
; Coger caracter actual de la cadena (apuntado por HL).
; Si el caracter es 0, fin de la cadena (salir)
; Incrementar HL para apuntar al siguiente caracter.
; Imprimir el carácter llamando a PrintChar_8x8
; Ajustar coordenada X en FONT_X
; Ajustar coordenada Y en FONT_Y

Es extremadamente importante acabar la cadena con un cero para que la rutina pueda ejecutar su condición de
salida. Si no se define la cadena correctamente, se continuará imprimiendo todo aquello apuntado por HL hasta
encontrar un valor cero en memoria.

Veamos el código de la rutina:

;-------------------------------------------------------------
; PrintString_8x8:
; Imprime una cadena de texto de un charset de fuente 8x8.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X = Coordenada X en baja resolucion (0-31)
; FONT_Y = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB = Atributo a utilizar en la impresion.
; Registro HL = Puntero a la cadena de texto a imprimir.
; Debe acabar en
;-------------------------------------------------------------
PrintString_8x8:
;;; Bucle de impresion de caracter
pstring8_loop:
LD A, (HL) ; Leemos un caracter de la cadena
OR A
RET Z ; Si es 0 (fin de cadena) volver
INC HL ; Siguiente caracter en la cadena
PUSH HL ; Salvaguardamos HL
CALL PrintChar_8x8 ; Imprimimos el caracter
POP HL ; Recuperamos HL

;;; Ajustamos coordenadas X e Y


LD A, (FONT_X) ; Incrementamos la X
INC A ; pero comprobamos si borde derecho
CP 31 ; X > 31?
JR C, pstring8_noedgex ; No, se puede guardar el valor

LD A, (FONT_Y) ; Cogemos coordenada Y


CP 23 ; Si ya es 23, no incrementar
JR NC, pstring8_noedgey ; Si es 23, saltar

INC A ; No es 23, cambiar Y


LD (FONT_Y), A

pstring8_noedgey:
LD (FONT_Y), A ; Guardamos la coordenada Y
XOR A ; Y ademas hacemos A = X = 0

pstring8_noedgex
LD (FONT_X), A ; Almacenamos el valor de X
JR pstring8_loop

La anterior rutina sería llamada como en el siguiente ejemplo:

;;; Prueba de impresion de cadenas:

LD HL, charset1-256 ; Saltamos los 32 caracteres iniciales


LD (FONT_CHARSET), HL
LD A, 64+3
LD (FONT_ATTRIB), A
LD HL, cadena
LD A, 0
LD (FONT_X), A
LD A, 15
LD (FONT_Y), A
CALL PrintString_8x8
RET

cadena DB "PRUEBA DE IMPRESION DE TEXTO DE UNA CADENA LARGA.", 0

Y el resultado en pantalla es:


La forma más óptima de programar la rutina de impresión consistiría en integrar el código de PrintChar_8x8
dentro de PrintString_8x8 evitando el recálculo de offset en pantalla en cada carácter. Se utilizaría DE como
puntero en la fuente y HL como puntero en pantalla. Una vez calculada la posición inicial en pantalla para el
primer carácter se variaría HL apropiadamente, incrementandolo en 1 para avances hacia la derecha tras la
impresión de un carácter. Los retornos de carro se realizarían restando 31 a L y ejecutando
Caracter_Abajo_HL. De esta forma se evitaría no sólo el CALL y RET contra PrintChar_8x8 sino que tampoco
habría que realizar el continuo cálculo de la posición destino. Después habría que trazar los atributos repitiendo
el proceso desde el inicio de la cadena.

Normalmente no se suele imprimir texto durante el desarrollo del juego (al menos no en el bucle principal de
juego), por lo que puede no ser necesario llegar a este extremo de optimización a cambio de una mayor
ocupación de las rutinas en memoria.

Si vamos a utilizar la impresión de cadenas en la introducción del juego o programa, los menúes, la descripción
de las fases, los créditos, la pausa o los mensajes entre nivel y nivel o el final del juego, probablemente será
suficiente con la rutina que acabamos de ver.

Las cadenas de texto que se puedan usar en el bucle principal del programa deberían imprimirse como Sprites,
y los valores numéricos como impresión tipo “PrintChar_8x8” de cada uno de sus dígitos, realizando sólo una
conversión numérica del número de dígitos realmente necesario.

Impresión de valores numéricos

Hemos visto hasta ahora cómo imprimir cadenas de texto ASCII, pero en muchas ocasiones nos veremos en la
necesidad de imprimir el valor numérico contenido por un registro o una variable de memoria.

Para poder realizar esta impresión necesitamos rutinas que conviertan un valor numérico en una representación
ASCII, la cual imprimiremos luego con PrintString_8x8. Según la base de conversión, necesitaremos las
siguientes rutinas:

• Bin2String : Convierte el valor numérico de un registro o variable en una cadena ASCII con la
representación binaria (unos y ceros) de dicho valor (base=2).
• Hex2String : Convierte el valor numérico de un registro o variable en una cadena ASCII con la
representación en hexadecimal (2 o 4 dígitos del 0 a la F) de dicho valor (base=16).
• Int2String : Convierte el valor numérico sin signo de un registro o variable en una cadena ASCII con la
representación decimal (N dígitos 0-9) de dicho valor (base=10).
El parámetro de entrada a las rutinas será L para valores de 8 bits o HL para valores de 16 bits, y todas las
rutinas utilizarán un área temporal de memoria para escribir en ella la cadena resultante de la conversión:

;;; Espacio de almacenamiento de las rutinas de conversion:

conv2string DS 18

Las rutinas acabarán el proceso de conversión agregrando un 0 (END OF STRING) tras los ASCIIs obtenidos,
con el objetivo de que la cadena pueda ser directamente impresa con las funciones vistas hasta ahora.

De esta forma, para imprimir en pantalla en formato Decimal el valor de 8 bits del registro A, haríamos lo
siguiente:

;;; Guardar en L el valor a convertir y llamar a rutina


LD L, A
CALL Dec2String_8

;;; Imprimir la cadena resultante de la conversion:


LD HL, conv2string
CALL PrintString_8x8

Veamos las diferentes rutinas:

Bin2String: Conversión a representación binaria


La conversión de un valor numérico a una representación binaria se basa en testear el estado de cada uno de los
bits del registro “parámetro” y almacenar en la cadena destino apuntada por DE un valor ASCII '0' ó '1' según el
resultado del testeo.

En lugar de ejecutar 8 ó 16 comparaciones con el comando BIT, utilizaremos la rotación a la derecha del
registro parámetro de forma que el bit a comprobar sea desplazado al Carry Flag y podamos testear su valor con
un JR NC o JR C.

La rutina de conversión tiene 2 puntos de entrada diferentes según necesitemos convertir un número de 8 bits
(valor en registro L) o de 16 bits (valor en registro HL), pero utiliza el mismo core de conversión para ambos
casos:

;-------------------------------------------------------------
; Bin2String_8 y Bin2String_16:
; Convierte el valor numerico de L o de HL en una cadena de
; texto con su representacion en binario (acabada en 0).
;
; IN: L = Numero a convertir para la version de 8 bits
; HL = Numero a convertir para la version de 16 bits
; OUT: [conv2string] = Cadena con la conversion a BIN.
; Usa: BC
;-------------------------------------------------------------

Bin2String_16: ; Punto de entrada de 16 bits


LD DE, conv2string ; DE = puntero cadena destino
LD C, H ; C = a convertir (parte alta)
CALL Bin2String_convert ; Llamamos a rutina conversora
JR Bin2String_8b ; Convertimos la parte baja,
; saltando el LD DE, conv2string

Bin2String_8: ; Punto de entrada de 8 bits


LD DE, conv2string ; DE = puntero cadena destino
Bin2String_8b:
LD C, L ; C = a convertir (parte baja)
CALL Bin2String_convert ; Llamamos a rutina conversora
XOR A
LD (DE), A ; Guardar End Of String
RET

Bin2String_convert:
LD B, 8 ; 8 iteraciones
b2string_loop: ; Bucle de conversion
RL C
LD A, '1' ; Valor en A por defecto
JR NC, b2string_noC
LD (DE), A ; Lo almacenamos en la cadena
INC DE
DJNZ b2string_loop
RET
b2string_noC:
DEC A ; A = '0'
LD (DE), A
INC DE ; Lo almacenamos y avanzamos
DJNZ b2string_loop
RET

En el área de memoria apuntada por conv2string tendremos la representación binaria en ASCII del valor en L o
HL, acabado en un carácter 0, lista para imprimir con PrintString_8x8.

Hex2String: Conversión a representación hexadecimal


Veamos la rutina de conversión de un valor numérico a una representación hexadecimal:

;-------------------------------------------------------------
; Hex2String_8 y Hex2String_16:
; Convierte el valor numerico de L o de HL en una cadena de
; texto con su representacion en hexadecimal (acabada en 0).
; Rutina adaptada de http://baze.au.com/misc/z80bits.html .
;
; IN: L = Numero a convertir para la version de 8 bits
; HL = Numero a convertir para la version de 16 bits
; OUT: [conv2string] = Cadena con la conversion a HEX.
;-------------------------------------------------------------
Hex2String_16:
LD DE, conv2string ; Cadena destino
LD A, H
CALL B2AHex_Num1 ; Convertir Hex1 de H
LD A, H
CALL B2AHex_Num2 ; Convertir Hex2 de H
JR Hex2String_8b ; Realizar conversion de L

Hex2String_8: ; Entrada para la rut de 8 bits


LD DE, conv2string

Hex2String_8b:
LD A, L
CALL B2AHex_Num1 ; Convertir Hex1 de L
LD A, L
CALL B2AHex_Num2 ; Convertir Hex2 de L
XOR A
LD (DE), A ; Guardar End Of String
RET

B2AHex_Num1:
RRA
RRA
RRA ; Desplazamos 4 veces >>
RRA ; para poder usar el siguiente bloque

B2AHex_Num2:
OR $F0 ; Enmascaramos 11110000
DAA ; Ajuste BCD
ADD A, $A0
ADC A, $40
LD (DE), A ; Guardamos dato
INC DE
RET

La rutina siempre devolverá una cadena de 2 caracteres + End_Of_String para valores de 8 bits, y de 4
caracteres + End_Of_String para valores de 16 bits.

Int2String: Conversión a representación decimal


Finalmente, veamos la rutina que convierte un valor numérico en su representación en formato decimal natural
(representación de número positivo).

;-----------------------------------------------------------------
; Int2String_8 e Int2String_16:
; Convierte un valor de 16 bits a una cadena imprimible.
;
; Entrada: HL = valor numerico a convertir
; Salida: Cadena en int2dec16_result .
; De: Milos Bazelides http://baze.au.com/misc/z80bits.html
;-----------------------------------------------------------------
Int2String_16:
LD DE, conv2string ; Apuntamos a cadena destino
LD BC, -10000 ; Calcular digito decenas de miles
CALL Int2Dec_num1
LD BC, -1000 ; Calcular digito miles
CALL Int2Dec_num1
JR Int2String_8b ; Continuar en rutina de 8 bits (2)

Int2String_8: ; Punto de entrada de rutina 8 bits


LD DE, conv2string ; Apuntar a cadena destino
LD H, 0 ; Parte alta de HL = 0

Int2String_8b: ; rutina de 8 bits (2)


LD BC, -100 ; Calcular digito de centenas
CALL Int2Dec_num1
LD C, -10 ; Calcular digito de decenas
CALL Int2Dec_num1
LD C, B ; Calcular unidades
CALL Int2Dec_num1
XOR A
LD (DE), A ; Almacenar un fin de cadena
RET

Int2Dec_num1:
LD A,'0'-1 ; Contador unidades, empieza '0'-1

Int2Dec_num2:
INC A ; Incrementamos el digito ('0', ... '9')
ADD HL, BC ; Restamos "unidades" hasta sobrepasarlo
JR C, Int2Dec_num2 ; Repetir n veces
SBC HL, BC ; Deshacemos el último paso
LD (DE), A ; Almacenamos el valor
INC DE
RET
Esta rutina almacena en la cadena todos los valores '0' que obtiene, incluídos los de las unidades superiores al
primer dígito del número (leading zeros). Así, la conversión del valor de 8 bits 17 generará una cadena “017” y
la conversión del número de 16 bits 1034 generará la cadena “01034”.

Eliminando los “leading zeros”

Si no queremos imprimir los ceros que hay al principio de una cadena apuntada por HL, podemos utilizar la
siguiente rutina que incrementa HL mientras encuentre ceros ASCII al principio de la cadena, y sale de la rutina
cuando encuentra el fin de cadena o un carácter distinto de '0'.

;-----------------------------------------------------------------
; Incrementar HL para saltar los 0's a principio de cadena
; (utilizar tras llamada a Int2String_8 o Int2String_16).
;-----------------------------------------------------------------
INC_HL_Remove_Leading_Zeros:
LD A, (HL) ; Leemos caracter de la cadena
OR A
RET Z ; Fin de cadena -> volver
CP '0'
RET NZ ; Distinto de '0', volver
INC HL ; '0', incrementar HL y repetir
JR INC_HL_Remove_Leading_Zeros

La rutina de conversión, en conjunción con la subrutina para eliminar los leading zeros se utilizaría de la
siguiente forma:

;;; Imprimir variable de 8 bits (podría ser un registro)


LD A, (variable_8bits)
LD L, A
CALL Int2String_8
LD HL, conv2string
CALL INC_HL_Remove_Leading_Zeros
CALL PrintString_8x8

;;; Imprimir variable de 16 bits


LD BC, (variable_16bits)
PUSH BC
POP HL ; HL = BC
CALL Int2String_16
LD HL, conv2string
CALL INC_HL_Remove_Leading_Zeros
CALL PrintString_8x8

Justificando los “leading zeros”

Finalmente, el usuario climacus en los foros de Speccy.org nos ofrece la siguiente variación de
INC_HL_Remove_Leading_Zeros para que la rutina imprima espacios en lugar de “leading zeros”, lo que
provoca que el texto en pantalla esté justificado a la derecha en ocupando siempre 3 (valores de 8 bits) ó 5
(valores de 16 bits) caracteres. Esto permite que los valores impresos puedan sobreescribir en pantalla valores
anteriores aunque estemos imprimiendo un valor “menor” que el que reside en pantalla:

INC_HL_Justify_Leading_Zeros:
LD A, (HL) ; Leemos caracter de la cadena
OR A
RET Z ; Fin de cadena -> volver
CP '0'
RET NZ ; Distinto de '0', volver
INC HL ; '0', incrementar HL y repetir
LD A, ' '
CALL Font_Blank ; Imprimimos espacio y avanzamos
JR INC_HL_Justify_Leading_Zeros

De esta forma, podemos tener en pantalla un valor “12345” que se vea sobreescrito por un valor “100” al
imprimir “ 100” (con 2 espacios delante).

También podemos sustituir “CALL Font_Blank” por un simple “CALL Font_Inc_X” para que se realice el
avance del cursor horizontalmente pero sin la impresión del carácter espacio.

Int2String_8_2Digits: Valores enteros de 2 dígitos


En el caso de que necesitemos imprimir un valor numérico de 2 digítos (00-99), incluyendo el posible cero
inicial (en valores 1-9) o los dos ceros para el valor 00, podemos utilizar la siguiente rutina:

;-----------------------------------------------------------------------
; Int2String_8_2Digits: Convierte el valor del registro A en una
; cadena de texto de max. 2 caracteres (0-99) decimales en DE.
; IN: A = Numero a convertir
; OUT: DE = 2 bytes con los ASCIIs
; Basado en rutina dtoa2d de:
; http://99-bottles-of-beer.net/language-assembler-%28z80%29-813.html
;-----------------------------------------------------------------------
Int2String_8_2Digits:
LD D, '0' ; Empezar en ASCII '0'
DEC D ; Decrementar porque el bucle hace INC
LD E, 10 ; Base 10
AND A ; Carry Flag = 0

dtoa2dloop:
INC D ; Incrementar numero de decenas
SUB E ; Quitar una unidad de decenas de A
JR NC, dtoa2dloop ; Si A todavia no es negativo, seguir
ADD A, E ; Decrementado demasiado, volver atras
ADD A, '0' ; Convertir a ASCII
LD E, A ; E contiene las unidades
RET

Este formato resulta especialmente útil para la impresión de variables temporales (horas, minutos, segundos), o
datos con valores máximos limitados como vidas (0-99) o niveles (0-99).

La forma de imprimir un valor con esta rutina sería el siguiente:

LD A, (vidas)
CALL Int2String_8_2Digits

;; Imprimir parte alta (decenas)


LD A, 0
LD (FONT_X), A
LD A, D
CALL PrintChar_8x8

;;; Imprimir parte baja (unidades)


LD A, 1
LD (FONT_X), A
LD A, E
CALL PrintChar_8x8

En un posterior apartado de este mismo capítulo veremos cómo integrar estas funciones en las rutinas de
impresión de cadenas con formato.
Fuente estándar 8x8 de la ROM
En el Spectrum disponemos de un tipo de letra estándar de 8×8 pregrabado en ROM. Los caracteres
imprimibles alojados en la ROM del Spectrum van desde el 32 (espacio) al 127 (carácter de copyright),
empezando el primero en $3D00 (15161 decimal) y acabando el último en $3FFF (16383, el último byte de la
ROM).

Existe una variable del sistema llamada CHARS (de 16 bits, ubicada en las direcciones 23606 y 23607) que
contiene la dirección de memoria del juego de caracteres que esté en uso por BASIC.

Por defecto, CHARS contiene el valor $3D00 (el tipo de letra estándar) menos 256. El hecho de restar 256 al
inicio real de la fuente es porque los caracteres definidos en la ROM empiezan en el 32 y restando 256 (8 bytes
por carácter para 32 caracteres = 256 bytes), al igual que hicimos nosotros con nuestro charset personalizado,
podemos hacer coincidir un ASCII > 32 con CHARS+(8*VALOR_ASCII).

El valor por defecto de CHARS es, pues, $3D00 - $0100 = $3C00.

El juego de caracteres estándar es inmutable al estar en ROM. La variable CHARS permitía, en el BASIC del
Spectrum, definir un juego de caracteres personalizado en RAM y apuntar CHARS a su dirección en memoria.
La definición de los 21 UDGs (19 en el +2A/+3) también está en RAM (desde $FF58 a $FFFF), ya que deben
de ser personalizables por el usuario.

Veamos el aspecto de la tipográfia estándar del Spectrum:

El juego de caracteres estándar de la ROM

El formato en memoria de la fuente de la ROM es idéntico a un spriteset de 8×8 sin atributos, tal y como hemos
definido las fuentes de texto personalizadas de nuestras rutinas y ejemplos anteriores. A partir de $3D00
empiezan los 8 bytes de datos (8 scanlines) del carácter 32, a los que siguen los 8 scanlines del carácter 33, etc.,
así hasta el carácter 127.

Gracias a esto podemos utilizar esta tipografía en nuestros juegos y programas (ahorrando así tener que definir
nuestro propio charset y ocupar memoria con él) directamente con las rutinas de impresión de caracteres y
cadenas que hemos utilizado con las fuentes de texto personalizables. Basta con establecer FONT_CHARSET a
la dirección adecuada, $3C00:

LD HL, 15616-256 ; Saltamos los 32 caracteres iniciales


LD (FONT_CHARSET), HL ; Ya podemos utilizar la tipografia del
; Spectrum con nuestras rutinas.
Esto nos permite ahorrar 768 bytes de memoria en nuestro programa (el de un charset personalizado) y disponer
de un recurso para la impresión de texto con un aspecto “estándar” (al que el usuario ya está acostumbrado).

Sistema de gestión de texto


Si estamos escribiendo un programa o juego que requiera la impresión de gran cantidad de texto puede resultar
imprescindible disponer de un pequeño sistema para almacenar los datos sobre la posición actual del “cursor”
en pantalla, color y estilo de la fuente, etc. Asímismo, también resulta especialmente útil un set de rutinas para
modificar todas estas variables fácilmente e imprimir cadenas con códigos de control directamente embebidos
dentro de las mismas.

Normalmente no se usará este tipo de rutinas en un juego arcade o videoaventura, pero puede aprovecharse en
programas no lúdicos y en juegos basados en texto o con gran cantidad de texto (managers deportivos,
aventuras de texto, RPGs, etc).

Ya hemos visto cómo las rutinas PrintChar_8x8 y PrintString_8x8 hacen uso de las variables FONT_X,
FONT_Y, FONT_CHARSET y FONT_ATTRIB. En este apartado definiremos más variables, funciones para
manipularlas y nuevas funciones de impresión que hagan uso avanzado de ambas.

La sección sobre sistemas de gestión de texto se divide en:

• Descripción de las variables globales que se usarán.


• Rutina de impresión de caracteres con diferentes estilos.
• Descripción de códigos de control que se usarán en las cadenas.
• Rutina de impresión de cadenas avanzada que haga uso de los códigos de control.
• Rutina de impresión de cadenas extendida que soporte códigos de variable.

Cuando trabajamos con texto tenemos que tener en cuenta que no se suelen requerir niveles de optimización de
tiempos de ejecución como en el caso de la rutinas de impresión de sprites. En este caso, dado que el tiempo de
“lectura” del usuario es significativamente mayor que el tiempo de ejecución, las rutinas deben tratar de ahorrar
espacio en memoria para que el programa pueda disponer de la mayor cantidad de texto posible.

Variables de nuestro sistema de gestión

Estas son las variables que utilizaremos en nuestras rutinas:

• FONT_X, FONT_Y : Coordenadas X e Y en baja resolución (comenzando en 0) de la posición actual


para la próxima impresión de un carácter (cursor).
• FONT_CHARSET : Apunta al spriteset de la fuente de texto (charset) a utilizar. El valor por defecto es
$3D00-256 (la fuente de la ROM).
• FONT_ATTRIB : Almacena el atributo en uso para la impresión de caracteres.
• FONT_STYLE : Almacena un valor numérico que define el estilo de impresión a utilizar. Por defecto
es 0 (estilo normal).

También utilizaremos algunas constantes que se modificarán sólo en tiempo de ensamblado:

• FONT_SRCWIDTH y FONT_SRCHEIGHT : Definen el ancho y alto de la pantalla en caracteres.


Para fuentes de 8×8, vale 32 y 24 respectivamente.

En código de programa:

FONT_CHARSET DW $3D00-256
FONT_ATTRIB DB 56 ; Negro sobre gris
FONT_STYLE DB 0
FONT_X DB 0
FONT_Y DB 0
FONT_SCRWIDTH EQU 32
FONT_SCRHEIGHT EQU 24

El resto de constantes (como los códigos de control) los veremos más adelante en sus correspondientes
apartados.

Subrutinas de gestión de posición y atributos

Una vez definidas las diferentes variables, necesitamos funciones básicas para gestionarlas. Nótese que ninguna
de las siguientes funciones provoca impresión de datos en pantalla, sólo altera el estado de las variables que
acabamos de ver y que después utilizarán las funciones de impresión:

;-------------------------------------------------------------
FONT_CHARSET DW $3D00-256
FONT_ATTRIB DB 56 ; Negro sobre gris
FONT_STYLE DB 0
FONT_X DB 0
FONT_Y DB 0
FONT_SCRWIDTH EQU 32
FONT_SCRHEIGHT EQU 24

FONT_NORMAL EQU 0
FONT_BOLD EQU 1
FONT_UNDERSC EQU 2
FONT_ITALIC EQU 3

;-------------------------------------------------------------
; Establecer el CHARSET en USO
; Entrada : HL = direccion del charset en memoria
;-------------------------------------------------------------
Font_Set_Charset:
LD (FONT_CHARSET), HL
RET

;-------------------------------------------------------------
; Establecer el estilo de texto en uso.
; Entrada : A = estilo
;-------------------------------------------------------------
Font_Set_Style:
LD (FONT_STYLE), A
RET

;-------------------------------------------------------------
; Establecer la coordenada X en pantalla.
; Entrada : A = coordenada X
;-------------------------------------------------------------
Font_Set_X:
LD (FONT_X), A
RET

;-------------------------------------------------------------
; Establecer la coordenada Y en pantalla.
; Entrada : A = coordenada Y
;-------------------------------------------------------------
Font_Set_Y:
LD (FONT_Y), A
RET

;-------------------------------------------------------------
; Establecer la posicion X,Y del curso de fuente en pantalla.
; Entrada : B = Coordenada Y
; C = Coordenada X
;-------------------------------------------------------------
Font_Set_XY:
LD (FONT_X), BC
RET

;-------------------------------------------------------------
; Establecer un valor de tinta para el atributo en curso.
; Entrada : A = Tinta (0-7)
; Modifica: AF
;-------------------------------------------------------------
Font_Set_Ink:
PUSH BC ; Preservamos registros
AND 7 ; Borramos bits 7-3
LD B, A ; Lo guardamos en B
LD A, (FONT_ATTRIB) ; Cogemos el atributo actual
AND %11111000 ; Borramos el valor de INK
OR B ; Insertamos INK en A
LD (FONT_ATTRIB), A ; Guardamos el valor de INK
POP BC
RET

;-------------------------------------------------------------
; Establecer un valor de papel para el atributo en curso.
; Entrada : A = Papel (0-7)
; Modifica: AF
;-------------------------------------------------------------
Font_Set_Paper:
PUSH BC ; Preservamos registros
AND 7 ; Borramos bits 7-3
RLCA ; A = 00000XXX -> 0000XXX0
RLCA ; A = 000XXX00
RLCA ; A = 00XXX000 <-- Valor en paper
LD B, A ; Lo guardamos en B
LD A, (FONT_ATTRIB) ; Cogemos el atributo actual
AND %11000111 ; Borramos los datos de PAPER
OR B ; Insertamos PAPER en A
LD (FONT_ATTRIB), A ; Guardamos el valor de PAPER
POP BC
RET

;-------------------------------------------------------------
; Establecer un valor de atributo para la impresion.
; Entrada : A = Tinta
;-------------------------------------------------------------
Font_Set_Attrib:
LD (FONT_ATTRIB), A
RET

;-------------------------------------------------------------
; Establecer un valor de brillo (1/0) en el atributo actual.
; Entrada : A = Brillo (0-7)
; Modifica: AF
;-------------------------------------------------------------
Font_Set_Bright:
AND 1 ; A = solo bit 0 de A
LD A, (FONT_ATTRIB) ; Cargamos en A el atributo
JR NZ, fsbright_1 ; Si el bit solicitado era
RES 6, A ; Seteamos a 0 el bit de flash
LD (FONT_ATTRIB), A ; Escribimos el atributo
RET
fsbright_1:
SET 6, A ; Seteamos a 1 el bit de brillo
LD (FONT_ATTRIB), A ; Escribimos el atributo
RET

;-------------------------------------------------------------
; Establecer un valor de flash (1/0) en el atributo actual.
; Entrada : A = Flash (1/0)
; Modifica: AF
;-------------------------------------------------------------
Font_Set_Flash:
AND 1 ; A = solo bit 0 de A
LD A, (FONT_ATTRIB) ; Cargamos en A el atributo
JR NZ, fsflash_1 ; Si el bit solicitado era
RES 7, A ; Seteamos a 0 el bit de flash
LD (FONT_ATTRIB), A ; Escribimos el atributo
RET
fsflash_1:
SET 7, A ; Seteamos a 1 el bit de flash
LD (FONT_ATTRIB), A ; Escribimos el atributo
RET

;-------------------------------------------------------------
; Imprime un espacio, sobreescribiendo la posicion actual del
; cursor e incrementando X en una unidad.
; de la pantalla (actualizando Y en consecuencia).
; Modifica: AF
;-------------------------------------------------------------
Font_Blank:
LD A, ' ' ; Imprimir caracter espacio
PUSH BC
PUSH DE
PUSH HL
CALL PrintChar_8x8 ; Sobreescribir caracter
POP HL
POP DE
POP BC
CALL Font_Inc_X ; Incrementamos la coord X
RET

;-------------------------------------------------------------
; Incrementa en 1 la coordenada X teniendo en cuenta el borde
; de la pantalla (actualizando Y en consecuencia).
; Modifica: AF
;-------------------------------------------------------------
Font_Inc_X:
LD A, (FONT_X) ; Incrementamos la X
INC A ; pero comprobamos si borde derecho
CP FONT_SCRWIDTH-1 ; X > ANCHO-1?
JR C, fincx_noedgex ; No, se puede guardar el valor
CALL Font_CRLF
RET

fincx_noedgex:
LD (FONT_X), A ; Establecemos el valor de X
RET

;-------------------------------------------------------------
; Produce un LineFeed (incrementa Y en 1). Tiene en cuenta
; las variables de altura de la pantalla.
; Modifica: AF
;-------------------------------------------------------------
Font_LF:
LD A, (FONT_Y) ; Cogemos coordenada Y
CP FONT_SCRHEIGHT-1 ; Estamos en la parte inferior
JR NC, fontlf_noedge ; de pantalla? -> No avanzar
INC A ; No estamos, avanzar
LD (FONT_Y), A

fontlf_noedge:
RET

;-------------------------------------------------------------
; Produce un Retorno de Carro (Carriage Return) -> X=0.
; Modifica: AF
;-------------------------------------------------------------
Font_CR:
XOR A
LD (FONT_X), A
RET

;-------------------------------------------------------------
; Provoca un LF y un CR en ese orden.
; Modifica: AF
;-------------------------------------------------------------
Font_CRLF:
CALL Font_LF
CALL Font_CR
RET

;-------------------------------------------------------------
; Imprime un tabulador (3 espacios) mediante PrintString.
; Modifica: AF
;-------------------------------------------------------------
Font_Tab:
PUSH BC
PUSH DE
PUSH HL
LD HL, font_tab_string
CALL PrintString_8x8 ; Imprimimos 3 espacios
POP HL
POP DE
POP BC
RET

font_tab_string DB " ", 0

;-------------------------------------------------------------
; Decrementa la coordenada X, simultando un backspace.
; No realiza el borrado en si.
; Modifica: AF
;-------------------------------------------------------------
Font_Dec_X:
LD A, (FONT_X) ; Cargamos la coordenada X
OR A
RET Z ; Es cero? no se hace nada (salir)
DEC A ; No es cero? Decrementar
LD (FONT_X), A
RET ; Salir

;-------------------------------------------------------------
; Decrementa la coordenada X, simultando un backspace.
; Realiza el borrado imprimiendo un espacio con LD.
; Modifica: AF
;-------------------------------------------------------------
Font_Backspace:
CALL Font_Dec_X
LD A, ' ' ; Imprimir caracter espacio
PUSH BC
PUSH DE
PUSH HL
CALL PrintChar_8x8 ; Sobreescribir caracter
POP HL
POP DE
POP BC
RET ; Salir

Podemos llamar a estas funciones desde nuestro programa en lugar de incluir una y otra vez el código necesario
para efectuar una acción:

LD HL, micharset
CALL Font_Set_Charset

LD A, 1+(7*8)
Call Font_Set_Attrib

LD A, FONT_NORMAL
CALL Font_Set_Style
(...)

Impresión de caracteres con estilos

Aunque ya hemos visto una rutina PrintChar_8x8 para impresión de caracteres, vamos a implementar a
continuación una nueva versión de la misma con la posibilidad de utilizar diferente estilos de fuente a partir de
la fuente original.

Mediante un único juego de caracteres podemos simular estilos de texto a través de código, manipulando “al
vuelo” los datos del charset antes de imprimirlos. Esto nos evita la necesidad de tener múltiples charsets de
texto para distintos estilos con la consiguiente ocupación de espacio en memoria.

Los estilos básicos que podemos conseguir al vuelo son normal, negrita, cursiva y subrayado.

Las rutinas de impresión para los 4 estilos esencialmente iguales salvo por el bucle de impresión, por lo que
vamos a utilizar una variable global llamada FONT_STYLE para indicar el estilo actual en uso, y
modificaremos la rutina PrintChar_8x8 para que haga uso del valor del estilo y modifique el bucle de impresión
en consecuencia.

;--- Variables de fuente --------------------


FONT_CHARSET DW $3D00-256
FONT_ATTRIB DB 56 ; Negro sobre gris
FONT_STYLE DB 0
FONT_X DB 0
FONT_Y DB 0

;--- Constantes predefinidas ----------------


FONT_NORMAL EQU 0
FONT_BOLD EQU 1
FONT_UNDERSC EQU 2
FONT_ITALIC EQU 3

;--- Nueva funcion PrintChar_8x8 ------------


PrintChar_8x8:
;;; Calcular coordenadas destino en Pantalla en DE
;;; Calcular posicion origen (array sprites) en HL
;;; Obtener el valor de FONT_STYLE
;;; Si es == FONT_NORMAL -> Bucle_Impresion_Normal
;;; Si es == FONT_BOLD -> Bucle_Impresion_Negrita
;;; Si es == FONT_UNDERSC -> Bucle_Impresion_Subrayado
;;; Si es == FONT_ITALIC -> Bucle_Impresion_Cursiva
;;; Calcular posicion destino atributo.
;;; Imprimir Atributo.

El esqueleto de la función PrintChar_8x8, a falta de introducir los bucles de impresión, ya lo conocemos:

;-------------------------------------------------------------
; PrintChar_8x8:
; Imprime un caracter de 8x8 pixeles de un charset usando
; el estilo especificado en FONT_STYLE.
;-------------------------------------------------------------
PrintChar_8x8:

LD BC, (FONT_X) ; B = Y, C = X
EX AF, AF' ; Nos guardamos el caracter en A'

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A ; DE contiene ahora la direccion destino.

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*8)
EX AF, AF' ; Recuperamos el caracter a dibujar de A'
LD BC, (FONT_CHARSET)
LD H, 0
LD L, A
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, BC ; HL = BC + HL = FONT_CHARSET + (A * 8)

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; INSERTAR AQUI BUCLES DE IMPRESION SEGUN ESTILO
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; (...)

;;; Impresion del caracter finalizada

;;; Impresion de atributos


LD A, H ; Recuperamos el valor inicial de HL
SUB 8 ; Restando los 8 scanlines avanzados

;;; Calcular posicion destino en area de atributos en DE.


; A = H
RRCA ; Codigo de Get_Attr_Offset_From_Image
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L

;;; Escribir el atributo en memoria


LD A, (FONT_ATTRIB)
LD (DE), A ; Escribimos el atributo en memoria
RET

La manera de obtener los diferentes estilos es la siguiente:

Estilo normal: No se realiza ningún tipo de modificación sobre los datos del carácter: se imprimen tal cual se
obtienen del spriteset en un bucle de 8 iteraciones:

;;;;;; Estilo NORMAL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


LD B, 8 ; 8 scanlines a dibujar
drawchar_loop_normal:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_normal
JR pchar8_printattr ; Impresion de atributos

Estilo negrita (bold): Para simular el efecto de la negrita necesitamos aumentar el grosor del carácter. Para eso,
leemos cada scanline y realizamos un OR de dicho scanline con una copia del mismo desplazada hacia la
derecha o hacia la izquierda.

;;;;;; Estilo NEGRITA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


LD B, 8 ; 8 scanlines a dibujar
drawchar_loop_bold:
LD A, (DE) ; Tomamos el dato del caracter
LD C, A ; Creamos copia de A
RRCA ; Desplazamos A
OR C ; Y agregamos C
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_bold
JR pchar8_printattr ; Impresion de atributos

Estilo Subrayado (underscore): Basta con dibujar los 7 primeros scanlines del carácter correctamente, y trazar
como octavo scanline un valor 255 (8 píxeles a 1, una línea horizontal). De esta forma el último scanline se
convierte en el subrayado.

;;;;;; Estilo SUBRAYADO ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


LD B, 7 ; 7 scanlines a dibujar normales
drawchar_loop_undersc:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_undersc

;;; El octavo scanline, una linea de subrayado


LD A, 255 ; Ultima linea = subrayado
LD (HL), A
INC H ; Necesario para el SUB A, 8
JR pchar8_printattr ; Impresion de atributos
Estilo Cursiva (italic): Finalmente, el estilo de letra cursiva implica imprimir los 3 primeros scanlines
desplazados hacia la derecha, los 2 centrales sin modificación y los 3 últimos desplazados hacia la izquierda:

;;;;;; Estilo ITALICA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; 3 primeros scanlines, a la derecha,
LD B, 3
drawchar_loop_italic1:
LD A, (DE) ; Tomamos el dato del caracter
SRA A ; Desplazamos A a la derecha
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_italic1

;;; 2 siguientes scanlines, sin tocar


LD B, 2

drawchar_loop_italic2:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_italic2

LD B, 3
drawchar_loop_italic3:
;;; 3 ultimos scanlines, a la izquierda,
LD A, (DE) ; Tomamos el dato del caracter
SLA A ; Desplazamos A
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_italic3
JR pchar8_printattr

El código completo y definitivo de la función de impresión de caracteres con estilo es el siguiente:

;-------------------------------------------------------------
; PrintChar_8x8:
; Imprime un caracter de 8x8 pixeles de un charset usando
; el estilo especificado en FONT_STYLE
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X = Coordenada X en baja resolucion (0-31)
; FONT_Y = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB = Atributo a utilizar en la impresion.
; FONT_STYLE = Estilo a utilizar (0-N).
; Registro A = ASCII del caracter a dibujar.
;-------------------------------------------------------------
PrintChar_8x8:

LD BC, (FONT_X) ; B = Y, C = X
EX AF, AF' ; Nos guardamos el caracter en A'

;;; Calculamos las coordenadas destino de pantalla en DE:


LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A ; DE contiene ahora la direccion destino.

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*8)
EX AF, AF' ; Recuperamos el caracter a dibujar de A'
LD BC, (FONT_CHARSET)
LD H, 0
LD L, A
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, BC ; HL = BC + HL = FONT_CHARSET + (A * 8)

EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

;;; NUEVO: Verificacion del estilo actual


LD A, (FONT_STYLE) ; Obtenemos el estilo actual
OR A
JR NZ, pchar8_estilos_on ; Si es != cero, saltar

;;;;;; Estilo NORMAL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


LD B, 8 ; 8 scanlines a dibujar
drawchar_loop_normal:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_normal
JR pchar8_printattr ; Imprimir atributos

pchar8_estilos_on:
CP FONT_BOLD ; ¿Es estilo NEGRITA?
JR NZ, pchar8_nobold ; No, saltar

;;;;;; Estilo NEGRITA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


LD B, 8 ; 8 scanlines a dibujar
drawchar_loop_bold:
LD A, (DE) ; Tomamos el dato del caracter
LD C, A ; Creamos copia de A
RRCA ; Desplazamos A
OR C ; Y agregamos C
LD (HL), A ; Establecemos el valor en videomemoria
INC DE ; Incrementamos puntero en caracter
INC H ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawchar_loop_bold
JR pchar8_printattr

pchar8_nobold:
CP FONT_UNDERSC ; ¿Es estilo SUBRAYADO?
JR NZ, pchar8_noundersc ; No, saltar

;;;;;; Estilo SUBRAYADO ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


LD B, 7 ; 7 scanlines a dibujar normales
drawchar_loop_undersc:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_undersc

;;; El octavo scanline, una linea de subrayado


LD A, 255 ; Ultima linea = subrayado
LD (HL), A
INC H ; Necesario para el SUB A, 8
JR pchar8_printattr

pchar8_noundersc:
CP FONT_ITALIC ; ¿Es estilo ITALICA?
JR NZ, pchar8_UNKNOWN ; No, saltar
;;;;;; Estilo ITALICA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; 3 primeros scanlines, a la derecha,
LD B, 3
drawchar_loop_italic1:
LD A, (DE) ; Tomamos el dato del caracter
SRA A ; Desplazamos A a la derecha
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_italic1

;;; 2 siguientes scanlines, sin tocar


LD B, 2

drawchar_loop_italic2:
LD A, (DE) ; Tomamos el dato del caracter
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_italic2

LD B, 3
drawchar_loop_italic3:
;;; 3 ultimos scanlines, a la izquierda,
LD A, (DE) ; Tomamos el dato del caracter
SLA A ; Desplazamos A
LD (HL), A ; Establecemos el valor en videomemoria
INC DE
INC H
DJNZ drawchar_loop_italic3
JR pchar8_printattr

pchar8_UNKNOWN: ; Estilo desconocido...


LD B, 8 ; Lo imprimimos con el normal
JR drawchar_loop_normal ; (estilo por defecto)

;;; Impresion de los atributos


pchar8_printattr:

LD A, H ; Recuperamos el valor inicial de HL


SUB 8 ; Restando los 8 scanlines avanzados

;;; Calcular posicion destino en area de atributos en DE.


; A = H
RRCA ; Codigo de Get_Attr_Offset_From_Image
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L

;;; Escribir el atributo en memoria


LD A, (FONT_ATTRIB)
LD (DE), A ; Escribimos el atributo en memoria
RET

Si definimos esta función PrintChar_8x8 en nuestro programa, la función PrintString_8x8 hará uso de ella y
podremos imprimir cadenas en diferentes estilos, como en el siguiente ejemplo:

; Ejemplo de estilos de fuente


ORG 32768

LD HL, $3D00-256 ; Saltamos los 32 caracteres iniciales


LD (FONT_CHARSET), HL
LD A, 1+(7*8)
LD (FONT_ATTRIB), A

;;; Probamos los diferentes estilos: NORMAL


LD A, FONT_NORMAL
LD (FONT_STYLE), A
LD HL, cadena1
LD A, 4
LD (FONT_Y), A
XOR A
LD (FONT_X), A
CALL PrintString_8x8

;;; Probamos los diferentes estilos: NEGRITA


LD A, FONT_BOLD
LD (FONT_STYLE), A
LD HL, cadena2
LD A, 6
LD (FONT_Y), A
XOR A
LD (FONT_X), A
CALL PrintString_8x8

;;; Probamos los diferentes estilos: CURSIVA


LD A, FONT_ITALIC
LD (FONT_STYLE), A
LD HL, cadena3
LD A, 8
LD (FONT_Y), A
XOR A
LD (FONT_X), A
CALL PrintString_8x8

;;; Probamos los diferentes estilos: SUBRAYADO


LD A, FONT_UNDERSC
LD (FONT_STYLE), A
LD HL, cadena4
LD A, 10
LD (FONT_Y), A
XOR A
LD (FONT_X), A
CALL PrintString_8x8

loop:
JR loop

RET

cadena1 DB "IMPRESION CON ESTILO NORMAL.", 0


cadena2 DB "IMPRESION CON ESTILO NEGRITA.", 0
cadena3 DB "IMPRESION CON ESTILO CURSIVA.", 0
cadena4 DB "IMPRESION CON ESTILO SUBRAYADO.", 0

;-------------------------------------------------------------
FONT_CHARSET DW $3D00-256
FONT_ATTRIB DB 56 ; Negro sobre gris
FONT_STYLE DB 0
FONT_X DB 0
FONT_Y DB 0
FONT_NORMAL EQU 0
FONT_BOLD EQU 1
FONT_UNDERSC EQU 2
FONT_ITALIC EQU 3
FONT_SCRWIDTH EQU 32
FONT_SCRHEIGHT EQU 24

El anterior ejemplo (que usa el juego de caracteres estándar de la ROM) produce el siguiente resultado en
pantalla:
La rutina de impresión PrintChar_8x8 es ahora ligeramente más lenta que la original, pero a cambio nos
permite diferentes estilos de texto. Para la impresión de texto con estilo normal, sólo le hemos añadido el
siguiente código adicional a ejecutar:

LD A, (FONT_STYLE)
OR A
JR NZ, pchar8_estilos_on ; Aqui no se produce salto

JR pchar8_printattr ; Este salto si se produce

Son 13 (LD) + 4 (OR) + 7 (JR NZ sin salto) + 12 (JR) = 36 t-estados adicionales por carácter en estilo normal a
cambio de la posibilidad de disponer de 4 estilos de texto diferentes para cualquier charset, incluído el de la
ROM.

En la rutina se han utilizado operaciones de transferencia LD para imprimir los caracteres, lo que implica que
no se respeta el fondo sobre el que se imprime, y se ponen a cero en pantalla todos los píxeles a cero en el
charset. Este suele ser el sistema de impresión habitual puesto que el texto, para hacerlo legible, suele
imprimirse sobre áreas en blanco de la pantalla, y un caracter impreso sobre otro debe sobreescribir totalmente
al primero.

No obstante, la rutina PrintChar_8x8 puede ser modificada por el lector, para utilizar operaciones OR en la
transferencia a pantalla y por tanto respetar el contenido de pantalla al imprimir el carácter.

Impresión de cadenas con códigos de control

Llegados a este punto, tenemos:

• Un sistema de variables que controlan los datos sobre la impresión: posición, estilo, color, fuente de
texto, etc.
• Un set de funciones que permite modificar estas variables fácilmente.
• Una función de impresión de caracteres 8×8 que utiliza estas variables para imprimir el carácter
indicado en la posición adecuada, con el estilo seleccionado y con el color y fondo elegidos.
El siguiente paso en la escala de la gestión del texto sería la impresión de cadenas con formato para que
aproveche nuestras nuevas funciones extendidas. Para esto modificaremos la rutina PrinString_8x8 vista al
principio del capítulo de forma que haga uso no sólo de FONT_X y FONT_Y sino también de funciones
adicionales que especificaremos en la cadena mediante códigos de control o tokens.

Los códigos de control que vamos a definir y utilizar serán los siguientes:

Código de control Significado


0 Indica Fin de cadena
1 Cambiar estilo (seguido del byte con el estilo)
2 Cambiar posicion x (seguido de la coordenada x)
3 Cambiar posicion y (seguido de la coordenada y)
4 Cambiar color tinta (seguido del color de tinta)
5 Cambiar color de papel (seguido del color de papel)
6 Cambiar color de atributo (seguido del byte de atributo)
7 Cambiar Brillo ON / OFF (seguido de 1 ó 0)
8 Cambiar Flash ON / OFF (seguido de 1 ó 0)
10 Provocar LF (LineFeed = avance de linea) (y+=1)
11 Provocar avance de linea y retorno de carro (LF y CR) (y+=1 y x=0)
12 Avanzar 1 caracter sin imprimir nada (x+=1)
13 Provocar CR (retorno de carro o Carriage Return) (x=0)
Backspace: borrar el caracter de (x-1 si x>0)
14
imprimiendo un espacio y decrementando x.
15 Tabulador (impresion de 3 espacios por llamada a PrintString)

Así pues, declaramos las siguientes constantes predefinidas:

FONT_EOS EQU 0 ; End of String


FONT_SET_STYLE EQU 1
FONT_SET_X EQU 2
FONT_SET_Y EQU 3
FONT_SET_INK EQU 4
FONT_SET_PAPER EQU 5
FONT_SET_ATTRIB EQU 6
FONT_SET_BRIGHT EQU 7
FONT_SET_FLASH EQU 8
FONT_XXXXXX EQU 9 ; Libre para ampliaciones
FONT_LF EQU 10
FONT_CRLF EQU 11
FONT_BLANK EQU 12
FONT_CR EQU 13
FONT_BACKSPACE EQU 14
FONT_TAB EQU 15
FONT_INC_X EQU 16 ; De la 17 a la 31 libres

Esto nos permitirá definir las cadenas como, por ejemplo:

cadena DB "Cadena de texto ", FONT_SET_INK, 2, "ROJO ", FONT_CRLF


DB FONT_SET_INK, 1, "AHORA AZUL"
DB FONT_SET_Y, 20, "AHORA EN LA LINEA 20", FONT_EOS

La rutina de impresión de cadenas deberá interpretar cada byte de la misma determinando:


• Si es un código ASCII >= 32 → Imprimir el caracter en FONT_X, FONT_Y (y variar las coordenadas).
• Si es un código de control 0 (EOS) → Fin de la rutina
• Si es un código entre el 1 y el 9 → Recoger parámetro en cadena (siguiente byte) y llamar a la función
apropiada.
• Si es un código entre el 10 y el 31 → Llamar a la función apropiada (no hay parámetro).

Esto nos permitirá trabajar con cadenas de texto con múltiples formatos sin tener que realizar el
posicionamiento, cambio de color, de papel, gestión de los retornos de carro, etc. en nuestro código, con un
gran ahorro en código de manipulación gracias a nuestra nueva rutina genérica de impresión.

Llamaremos a esta rutina de impresión con formato PrintString_8x8_Format, y tendrá el siguiente


pseudocódigo:

;;; HL = Cadena que imprimir

PrintString_8x8_Format:
;;;
bucle:
;;; Coger caracter apuntado por HL.
;;; Incrementar HL
;;; Si HL es mayor que 32 :
;;; Imprimir caracter en FONT_X,FONT_Y
;;; Avanzar el puntero FONT_X
;;; Si HL es menor que 31 :
;;; Si es CERO, salir de la rutina con RET Z.
;;; Si es FONT_SET_SETSTYLE (1):
;;; Coger el siguiente byte de la cadena (el estilo)
;;; Llamar a función Font_Set_Style
;;; Si es FONT_SET_X (2):
;;; Coger el siguiente byte de la cadena (coordenada X)
;;; Llamar a función Font_Set_X
;;; Si es FONT_SET_Y (3):
;;; Coger el siguiente byte de la cadena (coordenada X)
;;; Llamar a función Font_Set_X
;;; (...)
;;; (...)
;;; (...)
;;; Si es FONT_BACKSPACE (14):
;;; Llamar a funcion Font_Backspace
;;; Si es FONT_TAB (15):
;;; Llamar a funcion Font_Tab
;;; Saltar a bucle (se saldrá con el RET Z)

El pseudocódigo que acabamos de ver utiliza gran cantidad de controles de flujo condicionales para decidir a
qué rutina debemos de saltar y si tenemos que recoger un parámetro de la cadena (leer valor apuntado por HL e
incrementar HL) o no.

En lugar de utilizar un enorme bloque de código con gran cantidad de saltos, vamos a emplear una tabla de
saltos. Para ello creamos una tabla en memoria que contenga las direcciones de salto de todos los códigos de
control, excepto el cero:

;-------------------------------------------------------------
; Tabla con las direcciones de las 16 rutinas de cambio.
;-------------------------------------------------------------
FONT_CALL_JUMP_TABLE:
DW 0000, Font_Set_Style, Font_Set_X, Font_Set_Y, Font_Set_Ink
DW Font_Set_Paper, Font_Set_Attrib, Font_Set_Bright
DW Font_Set_Flash, 0000, Font_LF, Font_CRLF, Font_Blank
DW Font_CR, Font_Backspace, Font_Tab, Font_Inc_X

Ahora, suponiendo que tengamos en A el código de control, la rutina en pseudocódigo podría ser así:

PrintString_8x8_Format:
bucle:
;;; Coger caracter apuntador por HL.
;;; Incrementar HL
;;; Si HL es mayor que 32 :
;;; Imprimir caracter en FONT_X,FONT_Y
;;; Avanzar el puntero FONT_X
;;; Si HL es menor que 31 :
;;; Si es CERO, salir de la rutina con RET Z.
;;; Calculamos DIR_SALTO = TABLA_SALTOS [ COD_CONTROL ]
;;; Como la tabla es de 2 bytes -> DIR_SALTO = TABLA_SALTOS + COD_CONTROL*2
;;; Si es menor que 10, requiere recoger parametro
;;; Recoger parametro
;;; Si es mayor que 10, no requiere recoger parametro
;;; Saltar a la dirección DIR_SALTO
;;; Saltar a bucle (se saldrá con el RET Z)

Veamos el código de la rutina definitiva:

;-------------------------------------------------------------
; Tabla con las direcciones de las 16 rutinas de cambio.
; Notese que la 9 queda libre para una posible ampliacion.
;-------------------------------------------------------------
FONT_CALL_JUMP_TABLE:
DW 0000, Font_Set_Style, Font_Set_X, Font_Set_Y, Font_Set_Ink
DW Font_Set_Paper, Font_Set_Attrib, Font_Set_Bright
DW Font_Set_Flash, 0000, Font_LF, Font_CRLF, Font_Blank
DW Font_CR, Font_Backspace, Font_Tab, Font_Inc_X

;-------------------------------------------------------------
; PrintString_8x8_Format:
; Imprime una cadena de texto de un charset de fuente 8x8.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X = Coordenada X en baja resolucion (0-31)
; FONT_Y = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB = Atributo a utilizar en la impresion.
; Registro HL = Puntero a la cadena de texto a imprimir.
; Debe acabar en un cero.
; Usa: DE, BC
;-------------------------------------------------------------
PrintString_8x8_Format:

;;; Bucle de impresion de caracter


pstring8_loop:
LD A, (HL) ; Leemos un caracter de la cadena
INC HL ; Apuntamos al siguiente caracter

CP 32 ; Es menor que 32?


JP C, pstring8_ccontrol ; Si, es un codigo de control, saltar

PUSH HL ; Salvaguardamos HL
CALL PrintChar_8x8 ; Imprimimos el caracter
POP HL ; Recuperamos HL

;;; Avanzamos el cursor usando Font_Blank, que incrementa X


;;; y actualiza X e Y si se llega al borde de la pantalla
CALL Font_Inc_X ; Avanzar coordenada X
JR pstring8_loop ; Continuar impresion hasta CHAR=0

pstring8_ccontrol:
OR A ; A es cero?
RET Z ; Si es 0 (fin de cadena) volver

;;; Si estamos aqui es porque es un codigo de control distinto > 0


;;; Ahora debemos calcular la direccion de la rutina que lo atendera.
;;; Calculamos la direccion destino a la que saltar usando
;;; la tabla de saltos y el codigo de control como indice
EX DE, HL
LD HL, FONT_CALL_JUMP_TABLE
RLCA ; A = A * 2 = codigo de control * 2
LD C, A
LD B, 0 ; BC = A*2
ADD HL, BC ; HL = DIR FONT_CALL_JUMP_TABLE+(CodControl*2)
LD C, (HL) ; Leemos la parte baja de la direccion en C...
INC HL ; ... para no corromper HL y poder leer ...
LD H, (HL) ; ... la parte alta sobre H ...
LD L, C ; No hemos usado A porque se usa en el CP

;;; Si CCONTROL>0 y CCONTROL<10 -> recoger parametro y saltar a rutina


;;; Si CCONTROL>9 y CCONTROL<32 -> saltar a rutina sin recogida
CP 18 ; Comprobamos si (CCONTROL-1)*2 < 18
JP NC, pstring8_noparam ; Es decir, si CCONTROL > 9, no hay param

;;; Si CCONTROL < 10 -> recoger parametro:


LD A, (DE) ; Cogemos el parametro en cuestion de la cadena
INC DE ; Apuntamos al siguiente caracter

;;; Realizamos el salto a la rutina con o sin parametro recogido


pstring8_noparam:
LD BC, pstring8_retaddr ; Ponemos en BC la dir de retorno
PUSH BC ; Hacemos un push de la dir de retorno
JP (HL) ; Saltamos a la rutina seleccionada

;;; Este es el punto al que volvemos tras la rutina


pstring8_retaddr:
EX DE, HL ; Recuperamos en HL el puntero a cadena
JR pstring8_loop ; Continuamos en el bucle

El esqueleto de la rutina y la parte de impresión ya la conocemos, porque es idéntica a PrintString_8x8. El


principal añadido es la interpretación de los códigos de control, donde la parte más interesante es la
construcción y uso de la tabla de saltos:

Una vez ubicadas todas las diferentes direcciones de las rutinas en FONT_CALL_JUMP_TABLE, podemos
utilizar el valor del registro A para direccionar la tabla. Para ello debemos multiplicar A por 2 ya que cada
dirección consta de 2 bytes. Cargando A*2 en BC podemos calcular la dirección destino en la tabla como
HL+BC (BASE+DESPLAZAMIENTO = BASE+COD_CONTROL*2). Leyendo el valor apuntado por HL
obtenemos la dirección de la tabla, es decir, la dirección de la rutina que puede interpretar el código de control
que hemos recibido.

;;; Calculamos la direccion destino a la que saltar usando


;;; la tabla de saltos y el codigo de control como indice
EX DE, HL
LD HL, FONT_CALL_JUMP_TABLE
RLCA ; A = A * 2 = codigo de control * 2
LD C, A
LD B, 0 ; BC = A*2
ADD HL, BC ; HL = DIR FONT_CALL_JUMP_TABLE+(CodControl*2)
LD C, (HL) ; Leemos la parte baja de la direccion en C...
INC HL ; ... para no corromper HL y poder leer ...
LD H, (HL) ; ... la parte alta sobre H ...
LD L, C ; No hemos usado A porque se usa en el CP
; HL = FONT_CALL_JUMP_TABLE+(CodControl*2)

; (...) ; Codigo de recogida de parametro si procede

LD BC, pstring8_retaddr ; Ponemos en BC la dir de retorno


PUSH BC ; Hacemos un push de la dir de retorno
JP (HL) ; Saltamos a la rutina seleccionada
;;; Este es el punto al que volvemos tras la rutina
pstring8_retaddr:

Con el anterior cálculo, por ejemplo, si recibimos un código de control 6, se pondrá en HL la dirección de
memoria contenida en FONT_CALL_JUMP_TABLE+(6*2), que es el valor Font_Set_Attrib, que el
ensamblador sustituirá en la tabla durante el proceso de ensamblado por la dirección de memoria de dicha
rutina.

Nótese cómo después de calcular el valor de salto correcto para HL tenemos que simular un “CALL HL”, que
no forma parte del juego de instrucciones del Spectrum. ¿Cómo realizamos esto? Utilizando la pila y la
instrucción JP. Recordemos que un CALL es un salto a subrutina, lo cual implica introducir en la pila la
dirección de retorno y salta a la dirección de la rutina. Cuando la rutina realice el RET, se extrae de la pila la
dirección de retorno para continuar el flujo del programa.

En el código anterior introducimos en el registro BC la dirección de la etiqueta pstring8_retaddr, que es la


posición exacta de memoria después del salto. Una vez introducida en la pila la dirección de retorno, saltamos
con el salto incondicional JP (HL) a la rutina especificada por el código de control. La subrutina efectuará la
tarea correspondiente y volverá con un RET, provocando que la rutina de impresión de cadenas continúe en
pstring8_retaddr, que es la dirección que el RET extraerá de la pila para volver.

Hemos hecho distinción de 2 tipos de subrutinas de control, ya que las 9 primeras requieren recoger un
parámetro de la cadena (apuntado por HL) y las restantes no. El cálculo de la dirección de salto es igual en
todos los casos pero para las 9 primeras es necesario obtener el dato adicional al código de control en el registro
A antes de saltar. El registro A es el parámetro común en todas las subrutinas de gestión de códigos de control
que requieren parámetros, algo necesario para poder usar las rutinas vía tabla de saltos.

La comprobación de si debemos recoger o no parámetro desde la cadena la realizamos con el siguiente código:

;;; Si CCONTROL>0 y CCONTROL<10 -> recoger parametro y saltar a rutina


;;; Si CCONTROL>9 y CCONTROL<32 -> saltar a rutina sin recogida
CP 18 ; Comprobamos si (CCONTROL-1)*2 < 18
JP NC, pstring8_noparam ; Es decir, si CCONTROL > 9, no hay param

En lugar de volver a dividir el código de control entre 2 (recordemos que se multiplicó por 2 para el cálculo de
la dirección de salto) y comprobar si es > 9, podemos comprobar directamente si es > 9*2 = 18.

Tras interpretar el código de control, bastará con volver a saltar al principio de la rutina para continuar con el
siguiente carácter. Todo el proceso se repetirá hasta recibir en A un código de control 0 (FONT_EOS, de
FONT_END_OF_STRING).

Una vez explicada la rutina, veamos un ejemplo de cómo podríamos utilizarla en nuestros programas:

; Ejemplo de gestion de texto


ORG 32768

LD HL, $3D00-256 ; Saltamos los 32 caracteres iniciales


CALL Font_Set_Charset

LD A, 1+(7*8)
Call Font_Set_Attrib

;;; Probamos los diferentes estilos: NORMAL


LD A, FONT_NORMAL
CALL Font_Set_Style
LD HL, cadena1
LD B, 4
LD C, 0
CALL Font_Set_XY
CALL PrintString_8x8_Format

loop:
JR loop

cadena1 DB "SALTO DE", FONT_LF, "LINEA ", FONT_SET_X, 4, FONT_SET_Y, 9


DB "IR A (4,9) ", FONT_SET_INK, 2, "ROJO "
DB FONT_SET_INK, 0, "NEGRO", FONT_CRLF, FONT_LF
DB "CRLF+LF ", FONT_SET_STYLE, FONT_UNDERSC, "ESTILO SUBRAYADO"
DB FONT_CRLF, FONT_CRLF, FONT_SET_STYLE, FONT_BOLD, "NEGRITA"
DB FONT_CRLF, FONT_CRLF, FONT_SET_STYLE, FONT_ITALIC, "CURSIVA"
DB FONT_CRLF, FONT_CRLF, FONT_TAB, FONT_SET_PAPER, 0
DB FONT_SET_INK, 6, "TABULADOR + INK 6 PAPER 0"
DB FONT_EOS

La salida en pantalla del anterior ejemplo (añadiendo las funciones correspondientes al código):

Es importante destacar que podríamos ampliar la rutina de impresión con más códigos de control y funciones.
Con la configuración que hemos visto, el código de control 9 queda libre para la introducción de una función
adicional que requiera parámetro, y del 17 al 31 podemos añadir más funciones de formato que no requieran
parámetros (por ejemplo, combinaciones de color y estilos concretos en una función que cambie BRIGHT,
FLASH, INK, PAPER y STYLE, o incluso cambios entre diferentes charsets).

Si necesitaremos más “espacio” para rutinas con parámetro, podríamos “reubicar” los códigos de control por
encima del 10 (cambiando los EQUs) y modificando el CP de la rutina que determinar si el control-code tiene
parámetro o no.

Recomendamos al lector que utilice siempre en sus cadenas los códigos de control mediante las constantes
EQU en lugar de utilizar los códigos numéricos en sí mismos. Esto permite reubicar los valores numéricos (los
EQUs) sin modificar las cadenas. Recordemos que el ensamblador sustituirá las constantes por sus valores
numéricos durante el proceso de ensamblado, por lo que la ocupación en las cadenas definitivas no será
“mayor” al usar las constantes. El único código de control que no debe reubicarse nunca es FONT_EOS (0).

Finalmente, creemos importante indicar al lector que para marcar claramente la dirección de salto del código de
control 9 (que no está en uso) se ha usado la cadena “0000”, pero probablemente sería más seguro el colocar la
dirección de una rutina como FONT_TAB o FONT_CRLF. De esta forma, ante un error del programador al
escribir una cadena y utilizar el inexistente código 9 en ella, evitaremos que se produzca un reset (JP $0000)
que nos cueste gran cantidad de horas de encontrar / depurar.

En cuanto a las diferencias en tiempo de ejecución de PrintString_8x8_Format vs PrintString_8x8, cabe


destacar que el coste adicional de la rutina para la impresión del texto normal (ASCIIs < 32) se reduce a las
siguientes 2 instrucciones adicionales:

CP 32 ; Es menor que 32?


JP C, pstring8_ccontrol ; Si, es un codigo de control, saltar
Aparte de eso, se ha sustituído el código de avance de la coordenada X por el de las rutinas genéricas vistas
anteriormente, lo que añade un CALL Font_Inc_X (y su RET) adicional. Así pues, el coste en tiempo de
ejecución no difiere apenas de la función sin códigos de control.

En el caso del código de fin de cadena (EOS = 0), ya no se sale de la rutina con un RET Z sino que se pasa por
el CP 32 y se realiza el salto a pstring8_ccontrol.

Sí que hay un coste real en la ocupación de memoria, puesto que todas las funciones auxiliares de control que
hemos definido seguramente pueden no resultarnos útiles en la programación de un juego donde no se utilice
apenas texto. Ese código adicional sumado a la gestión de códigos de control de la rutina y a la tabla de saltos
puede ser espacio utilizable por nosotros si empleados la rutina sin formato PrintString_8x8.

Donde no hay duda de la gran utilidad de las anteriores rutinas es en cualquier juego basado en texto, donde nos
evitamos realizar el formato de los textos en base a programación y llamadas continuadas a las funciones de
formato, posicionamiento, etc. Bastará con definir las cadenas en nuestro programa con el formato adecuado. El
ahorro en líneas de código será muy considerable.

Impresión avanzada: datos variables

Nuestro siguiente objetivo es extender PringString_8x8_Format para permitir la utilización de códigos de


control que representen valores de variables. El objetivo es simular la funcionalidad de la función printf() del
lenguaje C, el cual permite impresiones de cadena como la siguiente:

printf( "Jugador %s: Tienes %d vidas.", nombre, vidas );

Para eso vamos a crear una nueva rutina PrintString_8x8_Format_Args que además de los códigos de control
de formato, comprenda códigos para la impresión de variables de cadena o numéricas en representación
decimal, hexadecimal o binaria.

Los nuevos códigos de control imitarán el formato de C (símbolo de % seguido de un identificador del tipo de
variable) y podrán estar así integrados dentro del propio texto:

Código de control Significado


%d Imprimir argumento número entero de 8 bits en formato decimal
%D Imprimir argumento número entero de 16 bits en formato decimal
%t Imprimir argumento número entero 0-99 con 2 dígitos incluyendo ceros
%x Imprimir argumento de 8 bits en formato hexadecimal
%X Imprimir argumento de 16 bits en formato hexadecimal
%b Imprimir argumento de 8 bits en formato binario
%B Imprimir argumento de 16 bits en formato binario
%s Imprimir argumento de tipo cadena (acabada en 0 / EOS)
%% Símbolo del porcentaje (%)

De esta forma podremos definir cadenas como:

cadena1 DB "Has obtenido %D puntos", FONT_EOS


cadena2 DB "El numero %d en binario es %b", FONT_EOS
cadena3 DB "Has tenido un %d %% de aciertos", FONT_EOS
cadena4 DB "Bienvenido al juego, %s", FONT_EOS
cadena5 DB "Hora: %t:%t", FONT_EOS

Nótese que podríamos haber empleado el sistema de códigos de formato con los ASCIIs libres entre el 17 y el
31. El lector puede adaptar fácilmente la rutina a ese sistema si así lo deseara.

Volvamos a PrintString_8x8_Format_Args: Nuestra nueva rutina deberá recibir ahora un parámetro adicional:
además de la cadena a imprimir en HL, deberemos apuntar el registro IX a un array con los datos a sustuitir, o
apuntando a una única variable de memoria si sólo hay un parámetro.

La rutina es similar a PrintString_8x8_Format, pero añadiendo lo siguiente:

;;; HL = Cadena que imprimir

PrintString_8x8_Format_Args:
;;;
bucle:
;;; Coger caracter apuntado por HL.
;;; Incrementar HL
;;; Si HL es mayor que 32 :
;;; Si es un caracter '%':
;;; Si el siguiente caracter no es %:
;;; Saltamos a seccion de codigo_gestion_ARGS
;;; Imprimir caracter en FONT_X,FONT_Y
;;; Avanzar el puntero FONT_X
;;; Si HL es menor que 31 :
;;; Si es CERO, salir de la rutina con RET Z.
(...)

codigo_gestion_ARGS:
;;; Llegamos aqui con el codigo en A
;;; Si el codigo es 'd' -> Saltar a gestion de tipo int8
;;; Si el codigo es 'D' -> Saltar a gestion de tipo int16
;;; Si el codigo es 't' -> Saltar a gestion de tipo int8_2digits
;;; Si el codigo es 'x' -> Saltar a gestion de tipo hex8
;;; Si el codigo es 'X' -> Saltar a gestion de tipo hex16
;;; Si el codigo es 'b' -> Saltar a gestion de tipo bin8
;;; Si el codigo es 'B' -> Saltar a gestion de tipo bin16
;;; Si el codigo es 's' -> Saltar a gestion de tipo string

En código:

loop:
LD A, (HL) ; Leemos un caracter de la cadena

;;; (...)

CP '%' ; Es un caracter %?
JR NZ, pstring8_novar ; Comprobamos si es variable
LD A, (HL) ; Cogemos en A el siguiente char
INC HL
CP '%' ; Es otro caracter %? (leido %%?)
JR NZ, pstring8v_var ; No, es una variable -> Saltar
; Si, era %, seguir para imprimirlo
(...)
;;; Aqui se gestionan los codigos de control con % (tipo = A)
pstring8v_var:
;;; comprobamos los tipos y saltamos a sus rutinas de gestion
CP 'd'
JR Z, pstring8v_int8
CP 't'
JR Z, pstring8v_int8_2d
CP 'D'
JR Z, pstring8v_int16
CP 's'
JR Z, pstring8v_string
CP 'x'
JR Z, pstring8v_hex8
CP 'X'
JR Z, pstring8v_hex16
CP 'b'
JP Z, pstring8v_bin8
CP 'B'
JP Z, pstring8v_bin16
JP pstring8_novar ; Otro: imprimir caracter tal cual

Las diferentes porciones de código a las que saltaremos según el tipo de dato a imprimir harán uso de las
funciones de conversión de valor numérico a cadena que ya vimos en un anterior apartado de este capítulo. Por
ejemplo:

CP 'd'
JR Z, pstring8v_int8

(...)
pstring8v_int8:
PUSH HL
LD L, (IX+0)
INC IX
CALL Int2String_8
LD HL, conv2string
CALL INC_HL_Remove_Leading_Zeros
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

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

;-------------------------------------------------------------
; PrintString_8x8_Format_Args:
; Imprime una cadena de texto de un charset de fuente 8x8.
; Soporta codigos de control y argumentos.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X = Coordenada X en baja resolucion (0-31)
; FONT_Y = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB = Atributo a utilizar en la impresion.
; Registro HL = Puntero a la cadena de texto a imprimir.
; Debe acabar en cero (FONT_EOS).
; Registro IX = Puntero al listado de argumentos (si hay).
; Usa: DE, BC
;-------------------------------------------------------------
PrintString_8x8_Format_Args:

;;; Bucle de impresion de caracter


pstring8v_loop:
LD A, (HL) ; Leemos un caracter de la cadena
INC HL ; Apuntamos al siguiente caracter

CP 32 ; Es menor que 32?


JP C, pstring8v_ccontrol ; Si, es un codigo de control, saltar

CP '%' ; Es un caracter %?
JR NZ, pstring8_novar ; Comprobamos si es variable
LD A, (HL) ; Cogemos en A el siguiente char
INC HL
CP '%' ; Es otro caracter %? (leido %%?)
JR NZ, pstring8v_var ; No, es una variable -> Saltar
; Si, era %, seguir para imprimirlo
pstring8_novar:
PUSH HL ; Salvaguardamos HL
CALL PrintChar_8x8 ; Imprimimos el caracter
POP HL ; Recuperamos HL
;;; Avanzamos el cursor usando Font_Blank, que incrementa X
;;; y actualiza X e Y si se llega al borde de la pantalla
CALL Font_Inc_X ; Avanzar coordenada X
JR pstring8v_loop ; Continuar impresion hasta CHAR=0

pstring8v_ccontrol:
OR A ; A es cero?
RET Z ; Si es 0 (fin de cadena) volver

;;; Si estamos aqui es porque es un codigo de control distinto > 0


;;; Ahora debemos calcular la direccion de la rutina que lo atendera.

;;; Calculamos la direccion destino a la que saltar usando


;;; la tabla de saltos y el codigo de control como indice
EX DE, HL
LD HL, FONT_CALL_JUMP_TABLE
DEC A ; Decrementamos A (puntero en tabla)
RLCA ; A = A * 2 = codigo de control * 2
LD C, A
LD B, 0 ; BC = A*2
ADD HL, BC ; HL = DIR FONT_CALL_JUMP_TABLE+(CodControl*2)
LD C, (HL)
INC HL
LD H, (HL)
LD L, C ; Leemos la direccion de la tabla en HL

;;; Si CCONTROL>0 y CCONTROL<10 -> recoger parametro y saltar a rutina


;;; Si CCONTROL>9 y CCONTROL<32 -> saltar a rutina sin recogida
CP 18 ; Comprobamos si (CCONTROL-1)*2 < 18
JP NC, pstring8v_noparam ; Es decir, si CCONTROL > 9, no hay param

;;; Si CCONTROL < 10 -> recoger parametro:


LD A, (DE) ; Cogemos el parametro en cuestion de la cadena
INC DE ; Apuntamos al siguiente caracter

;;; Realizamos el salto a la rutina con o sin parametro recogido


pstring8v_noparam:
LD BC, pstring8v_retaddr ; Ponemos en BC la dir de retorno
PUSH BC ; Hacemos un push de la dir de retorno
JP (HL) ; Saltamos a la rutina seleccionada

;;; Este es el punto al que volvemos tras la rutina


pstring8v_retaddr:
EX DE, HL ; Recuperamos en HL el puntero a cadena
JR pstring8v_loop ; Continuamos en el bucle

;;; Aqui se gestionan los codigos de control con % (tipo = A)


pstring8v_var:
;;; comprobamos los tipos y saltamos a sus rutinas de gestion
CP 'd'
JR Z, pstring8v_int8
CP 't'
JR Z, pstring8v_int8_2d
CP 'D'
JR Z, pstring8v_int16
CP 's'
JR Z, pstring8v_string
CP 'x'
JR Z, pstring8v_hex8
CP 'X'
JP Z, pstring8v_hex16
CP 'b'
JP Z, pstring8v_bin8
CP 'B'
JP Z, pstring8v_bin16
JP pstring8_novar ; Otro: imprimir caracter tal cual

;----------------------------------------------------------
pstring8v_int8:
PUSH HL
LD L, (IX+0)
INC IX
CALL Int2String_8
LD HL, conv2string
CALL INC_HL_Remove_Leading_Zeros
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_int8_2d:
PUSH HL
LD A, (IX+0)
INC IX
CALL Int2String_8_2Digits
LD A, D ; Resultado conversion en DE
PUSH DE
CALL PrintChar_8x8 ; Imprimir parte alta (decenas)
CALL Font_Inc_X
POP DE
LD A, E
CALL PrintChar_8x8 ; Imprimir parte alta (decenas)
CALL Font_Inc_X
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_int16:
PUSH HL
LD L, (IX+0)
INC IX
LD H, (IX+0)
INC IX
CALL Int2String_16
LD HL, conv2string
CALL INC_HL_Remove_Leading_Zeros
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_string:
PUSH HL
PUSH IX ; HL = IX
POP HL
call PrintString_8x8 ; Imprimimos cadena
POP HL
pstring8v_strloop: ; Incrementamos IX hasta el fin
LD A, (IX+0) ; de la cadena, recorriendola
INC IX ; hasta (IX) = 0
OR A
JR NZ, pstring8v_strloop

; De esta forma IX ya apunta al siguiente argumento


JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_hex8:
PUSH HL
LD L, (IX+0)
INC IX
LD L, 40
CALL Hex2String_8
LD HL, conv2string
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal
;----------------------------------------------------------
pstring8v_hex16:
PUSH HL
LD L, (IX+0)
INC IX
LD H, (IX+0)
INC IX
CALL Hex2String_16
LD HL, conv2string
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_bin8:
PUSH HL
LD L, (IX+0)
INC IX
CALL Bin2String_8
LD HL, conv2string
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_bin16:
PUSH HL
LD L, (IX+0)
INC IX
LD H, (IX+0)
INC IX
CALL Bin2String_16
LD HL, conv2string
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

La llamada a la rutina de impresión con parámetros se realiza apuntando HL a la cadena con formato e IX al
listado de parámetros:

; Ejemplo de impresion de texto con argumentos


ORG 32768

LD HL, $3D00-256
CALL Font_Set_Charset

LD BC, $0400
CALL Font_Set_XY

LD HL, cadena1
LD IX, args1
CALL PrintString_8x8_Format_Args

LD HL, cadena2
LD IX, args2
CALL PrintString_8x8_Format_Args

loop:
JR loop
RET

cadena1 DB "VALOR 8 bits: 40", FONT_CRLF, FONT_CRLF


DB "Decimal: %d" , FONT_CRLF
DB "Hexadecimal: $%x" , FONT_CRLF
DB "Binario: %%%b" , FONT_CRLF
DB FONT_CRLF, FONT_CRLF
DB "VALOR 16 bits: 1205", FONT_CRLF, FONT_CRLF
DB "Decimal: %D" , FONT_CRLF
DB "Hexadecimal: $%X" , FONT_CRLF
DB "Binario: %%%B" , FONT_CRLF, FONT_CRLF, FONT_CRLF
DB FONT_EOS

args1 DB 40, 40, 040


DW 1205, 1205, 1205

cadena2 DB "2 CADENAS:", FONT_CRLF, FONT_CRLF


DB "Cadenas: %t: %s y %s"
DB FONT_EOS

args2 DB 2, "cad 1", FONT_EOS, "cad 2", FONT_EOS

No es necesario que el vector de parámetros contenga más de un elemento. Podemos utilizar la rutina
directamente con una variable de datos para imprimir su valor:

LD HL, cadvidas ; Cadena


LD IX, vidas ; Variable de argumentos
CALL PrintString_8x8_Format_Args ; Imprimir

(...)

cadvidas DB "Tienes %d vidas", FONT_EOS


vidas DB 10

Sí que hay que ser especialmente cuidadoso a la hora de definir los parámetros en la variable que apuntamos
con IX: es importante que cada parámetro tenga su tamaño adecuado (DB, DW), y que no le falten los End Of
String (0) a las cadenas.

Nótese que los parámetros que se imprimen pueden ser modificados por el programa, por lo que esta rutina es
muy útil en juegos o programas que trabajen con muchos datos a mostrar.

Añadiendo más códigos de control

El sistema que acabamos de ver permite su ampliación con nuevos tipos de datos o métodos de impresión
específicos. Supongamos por ejemplo que queremos añadir 2 nuevos tipos de impresión de valores enteros, uno
en el que se añadan los ceros al inicio de las cadenas resultantes de la conversión, y otro que permita la
impresión justificada.

Para ello creamos los nuevos “códigos de control”:


Código de
Significado
control
%z Imprimir argumento número entero de 8 bits en formato decimal con sus leading zeros
%Z Imprimir argumento número entero de 16 bits en formato decimal con sus leading zeros
Imprimir argumento número entero de 8 bits en formato decimal justificado a derecha (3
%j
caracteres)
Imprimir argumento número entero de 16 bits en formato decimal justificado a derecha (5
%J
caracteres)

A continuación realizamos las modificaciones adecuadas a la rutina PrintString_8x8_Format_Args:

PrintString_8x8_Format_Args:

;;; (...)

;;; Aqui se gestionan los codigos de control con % (tipo = A)


pstring8v_var:
;;; comprobamos los tipos y saltamos a sus rutinas de gestion
(...)
CP 'z'
JR Z, pstring8v_int8_zeros
CP 'Z'
JR Z, pstring8v_int16_zeros
CP 'j'
JR Z, pstring8v_int8_justify
CP 'J'
JR Z, pstring8v_int16_justify
JP pstring8_novar

;----------------------------------------------------------
pstring8v_int8_zeros:
PUSH HL
LD L, (IX+0)
INC IX
CALL Int2String_8
LD HL, conv2string
CALL PrintString_8x8 ; No llamamos a Remove_Leading_Zeros
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_int16_zeros:
PUSH HL
LD L, (IX+0)
INC IX
LD H, (IX+0)
INC IX
CALL Int2String_16
LD HL, conv2string
CALL PrintString_8x8 ; No llamamos a Remove_Leading_Zeros
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_int8_justify:
PUSH HL
LD L, (IX+0)
INC IX
CALL Int2String_8
LD HL, conv2string ; Llamamos a funcion Justify
CALL INC_HL_Justify_Leading_Zeros
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

;----------------------------------------------------------
pstring8v_int16_justify:
PUSH HL
LD L, (IX+0)
INC IX
LD H, (IX+0)
INC IX
CALL Int2String_16
LD HL, conv2string ; Llamamos a funcion Justify
CALL INC_HL_Justify_Leading_Zeros
CALL PrintString_8x8
POP HL
JP pstring8v_loop ; Volvemos al bucle principal

(...)

Lo normal a la hora de utilizar PrintString_Format_Args en nuestro programa es que eliminemos todos aquellos
códigos de control (y sus rutinas de chequeo y de gestión) de los cuales no vayamos a hacer uso, con el
consiguiente ahorro de memoria (desaparecen las instrucciones de las subrutinas de gestión).

Lectura de texto desde teclado

La posibilidad de leer una cadena tecleada por el usuario puede resultar fundamental en programas basados en
texto. En algunos juegos podríamos aprovecharla para introducción de claves de acceso, pero en aventuras de
texto o programas puede resultar imprescindible.

Una rutina de lectura de teclado recibirá como parámetro un puntero a un área vacía en memoria con suficiente
espacio libre para la cadena, así como el tamaño máximo de cadena que queremos leer (un límite que asegure
que no escribimos contenido fuera del área reservada y apuntada por HL).

En el artículo dedicado al teclado estudiamos rutinas de lectura del mismo que nos proporcionaban scancodes
de las teclas pulsadas. También vimos rutinas de obtención del código ASCII correspondiente a un scancode
dado.

En este caso necesitaremos una rutina más “avanzada”, que permita detectar el uso de CAPS SHIFT y DELETE
(CAPS SHIFT + '0') y que distinga por tanto entre mayúsculas y minúsculas. Para eso, utilizaremos la rutina de
escaneo de teclado y conversión a ASCII de la ROM del Spectrum (KEY_SCAN), ubicada en la dirección de
memoria $028E de la ROM.

Al realizar un CALL a KEY_SCAN se produce una lectura de todas las filas del teclado seguida de una
decodificación del resultado de la lectura. La rutina de la ROM coloca entonces en la variable del sistema
LAST_K (dirección 23560) el código ASCII de la tecla pulsada. KEY_SCAN también decodifica las teclas
extendidas y LAST_K nos puede servir también para detectar ENTER (código ASCII 13) o DELETE (código
ASCII 12).

El desarrollo de la rutina será el siguiente:

;;; Utilizar HL como puntero a una cadena vacía o con contenido.


;;; Utilizar un contador de tamaño de la cadena con valor inicial = longitud maxima - 1
;;; Imprimir cursor.
;;; En un bucle:
; Bucle lectura teclado:
; Hacer LAST_K = 0
; Leer el estado del teclado con KEY_SCAN.
; Repetir hasta que LAST_K sea distinto de 0.
; Si se ha pulsado la tecla de ENTER (LAST_K==13):
; Borrar el cursor en pantalla.
; Insertar un cero en la cadena (End Of String) y salir.
; Si se ha pulsado la tecla de DELETE o CAPS+'0' (LAST_K==12):
; Si no estamos en el primer caracter de la cadena:
; Borrar el cursor en pantalla.
; Realizar un FONT_BACKSPACE.
; Incrementar contador de longitud (cabe un caracter más).
; Si se ha pulsado una tecla de ASCII >= 32:
; Si el contador de longitud es mayor que 0:
; Insertar el ASCII en (HL) e incrementar HL.
; Imprimirla en pantalla e incrementar FONT_X.
; Decrementar contador de longitud.
; Reimprimir cursor.

Veamos el código de la rutina InputString_8x8:

LAST_K EQU 23560


KEY_SCAN EQU $028E

;-------------------------------------------------------------
; InputString_8x8:
; Obtiene una cadena de texto desde teclado.
;
; Entrada:
; Registro HL = Puntero a la cadena de texto a obtener.
; Registro A = Longitud maxima de la cadena a obtener
; Usa:
;-------------------------------------------------------------
InputString_8x8:

PUSH HL ; Guardamos el puntero a la cadena


PUSH DE
PUSH BC ; Modificados por KEY_SCAN

LD (inputs_counter), A ; Contador de caracteres a usar


LD (inputs_limit), A ; Guardamos la longitud maxima

inputs_start:
LD A, '_' ; Imprimir nuevo cursor
CALL Font_SafePrintChar_8x8

XOR A
LD (LAST_K), A ; Limpiar ultima tecla pulsada

inputs_loop:
PUSH HL ; KEY_SCAN modifica HL -> preservar
CALL KEY_SCAN ; Escanear el teclado
POP HL
LD A, (LAST_K) ; Obtener el valor decodificado

CP 13
JR Z, inputs_end ; Es enter? -> fin de rutina

CP 12
JR Z, inputs_delete ; Es DELETE? -> borrar caracter

CP 32
JR C, inputs_loop ; Es < 32? -> repetir bucle escaneo

;;; Si estamos aqui, ASCII >= 32 -> Es caracter valido -> Guardiar
EX AF, AF' ; Nos guardamos el valor ASCII en A'

;;; Comprobacion de longitud maxima de cadena


LD A, (inputs_counter) ; A = caracteres disponibles
OR A ; Comprobar si es 0
JR Z, inputs_loop ; Si es cero, no insertar caracter
DEC A
LD (inputs_counter), A ; Decrementar espacio disponible

EX AF, AF' ; Recuperamos ASCII de A'


LD (HL), A ; Guardamos el caracter leido
INC HL ; Avanzamos al siguiente caracter e imprimir
CALL Font_SafePrintChar_8x8
CALL Font_Inc_X
JR inputs_start ; Repetir continuamente hasta ENTER

;;; Codigo a ejecutar cuando se pulsa enter


inputs_end: ; ENTER pulsado -> Fin de la rutina

LD A, ' ' ; Borramos de la pantalla el cursor


CALL Font_SafePrintChar_8x8
XOR A
LD (HL), A ; Almacenamos un FIN DE CADENA en HL
POP BC
POP DE ; Recuperamos valores de registros
POP HL ; Recuperamos el inicio de la cadena
RET

;;; Codigo a ejecutar cuando se pulsa DELETE


inputs_delete: ; DELETE pulsado -> Borrar caracter
LD A, (inputs_limit)
LD B, A
LD , (inputs_counter)
CP B ; Si char_disponibles-limite == 0 ...
JR Z, inputs_loop ; ... no podemos borrar (inicio de cadena)

INC A ; Si no, si que podemos borrar:


LD (inputs_counter), A ; incrementar espacio disponible

DEC HL ; Decrementar puntero en la cadena


LD A, ' ' ; Borrar cursor y caracter anterior
CALL Font_SafePrintChar_8x8
CALL Font_Dec_X
JR inputs_start ; Bucle principal

inputs_counter DB 0
inputs_limit DB 0

InputString_8x8 utiliza una nueva subrutina llamada Font_SafePrintChar8x8 que no es más que una
encapsulación del PrintChar_8x8 original en la que se preservan y restauran los valores de los registros que
modifica internamente PrintChar:

;-------------------------------------------------------------
; Ejecuta PrintChar_8x8 preservando registros
;-------------------------------------------------------------
Font_SafePrintChar_8x8
PUSH BC
PUSH DE
PUSH HL ; Preservar registros
CALL PrintChar_8x8 ; Imprimir caracter
POP HL ; Recuperar registros
POP DE
POP BC
RET

Veamos un ejemplo de uso de nuestra nueva función de INPUT:

; Ejemplo de input de texto


ORG 32768

LD HL, $3D00-256
CALL Font_Set_Charset

;;; Imprimir cadena "Introduce un texto:"


LD HL, cadena1
LD B, 4
LD C, 0
CALL Font_Set_XY
CALL PrintString_8x8_Format

;;; Obtener el input del usuario


LD HL, input1
LD A, 20
CALL InputString_8x8

;;; Imprimir "Tu cadena es:" + la cadena resultante


LD HL, cadena2
CALL PrintString_8x8_Format

LD HL, input1
CALL PrintString_8x8_Format

RET

cadena1 DB "Introduce un texto:", FONT_CRLF, FONT_CRLF


DB FONT_SET_INK, 2, FONT_SET_STYLE, FONT_BOLD
DB "> ", FONT_EOS

cadena2 DB FONT_CRLF, FONT_CRLF, FONT_SET_STYLE, FONT_NORMAL


DB FONT_SET_INK, 0, "Tu cadena es: ", FONT_CRLF, FONT_CRLF
DB FONT_SET_INK, 2, FONT_SET_STYLE, FONT_BOLD, FONT_EOS

input1 DS 35
DB 0

Si nuestro programa o juego va a requerir un posibilidades de introducción o edición de textos avanzadas, sería
aconsejable ampliar la rutina anterior con nuevas opciones o mejoras como las siguientes:

• Permitir edición multilínea. La rutina actual no permite trabajar (al menos en cuanto al borrado) con
entrada de texto de múltiples líneas. Se podría editar la rutina para permitir editar más de una línea de
texto, realizando una versión especial de Font_Dec_X que decremente el valor de FONT_Y y ponga
FONT_X=0 cuando tratemos de borrar desde el margen izquierdo de la pantalla.
• Habilitar el uso de las teclas de cursor para moverse entre los caracteres de la cadena y así permitir
edición avanzada. La rutina debería basarse entonces en un FONT_X y FONT_Y propios y ya no se
podría utilizar FONT_BACKSPACE para el borrado. Además, al insertar un carácter en el interior de la
cadena habría que mover todos los caracteres en memoria una posición a la derecha y redibujar la
cadena completa en pantalla. El cursor podría simularse entonces con FLASH o subrayando la letra
actual (por lo que no serviría para editar texto subrayado).
• Permitir llamar a la función con una cadena ya en la zona apuntada por HL. En conjunción con la
mejora anterior permitiría editar texto anteriormente introducido.

En cualquier caso, la rutina que acabamos de ver es más que suficiente para recoger cadenas simples en nuestro
programa.

Fuentes de 4x8 píxeles (64 caracteres en pantalla)


La resolución de texto del Spectrum es bastante reducida, con sus 32 caracteres en pantalla por línea. Esto
provoca limitaciones en programas o juegos de texto que requieren mostrar muchas cadenas de caracteres por
pantalla.

Para solucionar esto podemos utilizar una fuente de tamaño 4×8 que nos permita ubicar 2 letras en un mismo
bloque de pantalla, proporcionándonos una resolución de 64×24 caracteres.

Dibujar letras distinguibles en 4×8 píxeles no es sencillo, pero existe una fuente de texto ya creada y código
para su impresión, creados por Andrew Owen (http://chuntey.wordpress.com) y Tony Samuels (Your Spectrum
#13, Abril de 1985).

La fuente de texto en 4×8 tiene el siguiente aspecto:

Para utilizar este set de caracteres sólo tendremos que realizar una nueva rutina de impresión llamada
PrintChar_4x8 y modificar la variable que define la anchura de la pantalla, FONT_SWIDTH (que pasa de
valer 32 a 64).

La definición de la fuente de 4×8 píxeles en formato DB es la siguiente:

; half width 4x8 font - 384 bytes


charset_4x8:
DB $00,$02,$02,$02,$02,$00,$02,$00,$00,$52,$57,$02,$02,$07,$02,$00
DB $00,$25,$71,$62,$32,$74,$25,$00,$00,$22,$42,$30,$50,$50,$30,$00
DB $00,$14,$22,$41,$41,$41,$22,$14,$00,$20,$70,$22,$57,$02,$00,$00
DB $00,$00,$00,$00,$07,$00,$20,$20,$00,$01,$01,$02,$02,$04,$14,$00
DB $00,$22,$56,$52,$52,$52,$27,$00,$00,$27,$51,$12,$21,$45,$72,$00
DB $00,$57,$54,$56,$71,$15,$12,$00,$00,$17,$21,$61,$52,$52,$22,$00
DB $00,$22,$55,$25,$53,$52,$24,$00,$00,$00,$00,$22,$00,$00,$22,$02
DB $00,$00,$10,$27,$40,$27,$10,$00,$00,$02,$45,$21,$12,$20,$42,$00
DB $00,$23,$55,$75,$77,$45,$35,$00,$00,$63,$54,$64,$54,$54,$63,$00
DB $00,$67,$54,$56,$54,$54,$67,$00,$00,$73,$44,$64,$45,$45,$43,$00
DB $00,$57,$52,$72,$52,$52,$57,$00,$00,$35,$15,$16,$55,$55,$25,$00
DB $00,$45,$47,$45,$45,$45,$75,$00,$00,$62,$55,$55,$55,$55,$52,$00
DB $00,$62,$55,$55,$65,$45,$43,$00,$00,$63,$54,$52,$61,$55,$52,$00
DB $00,$75,$25,$25,$25,$25,$22,$00,$00,$55,$55,$55,$55,$27,$25,$00
DB $00,$55,$55,$25,$22,$52,$52,$00,$00,$73,$12,$22,$22,$42,$72,$03
DB $00,$46,$42,$22,$22,$12,$12,$06,$00,$20,$50,$00,$00,$00,$00,$0F
DB $00,$20,$10,$03,$05,$05,$03,$00,$00,$40,$40,$63,$54,$54,$63,$00
DB $00,$10,$10,$32,$55,$56,$33,$00,$00,$10,$20,$73,$25,$25,$43,$06
DB $00,$42,$40,$66,$52,$52,$57,$00,$00,$14,$04,$35,$16,$15,$55,$20
DB $00,$60,$20,$25,$27,$25,$75,$00,$00,$00,$00,$62,$55,$55,$52,$00
DB $00,$00,$00,$63,$55,$55,$63,$41,$00,$00,$00,$53,$66,$43,$46,$00
DB $00,$00,$20,$75,$25,$25,$12,$00,$00,$00,$00,$55,$55,$27,$25,$00
DB $00,$00,$00,$55,$25,$25,$53,$06,$00,$01,$02,$72,$34,$62,$72,$01
DB $00,$24,$22,$22,$21,$22,$22,$04,$00,$56,$A9,$06,$04,$06,$09,$06

Dado que cada caracter es de 4×8 bytes, podemos almacenar en un mismo byte bien 2 scanlines de un mismo
ASCII o bien 2 scanlines de 2 ASCIIs consecutivos.

En nuestro caso, cada byte de la fuente contiene los datos de 2 caracteres, por lo que 8 bytes del array tienen los
8 scanlines de 2 ASCIIs consecutivos. El nibble superior (4 bits superiores) de cada byte tiene los datos de un
carácter ASCII y el nibble inferior los del siguiente. Así, el primer byte del array tiene el scanline superior (0)
de los ASCIIs 32 (nibble alto) y 33 (nibble bajo).

Esta disposición de 2 scanlines por byte permite un ahorro de memoria tal que la fuente completa de texto con
96 caracteres ocupe 768/2 = 384 bytes.

Para posicionarnos en esta fuente desde su base con el objetivo de localizar un carácter en A, deberemos dividir
el valor del carácter entre 2 ya que cada byte referencia a 2 caracteres. El resto de la división entre 2 (par o
impar) nos indica si el carácter que buscamos está en el nibble superior o el inferior de los 8 bytes consecutivos
a leer.

Por otra parte, la pantalla tiene “físicamente” 32 bloques, pero nosotros vamos a imprimir 64 caracteres, por lo
que cada bloque puede contener 2 caracteres. Cuando especificamos una coordenada X para imprimir, la rutina
necesita dividirla por 2 para saber en qué carácter de pantalla irá impresa la letra. Una vez calculado este
carácter, la letra puede ir en la parte “izquierda” del bloque de pantalla (4 bits superiores) o en la parte derecha
(4 bits inferiores del bloque). El resto de la división entre 2 de la coordenada X nos indica en cuál de las 2
partes se debe imprimir el carácter.

La rutina de impresión de caracteres de 4×8 es bastante parecida a la rutina estándar de 8×8 salvo por las
divisiones entre 2 del carácter ASCII y de la posición X en pantalla para el cálculo del origen en la fuente y del
destino en vram.

La impresión del carácter en sí mismo también cambia, ya que existen 4 posibilidades de impresión que
requieren 4 porciones de código diferentes:

Como en cada byte de la fuente se definen 2 caracteres (izquierdo y derecho) y a su vez a la hora imprimir en
pantalla hay 2 posibilidades de impresión en el mismo bloque (parte izquierda y parte derecha del bloque),
necesitamos 4 rutinas que cubran esas cuatro posibilidades.

• Imprimir un carácter de la “parte izquierda” (nibble alto en datos de caracter) de la fuente en la “parte
izquierda” de un bloque de pantalla (nibble alto del byte en videoram).
• Imprimir un carácter de la “parte derecha” (nibble bajo en datos de caracter) de la fuente en la “parte
izquierda” de un bloque de pantalla (nibble alto del byte en videoram).
• Imprimir un carácter de la “parte izquierda” (nibble alto en datos de caracter) de la fuente en la “parte
derecha” de un bloque de pantalla (nibble bajo del byte en videoram).
• Imprimir un carácter de la “parte derecha” (nibble bajo en datos de caracter) de la fuente en la “parte
derecha” de un bloque de pantalla (nibble bajo del byte en videoram).
Al trazar el carácter en pantalla tenemos que hacerlo con OR para respetar otro posible carácter de 4×8 que
pueda haber en el mismo bloque, ya lo estemos imprimiendo en la parte izquierda de un bloque (respetar el
nibble de la derecha) o en la derecha (respetar el nibble de la izquierda).

Veamos la rutina de impresión PrintChar_4x8 seguida de la 4 subrutinas de volcado de carácter que son
llamados una vez calculados HL y DE como origen y destino.

;-------------------------------------------------------------
; PrintChar_4x8:
; Imprime un caracter de 4x8 pixeles de un charset.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X = Coordenada X en baja resolucion (0-31)
; FONT_Y = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB = Atributo a utilizar en la impresion.
; Registro A = ASCII del caracter a dibujar.
;-------------------------------------------------------------
PrintChar_4x8:

RRA ; Dividimos A por 2 (resto en CF)


PUSH AF ; Guardamos caracter y CF en A'

;;; Calcular posicion origen (array fuente) en HL como:


;;; direccion = base_charset + ((CARACTER/2)*8)
LD BC, (FONT_CHARSET)
LD H, 0
LD L, A
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, BC ; HL = Direccion origen de A en fuente

;;; Calculamos las coordenadas destino de pantalla en DE:


LD BC, (FONT_X) ; B = Y, C = X
RR C
LD A, B
AND $18
ADD A, $40
LD D, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD E, A ; DE contiene ahora la direccion destino.

;;; Calculamos posición en pantalla. Tenemos que dividirla por 2 porque


;;; en cada columna de pantalla caben 2 caracteres. Usaremos el resto
;;; (Carry) para saber si va en la izq (CF=0) o der (CF=1) del caracter.
LD A, (FONT_X) ; Volvemos a leer coordenada X
RRA ; Dividimos por 2 (posicion X en pantalla)
; Ademas el carry tiene el resto (par/impar)
JR NC, pchar4_x_odd ; Saltar si es columna impar (por el CF)

;;; Ahora tenemos que imprimir el caracter en pantalla. Hemos saltado


;;; a pchar4_x_even o pchar4_x_odd segun si la posicion en pantalla es
;;; par o impar, pero cada una de estas 2 opciones nos da la posibilidad
;;; de usar una rutina u otra segun si el caracter ASCII es par o impar
;;; ya que tenemos que cogerlo de la fuente de una forma u otra
;;; Posicion de columna en pantalla par:
pchar4_x_even :
POP AF ; Restaura A=char y CF=si es char par/impar
JR C, pchar4_l_on_l
JR pchar4_r_on_l
pchar4_x_odd:
POP AF ; Restaura A=char y CF=si es char par/impar
JR NC, pchar4_r_on_r
JR pchar4_l_on_r

pchar4_continue:

;;; Impresion de los atributos


pchar4_printattr:

LD A, D ; Recuperamos el valor inicial de DE


SUB 8 ; Restando los 8 scanlines avanzados

;;; Calcular posicion destino en area de atributos en HL.


RRCA ; A ya es = D, listo para rotar
RRCA ; Codigo de Get_Attr_Offset_From_Image
RRCA
AND 3
OR $58
LD H, A
LD L, E

;;; Escribir el atributo en memoria


LD A, (FONT_ATTRIB)
LD (HL), A ; Escribimos el atributo en memoria
RET

;;;------------------------------------------------------------------
;;; "Subrutinas" de impresion de caracter de 4x8
;;; Entrada: HL = posicion del caracter en la fuente 4x8
;;; DE = posicion en pantalla del primer scanline
;;;------------------------------------------------------------------

;;;----------------------------------------------------
pchar4_l_on_l:
LD B, 8 ; 8 scanlines / iteraciones
pchar4_ll_lp:
LD A, (DE) ; Leer byte de la pantalla
AND %11110000
LD C, A ; Nos lo guardamos en C
LD A, (HL) ; Cogemos el byte de la fuente
AND %00001111
OR C ; Lo combinamos con el fondo
LD (DE), A ; Y lo escribimos en pantalla
INC D ; Siguiente scanline
INC HL ; Siguiente dato del "sprite"
DJNZ pchar4_ll_lp
JR pchar4_continue ; Volver tras impresion

;;;----------------------------------------------------
pchar4_r_on_r:
LD B, 8 ; 8 scanlines / iteraciones
pchar4_rr_lp:
LD A, (DE) ; Leer byte de la pantalla
AND %00001111
LD C, A ; Nos lo guardamos en C
LD A, (HL) ; Cogemos el byte de la fuente
AND %11110000
OR C ; Lo combinamos con el fondo
LD (DE), A ; Y lo escribimos en pantalla
INC D ; Siguiente scanline
INC HL ; Siguiente dato del "sprite"
DJNZ pchar4_rr_lp
JR pchar4_continue ; Volver tras impresion
;;;----------------------------------------------------
pchar4_l_on_r:
LD B, 8 ; 8 scanlines / iteraciones
pchar4_lr_lp:
LD A, (DE) ; Leer byte de la pantalla
AND %00001111
LD C, A ; Nos lo guardamos en C
LD A, (HL) ; Cogemos el byte de la fuente
RRCA ; Lo desplazamos 4 veces >> dejando
RRCA ; lo bits 4 al 7 vacios
RRCA
RRCA
AND %11110000
OR C ; Lo combinamos con el fondo
LD (DE), A ; Y lo escribimos en pantalla
INC D ; Siguiente scanline
INC HL ; Siguiente dato del "sprite"
DJNZ pchar4_lr_lp
JR pchar4_continue ; Volver tras impresion

;;;----------------------------------------------------
pchar4_r_on_l:
LD B, 8 ; 8 scanlines / iteraciones
pchar4_rl_lp:
LD A, (DE) ; Leer byte de la pantalla
AND %11110000
LD C, A ; Nos lo guardamos en C
LD A, (HL) ; Cogemos el byte de la fuente
RLCA ; Lo desplazamos 4 veces << dejando
RLCA ; los bits 0 al 3 vacios
RLCA
RLCA
AND %00001111
OR C ; Lo combinamos con el fondo
LD (DE), A ; Y lo escribimos en pantalla
INC D ; Siguiente scanline
INC HL ; Siguiente dato del "sprite"
DJNZ pchar4_rl_lp
JR pchar4_continue ; Volver tras impresion

Para poder utilizar estas rutinas con nuestro sistema de impresión habría que cambiar la constante que define el
tamaño de anchura de la pantalla en caracteres:

FONT_SCRWIDTH EQU 64

También habría que crear un PrintString_4x8 idéntico a PrintString_8x8 pero que llame a PrintChar_4x8, y
modificar aquellas rutinas que hagan referencia a una de estas 2 funciones, como por ejemplo:

Font_Backspace:
CALL Font_Dec_X
LD A, ' ' ; Imprimir caracter espacio
PUSH BC
PUSH DE
PUSH HL
CALL PrintChar_4x8 ; Sobreescribir caracter
POP HL
POP DE
POP BC
RET ; Salir

Una vez realizados estos cambios, podemos utilizar la fuente de 4×8 con nuestro sistema de gestión de texto.
Veamos un sencillo ejemplo:

; Ejemplo de fuente de 4x8 pixeles (64 caracteres por linea)


ORG 32768
LD HL, charset_4x8-128 ; Inicio charset - (256/2)
CALL Font_Set_Charset

LD HL, cadena1
LD BC, $0400 ; X=00, Y=04
CALL Font_Set_XY
CALL PrintString_4x8_Format

loop:
JR loop
RET

cadena1 DB "Fuente de 4x8", FONT_CRLF, FONT_CRLF


DB "Esto es un texto impreso a 64 columnas con fuente "
DB "de 4x8 pixeles. La letra tiene la mitad de anchura "
DB "que la estandar pero todavia se lee con facilidad."
DB FONT_CRLF, FONT_CRLF
DB "Con esta fuente se pueden realizar juegos basados en "
DB "texto pero hay que tener en cuenta que los atributos "
DB FONT_SET_INK, 2, "afectan a 2 caracteres ", FONT_SET_INK, 0
DB "a la vez, por lo que es mejor no cambiar el paper y "
DB "modificarlos solo antes o tras un espacio."
DB FONT_EOS

Nótese cómo hemos inicializado FONT_CHARSET a la dirección de la fuente menos 128 en lugar de restarle
256. Esto se debe a que la fuente tiene 2 caracteres definidos en cada byte y vamos a dividir el ASCII entre 2 en
nuestra rutina, por lo que el carácter en que empieza nuestra fuente, el 32, está en charset4x8 - (256/2) =
charset4x8 - 128.

Otro detalle importante es el tema de los atributos: como cada bloque de pantalla contiene 2 caracteres, no
podemos establecer atributos diferentes para 2 caracteres del mismo byte. Por esto, hay que ser cauto a la hora
de establecer atributos. La solución más sencilla es cambiar las tintas en posiciones donde haya espacios, ya
que en ese caso el cambio será efectivo en la letra deseada si ésta es la primera del byte, o en el espacio seguido
de la letra deseada si está en la parte derecha. Los cambios de PAPER, BRIGHT o FLASH supondrán
problemas si no se realizan siempre en posiciones de pantalla pares.

Finalmente, debido al reducido tamaño de la fuente no se han definido funciones de estilo, ya que es muy difícil
realizar estilos de negrita o cursiva con una definición de 4×8. La única opción viable es la creación de un estilo
de subrayado creando 4 funciones de impresión 4×8 adicionales (para las 4 combinaciones de par/impar en
cuanto a ASCII/pantalla) donde se tracen 7 scanlines y el último se trace como %11110000 ó %00001111.
Gráficos (y V): Técnicas de mapeado por bloques
En el amplio catálogo de software del Spectrum existen juegos con decenas y hasta cientos de pantallas. ¿Cómo
es posible que, a 7KB por pantalla, quepan tantas “localidades” de juego en la memoria del Spectrum?

Lo que posibilita esta variedad de pantallas de juego es la técnica de mapeado por bloques.

En esta técnica se utiliza un spriteset de bitmaps denominados tiles y se definen en memoria mapas basados en
identificadores numéricos donde cada valor representa un tile concreto. Así, se compone cada pantalla de juego
como una matriz de “tiles” a dibujar.

Un sólo spriteset (tileset) lo suficientemente variado y la habilidad del diseñador y del programador pueden dar
lugar a gran cantidad de pantallas y un amplio mapeado con un mínimo uso de memoria, todo basado en la
composición de la pantalla como repetición de tiles.

En este capítulo veremos cómo definir las pantallas del mapeado y rutinas para imprimir estas pantallas en la
videomemoria.

Creación de mapas a partir de bloques o tiles


Como ya vimos en el capítulo dedicado a Sprites en baja resolución, podemos generar las pantallas de nuestros
juegos como repetición de bitmaps tomados de un set gráfico a partir del cual se puedan construir todo el
mapeado.

Cada uno de estos “bloques” a partir del cual componemos la pantalla recibe el nombre de tile.

Con los tiles podemos componer tanto pantallas individuales de juego como mapas de gran extensión en los que
podemos cambiar de una pantalla a otra constantemente (redibujando una pantalla entera cada vez) o incluso
scrollear porciones de la misma.

La pantalla se codifica como una matriz o vector de datos numéricos donde cada valor representa un índice en
el tileset (un bitmap concreto). La rutina de impresión recorre este vector/matriz y traza cada bitmap en su
posición correspondiente para generar la imagen que ve el jugador.

Pueden existir incluso identificadores numéricos de bloque que nuestra rutina trate de forma especial, para
definir por ejemplo bloques que no deben de ser dibujados y que permitan ver el fondo que hay en la pantalla,
como sucede en el caso de Sokoban:
Tilemap: componiendo un mapa en pantalla
a partir de tiles de un tileset/spriteset + mapa.

Crear los mapeados mediante tiles nos permite un enorme ahorro de memoria ya que en lugar de necesitar 6912
bytes por cada pantalla de juego nos basta con un tileset y pantallas formadas por repetición de los mismos. Una
pantalla completa (256×192) formada por tiles de 16×16 ocupará apenas (256/16)x (192/16) = 16×12 = 192
bytes. En los 7 KB que ocupa una pantalla gráfica entera podemos definir 36 pantallas de juego en base a tiles.

Una gran parte de los juegos de Spectrum tienen sus pantallas formadas por mapas de tiles, debido al gran
ahorro de memoria que suponen. El diseñador de los gráficos y de las pantallas será el principal responsable de
que existan suficientes bloques diferentes para representar todos los elementos del mapeado con suficiente
diversidad gráfica.

Y no sólo podemos realizar juego de puzzle tipo Puzznic, Plotting o Sokoban: en base a tiles podemos crear
videoaventuras, los mapeados de un juego de disparos, juegos de laberintos, plataformas, o generar todas las
pantallas en que se desenvuelva un arcade. Una vez impresa la pantalla en base a bloques, el juego puede
desarrollarse pixel a pixel en cuanto al movimiento del personaje principal y los enemigos y objetos.
Organización de los mapas en memoria
Los mapas están compuestos por pantallas, que representan la porción del mismo que resulta visible por el
usuario.

Cada nivel puede estar formado por una única pantalla (caso de juegos como Sokoban, Bubble Bobble, Manic
Miner, etc.), o puede estar formado por múltiples pantallas (R-Type, Rick Dangerous, Dynamite Dan, Sabre
Wulf, Into the eagle's Nest, etc.).

Comencemos examinando la forma de definir y trazar una única pantalla. Posteriormente veremos las
estructuras de datos necesarias para definir un mapa completo como una matriz de pantallas.

Pantallas del mapa


Las pantallas de un mapa son vectores de datos donde cada elemento representa un identificador de tile (valor
numérico 0-N) que utilizaremos para obtener desde el tileset el gráfico concreto a dibujar en esa posición.

Podemos organizar los tiles linealmente en memoria en formato horizontal o en formato vertical.

Una pantalla en formato horizontal contiene los identificadores de tiles de la pantalla en scanlines horizontales
de tiles, comenzando con la fila 0, a la cual le sigue la fila 1, la 2, la 3, etc.

El tileset de Sokoban (el primero de los sets gráficos disponibles) es el siguiente:

Veamos ampliados los bloques de 16×16:


Como puede apreciarse, hay 8 tiles gráficos que numeramos desde el 0 al 7 siendo el 0 un bloque vacío de
fondo negro.

Ahora veamos cómo codificar una pantalla utilizando el tileset que acabamos de ver. Utilizaremos para eso la
primera pantalla del juego, que es la siguiente:

Es una pantalla de 16×12 tiles formados por 2×2 bloques cada uno (16×16=256 y 12×16=192 → 256×192
pixeles). Hay 7 tiles gráficos, uno “en blanco” (el 0), y un tile que es transparente (el que permite que se vea el
fondo). Los tiles están impresos sobre un fondo negro decorado con diferentes logotipos y gráficos del robot
protagonista (de ahí la importancia de los tiles transparentes, que permiten que no se sobreescriba el fondo).

Veamos el proceso de construcción de la pantalla: Codificaremos cada “bloque” de pantalla con el identificador
numérico que lo representa en el tileset, siendo 0 el tile inicial de contenido vacío, y siendo 8 el código especial
que indique que no se debe dibujar el tile (de forma que sea transparente y deje ver el fondo).
Organización horizontal

Empecemos con la primera fila horizonzal de datos. Nótese que esta primera fila (fila superior de la pantalla) es
totalmente transparente (no se debe escribir ningún tile y debe verse el fondo original con el patrón de relleno),
por lo que se codifica como:

DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8

La siguiente fila (segunda fila horizontal de tiles) ya tiene 4 bloques gráficos (tiles 2, 3, 1, 4), en el centro de la
pantalla. Todos los demás tiles son transparentes (valor 8):

DEFB 8,8,8,8,8,8,2,3,1,4,8,8,8,8,8,8

Continuemos observando la pantalla del nivel 1 de Sokoban y el vector de datos que vamos generando: La
tercera fila tiene 7 tiles: 3 consecutivos con formas de paredes (tiles nº 1, 2 y 3), luego 2 tiles “vacíos” de fondo
(valor 0) y 2 tiles más de “paredes” (tiles nº 5 y 4). El resto de tiles son transparentes (valor 8):

DEFB 8,8,8,8,1,2,3,0,0,5,4,8,8,8,8,8

Si continuamos codificando cada línea horizontal de pantalla en nuestro vector de datos, obtenemos la pantalla
completa:

;;; Pantalla 1 de Sokoban codificada horizontalmente.


;;;
;;; El tile de valor 8 es un tile transparente (no esta en el tileset).

sokoban_LEVEL1_h:
DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,2,3,1,4,8,8,8,8,8,8
DEFB 8,8,8,8,1,2,3,0,0,5,4,8,8,8,8,8
DEFB 8,8,8,8,4,0,6,6,0,0,5,8,8,8,8,8
DEFB 8,8,8,8,5,0,0,6,0,0,4,8,8,8,8,8
DEFB 8,8,8,8,4,0,0,0,0,0,5,8,8,8,8,8
DEFB 8,8,8,8,5,2,3,0,0,2,3,8,8,8,8,8
DEFB 8,8,8,8,8,8,1,0,0,0,4,8,8,8,8,8
DEFB 8,8,8,8,8,8,4,7,7,7,5,8,8,8,8,8
DEFB 8,8,8,8,8,8,5,2,3,2,3,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8

Hemos ubicado los datos en una representación visual clara mediante un DEFB por cada fila horizontal, pero el
aspecto real de los datos en memoria es totalmente lineal:

sokoban_LEVEL1_h:
DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,2,3,1,4,(...),8,8,8,8

Así pues, cada “fila” de 16 tiles de la pantalla se codifica con 16 identificadores de tile. Hay un total de 12 filas,
por lo que acabamos obteniendo un vector de 16*12 = 192 bytes de datos para la pantalla.

Normalmente, la conversión de datos gráficos a identificadores de tile no se realiza manualmente sino que se
emplea un “programa de dibujo de mapeados” (editor de mapas) con el que “dibujamos tiles” utilizando el
tileset como paleta y que permite exportar el mapa directamente como datos.

Organización vertical
En el anterior ejemplo hemos organizado el mapa en formato horizontal. También habría cabido la posibilidad
de organizarlo verticalmente, es decir, creando un vector que almacenara los identificadores de tile de cada
columna de la pantalla.

Veamos de nuevo la pantalla inicial de Sokoban para codificarla verticalmente:

En este caso, la pantalla comenzaría con 4 columnas de datos “transparentes”.

DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8

La quinta columna ya tiene bloques gráficos :

DEFB 8,8,1,4,5,4,5,8,8,8,8,8

La sexta columna bloques gráficos y bloques “vacíos” (0):

DEFB 8,8,2,0,0,0,2,8,8,8,8,8

La pantalla completa codificada verticalmente sería la siguiente:

;;; Pantalla 1 de Sokoban codificada verticalmente.


;;;
;;; El tile de valor 8 es un tile transparente (no esta en el tileset).

sokoban_LEVEL1_v:
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,1,4,5,4,5,8,8,8,8,8
DEFB 8,8,2,0,0,0,2,8,8,8,8,8
DEFB 8,2,3,6,0,0,3,1,4,5,8,8
DEFB 8,3,0,6,6,0,0,0,7,2,8,8
DEFB 8,1,0,0,0,0,0,0,7,3,8,8
DEFB 8,4,5,0,0,0,2,0,7,2,8,8
DEFB 8,8,4,5,4,5,3,4,5,3,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
DEFB 8,8,8,8,8,8,8,8,8,8,8,8
En este caso nuestros “DEFBs” son de 12 bytes cada uno, teniendo un total de 16 columnas de 12 bytes = los
mismos 192 bytes de datos de pantalla.

Según codifiquemos el mapa en formato horizontal o vertical tendremos que programar una rutina de impresión
de pantalla u otra, ya que el orden de obtención de los datos desde el “vector de pantalla” es diferente.

La rutina de impresión vertical suele ser ligeramente más óptima que la horizontal dada la estructura de la
pantalla del Spectrum. Por contra, existe una tendencia generalizada a utilizar la codificación e impresión en
formato horizontal por similitud con nuestro sistema de escritura (de izquierda a derecha y de arriba a abajo) en
cuanto a la hora de “leer” los datos gráficos por parte del programador (visualmente hablando).

En nuestros ejemplos utilizaremos codificación horizontal y rutinas de impresión de filas (impresión horizontal)
siendo las rutinas de impresión vertical fácilmente deducibles a partir de estas.

Por otra parte, cabe destacar que podemos crear diferentes tilesets y una misma pantalla puede trazarse con
cualquiera de ellos permitiéndonos tener “sets gráficos” diferentes para que el usuario pueda elegir el más
acorde a sus gustos. El juego Sokoban, por ejemplo, incluye diferentes sets gráficos seleccionables por el
usuario; las pantallas de mapa son las mismas, y sólo cambia la dirección de origen de la que se leen los tiles (el
tileset) entre los varios disponibles (bastará con cambiar DM_SPRITES, la variable de memoria que utilizarán
nuestras rutinas, para apuntar a un tileset diferente).

Rutinas de impresión de pantallas

En este apartado vamos a ver rutinas diseñadas para dibujar una pantalla estática formada a base de tiles. La
rutina debe imprimir un mapa de ANCHOxALTO tiles a partir de una posición inicial (X_INICIO,Y_INICIO),
y está pensada para el trazado de una pantalla que no tiene scroll de ningún tipo y sobre la que después
trabajaremos.

Ejemplos de juegos con este “formato” de pantalla son Manic Miner, Bubble Bobble, Sabre Wulf, Dynamite
Dan, Sir Fred, etc…. La pantalla del mapa se dibuja una sóla vez y no se redibuja se o cambia de pantalla a
menos que el personaje cruce los límites de la misma.

Para juegos con scroll basados en tiles es necesario scrollear la pantalla y diseñar rutinas que impriman
porciones del mapeado (tiras verticales u horizontales de la pantalla entrante que aparezcan por alguna de las 4
direcciones), además de requerir una estructura de mapeado concreta, como veremos más adelante.

Aunque nos vamos a centrar ahora en la impresión de una sóla pantalla, no debemos olvidar que el mapa
completo del juego está formado por múltiples pantallas. El mapa en sí no será más que un array de direcciones
de las diferentes pantallas, del que obtendremos la dirección de la pantalla actual para proporcionársela a la
rutina de impresión.

Una primera aproximación a la impresión del mapa se basaría en la utilización de las rutinas de impresión de
Sprites ya vistas usando dos bucles (uno vertical y el otro, anidado, horizontal). La rutina recorrería toda la
pantalla del mapa e imprimiría cada tile utilizando DrawSprite:

;;; Aproximacion 1:
FOR Y=0 TO ALTO_PANTALLA_EN_TILES:
FOR X=0 TO ANCHO_PANTALLA_EN_TILES:
TILE = PANTALLA[x][y]
XTILE = X_INICIAL + X*ANCHO_TILE
YTILE = Y_INICIAL + Y*ALTO_TILE
CALL Draw_Sprite
La implementación de esta rutina no sería óptima porque implica gran cantidad de cálculos tanto en el bucle
interior como dentro de la rutina Draw_Sprite. Para cada tile de pantalla se realizaría un cálculo de dirección de
memoria a partir de las coordenadas X,Y.

Además, la obtención de cada TILE implica un acceso a una matriz de 2 dimensiones X,Y, que en nuestro array
de datos se corresponde con:

TILE = [ PANTALLA + (Y*ANCHO_PANTALLA_EN_TILES) + X ]

Veamos una aproximación mucho más óptima:

1.- Teniendo en cuenta que los datos de la pantalla son consecutivos en memoria, vamos a utilizar un puntero
para obtener los datos de los tiles linealmente sin tener que calcular la posición dentro del array de datos una y
otra vez. Bastará con incrementar nuestro puntero (DIR_PANTALLA) para apuntar al siguiente dato.

2.- En lugar de calcular la posición en videomemoria (DIR_MEM) de cada tile, calcularemos una sóla vez esta
posición para el primer tile de cada fila y avanzaremos diferencialmente a lo largo de la misma. De esta forma
sólo realizamos un cálculo de dirección de videomemoria por fila, y no por tile.

;;; Aproximacion 2b - Impresion horizontal con mapa horizontal.


;;; Calculamos la posicion en memoria del primer bloque de linea
;;; y despues nos movemos diferencialmente a los siguientes bloques
;;; El mapa se accede linealmente (no se indexa por x,y):

DIR_PANTALLA = Direccion de memoria de los datos de la PANTALLA actual


FOR Y=0 TO ALTO:
DIR_MEM = Posicion_Memoria( X_INICIAL, Y_INICIAL + Y*ALTO_BLOQUE )
FOR X=0 TO ANCHO_PANTALLA:
TILE = [DIR_PANTALLA]
DIR_PANTALLA = DIR_PANTALLA + 1
PUSH DIR_MEM
DIR_SPRITE = BASE_TILESET + (TILE*ANCHO_TILE*ALTO_TILE)
Dibujar Sprite desde DIR_SPRITE a DIR_MEM
POP DIR_MEM
DIR_MEM = DIR_MEM + ANCHO_TILE

Esta rutina sólo realiza un cálculo de posición de videomemoria por fila y además accede a nuestra pantalla de
mapa linealmente. Antes de dibujar el tile en pantalla hacemos un PUSH de la dirección actual de memoria ya
que la rutina de impresión la modifica. El posterior POP nos recupera la posición de impresión inicial de forma
que baste un simple incremento de la misma para apuntar en videomemoria a la posición del siguiente tile que
tenemos que dibujar.

Vamos a desarrollar un poco más la rutina en un pseudocódigo más parecido a ASM y con más detalles. Para
ello establecemos las siguientes premisas:

• Utilizaremos IX como puntero de la pantalla del mapa (DIR_PANTALLA).


• Utilizaremos el valor de tile 255 como un tile “especial” que la rutina no dibujará. Este tile será pues un
tile transparente que dejará ver el fondo en contraposición al típico bloque “0” vacío que borra un tile de
pantalla.

DrawMap:

IX = MAPA
B = ALTURA_MAPA_EN_TILES

bucle_altura:
Y = ALTURA_MAPA_EN_TILES - B
DE = DIR_MEM( X_INICIO, Y_INICIO + (Y*2) )
B = ANCHURA_MAPA_EN_TILES
bucle_anchura:
A = (IX)
INC IX

Si A == 255 :
JR saltar_bloque

PUSH HL
DIR_SPRITE = BASE_TILESET + (A*ANCHO_TILE*ALTO_TILE)
HL = BASE_TILESET + (A*8*TAMAÑO_BLOQUES_EN_CADA_TILE)
PUSH HL
Imprimir_Sprite_de_HL_a_DE
Convertir DIR HL imagen en DIR HL atributos
Imprimir_Atributos_Sprite
POP HL

saltar_bloque:
HL = HL + ANCHO_TILE
DJNZ bucle_anchura

DJNZ bucle_altura

Este algoritmo es genérico y puede ser optimizado personalizándolo a cada situación / tipo de juego que
estemos realizando.

Rutina para bloques de 16x16 con mapeados horizontales

Veamos una rutina de impresión de pantallas de mapa basadas en tiles de 16×16 píxeles, el tamaño más
habitual de tile para la resolución del Spectrum.

Con tiles de 16×16 píxeles podemos generar una pantalla de hasta 16×12 tiles utilizando 192 tiles por pantalla.
Con tiles referenciadas por variables de 1 byte, cada pantalla ocuparía, pues, 192 bytes.

Tamaños de 8×8 requerirían 768 bytes por pantalla y un tileset con gran cantidad de elementos para poder
componer las pantallas.

Tiles de mayores tamaños darían poca “resolución” para generar la pantalla, ya que serían demasiado grandes.
Por ejemplo, tiles de 32×32 pixeles generarían pantallas de 8×6 tiles (poca “resolución” de mapa).

La rutina que veremos ahora es genérica: permite especificar un ancho y alto de cada pantalla en tiles para
poder dibujar pantallas desde 1×1 a 16×12 tiles. Además podemos especificar una dirección de inicio de
impresión (inicio_x, inicio_y), de forma que podamos dibujar nuestro mapa en una posición concreta de
pantalla (por ejemplo, dentro de un marco, centrado, etc).

El hecho de ser una rutina genérica también le resta algo de velocidad en ciertos cálculos que podríamos evitar
o desenrollar para rutinas específicas. Si en nuestro juego todas las pantallas tienen el mismo tamaño y las
imprimimos siempre en la misma posición, podemos (y debemos) alterar la rutina para utilizar los valores
adecuados dentro de ella, modificar los cálculos y desenrollar los bucles utilizando estos datos constantes.

La rutina se basa en el pseudocódigo que vimos al inicio del capítulo, pero adaptado a tamaños de 16×16.
Veamos la rutina comentada:

;---------------------------------------------------------------
; DrawMap_16x16:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes) Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes) Direccion de la tabla de atributos.
; DM_MAP (2 bytes) Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte) Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte) Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH (1 byte) Ancho del mapa en tiles
; DM_HEIGHT (1 byte) Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16:

;;;;;; Impresion de la parte grafica de los tiles ;;;;;;


LD IX, (DM_MAP) ; IX apunta al mapa
LD A, (DM_HEIGHT)
LD B, A ; B = ALTO_EN_TILES (para bucle altura)

drawm16_yloop:
PUSH BC ; Guardamos el valor de B

LD A, (DM_HEIGHT) ; A = ALTO_EN_TILES
SUB B ; A = ALTO - iteracion_bucle = Y actual
RLCA ; A = Y * 2

;;; Calculamos la direccion destino en pantalla como


;;; DIR_PANT = DIRECCION(X_INICIAL, Y_INICIAL + Y*2)
LD BC, (DM_COORD_X) ; B = DB_COORD_Y y C = DB_COORD_X
ADD A, B
LD B, A
LD A, B
AND $18
ADD A, $40
LD H, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD L, A ; HL = DIR_PANTALLA(X_INICIAL,Y_INICIAL+Y*2)

LD A, (DM_WIDTH)
LD B, A ; B = ANCHO_EN_TILES

drawm16_xloop:
PUSH BC ; Nos guardamos el contador del bucle

LD A, (IX+0) ; Leemos un byte del mapa


INC IX ; Apuntamos al siguiente byte del mapa

CP 255 ; Bloque especial a saltar: no se dibuja


JP Z, drawm16_next

LD B, A
EX AF, AF' ; Nos guardamos una copia del bloque en A'
LD A, B

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*32)
EX DE, HL ; Intercambiamos DE y HL (DE=destino)
LD BC, (DM_SPRITES)
LD L, 0
SRL A
RR L
RRA
RR L
RRA
RR L
LD H, A
ADD HL, BC ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 32)
EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

PUSH HL ; Guardamos el puntero a pantalla recien calculado


PUSH HL

;;; Impresion de los primeros 2 bloques horizontales del tile

LD B, 8
drawm16_loop1:

LD A, (DE) ; Bloque 1: Leemos dato del sprite


LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla
LD A, (DE) ; Bloque 2: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC H ; Hay que sumar 256 para ir al siguiente scanline
DEC L ; pero hay que restar el INC L que hicimos.
DJNZ drawm16_loop1
INC L ; Decrementar el ultimo incrementado en el bucle

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


; desde el septimo scanline de la fila Y+1 al primero de la Y+2
LD A, L
ADD A, 31
LD L, A
JR C, drawm16_nofix_abajop
LD A, H
SUB 8
LD H, A
drawm16_nofix_abajop:

;;; Impresion de los segundos 2 bloques horizontales:


LD B, 8
drawm16_loop2:
LD A, (DE) ; Bloque 1: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla
LD A, (DE) ; Bloque 2: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC H ; Hay que sumar 256 para ir al siguiente scanline
DEC L ; pero hay que restar el INC L que hicimos.
DJNZ drawm16_loop2

;;; En este punto, los 16 scanlines del tile estan dibujados.

;;;;;; Impresion de la parte de atributos del tile ;;;;;;

POP HL ; Recuperar puntero a inicio de tile

;;; Calcular posicion destino en area de atributos en DE.


LD A, H ; Codigo de Get_Attr_Offset_From_Image
RRCA
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L ; DE tiene el offset del attr de HL

LD HL, (DM_ATTRIBS)
EX AF, AF' ; Recuperamos el bloque del mapa desde A'
LD C, A
LD B, 0
ADD HL, BC
ADD HL, BC
ADD HL, BC
ADD HL, BC ; HL = HL+HL=(DS_NUMSPR*4) = Origen de atributo

LDI
LDI ; Imprimimos la primeras fila de atributos

;;; Avance diferencial a la siguiente linea de atributos


LD A, E ; A = E
ADD A, 30 ; Sumamos A = A + 30 mas los 2 INCs de LDI.
LD E, A ; Guardamos en E (E = E+30 + 2 por LDI=E+32)
JR NC, drawm16_att_noinc
INC D
drawm16_att_noinc:
LDI
LDI ; Imprimimos la segunda fila de atributos

POP HL ; Recuperamos el puntero al inicio

drawm16_next:
INC L ; Avanzamos al siguiente tile en pantalla
INC L ; horizontalmente

POP BC ; Recuperamos el contador para el bucle


DEC B ; DJNZ se sale de rango, hay que usar DEC+JP
JP NZ, drawm16_xloop

;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)


POP BC
DEC B ; Bucle vertical
JP NZ, drawm16_yloop

RET

Nótese que hemos integrado las rutinas de impresión de sprites 16×16 con sus correspondientes cálculos dentro
del cuerpo de nuestra rutina de “mapeado”.

Por motivos de espacio (gran extensión de la rutina), la impresión de los 4 bloques gráficos se realiza en dos
bucles aunque la versión definitiva de nuestro programa debería desenrollarlos con los siguientes cambios:

1.- Eliminar el INC L tras el DJNZ drawm16_loop1.

2.- Eliminar los INC DE, INC H y DEC L antes del DJNZ drawm16_loop2.

Por supuesto, si se conocen de antemano otros parámetros fijos de la rutina (ancho, alto, posición inicial, etc)
podemos utilizar estos valores directamente (como constantes o incluso anticipar el cálculo de la dirección de
pantalla inicial o usar una tabla de direcciones iniciales de tile precalculadas) para acelerar la ejecución de la
misma.

No obstante, si estamos ante un juego de pantallas “fijas” y “estáticas”, el tiempo de dibujado de la pantalla será
prácticamente inapreciable para el jugador, por lo que no suele ser necesario realizar optimizaciones extremas.

Ejemplo: Impresión de una pantalla 16x16

Juntemos en un mismo programa la rutina de impresión de pantallas de mapa en 16×16, la pantalla del nivel 1
de Sokoban y su tileset, y el siguiente código de test:

; Ejemplo impresion mapa de 16x16


ORG 32768
CALL ClearScreen_Pattern ; Imprimimos patron de fondo

LD HL, sokoban1_gfx
LD (DM_SPRITES), HL
LD HL, sokoban1_attr
LD (DM_ATTRIBS), HL
LD HL, sokoban_LEVEL1
LD (DM_MAP), HL
LD A, 16
LD (DM_WIDTH), A ; ANCHO
LD A, 12
LD (DM_HEIGHT), A ; ALTO
XOR A
LD (DM_COORD_X), A ; X = Y = 0
LD (DM_COORD_Y), A ; Establecemos valores llamada

CALL DrawMap_16x16 ; Imprimir pantalla de mapa

loop:
JR loop

;-----------------------------------------------------------------------
ClearScreen_Pattern: ; Rutina para incluir:
(...) ; Rellenado de fondo con un patron
RET ; (del capitulo de Sprites Lowres)

;-----------------------------------------------------------------------
DM_SPRITES EQU 50020
DM_ATTRIBS EQU 50022
DM_MAP EQU 50024
DM_COORD_X EQU 50026
DM_COORD_Y EQU 50027
DM_WIDTH EQU 50028
DM_HEIGHT EQU 50029

;-----------------------------------------------------------------------
; Level 1 from Sokoban:
;-----------------------------------------------------------------------
sokoban_LEVEL1:
DEFB 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
DEFB 255,255,255,255,255,255,2,3,1,4,255,255,255,255,255,255
DEFB 255,255,255,255,1,2,3,0,0,5,4,255,255,255,255,255
DEFB 255,255,255,255,4,0,6,6,0,0,5,255,255,255,255,255
DEFB 255,255,255,255,5,0,0,6,0,0,4,255,255,255,255,255
DEFB 255,255,255,255,4,0,0,0,0,0,5,255,255,255,255,255
DEFB 255,255,255,255,5,2,3,0,0,2,3,255,255,255,255,255
DEFB 255,255,255,255,255,255,1,0,0,0,4,255,255,255,255,255
DEFB 255,255,255,255,255,255,4,7,7,7,5,255,255,255,255,255
DEFB 255,255,255,255,255,255,5,2,3,2,3,255,255,255,255,255
DEFB 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
DEFB 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255

;-----------------------------------------------------------------------
; ASM source file created by SevenuP v1.20
; SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain
; Pixel Size: ( 16, 128) - Char Size: ( 2, 16)
; Sort Priorities: X char, Char line, Y char
; Data Outputted: Gfx / Attr
;-----------------------------------------------------------------------

sokoban1_gfx:
DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
DEFB 127,252,193, 86,152, 2,180,170, 173, 86,153,254,194,170,255,254
DEFB 0, 0,102,102, 51, 50,153,152, 204,204,102,102, 51, 50, 0, 0
DEFB 127,102,205, 76,151, 24,205, 50, 151,102,205, 76,151, 24,205, 50
DEFB 131,102,153, 76,173, 24,181, 50, 153,102,195, 76,127, 24, 0, 0
DEFB 255,252,255,134,255, 50,255, 90, 255,106,255, 50,255,134,255,254
DEFB 255,254,255,254,255,254,255,250, 255,242,253,166,255,252, 0, 0
DEFB 127,252,205,134,151, 50,205,106, 151, 90,205, 50,151,134,205,254
DEFB 195,254,153,254,173,254,181,250, 153,242,195,166,127,252, 0, 0
DEFB 255,254,255,254,255,254,255,254, 255,254,255,254,191,254,255,254
DEFB 255,134,191, 50,255,106,191, 90, 159, 50,207,134,127,252, 0, 0
DEFB 0, 0,127,254, 95,250, 96, 6, 111,182,111,118, 96,230,109,214
DEFB 107,182,103, 6,110,246,109,246, 96, 6, 95,250,127,254, 0, 0
DEFB 0, 0,123,222,123,222, 96, 6, 96, 6, 0, 0, 96, 6, 96, 6
DEFB 96, 6, 96, 6, 0, 0, 96, 6, 96, 6,123,222,123,222, 0, 0

sokoban1_attr:
DEFB 0, 0, 0, 0, 5, 5, 70, 70, 5, 70, 5, 70, 69, 71, 69, 71
DEFB 5, 69, 69, 71, 69, 69, 71, 71, 2, 66, 66, 67, 6, 70, 70, 71

El resultado de la ejecución del anterior ejemplo es la siguiente pantalla:

Nótese que las pantallas de mapa no tienen por qué tener el tamaño exacto de 256×192 de la pantalla de TV.
Nuestra pantalla anterior ocupa más memoria de la estrictamente necesaria, ya que gran parte de la información
alrededor del “área de juego real” son bloques transparentes.

La misma pantalla codificada con un tamaño de 7×9 e impresa a partir de las coordenadas de pantalla (8,3)
ocuparía 63 bytes en lugar de 192 y produciría el mismo resultado visual:

;-----------------------------------------------------------------------
; Level 1 from Sokoban (7x9):
;-----------------------------------------------------------------------
sokoban_LEVEL1:
DEFB 255,255, 2, 3, 4, 1,255
DEFB 4, 2, 3, 0, 0, 5, 1
DEFB 1, 0, 6, 6, 0, 0, 5
DEFB 5, 0, 0, 6, 0, 0, 1
DEFB 1, 0, 0, 0, 0, 0, 5
DEFB 5, 2, 3, 0, 0, 2, 3
DEFB 255,255, 4, 0, 0, 0, 1
DEFB 255,255, 1, 7, 7, 7, 5
DEFB 255,255, 5, 2, 3, 2, 3

La pantalla de 7×9 se imprimiría así:

LD HL, sokoban1_gfx
LD (DM_SPRITES), HL
LD HL, sokoban1_attr
LD (DM_ATTRIBS), HL
LD HL, sokoban_LEVEL1
LD (DM_MAP), HL
LD A, 7
LD (DM_WIDTH), A ; ANCHO
LD A, 9
LD (DM_HEIGHT), A ; ALTO
LD A, 8
LD (DM_COORD_X), A ; X_INICIAL
LD A, 3
LD (DM_COORD_Y), A ; Y_INICIAL
CALL DrawMap_16x16 ; Imprimir pantalla de mapa

Si tenemos pantallas de diferentes tamaños podemos almacenarlas en memoria utilizando una estructura de
datos con 4 bytes de información por pantalla: ancho, alto, x_inicial e y_inicial. De esta forma cada pantalla
ocuparía en memoria sólo el espacio necesario para definirla, sin necesidad de que todas las pantallas se
adapten a un tamaño común. Nuestra rutina de cambio de pantalla recogería estos 4 bytes de datos de la tabla de
“tamaños y posiciones” para establecer los parámetros de entrada a DrawMap_16x16 y dibujar la pantalla en su
posición correcta.

Esto puede valer para juegos como Sokoban (donde cada pantalla puede tener un tamaño diferente) pero no
para juegos tipo plataformas/aventuras/arcade donde todas las pantallas tienen un tamaño fijo. En ese caso basta
con usar nuestra rutina con unos valores fijos para todas ellas.

Rutina para bloques de 8x8 con mapeados horizontales

La rutina para tiles de 8×8 es fácilmente adaptable a partir de la rutina de 16×16 modificando los bucles de
impresión y los cálculos (multiplicaciones y posicionamientos) para el nuevo tamaño de tile y atributo:

;---------------------------------------------------------------
; DrawMap_8x8:
; Imprime una pantalla de tiles de 8x8 pixeles.
;---------------------------------------------------------------
DrawMap_8x8:

;;;;;; Impresion de la parte grafica de los tiles ;;;;;;


LD IX, (DM_MAP) ; IX apunta al mapa
LD A, (DM_HEIGHT)
LD B, A ; B = ALTO_EN_TILES (para bucle altura)

drawm8_yloop:
PUSH BC ; Guardamos el valor de B

LD A, (DM_HEIGHT) ; A = ALTO_EN_TILES
SUB B ; A = ALTO - iteracion_bucle = Y actual
;;; NUEVO: Eliminamos RLCA (no multiplicar Y*2)

;;; Calculamos la direccion destino en pantalla como


;;; DIR_PANT = DIRECCION(X_INICIAL, Y_INICIAL + Y)
LD BC, (DM_COORD_X) ; B = DB_COORD_Y y C = DB_COORD_X
ADD A, B
LD B, A
LD A, B
AND $18
ADD A, $40
LD H, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD L, A ; HL = DIR_PANTALLA(X_INICIAL,Y_INICIAL+Y*2)

LD A, (DM_WIDTH)
LD B, A ; B = ANCHO_EN_TILES

drawm8_xloop:
PUSH BC ; Nos guardamos el contador del bucle

LD A, (IX+0) ; Leemos un byte del mapa


INC IX ; Apuntamos al siguiente byte del mapa

CP 255 ; Bloque especial a saltar: no se dibuja


JP Z, drawm8_next

LD B, A
EX AF, AF' ; Nos guardamos una copia del bloque en A'
LD A, B

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*8)
EX DE, HL ; Intercambiamos DE y HL (DE=destino)
LD BC, (DM_SPRITES)
LD L, A
LD H, 0
ADD HL, HL
ADD HL, HL
ADD HL, HL ;;; NUEVO: NUM_SPRITE*8 en lugar de *32
ADD HL, BC ; HL = BC + HL = DM_SPRITES + (DM_NUMSPR * 8)
EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

PUSH HL ; Guardamos el puntero a pantalla recien calculado


PUSH HL

;;; Impresion de los primeros 2 bloques horizontales del tile


LD B, 8

drawm8_loop: ;;; NUEVO: Bucle de impresion de 1 solo bloque


LD A, (DE) ; Bloque 1: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC H ; Hay que sumar 256 para ir al siguiente scanline
DJNZ drawm8_loop

;;; En este punto, los 8 scanlines del tile estan dibujados.

;;;;;; Impresion de la parte de atributos del tile ;;;;;;


POP HL ; Recuperar puntero a inicio de tile

;;; Calcular posicion destino en area de atributos en DE.


LD A, H ; Codigo de Get_Attr_Offset_From_Image
RRCA
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L ; DE tiene el offset del attr de HL

LD HL, (DM_ATTRIBS)
EX AF, AF' ; Recuperamos el bloque del mapa desde A'
LD C, A
LD B, 0 ;;; NUEVO: HL = HL+DM_NUMSPR (NO *4)
ADD HL, BC ; HL = HL+DM_NUMSPR = Origen de atributo

LD A, (HL)
LD (DE), A ;;; NUEVO: Impresion de un unico atributo.
POP HL ; Recuperamos el puntero al inicio

drawm8_next:
INC L ; Avanzamos al siguiente tile en pantalla

POP BC ; Recuperamos el contador para el bucle


DEC B ; DJNZ se sale de rango, hay que usar DEC+JP
JP NZ, drawm8_xloop

;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)


POP BC
DEC B ; Bucle vertical
JP NZ, drawm8_yloop

RET

El mapa como conjunto de pantallas


Ya conocemos la forma de diseñar e imprimir pantallas individuales de mapeado, pero seguimos necesitando
agrupar estas pantallas para componer un mapa de tamaño superior al del área de visión del jugador. En esta
sección veremos las estructuras de datos necesarias para definir el mapeado total del juego así como la
interconexión entre las diferentes pantallas.

Estructura de datos del mapa

Un mapa de tiles es la representación de un mapeado mediante la utilización de tiles, donde el ancho y alto del
mapa es mayor que el tamaño de una pantalla individual.

Las pantallas se interconectan formando un mapa. Esta conexión puede ser:

• Estática: El jugador cambia de una pantalla a otra al acabar el nivel, sin que las pantallas estén
“conectadas” realmente entre sí salvo por el número de nivel actual (Ejemplo: Manic Miner, Sokoban,
Bubble Bobble, Puzznic, Plotting…).
• Lineal en un sólo eje: Las pantallas se agrupan una a continuación de la otra con desarrollo del juego en
una dirección, ya sea izquierda-derecha (R-Type, Game Over, Target Renegade…) o arriba-abajo
(Flying Shark, Commando…).
• Lineal en dos ejes: Las pantallas se agrupan en forma de mapa bidimensional, permitiendo al jugador
moverse en el mapeado hacia arriba, abajo, izquierda o derecha (Sabre Wulf, Las Tres Luces de
Glaurung, Atic Atac, Sir Fred…).

La pantalla es una ventana dentro del mapeado, por lo que resulta necesario disponer de una estructura de datos
que nos permita representar los 3 modelos de mapa que acabamos de describir.

Hay 2 posibilidades de agrupación de las pantallas: como un array de pantallas, o como una matriz global de
mapeado.

Mapa como array de pantallas

El mapa como array de pantallas consiste en la creación de un array con las direcciones de datos de cada
pantalla de forma que podamos direccionarlo con la variable Pantalla_Actual para obtener la dirección donde
están los datos a imprimir.
Para hacer uso de este sistema debemos almacenar cada pantalla en memoria con una etiqueta identificativa
para nuestro programa ensamblador:

Pantalla_Inicio:
DB 0, 0, 0, 3, (...)

Pantalla_Salon:
DB 1, 2, 3, 4, (...)

Pantalla_Pasillo:
DB 2, 2, 2, 2, (...)

Pantalla_Escalera:
DB 4, 4, 5, 1, (...)

(...)

A continuación, definimos una tabla “Mapa” que contenga las direcciones de inicio de los datos de cada
pantalla:

Mapa:
DW Pantalla_Inicio ; Pantalla 0
DW Pantalla_Salon ; Pantalla 1
DW Pantalla_Pasillo ; Pantalla 2
DW Pantalla_Escalera ; Pantalla 3
(...) ; (etc...)
DW 0000 ; Fin de pantalla

Nuestro juego almacenará el identificador de la pantalla actual en una variable de programa (por ejemplo,
ID_PANTALLA). De esta forma podemos acceder a los datos de cada pantalla utilizando ID_PANTALLA
como índice en esta tabla:

DIR_DATOS_PANTALLA = Mapa[ ID_PANTALLA ]

O lo que es lo mismo, desplazándonos 2 bytes desde el inicio de nuestro mapa (ya que cada dirección ocupa 2
bytes) y leyendo el contenido de la dirección resultante:

DIR_DATOS_PANTALLA = [ Mapa + (ID_PANTALLA * 2) ]

Traducido a código, para obtener la dirección donde se aloja la pantalla actual de juego, asumiendo que su
identificador 0-N estuviera en la variable de memoria de 8 bits pantalla_actual:

;;; Calculamos la posicion de "pantalla_actual" en al tabla


LD BC, Mapa ; BC = Inicio de la tabla Mapa
LD A, (pantalla_actual) ; A = Pantalla actual
LD L, A
LD H, 0 ; HL = Pantalla actual
SLA L
RL H ; HL = Pantalla actual * 2
ADD HL, BC ; HL = Mapa + (Pantalla actual * 2)

;;; Ahora leemos de (HL) la dirección de dibujado en el mismo HL


LD A, (HL) ; Leemos la parte baja de la direccion en A
INC HL ; ... para no corromper HL y poder leer ...
PUSH HL
LD H, (HL) ; ... la parte alta sobre H ...
LD L, A
LD (DM_MAP), HL ; Almacenamos el mapa a imprimir
CALL DrawMap_16x16 ; Imprimimos el mapa

Para un juego de pantallas no conectadas (tipo Manic Miner o Sokoban), el movimiento de una pantalla a otra
se basaría en incrementar el valor de ID_PANTALLA cada vez que el jugador progrese un nivel en el juego, o
poner ID_PANTALLA = 0 cuando se finalice el juego o se inicie una nueva partida.
En juegos con desplazamiento en una única dirección (R-Type, Flying Shark, Game Over…), el
desplazamiento a la pantalla anterior o siguiente se realizará decrementando ID_PANTALLA si
ID_PANTALLA no es cero (límite izquierdo/inferior), o incrementando ID_PANTALLA si el valor
Mapa[ID_PANTALLA] es menor que el número máximo de pantallas (límite derecho/superior).

Para juegos con mapeados bidimensionales que requieran especificar una conexión concreta entre pantallas,
podemos agregar a nuestro vector de mapeado bytes adicionales que indiquen los identificadores de las
pantallas a las que deberíamos movernos si vamos en una determinada dirección.

Por ejemplo, en un hipotético juego que se desarrolle en una sóla dirección (ej: izquierda-derecha)
necesitaremos almacenar los IDs de las pantallas que tenemos a izquierda y a derecha de la pantalla actual:

;;; Vector de direcciones de pantalla.


;;; Contiene la direccion de cada pantalla en orden de ID,
;;; seguido de los IDs de las pantallas de su izquierda y
;;; su derecha. Se utiliza -1 para definir que no hay
;;; conexion con otras pantallas:
;;;
;;; Formato de cada pantalla + conexiones:
;;;
;;; DW DIR_DATOSPANTALLA
;;; DB ID_IZQUIERDA, ID_DERECHA

Mapa:
DW Pantalla_Inicio ; ID = 0
DB -1, 1 ; Conexiones izq y derecha ID 0
DW Pantalla_Salon ; ID = 1
DB 0, 2 ; Conexiones izq y derecha ID 1
DW Pantalla_Pasillo ; ID = 2
DB 1, 3 ; Conexiones izq y derecha ID 2
DW Pantalla_Escalera ; ID = 3
DB 3, -1 ; Conexiones izq y derecha ID 3
(...)

Para acceder ahora a los datos de una pantalla debemos desplazarnos 2 bytes por la dirección y 2 bytes por los 2
identificadores, es decir, un total de 4 bytes:

DIR_DATOS_PANTALLA = [ Mapa + (ID_PANTALLA * 4) ]


ID_PANTALLA_IZQUIERDA = [ Mapa + (ID_PANTALLA * 4) + 2 ]
ID_PANTALLA_DERECHA = [ Mapa + (ID_PANTALLA * 4) + 3 ]

Traducido a código, basta con multiplicar ID_PANTALLA por 2, 4 ó 6 (según la cantidad de IDs de conexión
que tengamos definidos en el mapa), apuntar HL, DE o IX a “Mapa”, sumarle el valor de la multiplicación y
leer los 6 bytes consecutivos incrementando este puntero.

BYTES_POR_PANTALLA EQU 4
RUTINA_ROM_HL_POR_DE EQU $30A9

;------------------------------------------------------------
; Obtener direccion donde se alojan los datos de la pantalla
; Entrada:
; L = pantalla
; BC = Mapa (direccion base)
; Salida:
; HL = Direccion de datos de la pantalla
;------------------------------------------------------------
Get_Screen_Pointer:
LD H, 0
LD D, H ; HL = PANTALLA
LD E, BYTES_POR_PANTALLA ; DE = BYTES POR PANTALLA
CALL RUTINA_ROM_HL_POR_DE ; HL = HL * DE
ADD HL, BC ; Lo sumamos al inicio del MAPA
RET ; HL = MAPA + (PANTALLA*BYTES)
Para realizar la multiplicación hemos utilizado la rutina HL=HL*DE de la ROM del Spectrum. Si no estamos
en un Spectrum sino que estamos programando para otro sistema Z80, bastará con llamar a la rutina de
multiplicación adecuada.

Podríamos haber realizado la multiplicación por 4 mediante desplazamientos, pero utilizando una rutina de
multiplicación nos aseguramos que Get_Screen_Pointer pueda ser utilizado para mapas que definan más
conexiones.

Ahora ya podemos acceder a los datos de una pantalla concreta:

;;; En el inicio del programa...


LD HL, sokoban1_gfx
LD (DM_SPRITES), HL
LD HL, sokoban1_attr
LD (DM_ATTRIBS), HL
LD A, 16
LD (DM_WIDTH), A ; ANCHO
LD A, 12
LD (DM_HEIGHT), A ; ALTO
XOR A
LD (DM_COORD_X), A ; X = Y = 0
LD (DM_COORD_Y), A ; Establecemos valores llamada

(...)

;;; En el bucle principal de nuestro programa:


DibujarPantalla:
LD BC, Mapa
LD A, (pantalla_actual)
LD L, A
CALL Get_Screen_Pointer ; HL = Datos de la pantalla
LD A, (HL) ; Leemos la parte baja de la direccion en A
INC HL ; ... para no corromper HL y poder leer ...
PUSH HL
LD H, (HL) ; ... la parte alta sobre H ...
LD L, A
LD (DM_MAP), HL ; Almacenamos el mapa a imprimir
CALL DrawMap_16x16 ; Imprimimos el mapa

POP HL ; Recuperamos el puntero a datos de pantalla


INC HL ; Avanzamos hasta el primer ID de conexion
LD A, (HL) ; Leemos conexion izquierda
LD (con_izquierda), A ; la almacenamos
INC HL ; Avanzamos hasta el segundo ID de conexion
LD A, (HL) ; Leemos conexion a derecha
LD (con_derecha), A ; la almacenamos

Con los datos en las variables con_izquierda y con_derecha podemos movernos a una de las 2 pantallas
cambiando el valor de pantalla_actual al de la pantalla correspondiente.

Nótese que la “costosa” multiplicación genérica se puede sustituir por desplazamientos (*2, *4…) si separamos
la tabla de pantallas en una tabla de direcciones y otra de conexiones:

Mapa:
DW Pantalla_Inicio ; ID = 0
DW Pantalla_Salon ; ID = 1
DW Pantalla_Pasillo ; ID = 2
DW Pantalla_Escalera ; ID = 3
(...)

Conexiones:
DB -1, 1 ; Conexiones izq y derecha ID 0
DB 0, 2 ; Conexiones izq y derecha ID 1
DB 1, 3 ; Conexiones izq y derecha ID 2
DB 3, -1 ; Conexiones izq y derecha ID 3
(...)

De esta forma podemos calcular las posiciones de los datos que necesitamos con simples operaciones de
desplazamiento. No obstante, tener los datos de las pantallas separados en 2 o más tablas es más “complicado”
de mantener manualmente a menos que estemos generado estas estructuras de mapa con algún programa propio
de diseño y exportación de mapeados que nos permita su exportación a este formado. Teniendo los datos por
separado es más “complicado” (o, al menos, no tan intuitivo) hacer cambios manuales en el código.

Si estuvieramos hablando de un juego con “scroll” de pantallas en las 4 direcciones, bastaría con definir 4
identificadores de conexión tras cada dirección de pantalla, y obtener los datos de cada pantalla saltando
2+1+1+1+1 = 6 bytes por cada pantalla:

DIR_DATOS_PANTALLA = [ Mapa + (ID_PANTALLA * 6) ]


ID_PANTALLA_IZQUIERDA = [ Mapa + (ID_PANTALLA * 4) + 2 ]
ID_PANTALLA_DERECHA = [ Mapa + (ID_PANTALLA * 4) + 3 ]
ID_PANTALLA_ARRIBA = [ Mapa + (ID_PANTALLA * 4) + 4 ]
ID_PANTALLA_ABAJO = [ Mapa + (ID_PANTALLA * 4) + 5 ]

(O, en caso de usar tablas separadas, se realizaría un desplazamiento a la izquierda para multiplicar por 2 en la
tabla de direcciones, y 2 desplazamientos para multiplicar por 4 en la de conexiones, como ya hemos visto en
un ejemplo anterior).

El movimiento por el mapa se basaría en establecer ID_PANTALLA a cualquiera de los cuatro valores siempre
que estos sean distintos de 255 (-1).

Nótese que estamos asumiendo que no hay más de 254 pantallas. En caso de requerir un mayor número de
pantallas, el identificador de tile deberá ser de 16 bits por lo que la definición de los identificadores de conexión
sería de tipo DW en lugar de DB y cambiarían los valores de las multiplicaciones:

Mapa:
DW Pantalla_Inicio ; ID = 0
DW -1, 1 ; Conexiones izq y derecha ID 0

Multiplicamos por 6 ya que ahora cada pantalla ocupa 6 bytes (2 de la dirección y 4 de los identificadores de
conexión).

Si necesitaramos definir más datos de la pantalla (como por ejemplo, el “título” de la misma, al estilo Manic
Miner), podríamos añadir a nuestra estructura 2 bytes con la dirección en memoria de una cadena de texto (una
dirección para cada pantalla individual del mapa.) Esto implicaría modificar la cantidad de bytes por los que se
multiplica para obtener la dirección que contiene los datos de una pantalla concreta:

;;; Vector de direcciones de pantalla.


Mapa:
DW Pantalla_Inicio ; ID = 0
DB -1, 1 ; Conexiones izq y derecha ID 0
DW titulo_inicio ; Direccion de la cadena de titulo
DW Pantalla_Salon ; ID = 1
DB 0, 2 ; Conexiones izq y derecha ID 1
DW titulo_salon ; Direccion de la cadena de titulo
DW Pantalla_Pasillo ; ID = 2
DB 1, 3 ; Conexiones izq y derecha ID 2
DW titulo_pasillo
DW Pantalla_Escalera ; ID = 3
DW titulo_escalera
DB 3, -1 ; Conexiones izq y derecha ID 3
(...)

titulo_inicio DB "La pantalla de inicio", 0


titulo_salon DB "El salon", 0
titulo_pasillo DB "El pasillo", 0
titulo_escalera DB "La escalera", 0
Con estos 2 bytes adicionales de nombre por cada pantalla a los 4 que ya se utilizaban, el anterior ejemplo
requeriría el siguiente cálculo para acceder a los datos de una pantalla concreta:

DIR_DATOS_PANTALLA = [ Mapa + (ID_PANTALLA * 6) ]


ID_PANTALLA_IZQUIERDA = [ Mapa + (ID_PANTALLA * 6) + 2 ]
ID_PANTALLA_DERECHA = [ Mapa + (ID_PANTALLA * 6) + 3 ]
TITULO_PANTALLA = [ Mapa + (ID_PANTALLA * 6) + 4 ]

De nuevo, como vimos antes, es posible mantener los datos las pantallas en diferentes tablas (tabla de direccion
de datos, tabla de conexiones, tabla de títulos) para facilitar el acceso a los mismos vía operaciones de
desplazamiento, aunque si la obtención de estos datos no es prioritaria, el poder acceder a ellos mediante una
única operación y un único puntero puede acabar resultando más rápido (o simplemente, cómodo) que en
múltiples tablas.

Repetición de pantallas

Gracias a nuestro mapa definido como “vector” de pantallas podemos repetir pantallas en nuestro mapeado sin
duplicar los datos gráficos.

Por ejemplo, si en nuestro hipotético juego de desarrollo lineal izquierda-derecha tenemos 3 pasillos iguales,
podemos duplicar las entradas de pantalla con diferentes identificadores de conexión:

Mapa:
DW Pantalla_Inicio ; ID = 0
DB -1, 1 ; Conexiones izq y derecha ID 0
DW Pantalla_Salon ; ID = 1
DB 0, 2 ; Conexiones izq y derecha ID 1
DW Pantalla_Pasillo ; ID = 2
DB 1, 3 ; Conexiones izq y derecha ID 2
DW Pantalla_Pasillo ; ID = 3
DB 2, 4 ; Conexiones izq y derecha ID 3
DW Pantalla_Pasillo ; ID = 4
DB 3, 5 ; Conexiones izq y derecha ID 4
DW Pantalla_Escalera ; ID = 5
DB 4, -1 ; Conexiones izq y derecha ID 5
(...)

De esta forma hemos definido 3 pantallas de nuestro juego mediante un mismo bloque de datos de tiles. Hemos
creado 3 “pasillos” consecutivos, y nada nos impide volver a utilizar este mismo bloque en otros áreas del
mapeado que también tengan pasillos.

Con este tipo de trucos podemos exprimir la escasa memoria de nuestro Spectrum y crear mapeados de gran
tamaño.

Transiciones entre 2 pantallas de mapa

Cuando el jugador pasa de una pantalla a otra debemos realizar el borrado de la pantalla en curso y la impresión
de la nueva. Para realizar esto tenemos diferentes opciones:

• Transición abrupta: Simplemente realizamos un borrado del área de pantalla donde se dibuja el mapa y
trazamos sobre ese área los datos de la siguiente pantalla. Es el método más común (y el más rápido),
utilizado en la mayoría de juegos.
• Fundido de pantalla: Realizamos un fundido de la pantalla actual a negro, dibujamos la nueva pantalla
con atributos a negro, y realizamos un fundido desde negro a los nuevos atributos.
• Scroll de pantallas: Realizamos un scroll entre la pantalla que sale y la que entra. Consiste en realizar
una “salida” de la pantalla actual en la dirección contraria de los límites cruzados por el personaje (por
ejemplo: si éste sale por la derecha, scrolleamos la pantalla actual hacia la izquierda), de forma que para
cada línea de la pantalla saliente aparezca por la derecha una línea del mapa entrante (la nueva pantalla
actual). Este scroll, que debe de ser rápido para no ralentizar el cambio de pantallas en el juego, requiere
la realización de 4 rutinas específicas que realicen scroll de una porción de la pantalla y que permitan
dibujar N filas o N columnas de la nueva pantalla entrante a partir de una posición (x,y) dada.

El mapa como array global de mapeado

Existe una alternativa a la definir cada pantalla de juego por separado, y es la de disponer de una matriz global
que comprenda todo el mapeado. Esta matriz contiene todos los datos de las pantallas del mapa, linealmente:

;;; Sea un juego de 8x8 bloques por pantalla


;;; formado por un mapa de 2x2 pantallas.
;;;
;;; Sean "A" los datos de la primera pantalla.
;;; Sean "B" los datos de la segunda pantalla.
;;; Sean "C" los datos de la tercera pantalla.
;;; Sean "D" los datos de la tercera pantalla.

Mapa
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D

Nosotros sólo vemos un área de 8×8 bloques del anterior mapeado, que inicialmente podría ser, por ejemplo, la
que comprende todos los bloques “A”. Se utiliza un puntero 2D “xmapa,ymapa” o uno lineal (mapa_pos) para
conocer la posición de la “ventana de visión” de 8×8 bloques (en nuestro ejemplo) dentro del mapa.

Podemos “movernos” en el mapa simplemente modificando las coordenadas del puntero de la ventana de visión
y redibujando en pantalla el área de 8×8 bloques que comienza en dicho puntero.

Por ejemplo, supongamos que la posición inicial del área de visión es (0,0), con lo que la primera pantalla
impresa serán los 8×8 bloques “A” del ejemplo. Para avanzar hacia la derecha basta con incrementar el puntero
“xmapa” con lo que la siguiente impresión de la pantalla mostrará 7 columnas “A” y una columna “B” en el
extremo derecho de la pantalla.

La impresión de este tipo de pantallas requiere una rutina similar a las rutinas de impresión sin agrupación que
ya hemos visto (DrawMap_16x16), pero modificada en los siguientes términos:
• Cambio 1: Debe de calcular la posición inicial de lectura de datos para la impresión como Mapa +
(ymapa*ANCHO_MAPA) + xmapa.
• Cambio 2: Una vez impreso un scanline horizontal de ANCHO_PANTALLA datos, debe de avanzar el
registro usado como puntero de datos en el mapa un total de ANCHO_MAPA-ANCHO_PANTALLA
bytes (para posicionarse en el siguiente scanline de datos del mapa).

La rutina de impresión de este tipo de mapas tiene el primero de los cambios descritos al principio de la misma:

DrawMap_16x16_Map:

LD IX, (DM_MAP) ; IX apunta al mapa

;;; NUEVO: Posicionamos el puntero de mapa en posicion inicial.


LD HL, (DM_MAPY)
LD DE, ANCHO_MAPA_TILES
CALL MULT_HL_POR_DE ; HL = (ANCHO_MAPA * MAPA_Y)
LD BC, (DM_MAPX)
ADD HL, BC ; HL = MAPA_X + (ANCHO_MAPA * MAPA_Y)
EX DE, HL
ADD IX, DE ; IX = Inicio_Mapa + HL
;;; FIN NUEVO

El segundo de los cambios está localizado al final de la rutina, al final de cada iteración de scanline horizontal:

;;; NUEVO: Incrementar puntero de mapa a siguiente linea


LD BC, ANCHO_MAPA_TILES - ANCHO_PANTALLA
ADD IX, BC
;;; FIN NUEVO

;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)


POP BC
DEC B ; Bucle vertical
JP NZ, drawmg16_yloop

RET

La rutina completa es la siguiente:

;-------------------------------------------------------------
DM_SPRITES EQU 50020
DM_ATTRIBS EQU 50022
DM_MAP EQU 50024
DM_COORD_X EQU 50026
DM_COORD_Y EQU 50027
DM_WIDTH EQU 50028
DM_HEIGHT EQU 50029
DM_MAPX EQU 50030
DM_MAPY EQU 50032

;-------------------------------------------------------------
; Algunos valores hardcodeados para el ejemplo, en la rutina
; final se puede utilizar DM_WIDTH y DM_HEIGHT.
;-------------------------------------------------------------
ANCHO_MAPA_TILES EQU 32
ALTO_MAPA_TILES EQU 24
ANCHO_PANTALLA EQU 14
ALTO_PANTALLA EQU 11

;;; Rutina de la ROM del Spectrum, en otros sistemas


;;; sustituir por una rutina especifica de multiplicacion
MULT_HL_POR_DE EQU $30A9
;---------------------------------------------------------------
; DrawMap_16x16_Map:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes) Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes) Direccion de la tabla de atributos.
; DM_MAP (2 bytes) Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte) Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte) Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH (1 byte) Ancho del mapa en tiles
; DM_HEIGHT (1 byte) Alto del mapa en tiles
; DM_MAPX (2 bytes) Coordenada X en mapa.
; DM_MAPY (2 bytes) Coordenada Y en mapa.
;---------------------------------------------------------------
DrawMap_16x16_Map:

LD IX, (DM_MAP) ; IX apunta al mapa

;;; NUEVO: Posicionamos el puntero de mapa en posicion inicial.


LD HL, (DM_MAPY)
LD DE, ANCHO_MAPA_TILES
CALL MULT_HL_POR_DE ; HL = (ANCHO_MAPA * MAPA_Y)
LD BC, (DM_MAPX)
ADD HL, BC ; HL = MAPA_X + (ANCHO_MAPA * MAPA_Y)
EX DE, HL
ADD IX, DE ; IX = Inicio_Mapa + HL
;;; FIN NUEVO

LD A, (DM_HEIGHT)
LD B, A ; B = ALTO_EN_TILES (para bucle altura)

drawmg16_yloop:
PUSH BC ; Guardamos el valor de B

LD A, (DM_HEIGHT) ; A = ALTO_EN_TILES
SUB B ; A = ALTO - iteracion_bucle = Y actual
RLCA ; A = Y * 2

;;; Calculamos la direccion destino en pantalla como


;;; DIR_PANT = DIRECCION(X_INICIAL, Y_INICIAL + Y*2)
LD BC, (DM_COORD_X) ; B = DB_COORD_Y y C = DB_COORD_X
ADD A, B
LD B, A
LD A, B
AND $18
ADD A, $40
LD H, A
LD A, B
AND 7
RRCA
RRCA
RRCA
ADD A, C
LD L, A ; HL = DIR_PANTALLA(X_INICIAL,Y_INICIAL+Y*2)

LD A, (DM_WIDTH)
LD B, A ; B = ANCHO_EN_TILES

drawmg16_xloop:
PUSH BC ; Nos guardamos el contador del bucle

LD A, (IX+0) ; Leemos un byte del mapa


INC IX ; Apuntamos al siguiente byte del mapa

CP 255 ; Bloque especial a saltar: no se dibuja


JP Z, drawmg16_next
LD B, A
EX AF, AF' ; Nos guardamos una copia del bloque en A'
LD A, B

;;; Calcular posicion origen (array sprites) en HL como:


;;; direccion = base_sprites + (NUM_SPRITE*32)
EX DE, HL ; Intercambiamos DE y HL (DE=destino)
LD BC, (DM_SPRITES)
LD L, 0
SRL A
RR L
RRA
RR L
RRA
RR L
LD H, A
ADD HL, BC ; HL = BC + HL = DM_SPRITES + (DM_NUMSPR * 32)
EX DE, HL ; Intercambiamos DE y HL (DE=origen, HL=destino)

PUSH HL ; Guardamos el puntero a pantalla recien calculado


PUSH HL

;;; Impresion de los primeros 2 bloques horizontales del tile

LD B, 8
drawmg16_loop1:

LD A, (DE) ; Bloque 1: Leemos dato del sprite


LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla
LD A, (DE) ; Bloque 2: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC H ; Hay que sumar 256 para ir al siguiente scanline
DEC L ; pero hay que restar el INC L que hicimos.
DJNZ drawmg16_loop1
INC L ; Decrementar el ultimo incrementado en el bucle

; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)


; desde el septimo scanline de la fila Y+1 al primero de la Y+2
LD A, L
ADD A, 31
LD L, A
JR C, drawmg16_nofix_abajop
LD A, H
SUB 8
LD H, A
drawmg16_nofix_abajop:

;;; Impresion de los segundos 2 bloques horizontales:


LD B, 8
drawmg16_loop2:
LD A, (DE) ; Bloque 1: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC L ; Incrementar puntero en pantalla
LD A, (DE) ; Bloque 2: Leemos dato del sprite
LD (HL), A ; Copiamos dato a pantalla
INC DE ; Incrementar puntero en sprite
INC H ; Hay que sumar 256 para ir al siguiente scanline
DEC L ; pero hay que restar el INC L que hicimos.
DJNZ drawmg16_loop2

;;; En este punto, los 16 scanlines del tile estan dibujados.

;;;;;; Impresion de la parte de atributos del tile ;;;;;;


POP HL ; Recuperar puntero a inicio de tile

;;; Calcular posicion destino en area de atributos en DE.


LD A, H ; Codigo de Get_Attr_Offset_From_Image
RRCA
RRCA
RRCA
AND 3
OR $58
LD D, A
LD E, L ; DE tiene el offset del attr de HL

LD HL, (DM_ATTRIBS)
EX AF, AF' ; Recuperamos el bloque del mapa desde A'
LD C, A
LD B, 0
ADD HL, BC
ADD HL, BC
ADD HL, BC
ADD HL, BC ; HL = HL+HL=(DM_NUMSPR*4) = Origen de atributo

LDI
LDI ; Imprimimos la primeras fila de atributos

;;; Avance diferencial a la siguiente linea de atributos


LD A, E ; A = E
ADD A, 30 ; Sumamos A = A + 30 mas los 2 INCs de LDI.
LD E, A ; Guardamos en E (E = E+30 + 2 por LDI=E+32)
JR NC, drawmg16_att_noinc
INC D
drawmg16_att_noinc:
LDI
LDI ; Imprimimos la segunda fila de atributos

POP HL ; Recuperamos el puntero al inicio

drawmg16_next:
INC L ; Avanzamos al siguiente tile en pantalla
INC L ; horizontalmente

POP BC ; Recuperamos el contador para el bucle


DEC B ; DJNZ se sale de rango, hay que usar DEC+JP
JP NZ, drawmg16_xloop

;;; NUEVO: Incrementar puntero de mapa a siguiente linea


LD BC, ANCHO_MAPA_TILES - ANCHO_PANTALLA
ADD IX, BC
;;; FIN NUEVO

;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)


POP BC
DEC B ; Bucle vertical
JP NZ, drawmg16_yloop

RET

Además es necesario realizar rutinas adicionales que gestionen el movimiento por pantalla alterando
DM_MAPX y DM_MAPY sin permitir incrementarlos más allá de (Ancho_Mapa-Ancho_Pantalla) y
(Alto_Mapa-Alto_Pantalla) o decrementarlos por debajo de cero:

;-------------------------------------------------------------
; Incrementar la variable DM_MAPX para scrollear a la derecha.
;-------------------------------------------------------------
Map_Inc_X:
LD HL, (DM_MAPX)

;;; Comparacion 16 bits de HL y (ANCHO_MAPA-ANCHO_PANTALLA)


LD A, H
CP (ANCHO_MAPA_TILES-ANCHO_PANTALLA) / 256
RET NZ
LD A, L
CP (ANCHO_MAPA_TILES-ANCHO_PANTALLA) % 256
RET Z

INC HL ; No eran iguales, podemos incrementar.


LD (DM_MAPX), HL
RET

;-------------------------------------------------------------
; Incrementar la variable DM_MAPY para scrollear hacia abajo.
;-------------------------------------------------------------
Map_Inc_Y:
LD HL, (DM_MAPY)

;;; Comparacion 16 bits de HL y (ALTO_MAPA-ALTO_PANTALLA)


LD A, H
CP (ALTO_MAPA_TILES-ALTO_PANTALLA) / 256
RET NZ
LD A, L
CP (ALTO_MAPA_TILES-ALTO_PANTALLA) % 256
RET Z

INC HL ; No eran iguales, podemos incrementar.


LD (DM_MAPY), HL
RET

;-------------------------------------------------------------
; Decrementar la variable DM_MAPX para scrollear a la izq.
;-------------------------------------------------------------
Map_Dec_X:
LD HL, (DM_MAPX)
LD A, H
AND A
JR NZ, mapdecx_doit ; Verificamos que DM_MAPX no sea 0
LD A, L
AND A
RET Z
mapdecx_doit:
DEC HL
LD (DM_MAPX), HL ; No es cero, podemos decrementar
RET

;-------------------------------------------------------------
; Decrementar la variable DM_MAPY para scrollear hacia arriba.
;-------------------------------------------------------------
Map_Dec_Y:
LD HL, (DM_MAPY)
LD A, H
AND A
JR NZ, mapdecy_doit ; Verificamos que DM_MAPX no sea 0
LD A, L
AND A
RET Z
mapdecy_doit:
DEC HL
LD (DM_MAPY), HL ; No es cero, podemos decrementar
RET

El incremento de DM_MAPX y DM_MAPY requiere verificar que ninguna de las 2 variables excede
ANCHO_MAPA-ANCHO_PANTALLA y ALTO_MAPA-ALTO_PANTALLA respectivamente. Sus
decrementos requieren comprobar que el valor actual de estas variables no es cero.

Utilicemos las anteriores rutinas en un programa de ejemplo en el que podemos mover una ventana de 14×11
bloques a través de un mapa de 32×24 tiles usando las teclas O, P, Q y A:
; Ejemplo impresion mapa de 16x16 desde array global
ORG 32768

LD HL, sokoban1_gfx
LD (DM_SPRITES), HL
LD HL, sokoban1_attr
LD (DM_ATTRIBS), HL
LD HL, mapa_ejemplo
LD (DM_MAP), HL
LD A, ANCHO_PANTALLA
LD (DM_WIDTH), A
LD A, ALTO_PANTALLA
LD (DM_HEIGHT), A
XOR A
LD (DM_COORD_X), A
LD (DM_COORD_Y), A
LD (DM_MAPX), A ; Establecemos MAPX, MAPY iniciales = 0
LD (DM_MAPY), A

redraw:
CALL DrawMap_16x16_Map ; Imprimir pantalla de mapa

bucle:
CALL LEER_TECLADO ; Leemos el estado de O, P, Q, A

BIT 0, A ; Modificamos MAPX y MAPY segun OPQA


JR Z, nopulsada_q
CALL Map_Dec_Y
JR redraw
nopulsada_q:
BIT 1, A
JR Z, nopulsada_a
CALL Map_Inc_Y
JR redraw
nopulsada_a:
BIT 2, A
JR Z, nopulsada_p
CALL Map_Inc_X
JR redraw
nopulsada_p:
BIT 3, A
JR Z, nopulsada_o
CALL Map_Dec_X
JR redraw
nopulsada_o:
JR bucle

loop:
JR loop

;-------------------------------------------------------------
; LEER_TECLADO: Lee el estado de O, P, Q, A, y devuelve
; en A el estado de las teclas (1=pulsada, 0=no pulsada).
; El byte está codificado tal que:
;
; BITS 3 2 1 0
; SIGNIFICADO LEFT RIGHT DOWN UP
;-------------------------------------------------------------
LEER_TECLADO:
LD D, 0
LD BC, $FBFE
IN A, (C)
BIT 0, A ; Leemos la tecla Q
JR NZ, Control_no_up ; No pulsada, no cambiamos nada en D
SET 0, D ; Pulsada, ponemos a 1 el bit 0
Control_no_up:

LD BC, $FDFE
IN A, (C)
BIT 0, A ; Leemos la tecla A
JR NZ, Control_no_down ; No pulsada, no cambianos nada en D
SET 1, D ; Pulsada, ponemos a 1 el bit 1
Control_no_down:

LD BC, $DFFE
IN A, (C)
BIT 0, A ; Leemos la tecla P
JR NZ, Control_no_right ; No pulsada
SET 2, D ; Pulsada, ponemos a 1 el bit 2
Control_no_right:
; BC ya vale $DFFE, (O y P en misma fila)
BIT 1, A ; Tecla O
JR NZ, Control_no_left
SET 3, D
Control_no_left:

LD A, D ; Devolvemos en A el estado de las teclas


RET

;-------------------------------------------------------------
DM_SPRITES EQU 50020
DM_ATTRIBS EQU 50022
DM_MAP EQU 50024
DM_COORD_X EQU 50026
DM_COORD_Y EQU 50027
DM_WIDTH EQU 50028
DM_HEIGHT EQU 50029
DM_MAPX EQU 50030
DM_MAPY EQU 50032

;-------------------------------------------------------------
; Algunos valores hardcodeados para el ejemplo, en la rutina
; final se puede utilizar DM_WIDTH y DM_HEIGHT.
;-------------------------------------------------------------
ANCHO_MAPA_TILES EQU 32
ALTO_MAPA_TILES EQU 24
ANCHO_PANTALLA EQU 14
ALTO_PANTALLA EQU 11

;;; Rutina de la ROM del Spectrum, en otros sistemas


;;; sustituir por una rutina especifica de multiplicacion
MULT_HL_POR_DE EQU $30A9

;-----------------------------------------------------------------------
;;; Nuestra pantalla de ejemplo de 32x24 bloques:
;-----------------------------------------------------------------------
mapa_ejemplo:
DEFB 1,2,1,1,2,1,2,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,2,3,2,3,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,1
DEFB 1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,7,7,0,0,0,1
DEFB 1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,7,7,7,0,0,1
DEFB 1,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,5,0,0,0,0,0,0,2,3,2,3,2,0,1
DEFB 1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,4,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,5,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,4,2,3,2,3,0,0,0,0,2,3,2,3,2,3,2,3,4,2,3,2,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,2,3,2,3,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,0,6,6,6,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,0,2,3,2,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,0,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,6,6,0,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,6,6,6,6,0,0,0,1
DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,2,3,2,3,2,3,2,3,0,0,1
DEFB 1,0,2,3,2,2,2,3,2,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
DEFB 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1

Veamos una captura del resultado del anterior ejemplo después de moverse a través del mapeado:

El anterior ejemplo es meramente ilustrativo del uso de la rutina de impresión: para un juego basado en scroll
(como simula el ejemplo con la pulsación de teclas) resultará mucho más rápido y eficiente realizar un scroll
del contenido del área de juego y trazar sólo la fila o columna de datos que “entra” en el área de visión del
jugador.

Para esta implementación serían necesarias 4 rutinas de scroll de una porción de videomemoria, según el tipo de
movimiento, y la impresión en la primera/última fila/columna de visión del dato de la pantalla entrante. Sigue
siendo necesario modificar DM_MAPX y DM_MAPY.

El scroll mediante instrucciones de transferencia de N-1 FILAS o N-1 COLUMNAS de pantalla gráfica y de
atributos resultará bastante más rápido y eficiente que la rutina de impresión de sprites integrada en DrawMap,
al no tener que realizar apenas cálculos.

Como desventaja principal de los mapeados basados en arrays globales de tiles, en este tipo de mapeados no
podemos utilizar de una forma inmediata las técnicas de “agrupación” o “compresión” que veremos a
continuación.

Mapeados diferenciales
Las pantallas de tamaño fijo almacenan la información tanto de los bloques “no dibujables” (fondos
transparentes o sólidos) como de los dibujables (los gráficos que forman la pantalla en sí misma). Esto supone
un pequeño desperdicio de memoria ya que almacenamos en el array de la pantalla datos que finalmente no
vamos a utilizar y que no aparecerán en pantalla.

Una pantalla de 16×12 tiles que ocupe todo el área visible ocupa 192 bytes, lo que nos permite un total de 85
pantallas en 16 KB de memoria. Si tenemos en cuenta que necesitamos espacio para los gráficos de personajes
y enemigos, el tileset, fuentes de texto, código del programa, textos, sonido, variables, nos encontramos con que
se establece un límite de cantidad de pantallas que podemos incorporar en nuestro programa en función de la
memoria libre que nos queda tras incorporar todos los elementos del mismo.

Para reducir el espacio que ocupan nuestras pantallas y por tanto poder incluir más pantallas en la misma
cantidad de memoria podemos utilizar diferentes métodos de codificación.
Uno de ellos podría basarse en la compresión por diferentes algoritmos del mapeado considerado globalmente:
si tomamos todo el bloque de datos con información sobre las pantallas y lo comprimimos antes de salvarlo a
cintar y lo descomprimimos al vuelo durante su carga reducimos la ocupación del binario resultante en cinta
pero no de la ocupación de datos en memoria.

Por esto, lo mejor es codificar o “comprimir” cada pantalla en sí misma y que la rutina de impresión la
desempaquete al vuelo.

La técnica que vamos a ver no es una compresión en sí misma sino que se basa en no almacenar en el vector de
datos de la pantalla los datos en blanco/transparentes. Los datos de la pantalla incluirán sólo los tiles que deben
de ser dibujados.

Si tenemos una fila de 16 tiles pero sólo 5 de ellos deben de ser dibujados, es absurdo almacenar la información
de los 11 tiles “en blanco”. A continuación veremos diferentes formas de codificar los datos de los tiles “reales”
y descartar los tiles “vacíos”.

Aunque los mapeados diferenciales consiguen su mayor compresión incluyendo técnicas de repetición de tiles y
de patrones, nosotros vamos a considerar las técnicas de compresión básica basadas simplemente en descartado
de tiles fondo/transparentes y en agrupación de scanlines.

Mapeados diferenciales con un único tileset

En las técnicas de mapeados diferenciales, el mapa no cambia: incluye (como mínimo) las direcciones de las
pantallas y las conexiones entre las mismas. Lo que sí que se ve modificada es la estructura de la pantalla, que
ahora no tiene un tamaño fijo (al no ser ya una matriz de Ancho*Alto tiles).

Este tamaño variable requiere finalizar los datos de la pantalla con un identificador para informar a las rutinas
de impresión de cuándo deben terminar su ejecución. En nuestro caso finalizaremos las pantallas con un valor
255 (-1).

A la hora de codificar las pantallas, podemos optar por diferentes “algoritmos”:

• Codificación básica: se almacenan en el vector “pantalla” los datos de cada tile que realmente deba de
ser impreso. Los datos mínimos necesarios son la coordenada X, la coordenada Y y el identificador de
tile.
• Codificación por agrupación horizontal: Para evitar incluir la coordenada X e Y en cada tile,
podemos agrupar tiles consecutivos horizontalmente y marcar sólo la posición del primero, ahorrando 2
bytes por cada tile que le sigue.
• Codificación por agrupación vertical: Para evitar incluir la coordenada X e Y en cada tile, podemos
agrupar tiles consecutivos verticalmente y marcar sólo la posición del primero, ahorrando 2 bytes por
cada tile bajo él.
• Codificación por agrupación mixta: Cada tile se codifica por agrupación horizontal o vertical según
produzca un mayor o menor ahorro de tamaño. La pantalla contiene primero los scanlines horizontales,
seguidos de un byte de valor 254, y después los scanlines verticales.

Veamos en detalle todos estos tipos de codificación.


Mapeado diferencial básico (sin agrupación)
Implementaremos codificación básica por scanlines horizontales, verticales, y mixtos utilizando un pequeño
programa en python creado específicamente para este capítulo. Con él codificaremos la pantalla 1 de Sokoban.

A esta pantalla tendremos que realizarle una pequeña modificación para mostrar las bondades de la
codificación: eliminar los tiles transparentes para que podamos codificarla ignorando los tiles vacíos.

Si nos fijamos en cualquier juego de plataformas, shooter, aventura, etc, veremos que, al contrario que en las
pantallas de ejemplo de Sokoban, no existen bloques transparentes y que la gran mayoría de los bloques en
pantalla son o bien “vacíos” (fondo sólido) o bien “transparentes” (fondo no sólido), siendo esos bloques el área
por donde se mueve el personaje y los enemigos. En la mayoría de juegos, pues, hay bloques vacíos o
transparentes pero no de ambos tipos.

Para mostrar el nivel de compresión que se conseguiría en un juego basado en tiles, supongamos que nuestro
juego de ejemplo (Sokoban) no tuviera transparencias y el fondo del área de juego fuera totalmente plana (un
mismo color) con lo que pudieramos ignorar la impresión de bloques 0 en lugar de la de los bloques 255:

Introducimos los datos del nivel 1 (cambiando los tiles transparentes por ceros, tal y como sería la pantalla de
cualquier otro juego) en un fichero pantalla.dat con el siguiente formato:

sokoban_LEVEL1:
DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DEFB 0,0,0,0,0,0,2,3,1,4,0,0,0,0,0,0
DEFB 0,0,0,0,1,2,3,0,0,5,4,0,0,0,0,0
DEFB 0,0,0,0,4,0,6,6,0,0,5,0,0,0,0,0
DEFB 0,0,0,0,5,0,0,6,0,0,4,0,0,0,0,0
DEFB 0,0,0,0,4,0,0,0,0,0,5,0,0,0,0,0
DEFB 0,0,0,0,5,2,3,0,0,2,3,0,0,0,0,0
DEFB 0,0,0,0,0,0,1,0,0,0,4,0,0,0,0,0
DEFB 0,0,0,0,0,0,4,7,7,7,5,0,0,0,0,0
DEFB 0,0,0,0,0,0,5,2,3,2,3,0,0,0,0,0
DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

Veamos el código de nuestro sencillo programa de “compresión/codificación” realizado en lenguaje python:

#!/usr/bin/python
#
# Convierte una pantalla de mapa en pantalla codificada por
# codif. basica, scanlines horizontales, verticales o mixtos.
# Permite agrupacion de coordenadas XY en un mismo byte con
# el flag -a.
#

import os, sys

# Variables de configuracion del script


ANCHO_MAPA = 16
ALTO_MAPA = 12
IGNORE_VALUES = 0
BLANK = 0
agrupar_xy = 0

MIXTA_CODIF_HORIZ = 0
MIXTA_CODIF_VERT = 1

#-----------------------------------------------------------------------
def Uso():
print "\nUso:", sys.argv[0], "TIPO_CODIFICACION [-a] nombre_fichero"
print "\n Flag TIPO_CODIFICACION:\n"
print " Basica = -b"
print " Scanlines horizontales = -h"
print " Scanlines verticales= -v"
print " Scanlines horizontales = -h"
print " Mixta horizontales/verticales = -m\n"
print " Flag opcional -a: (por defecto = off)"
print " Agrupar coordenadas X e Y en un mismo byte = -a\n"

sys.exit(1)

#-----------------------------------------------------------------------
def Consecutivos_Horiz( x, y, pantalla, ancho ):
cuales = []
tiles = []
while (x < ancho) and ( pantalla[y][x] != IGNORE_VALUES ):
cuales.append( x )
cuales.append( y )
tiles.append(pantalla[int(y)][int(x)])
x += 1
return cuales, tiles

#-----------------------------------------------------------------------
def Consecutivos_Vert( x, y, pantalla, alto ):
cuales = []
tiles = []
while (y < alto) and ( pantalla[y][x] != IGNORE_VALUES ):
cuales.append( x )
cuales.append( y )
tiles.append(pantalla[y][x])
y += 1
return cuales, tiles

#-----------------------------------------------------------------------
def Borrar_Consecutivos( pantalla, lista ):
for i in range(0,len(lista),2):
x = lista[i]
y = lista[i+1]
pantalla[y][x] = BLANK
return pantalla

#-----------------------------------------------------------------------
def Codificacion_Horiz_o_Vert( pantalla ):

COORD_Y = 0
mapa_codificado = []

# Procesamos la matriz de datos (la pantalla) segun la codificacion:


for y in range(0,ALTO_MAPA):
COORD_X = 0

# Procesar los valores separados por coma:


for valor in pantalla[y]:
if codificacion == "-b":
if valor != IGNORE_VALUES:
if agrupar_xy == 0:
mapa_codificado.append( COORD_X )
mapa_codificado.append( COORD_Y )
else:
mapa_codificado.append( (COORD_X*16) + COORD_Y )
mapa_codificado.append( valor )
elif codificacion == "-h" or codificacion == "-v":
if valor != IGNORE_VALUES:
if agrupar_xy == 0:
mapa_codificado.append( COORD_X )
mapa_codificado.append( COORD_Y )
else:
mapa_codificado.append( (COORD_X*16) + COORD_Y )
if codificacion == "-h":
a, b = Consecutivos_Horiz(COORD_X, COORD_Y, pantalla, ANCHO_MAPA)
else:
a, b = Consecutivos_Vert(COORD_X, COORD_Y, pantalla, ALTO_MAPA)
pantalla = Borrar_Consecutivos( pantalla, a )
mapa_codificado.extend( b )
mapa_codificado.append( 255 );
COORD_X += 1
COORD_Y += 1

return mapa_codificado

#-----------------------------------------------------------------------
def Tiles_Pendientes( pantalla ):
cuantos = 0
for y in range(0,len(pantalla)):
for value in pantalla[y]:
if value != IGNORE_VALUES:
cuantos += 1
return cuantos

#-----------------------------------------------------------------------
def Coordenadas_Mayor_Valor( matriz ):
maxx, maxy, maxv = 0, 0, 0

for y in range(0,len(matriz)):
for x in range(len(matriz[y])):
valor = matriz[y][x]
if valor != IGNORE_VALUES and valor > maxv:
maxv = valor
maxx = x
maxy = y

return maxx, maxy, maxv

#-----------------------------------------------------------------------
def Codificacion_Mixta( pantalla ):

mapa_codificado_h = []
mapa_codificado_v = []
mapa_codificado = []

# Repetir hasta que no queden tiles que codificar:


while Tiles_Pendientes(pantalla) != 0:

# Construir 2 tablas con la cantidad de tiles horizontales y


# verticales que salen de codificar cada posicion:
horizontales = [ [ 0 for i in range(0,ANCHO_MAPA) ] for j in range(0,ALTO_MAPA) ]
verticales = [ [ 0 for i in range(0,ANCHO_MAPA) ] for j in range(0,ALTO_MAPA) ]

COORD_Y = 0
for y in range(0,ALTO_MAPA):
COORD_X = 0
for valor in pantalla[y]:
if valor != IGNORE_VALUES:
a, b = Consecutivos_Horiz(COORD_X, COORD_Y, pantalla, ANCHO_MAPA)
c, d = Consecutivos_Vert(COORD_X, COORD_Y, pantalla, ALTO_MAPA)
horizontales[COORD_Y][COORD_X] = len(b)
verticales[COORD_Y][COORD_X] = len(d)
COORD_X += 1
COORD_Y += 1

# Una vez construida la tabla, buscar la posicion X,Y que


# tiene el valor mas alto y codificarla
max_hx, max_hy, maxh_v = Coordenadas_Mayor_Valor( horizontales )
max_vx, max_vy, maxv_v = Coordenadas_Mayor_Valor( verticales )

# Codificar con horizontal o vertical segun cual sea el mayor


if maxh_v >= maxv_v:
if agrupar_xy == 0:
mapa_codificado_h.append( max_hx )
mapa_codificado_h.append( max_hy )
else:
mapa_codificado_h.append( (max_hx*16) + max_hy )
a, b = Consecutivos_Horiz(max_hx, max_hy, pantalla, ANCHO_MAPA)
pantalla = Borrar_Consecutivos( pantalla, a )
mapa_codificado_h.extend( b )
mapa_codificado_h.append( 255 )
else:
if agrupar_xy == 0:
mapa_codificado_v.append( max_vx )
mapa_codificado_v.append( max_vy )
else:
mapa_codificado_h.append( (max_vx*16) + max_vy )
c, d = Consecutivos_Vert(max_vx, max_vy, pantalla, ALTO_MAPA)
pantalla = Borrar_Consecutivos( pantalla, c )
mapa_codificado_v.extend( d )
mapa_codificado_v.append( 255 )

# Sacamos las codificaciones en orden: primero horizontales, luego 254


# tras eliminar el 255 final de las horizontales, y luego verticales.
if mapa_codificado_h != []:
mapa_codificado.extend(mapa_codificado_h[:-1])

mapa_codificado.append( 254 )

if mapa_codificado_v != []:
mapa_codificado.extend(mapa_codificado_v)

return mapa_codificado

#-----------------------------------------------------------------------
def Imprimir_Resultados( codificacion, mapa_codificado ):
print " ; Flag codificacion:", codificacion, "-a" * agrupar_xy
print " ; Resultado:", len(mapa_codificado), "Bytes"
# Imprimimos los resultados de la codificacion
CONTADOR_DB = -1
for valor in mapa_codificado:
CONTADOR_DB += 1
if CONTADOR_DB == 0:
print " DB", str(valor),
elif CONTADOR_DB == 11:
print ",", str(valor)
CONTADOR_DB = -1
else:
print ",", str(valor),

#-----------------------------------------------------------------------
if __name__ == '__main__':

# Variables que utilizaremos


pantalla = []
COORD_X = 0
COORD_Y = 0

# Comprobar numero de argumentos + recoger y validar parametros:


if len( sys.argv ) < 3:
Uso()

if "-a" in sys.argv:
agrupar_xy = 1
args = [ arg for arg in sys.argv if arg != '-a' ]
else:
args = sys.argv[:]

if args[1][0] == '-':
codificacion=args[1]
fichero=args[2]
elif args[2][0] == '-':
codificacion=args[2]
fichero=args[1]
else:
Uso()

if codificacion[1] not in "bhvm":


Uso()

# Abrir el fichero de pantalla:


try:
fich = open( fichero )
except:
print "No se pudo abrir ", sys.argv[1]
sys.exit(0)

# Procesar el fichero "linea a linea"


for linea in fich.readlines():

pantalla.append( [] )

# Eliminamos todo lo que no sean numeros, coma, y retorno de carro


linea = filter(lambda c: c in "0123456789," + chr(10) + chr(13), linea)

# Si encontramos una coma en la linea, procesarla:


if ',' in linea:

# Partimos la linea en valores separados por comas:


linea = linea.strip()
rows = linea.split(',')
if len(rows) != ANCHO_MAPA:
print "ERROR: Ancho =", len(rows), "valores. Esperados =", ANCHO_MAPA
print "Finalizando programa: Por fvor corrija el fichero de datos."
sys.exit(2)

rows = [int(value) for value in rows]


pantalla[ COORD_Y ].extend( rows )

COORD_Y += 1
# Fin lectura datos fichero

if COORD_Y != ALTO_MAPA:
print "ERROR: Alto =", COORD_Y+1, "lineas. Esperados =", ALTO_MAPA
print "Finalizando programa: Por fvor corrija el fichero de datos."
sys.exit(2)

if codificacion in [ "-h", "-v", "-b" ]:


mapa_codificado = Codificacion_Horiz_o_Vert( pantalla )
elif codificacion == '-m':
mapa_codificado = Codificacion_Mixta( pantalla )

# Acabamos el mapa con el valor de fin de mapa


mapa_codificado.append( 255 )

# Imprimimos los resultados


Imprimir_Resultados( codificacion, mapa_codificado)

Ejecutamos el script de conversión mediante codificación básica (flag -b):

$ ./codificar_pantalla.py -b pantalla.dat
; Flag codificacion: -b
; Resultado: 106 Bytes
DB 6 , 1 , 2 , 7 , 1 , 3 , 8 , 1 , 1 , 9 , 1 , 4
DB 4 , 2 , 1 , 5 , 2 , 2 , 6 , 2 , 3 , 9 , 2 , 5
DB 10 , 2 , 4 , 4 , 3 , 4 , 6 , 3 , 6 , 7 , 3 , 6
DB 10 , 3 , 5 , 4 , 4 , 5 , 7 , 4 , 6 , 10 , 4 , 4
DB 4 , 5 , 4 , 10 , 5 , 5 , 4 , 6 , 5 , 5 , 6 , 2
DB 6 , 6 , 3 , 9 , 6 , 2 , 10 , 6 , 3 , 6 , 7 , 1
DB 10 , 7 , 4 , 6 , 8 , 4 , 7 , 8 , 7 , 8 , 8 , 7
DB 9 , 8 , 7 , 10 , 8 , 5 , 6 , 9 , 5 , 7 , 9 , 2
DB 8 , 9 , 3 , 9 , 9 , 2 , 10 , 9 , 3, 255

La codificación muestra, por ejemplo, cómo en la columna 6, fila 1, hay un tile 2 (lo cual es correcto). Le sigue
un tile 3 en (7,1), etc. Las filas y columnas se numeran desde cero y los valores 0 se ignoran (no se codifican).

El mapa acaba con un valor 255. Dado que las diferentes pantallas de juego no van a tener un tamaño fijo y
común, es necesario este byte de fin de pantalla para que nuestra rutina pueda determinar cuándo se ha
finalizado la impresión.

El script codifica esta pantalla con 106 bytes cuando el tamaño original era de 192, consiguiendo una
compresión de aprox. el 46% (casi la mitad de tamaño). Esto quiere decir que el mapeado de nuestro programa
podría ser (manteniendo este ratio de compresión en todas las pantallas), hasta casi el doble de grande que el
mapa máximo actual.

Veamos a continuación una sencilla rutina que permitiría imprimir un mapa con este tipo de codificación. La
rutina se basa en recorrer el array de datos de pantalla obtenido el valor X, Y y de TILE y llamando a
DrawSprite16x16 para la impresión de cada tile:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Basica:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes) Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes) Direccion de la tabla de atributos.
; DM_MAP (2 bytes) Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte) Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte) Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH (1 byte) Ancho del mapa en tiles
; DM_HEIGHT (1 byte) Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Basica:

LD HL, (DM_SPRITES)
LD (DS_SPRITES), HL ; Establecer tileset (graficos)
LD HL, (DM_ATTRIBS)
LD (DS_ATTRIBS), HL ; Establecer tileset (atributos)
LD BC, (DM_COORD_X) ; B = Y_INICIO, C = X_INICIO
LD DE, (DM_MAP) ; DE apunta al mapa

drawm16cb_loop:
LD A, (DE) ; Leemos el valor de COORD_X_TILE
INC DE ; Apuntamos al siguiente byte del mapa

CP 255 ; Bloque especial fin de pantalla


RET Z ; En ese caso, salir

ADD A, C ; A = (X_INICIO + COORD_X_TILE)


RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (DS_COORD_X), A ; Establecemos COORD_X a imprimir tile

LD A, (DE) ; Leemos el valor de COORD_Y_TILE


INC DE
ADD A, B ; A = (X_INICIO + COORD_X_TILE)
RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (DS_COORD_Y), A ; Establecemos COORD_Y a imprimir tile

LD A, (DE) ; Leemos el valor del TILE


INC DE
LD (DS_NUMSPR), A ; Establecemos el TILE

EXX ; Preservar todos los registros en shadows


CALL DrawSprite_16x16_LD ; Imprimir el tile con los parametros
EXX ; Recuperar valores de los registros

JR drawm16cb_loop ; Repetimos hasta encontrar el 255

Este tipo de rutina de impresión puede no ser tan rápida como una específica con el mapa en formato crudo
pero supone un gran ahorro de memoria y es posible que la diferencia de velocidad no sea importante o
perceptible. Hablamos de unas diferencias de tiempos de impresión que siempre que no tratemos con un juego
basado en scroll serán imperceptibles por el usuario: alguien que juegue al Manic Miner no podrá determinar si
la pantalla en la que va a jugar durante varios minutos ha sido dibujada en 0.03 o en 0.10 segundos. Aunque una
rutina tardara el triple que la otra, 1 décima de segundo de tiempo total de impresión es inapreciable para el
usuario.

Por otra parte, como ya no estamos dibujando los bloques 0, antes de llamar a la rutina de dibujado diferencial
es necesario borrar el contenido del área donde vamos a dibujar con el color plano del bloque 0 para que el
resultado de la impresión incluya los bloques vacíos en aquellas áreas en que no dibujamos. Es decir,
vaciaremos los “bloques vacíos” borrando inicialmente la pantalla antes de realizar la impresión de los “bloques
con datos”.

Para borrar este área de pantalla podemos utilizar un simple borrado de atributos (establecer todos los atributos
a cero) siempre y cuando los personajes del juego que se moverán sobre las áreas “vacías” se dibujen mediante
transferencia y no mediante operaciones lógicas. Recordemos que si hemos borrado mediante atributos en
negro estas áreas están vacías de color pero no de contenido gráfico y la impresión con OR haría aparecer el
antiguo contenido gráfico de la pantalla. En el caso de impresión de sprites con OR sobre el área “vacía” del
mapa sería necesario realizar el borrado previo a la impresión del mapa no como borrado de atributos sino
como borrado de zona gráfica y de atributos.

El siguiente programa ejemplo hace uso de la anterior rutina:

; Ejemplo impresion mapa de 16x16 codificacion basica


ORG 32768

;;; Borramos la pantalla (graficos y atributos)


XOR A
CALL ClearScreen
XOR A
CALL ClearAttributes

;;; Establecer valores de llamada:


LD HL, sokoban1_gfx
LD (DM_SPRITES), HL
LD HL, sokoban1_attr
LD (DM_ATTRIBS), HL
LD HL, sokoban_LEVEL1_codif_basica
LD (DM_MAP), HL
LD A, 16
LD (DM_WIDTH), A
LD A, 12
LD (DM_HEIGHT), A
XOR A
LD (DM_COORD_X), A
LD (DM_COORD_Y), A

;;; Impresion de pantalla por codificacion basica


CALL DrawMap_16x16_Cod_Basica

loop:
JR loop

DM_SPRITES EQU 50020


DM_ATTRIBS EQU 50022
DM_MAP EQU 50024
DM_COORD_X EQU 50026
DM_COORD_Y EQU 50027
DM_WIDTH EQU 50028
DM_HEIGHT EQU 50029

DS_SPRITES EQU 50000


DS_ATTRIBS EQU 50002
DS_COORD_X EQU 50004
DS_COORD_Y EQU 50005
DS_NUMSPR EQU 50006

El resultado de la ejecución es el siguiente:

Mapeado diferencial con agrupación por scanlines


La pega de la técnica de codificación básica es que por cada bloque a imprimir estamos añadiendo 2 bytes de
datos (coordenada X y coordenada Y) por lo que si más de 1/3 de los bloques totales de la pantalla son tiles a
imprimir obtenemos una pantalla con más tamaño que la original.

La solución es codificar tiles “consecutivos” de forma que sólo haya que indicar una coordenada X e Y iniciales
para cada “fila” o “columna” de tiles gráficos. De esta forma, 4 tiles gráficos consecutivos se codificarían
como:

DB coordenada_x_primer_tile, coordenada_y_primer_tile,
DB tile1, tile2, tile3, tile4, 255
; (255 = byte de fin de "scanline")

Codificar estos 4 tiles con codificación básica hubiera requerido 4*3 = 12 bytes, pero mediante este sistema se
requieren sólo 7.

La agrupación de tiles la podemos hacer buscando “conjuntos de tiles horizontales” o “verticales”. Según el
tipo de “scanline de tiles” que generemos, necesitaremos codificar la pantalla de una forma o de otra y utilizar
una rutina de impresión u otra.

Al final de nuestra pantalla necesitaremos un valor 255 adicional para que la rutina de impresión, al recogerlo
como coordenada X, detecta la finalización de la misma.

Mapeado diferencial con agrupación por scanlines horizontales


El script codificador en python que hemos visto en el apartado de codificación básica permite, mediante el flag
“-h” codificar la pantalla buscando ristras de tiles horizontales consecutivos y codificándolos de esta forma.
Nótese que un sólo tile sería codificado como una ristra de tamaño 1 (x, y, tile, 255).

Para empezar, se certifica que una codificación basada en scanlines horizontales de tiles da una pantalla
codificada resultante más reducida:

$ ./codificar_pantalla.py -h pantalla.dat
; Flag codificacion: -h
; Resultado: 87 Bytes
DB 6 , 1 , 2 , 3 , 1 , 4 , 255 , 4 , 2 , 1 , 2 , 3
DB 255 , 9 , 2 , 5 , 4 , 255 , 4 , 3 , 4 , 255 , 6 , 3
DB 6 , 6 , 255 , 10 , 3 , 5 , 255 , 4 , 4 , 5 , 255 , 7
DB 4 , 6 , 255 , 10 , 4 , 4 , 255 , 4 , 5 , 4 , 255 , 10
DB 5 , 5 , 255 , 4 , 6 , 5 , 2 , 3 , 255 , 9 , 6 , 2
DB 3 , 255 , 6 , 7 , 1 , 255 , 10 , 7 , 4 , 255 , 6 , 8
DB 4 , 7 , 7 , 7 , 5 , 255 , 6 , 9 , 5 , 2 , 3 , 2
DB 3 , 255 , 255

Con esta codificación la pantalla original de 192 bytes que ocupaba 106 bytes con codificación básica pasa a
requerir sólo 87 bytes mediante ristras de scanlines horizontales de tiles, lo que representa un 55% de
compresión.

Veamos un ejemplo de rutina para imprimir este tipo de pantallas codificadas:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Horiz:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes) Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes) Direccion de la tabla de atributos.
; DM_MAP (2 bytes) Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte) Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte) Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH (1 byte) Ancho del mapa en tiles
; DM_HEIGHT (1 byte) Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Horiz:

LD HL, (DM_SPRITES)
LD (DS_SPRITES), HL ; Establecer tileset (graficos)
LD HL, (DM_ATTRIBS)
LD (DS_ATTRIBS), HL ; Establecer tileset (atributos)
LD BC, (DM_COORD_X) ; B = Y_INICIO, C = X_INICIO
LD DE, (DM_MAP) ; DE apunta al mapa

LD HL, DS_COORD_X

drawm16ch_read:
LD A, (DE) ; Leemos el valor de COORD_X_TILE
INC DE ; Apuntamos al siguiente byte del mapa

drawm16ch_loop:
CP 255 ; Bloque especial fin de pantalla
RET Z ; En ese caso, salir

ADD A, C ; A = (X_INICIO + COORD_X_TILE)


RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (HL), A ; Establecemos COORD_X a imprimir tile

LD A, (DE) ; Leemos el valor de COORD_Y_TILE


INC DE
ADD A, B ; A = (Y_INICIO + COORD_Y_TILE)
RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (DS_COORD_Y), A ; Establecemos COORD_Y a imprimir tile

;;; Bucle impresion de todos los tiles del scanline (aunque sea 1 solo)

drawm16ch_tileloop:
LD A, (DE) ; Leemos el valor del TILE de pantalla
INC DE ; Incrementamos puntero

CP 255
JR Z, drawm16ch_read ; Si es fin de tile codificado, fin bucle

LD (DS_NUMSPR), A ; Establecemos el TILE

EXX ; Preservar todos los registros en shadows


CALL DrawSprite_16x16_LD ; Imprimir el tile con los parametros
EXX ; Recuperar valores de los registros

INC (HL) ; Avanzamos al siguiente tile


INC (HL) ; COORD_X = COORD_X + 2

JR drawm16ch_tileloop ; Repetimos hasta encontrar el 255

Mapeado diferencial con agrupación por scanlines verticales

Según el tipo de pantalla que estemos codificando, es posible que existan más agrupaciones de bloques en
“scanlines verticales” (columnas de bloques) que horizontales (filas de bloques). En ese caso, puede
convenirmos codificar los bloques por scanlines verticales.

Si utilizamos el script en python con el flag -v sobre la pantalla de ejemplo, obtenemos la siguiente pantalla
codificada:

$ ./codificar_pantalla.py -v pantalla.dat
; Flag codificacion: -v :
; Resultado: 78 Bytes
DB 6 , 1 , 2 , 3 , 6 , 255 , 7 , 1 , 3 , 255 , 8 , 1
DB 1 , 255 , 9 , 1 , 4 , 5 , 255 , 4 , 2 , 1 , 4 , 5
DB 4 , 5 , 255 , 5 , 2 , 2 , 255 , 10 , 2 , 4 , 5 , 4
DB 5 , 3 , 4 , 5 , 3 , 255 , 7 , 3 , 6 , 6 , 255 , 5
DB 6 , 2 , 255 , 6 , 6 , 3 , 1 , 4 , 5 , 255 , 9 , 6
DB 2 , 255 , 7 , 8 , 7 , 2 , 255 , 8 , 8 , 7 , 3 , 255
DB 9 , 8 , 7 , 2 , 255 , 255

En este caso tenemos un tamaño de pantalla de 78 bytes, todavía menor que la codificación por scanlines
horizontales. Para otras pantallas y casos el resultado de la codificación podría ser mejor con scanlines
horizontales.

A continuación se transcribe la rutina de impresión de una pantalla codificada en scanlines verticales:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Vert:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes) Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes) Direccion de la tabla de atributos.
; DM_MAP (2 bytes) Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte) Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte) Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH (1 byte) Ancho del mapa en tiles
; DM_HEIGHT (1 byte) Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Vert:

LD HL, (DM_SPRITES)
LD (DS_SPRITES), HL ; Establecer tileset (graficos)
LD HL, (DM_ATTRIBS)
LD (DS_ATTRIBS), HL ; Establecer tileset (atributos)
LD BC, (DM_COORD_X) ; B = Y_INICIO, C = X_INICIO
LD DE, (DM_MAP) ; DE apunta al mapa

LD HL, DS_COORD_Y ; CAMBIO: Ahora HL apunta a la variable Y

drawm16cv_read:
LD A, (DE) ; Leemos el valor de COORD_X_TILE
INC DE ; Apuntamos al siguiente byte del mapa

drawm16cv_loop:
CP 255 ; Bloque especial fin de pantalla
RET Z ; En ese caso, salir

ADD A, C ; A = (X_INICIO + COORD_X_TILE)


RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (DS_COORD_X), A ; CAMBIO: Establecemos COORD_X a imprimir tile

LD A, (DE) ; Leemos el valor de COORD_Y_TILE


INC DE
ADD A, B ; A = (Y_INICIO + COORD_Y_TILE)
RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (HL), A ; CAMBIO: Establecemos COORD_Y a imprimir tile

;;; Bucle impresion de todos los tiles del scanline (aunque sea 1 solo)

drawm16cv_tileloop:

LD A, (DE) ; Leemos el valor del TILE de pantalla


INC DE ; Incrementamos puntero

CP 255
JR Z, drawm16cv_read ; Si es fin de tile codificado, fin bucle

LD (DS_NUMSPR), A ; Establecemos el TILE

EXX ; Preservar todos los registros en shadows


CALL DrawSprite_16x16_LD ; Imprimir el tile con los parametros
EXX ; Recuperar valores de los registros

INC (HL)
INC (HL) ; COORD_Y = COORD_Y + 2

JR drawm16cv_tileloop ; Repetimos hasta encontrar el 255

Mapeado diferencial con agrupación mixta

Finalmente, cabe la posibilidad de que en una misma pantalla nos encontremos “filas” de tiles y “columnas” de
tiles que convenga codificar con un método u otro.

Por ejemplo, cabe la posibilidad de que en 3 tiles horizontales consecutivos nos encontremos con el que el de el
medio forme parte de una fila vertical de tiles de gran tamaño, por lo que nos interesará codificar ese tile como
scanline vertical antes que los 3 como horizontal.

El formato de salida que vamos a utilizar en las pantallas será el siguiente:


El codificador debe codificar todos los scanlines horizontales de tiles acabados en el identificador de fin de
scanline 255 menos el último, el cual acabará con un valor 254. A continuación almacenará todos los scanlines
verticales de tiles acabados en 255. Un valor 255 final indicará el fin de pantalla.

El valor 254 permite a la rutina de impresión saber cuándo hemos acabado de imprimir los scanlines
horizontales y le indica que debe interpretar todos los scanlines restantes como verticales. De esta forma nos
ahorramos el tener que incluir un byte con el tipo de codificación precediendo a cada scanline, y ahorrando así
1 byte adicional por “agrupación”.

Como desventaja, nuestro tileset sólo puede contener ahora 254 tiles (0-253).

Nuestro script de conversión en Python, con el flag “-m”, calcula todas las posibilidades de codificación de
cada tile y los procesa en orden de mayor a menor cantidad de tiles agrupados para codificar toda la pantalla en
un formato mixto donde cada ristra de datos de la pantalla tendría el siguiente aspecto:

Pantalla:
DB coordenada_x_primer_tile_horiz, coordenada_y_primer_tile_horiz,
DB tile1, tile2, (...), tileN, 255
DB coordenada_x_primer_tile_horiz, coordenada_y_primer_tile_horiz,
DB tile1, tile2, (...), tileN, 254
DB coordenada_x_primer_tile_vert, coordenada_y_primer_tile_vert,
DB tile1, tile2, (...), tileN, 255
DB coordenada_x_primer_tile_vert, coordenada_y_primer_tile_vert,
DB tile1, tile2, (...), tileN, 255
DB 255

; Donde:
; 255 como coordenada_x = fin de pantalla.
; 255 a final de scanline = byte de fin de "scanline"
; 254 a final de scanline = cambio de codificacion de horizontal a vertical

Veamos la compresión de la pantalla de Sokoban que hemos venido utilizando como ejemplo:

$ ./codificar_pantalla.py -m pantalla.dat
; Flag codificacion: -m
; Resultado: 72 Bytes
DB 6 , 1 , 2 , 3 , 1 , 4 , 255 , 6 , 8 , 4 , 7 , 7
DB 7 , 255 , 6 , 9 , 5 , 2 , 3 , 2 , 255 , 5 , 2 , 2
DB 3 , 255 , 6 , 3 , 6 , 6 , 255 , 5 , 6 , 2 , 3 , 255
DB 9 , 2 , 5 , 255 , 7 , 4 , 6 , 255 , 9 , 6 , 2 , 255
DB 6 , 7 , 1 , 254 , 10 , 2 , 4 , 5 , 4 , 5 , 3 , 4
DB 5 , 3 , 255 , 4 , 2 , 1 , 4 , 5 , 4 , 5 , 255 , 255

Hemos reducido el tamaño de la pantalla codificada de 192 bytes a 72 bytes (6 bytes menos que la mejor de las
anteriores codificaciones, la vertical).

Aunque 6 bytes pueda parecer un valor insignificante, 100 pantallas de juego con el mismo ahorro supone una
reducción de 600 bytes lo que permitiría añadir 8 pantallas de juego más, o tal vez más código, sonido o
gráficos.

Esta rutina producirá generalmente codificaciones mejores que las únicamente horizontales o verticales en
pantallas con “formas” variadas. Lo ideal es codificar cada pantalla con el tipo de codificación que mejores
resultados obtenga y utilizar una rutina de impresión que llame a una de las 3 rutinas (horizontal, vertical o
mixta) según cómo se haya codificado la pantalla.

La rutina de impresión de este tipo de codificación es una fusión entre las 2 rutinas anteriores, donde usaremos
en esta ocasión IX para acceder al mapeado y HL y DE apuntarán a las coordenadas X e Y para los 2 posibles
bucles de impresión que se utilizarán en función de si estamos imprimiendo scanlines horizontales o verticales.
Concretamente, tomamos como base la rutina de impresión de scanlines horizontales y utilizamos un sencillo
truco para convertirla en la rutina de impresión de scanlines verticales. Lo haremos mediante código
automodificable (self-modifying code).

Primero, cargamos en HL la dirección de la variable COORD_X y en DE la dirección de la variable


COORD_Y:

LD DE, DS_COORD_Y ; DE apunta a la coordenada Y


LD HL, DS_COORD_X ; HL apunta a la coordenada X

Despues, incluímos 2 instrucciones NOP (de 1 byte, con opcode 0), antes y después de los INC (HL) que
producen el incremento de la coordenada apuntada por HL (COORD_X). Además, establecemos 2 etiquetas del
programa ensamblador en las posiciones de los 2 NOPs para poder referenciar la dirección de memoria donde
se han ensamblado estos NOPs en el código:

drawm16cm_dir1:
NOP
INC (HL)
INC (HL) ; COORD = COORD + 2
drawm16cm_dir2:
NOP

Cuando entramos por primera vez en la rutina, y comenzamos a procesar “scanlines”, sabemos que son todos
horizontales hasta que encontremos el valor “254” (cambio de horizontales a verticales), por lo que nuestra
rutina de impresión de tiles en bucle horizontal funciona adecuadamente: se ejecuta un NOP (que no tiene
ningún efecto salvo el consumo de 4 ciclos de reloj), después los 2 INC (HL) y luego otro NOP, lo que produce
el incremento de COORD_X en 2 unidades (HL apunta a COORD_X).

Cuando la rutina encuentra un valor 254 como “fin de scanline” debe de cambiar al modo de impresión vertical,
por lo que dentro del bucle de procesado del scanline añadimos el siguiente código:

drawm16cm_tileloop:

(...)

CP 254
JR Z, drawm16cm_switch ; Codigo 254 -> cambiar a codif. vertical

(...)

drawm16cm_switch:
;;; Cambio de codificacion de horizontal a vertical:
LD A, $EB ; Opcode de EX DE, HL
LD (drawm16cm_dir1), A ; Lo escribimos sobre los NOPs
LD (drawm16cm_dir2), A
JR drawm16cm_read

Es decir, cuando se encuentra un valor 254 como fin de scanline saltamos a drawm16cm_switch, la cual escribe
un valor $EB (EX DE, HL) en las posiciones de memoria donde antes había un NOP, cambiando la porción de
código que habíamos visto antes por:

drawm16cm_dir1:
EX DE, HL
INC (HL)
INC (HL) ; COORD = COORD + 2
drawm16cm_dir2:
EX DE, HL

Esto provoca que, a partir de haber encontrado el 254 y hasta que finalice la rutina (código 255 de fin de
pantalla), los INC (HL) incrementen COORD_Y (debido al EX DE, HL) en lugar de COORD_X, convirtiendo
la rutina en un sistema de impresión de scanlines horizontales.
Cuando salimos de la rutina, esta se queda con los valores de “EX DE, HL” en memoria, por lo que la siguiente
vez que sea llamada tenemos que asegurarnos de que vuelven a estar los NOPs en su lugar, porque las pantallas
siempre empiezan por scanlines horizontales. Para lograr esto, nuestra rutina debe empezar por la colocación
del “NOP” en las direcciones apuntadas por las etiquetas:

DrawMap_16x16_Cod_Mixta:

XOR A ; Opcode de "NOP"


LD (drawm16cm_dir1), A ; Almacenar en la posicion de las labels
LD (drawm16cm_dir2), A

Veamos el código completo de la rutina de impresión mixta:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Mixta:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes) Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes) Direccion de la tabla de atributos.
; DM_MAP (2 bytes) Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte) Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte) Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH (1 byte) Ancho del mapa en tiles
; DM_HEIGHT (1 byte) Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Mixta:

XOR A ; Opcode de "NOP"


LD (drawm16cm_dir1), A ; Almacenar en la posicion de labels
LD (drawm16cm_dir2), A

LD HL, (DM_SPRITES)
LD (DS_SPRITES), HL ; Establecer tileset (graficos)
LD HL, (DM_ATTRIBS)
LD (DS_ATTRIBS), HL ; Establecer tileset (atributos)
LD BC, (DM_COORD_X) ; B = Y_INICIO, C = X_INICIO
LD IX, (DM_MAP) ; DE apunta al mapa

LD DE, DS_COORD_Y ; DE apunta a la coordenada Y


LD HL, DS_COORD_X ; HL apunta a la coordenada X

drawm16cm_read:
LD A, (IX+0) ; Leemos el valor de COORD_X_TILE
INC IX ; Apuntamos al siguiente byte del mapa

drawm16cm_loop:
CP 255 ; Bloque especial fin de pantalla
RET Z ; En ese caso, salir

ADD A, C ; A = (X_INICIO + COORD_X_TILE)


RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (HL), A ; Establecemos COORD_X a imprimir tile

LD A, (IX+0) ; Leemos el valor de COORD_Y_TILE


INC IX
ADD A, B ; A = (Y_INICIO + COORD_Y_TILE)
RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (DE), A ; Establecemos COORD_Y a imprimir tile

;;; Bucle impresion vertical de los N tiles del scanline


drawm16cm_tileloop:
LD A, (IX+0) ; Leemos el valor del TILE de pantalla
INC IX ; Incrementamos puntero
CP 255
JR Z, drawm16cm_read ; Si es fin de tile codificado, fin bucle

CP 254
JR Z, drawm16cm_switch ; Codigo 254 -> cambiar a codif. vertical

LD (DS_NUMSPR), A ; Establecemos el TILE


EXX ; Preservar todos los registros en shadows
CALL DrawSprite_16x16_LD ; Imprimir el tile con los parametros
EXX ; Recuperar valores de los registros

drawm16cm_dir1: ; Etiqueta con la direccion del NOP


NOP ; NOP->INC COORD_X, EX DE,HL->INC COORD_Y

INC (HL)
INC (HL) ; COORD = COORD + 2

drawm16cm_dir2: ; Etiqueta con la direccion del NOP


NOP ; NOP->INC COORD_X, EX DE,HL->INC COORD_Y

JR drawm16cm_tileloop ; Repetimos hasta encontrar el 255

drawm16cm_switch:
;;; Cambio de codificacion de horizontal a vertical:
LD A, $EB ; Opcode de EX DE, HL
LD (drawm16cm_dir1), A ; Ahora se hace el EX DE, HL y por lo tanto
LD (drawm16cm_dir2), A ; INC (HL) incrementa COORD_Y en vez de X
JR drawm16cm_read ; Volvemos al bucle de lectura

Con el sistema de “automodificación de código” nos ahorramos el disponer de 2 porciones de código para
realizar una misma tarea en la que cambia algún salto o alguna instrucción específica.

Esta técnica sirve para gran cantidad de optimizaciones en rutinas de este tipo: podemos por ejemplo en algunas
rutinas modificar el código en memoria para evitar saltos (reemplazar la instruccion de comparacion o de salto
por instrucciones que no lo provoquen o por NOPs) o evitar la duplicación de código.

Codificar X e Y en un único byte

Veamos una modificación del script/programa de codificación y de la rutinas de impresión que nos van a
resultar realmente útiles siempre que nuestro mapa no esté formado por tiles de 8×8 píxeles.

Si el tamaño en tiles de nuestra pantalla de juego es menor que 16×16 bloques podemos codificar las
coordenadas X e Y en un mismo byte.

Al ser el mapa de tamaño menor que 16×16, ambas coordenadas pueden ir en el rango 0 a 15, por lo que cada
una de las 2 coordenadas puede ser codificada en 4 bits. De esta forma, nuestro script codificador puede
componer un byte de posición con la coordenada X en el nibble alto de un byte y la coordenada Y en el nibble
bajo del mismo (o a la inversa).

De esta forma ahorramos 1 byte por cada scanline codificado, un ahorro que puede ser bastante significativo.

La modificación realizada en nuestro script codificador ha sido el cambiar en todos los métodos de codificación
las 2 líneas siguientes:

mapa_codificado.append( X )
mapa_codificado.append( Y )

por:

mapa_codificado.append( (X * 16) + Y )
Aunque hemos dicho que el tamaño máximo del mapa será de 16×16, la realidad es que puede ser como
máximo de 16×14, ya que si ambas coordenadas X e Y valen 15 ($F), el byte resultante compuesto sería $FF
que es el código de final de pantalla. De la misma forma, si X vale 15 e Y vale 14, el valor resultante, $FE,
sería confundido por la rutina mixta con el código especial de cambio de scanlines horizontales a verticales.

Así, con un mapa de 16×14, el máximo tamaño de pantalla que podemos ocupar según las dimensiones de cada
tile serían:

Tamaño de mapa Tamaño de tile Ancho de pantalla Alto de pantalla


16×14 8×8 128 píxeles 112 píxeles
16×12 16×16 256 píxeles 192 píxeles
8×6 32×32 256 píxeles 192 píxeles

Las dimensiones que podemos ver en la tabla hacen esta optimización inusable para juegos con tiles de 8×8
pixeles.

Por otra parte, recordemos que debemos modificar la rutina para que separe los bytes de COORD_X y
COORD_Y en 2 valores diferentes, lo que supone un pequeño tiempo adicional de procesado por cada scanline.

Nuestra pantalla de pruebas es de 16×12, por lo que podemos perfectamente codificarla con esta técnica,
utilizando el flag -a, además del tipo de codificación:

$ ./codificar_pantalla.py -m -a pantalla.dat
; Flag codificacion: -m -a
; Resultado: 60 Bytes
DB 97 , 2 , 3 , 1 , 4 , 255 , 104 , 4 , 7 , 7 , 7 , 255
DB 105 , 5 , 2 , 3 , 2 , 255 , 82 , 2 , 3 , 255 , 99 , 6
DB 6 , 255 , 86 , 2 , 3 , 255 , 146 , 5 , 255 , 116 , 6 , 255
DB 150 , 2 , 255 , 103 , 1 , 254 , 162 , 4 , 5 , 4 , 5 , 3
DB 4 , 5 , 3 , 255 , 66 , 1 , 4 , 5 , 4 , 5 , 255 , 255

En esta ocasión hemos obtenido la pantalla codificada con un total de 60 bytes, ya que nos hemos ahorrado 1
byte por cada scanline codificado.

Podemos modificar cualquiera de las rutinas de impresión que hemos visto para adaptarlas al uso de
coordenadas X e Y en un mismo byte, simplemente cambiando el código que recoge desde el mapa ambas
coordenadas.

Cambiamos:

;;; (venimos del LD A, (IX+0) / INC IX de la coordenada X)

;;; Sumamos la coordenada X recogida y obtenemos desde el mapa la Y:


ADD A, C ; A = (X_INICIO + COORD_X_TILE)
RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (HL), A ; Establecemos COORD_X a imprimir tile

LD A, (IX+0) ; Leemos el valor de COORD_Y_TILE


INC IX
ADD A, B ; A = (Y_INICIO + COORD_Y_TILE)
RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (DE), A ; Establecemos COORD_Y a imprimir tile

por:

PUSH AF
AND %11110000 ; Nos quedamos con la parte alta (COORD_X)
RRCA ; Pasamos parte alta a parte baja
RRCA ; con 4 desplazamientos
RRCA
RRCA ; Ya podemos sumar:
ADD A, C ; A = (X_INICIO + COORD_X_TILE)
RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (HL), A ; Establecemos COORD_X a imprimir tile

POP AF
AND %00001111 ; Nos quedamos con la parte baja (COORD_Y)

ADD A, B ; A = (Y_INICIO + COORD_Y_TILE)


RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (DE), A ; Establecemos COORD_Y a imprimir tile

De la rutina original hemos eliminado las instrucciones “LD A, (IX+0)” e “INC IX” (29 ciclos de reloj menos)
y hemos añadido PUSH/POP AF e instrucciones AND y RLCA (51 ciclos de reloj más) resultando en una
rutina que es 22 ciclos de reloj más lenta por scanline. Pero a cambio de estos 22 ciclos de reloj se pueden
producir grandes ahorros en las pantallas resultantes.

La rutina de impresión de 16×16 Mixta con coordenadas X e Y codificadas en un mismo byte quedaría como el
código que sigue:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Mixta_XY: (X e Y codificados en mismo byte)
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion Parametro
; --------------------------------------------------------------
; DM_* = Variables de MAPA
; DS_* = Variables de SPRITE
;---------------------------------------------------------------
DrawMap_16x16_Cod_Mixta_XY:

XOR A ; Opcode de "NOP"


LD (drawm16cmxy_dir1), A ; Almacenar en la posicion de las labels
LD (drawm16cmxy_dir2), A
LD HL, (DM_SPRITES)
LD (DS_SPRITES), HL ; Establecer tileset (graficos)
LD HL, (DM_ATTRIBS)
LD (DS_ATTRIBS), HL ; Establecer tileset (atributos)
LD BC, (DM_COORD_X) ; B = Y_INICIO, C = X_INICIO
LD IX, (DM_MAP) ; DE apunta al mapa

LD DE, DS_COORD_Y ; DE apunta a la coordenada Y


LD HL, DS_COORD_X ; HL apunta a la coordenada X

drawm16cmxy_read:
LD A, (IX+0) ; Leemos el valor de COORDENADAS
INC IX ; Apuntamos al siguiente byte del mapa

drawm16cmxy_loop:
CP 255 ; Bloque especial fin de pantalla
RET Z ; En ese caso, salir

PUSH AF ; Extraccion de coordenadas XY en X e Y


AND %11110000 ; Nos quedamos con la parte alta (COORD_X)
RRCA ; Pasamos parte alta a parte baja
RRCA ; con 4 desplazamientos
RRCA
RRCA ; Ya podemos sumar
ADD A, C ; A = (X_INICIO + COORD_X_TILE)
RLCA ; A = (X_INICIO + COORD_X_TILE) * 2
LD (HL), A ; Establecemos COORD_X a imprimir tile
POP AF
AND %00001111 ; Nos quedamos con la parte baja (COORD_Y)
ADD A, B ; A = (Y_INICIO + COORD_Y_TILE)
RLCA ; A = (Y_INICIO + COORD_Y_TILE)
LD (DE), A ; Establecemos COORD_Y a imprimir tile
;;; Bucle impresion vertical de los N tiles del scanline
drawm16cmxy_tileloop:
LD A, (IX+0) ; Leemos el valor del TILE de pantalla
INC IX ; Incrementamos puntero

CP 255
JR Z, drawm16cmxy_read ; Si es fin de tile codificado, fin bucle
CP 254
JR Z, drawm16cmxy_switch ; Codigo 254 -> cambiar a codif. vertical

LD (DS_NUMSPR), A ; Establecemos el TILE


EXX ; Preservar todos los registros en shadows
CALL DrawSprite_16x16_LD ; Imprimir el tile con los parametros
EXX ; Recuperar valores de los registros

drawm16cmxy_dir1: ; Etiqueta con la direccion del NOP


NOP ; NOP->INC COORD_X, EX DE,HL->INC COORD_Y
INC (HL)
INC (HL) ; COORD = COORD + 2
drawm16cmxy_dir2: ; Etiqueta con la direccion del NOP
NOP ; NOP->INC COORD_X, EX DE,HL->INC COORD_Y
JR drawm16cmxy_tileloop ; Repetimos hasta encontrar el 255

drawm16cmxy_switch:
;;; Cambio de codificacion de horizontal a vertical:
LD A, $EB ; Opcode de EX DE, HL
LD (drawm16cmxy_dir1), A ; Ahora se hace el EX DE, HL y por lo tanto
LD (drawm16cmxy_dir2), A ; INC (HL) incrementa COORD_Y en vez de X
JR drawm16cmxy_read

Codificar 2 tiles en un mismo byte

Si tenemos menos de 16 tiles podemos codificar 2 tiles en un mismo byte, suponiendo un ahorro de memoria de
un 50%. Esto limita mucho la riqueza gráfica del juego resultante a menos que dispongamos de diferentes
tilesets gráficos y que cada pantalla pueda tener asociado un set diferente, lo que nos limitaría de forma efectiva
a 16 tiles diferentes por pantalla.

La estructura de pantalla (o la de mapa) debería contener un “identificador de tileset” con el que referenciar al
conjunto de tiles que se usará para imprimirla.

Mapeado diferencial con diferentes codificaciones

Como para cada pantalla puede ser más apropiado un tipo de codificación que otro, podemos codificar cada una
de ellas con el método que resulte más adecuado y alterar nuestro el mapa o la propia pantalla para que
almacene también la información de tipo de codificación.

La forma más sencilla es que el primer byte de la pantalla contenga el tipo de codificación utilizado.

Una rutina “wrapper” (o “envoltorio”) de impresión de pantalla obtendría del mapa la dirección de la pantalla
en HL o DE, leería el tipo de codificación utilizado (primer byte de la pantalla), incrementaría HL o DE,
almacenaría el valor resultante en (DM_MAP) y llamaría a la función de impresión adecuada según el tipo de
codificación que acabamos de leer.

Como ID de codificación se podría utilizar, por ejemplo:

MAP_CODIF_NONE EQU 0
MAP_CODIF_HORIZ EQU 1
MAP_CODIF_VERT EQU 2
MAP_CODIF_MIXTA EQU 3
MAP_CODIF_BASICA EQU 4

Es difícil que la codificación básica sea más óptima que ninguna de las anteriores a menos que apenas haya
bloques “transparentes” o “vacíos”, pero aún así se ha contemplado su uso en la rutina. Hemos incluído también
la posibilidad de utilizar una pantalla sin agrupación indicando MAP_CODIF_NONE al inicio de la misma.

;---------------------------------------------------------------
; Llama a la rutina Draw_Map adecuada segun el tipo de
; codificacion de la pantalla apuntada en (DM_MAP).
;---------------------------------------------------------------
DrawMap_16x16_Codificada:
LD HL, (DM_MAP) ; HL apunta al mapa
LD A, (HL) ; Leemos tipo de codificacion
INC HL ; Incrementamos puntero (a datos)
LD (DM_MAP), HL ; Guardamos el valor en DM_MAP

AND A ; Es A == 0? (MAP_CODIF_NONE)
JR NZ, dm16c_nocero ; No
CALL DrawMap_16x16
RET

dm16c_nocero:
CP MAP_CODIF_HORIZ ; Es A == MAP_CODIF_HORIZ?
JR NZ, dm16c_nohoriz ; No, saltar
CALL DrawMap_16x16_Cod_Horiz
RET

dm16c_nohoriz:
CP MAP_CODIF_VERT ; Es A == MAP_CODIF_VERT?
JR NZ, dm16c_novert ; No, saltar
CALL DrawMap_16x16_Cod_Vert
RET

dm16c_novert:
CP MAP_CODIF_MIXTA ; Es A == MAP_CODIF_MIXTA?
JR NZ, dm16c_nomixta ; No, saltar
CALL DrawMap_16x16_Cod_Mixta
RET

dm16c_nomixta: ; Entonces es basica.


CALL DrawMap_16x16_Cod_Basica
RET

(También habríamos podido basar el salto en una tabla de salto, tal y como vimos en el capítulo dedicado a las
Fuentes de texto, sección de Impresión de cadenas con códigos de control).

Quedaría como responsabilidad del programador (o de un sencillo script) el codificar cada pantalla de las
diferentes formas posibles y grabar en el fichero de datos de pantallas final el resultado más óptimo precedido
del byte de tipo de codificación.

En cualquier caso, si detectamos que la técnica de compresión mixta obtiene mejores resultados para la gran
mayoría de las pantallas, podemos optar por codificar todo con el algoritmo mixto (aunque algunas pantallas
pudieran ocupar más con este método que con otro, serían una minoría) y así evitar la inclusión de las otras
rutinas de impresión y la rutina DrawMap_16x16_Codificada que acabamos de ver. Esta será, probablemente,
la mejor de las opciones si el espacio ahorrado por codificar cada pantalla con un sistema diferente es menor
que la inclusión en nuestro programa de las 4 rutinas (básica, horizontal, vertical, mixta) y la rutina wrapper que
acabamos de ver. Además, también ganaremos algo de tiempo en la impresión de las pantallas al saltarnos la
ejecución de la rutina wrapper.
Pantallas con blancos y transparencias

En el caso de la compresión básica no conseguíamos mejoras de tamaño en las pantallas si existían bloques
transparentes (255) además de bloques vacíos (0), ya que nosotros sólo podíamos considerar a nivel de
codificación uno de los 2 como “bloque no dibujable”.

Lo bueno de las técnicas de codificación por scanlines horizontales, verticales o mixtos es que los bloques
vacíos o los transparentes (según nos interese en el juego) se pueden codificar junto a los bloques normales,
modificando las variables IGNORE_VALUES y BLANK del script de codificación en python.

Concretamente, tenemos que establecer ambas variables con el identificador de tile que queremos “ignorar” (y
por lo tanto no codificar).

Aunque esto pueda implicar algunos bytes extra en la pantalla resultante, nos permite volver a tener
transparencia en nuestro juego. En algunos casos, los “bloques vacíos” permiten unir 2 o más de los antiguos
scanlines (antes separados por los blancos) con lo que se incrementa el ratio de compresión todavía más.

Por ejemplo, codificando la primera pantalla de Sokoban con sus transparencias obtenemos los siguientes
valores para, por ejemplo, la codificación horizontal:

$ grep -E "^(IGNORE_VALUES|BLANK)" codificar_pantalla.py


IGNORE_VALUES = 255
BLANK = 255

$ ./codificar_pantalla.py -m -a pantalla2.dat
; Flag codificacion: -m -a
; Resultado: 69 Bytes
DB 97 , 113 , 129 , 145 , 162 , 66 , 254 , 2 , 3 , 6 , 0 , 0
DB 3 , 1 , 4 , 5 , 255 , 3 , 0 , 6 , 6 , 0 , 0 , 0
DB 7 , 2 , 255 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 7 , 3
DB 255 , 4 , 5 , 0 , 0 , 0 , 2 , 0 , 7 , 2 , 255 , 4
DB 5 , 4 , 5 , 3 , 4 , 5 , 3 , 255 , 1 , 4 , 5 , 4
DB 5 , 255 , 2 , 0 , 0 , 0 , 2 , 255 , 255

En este caso, los bloques “0” (fondo negro) se han codificado junto a los demás bloques normales (mejorando
la “compresión”) y los bloques 255 (transparentes) no, por lo que si en los ejemplos anteriores eliminamos el
ClearScreen() y lanzamos el DrawMap correspondiente, obtenemos la siguiente pantalla:

No hay colisión entre el 255 “transparente” y el 255 “fin de scanline” porque el codificador lo que ha hecho es,
precisamente, no codificar los bloques 255 con lo que estos no aparecen en la pantalla resultante. Por contra, sí
que es necesario incluir los bloques 0 ya que pretendemos que sean dibujados para “tapar” el fondo (por eso se
ha cambiando BLANK e IGNORE_VALUES en el script codificador, cambiando 0 por 255).
Efectos sobre los tiles adyacentes al fondo

Otro efecto interesante para mejorar la riqueza gráfica de un juego es generar una “sombra” para aquellos tiles
que estén cercanos a otros tiles definidos como “fondo”.

Por ejemplo, la siguiente captura de pantalla de un juego de los Mojon Twins muestra cómo los muros
verticales rojos cercanos a la “bellota” proyectan hacia la derecha, sobre el suelo “morado”, una sombra
generada durante el proceso de trazado de la pantalla. Esta sombra hace que los tiles de suelo cercanos al muro
sean diferentes del resto de tiles de suelo, sin necesidad de haber definido un tile específico para ello.

Podemos utilizar esta técnica en caso de necesitar ahorrar memoria, aunque lo más rápido en términos de
trazado sería el disponer de tiles específicos “con sombras” y que el mapa los tenga definidos en las posiciones
necesarias.

El propio na_th_an nos comenta cómo utilizan esta técnica en los juego de su Colección Pretujao:

El sombreado se hace en tiempo real. Como sombreamos hacia abajo y hacia


la derecha, a la hora de pintar un tile de fondo se hace así:

Considérese un tile de 16x16 formado por 4 carácteres:

1 2
3 4

Los carácteres 1, 2, 3, y 4 están en el tileset. Además, tenemos unos


carácteres alternativos sombreados que llamaremos 1', 2' y 3' y 4' (aunque
este no se usa, lo dejamos por temas de velocidad).

Siendo (x, y) la posición de nuestro tile de fondo (a nivel de tiles):

- Pintamos el carácter 1 si el tile en (x-1, y-1) es fondo o el tile 1' si es obstáculo.


- Pintamos el carácter 2 si el tile en (x, y-1) es fondo o el tile 2' si es obstáculo.
- Pintamos el carácter 3 si el tile en (x-1, y) es fondo o el tile 3' si es obstáculo.
- Pintamos el carácter 4.

Eso cubre todas las combinaciones y es realmente rápido.

Resumen de resultados de codificaciones empleadas


Veamos una tabla resumen de los resultados de codificar nuestra pantalla de ejemplo (el nivel 1 de Sokoban)
con diferentes técnicas. Inclúimos además una estimación de cuánto ocuparían 100 pantallas de juego y cuántas
pantallas cabrían en 16KB de memoria asumiendo que, de media, todas ocuparan tras su codificación tamaños
parecidos del primer nivel:

Codificación Tamaño (bytes) Ocupación 100 pantallas Pantallas en 16KB


Datos en crudo 192 18.7 KB 85 pantallas
Codificación básica 106 10.3 KB 154 pantallas
Scanlines horizontales 87 8.4 KB 188 pantallas
Scanlines verticales 78 7.6 KB 210 pantallas
Scanlines mixtos 72 7 KB 227 pantallas
Scanlines mixtos + XY agrupados 60 5.8 KB 273 pantallas

Estos datos son una estimación muy general porque no todas las pantallas de Sokoban ocuparán lo mismo una
vez codificadas, pero si utilizamos la técnica de codificación más adecuada a cada pantalla podemos acercarnos
mucho a esas 273 pantallas totales en 16 KB de memoria, muy alejadas de las 85 que caben con los datos “en
crudo”.

Posibles mejoras en el codificador y las rutinas de impresión

Detección de situaciones especiales de codificación mixta

Una posible mejora sería la de modificar el script de codificación mixta para que detecte ciertas situaciones en
la que no es óptimo actualmente. El algoritmo que se ha aplicado en el codificador es un algoritmo genérico
basado en buscar la mayor cantidad de tiles consecutivos para una posterior codificación horizontal o vertical
por orden de cantidad de tiles. Este algoritmo no detecta determinadas situaciones donde la mejor codificación
no es la que más cantidad de tiles consecutivos consigue.

Por ejemplo, supongamos esta situación:

0001234000
0000010000
0000010000
0000010000
0000010000
0001234000

Tenemos dos filas horizontales de 4 bloques y una columna vertical de 5. El algoritmo utilizado determinaría
que 5 es mayor que 4 por lo que codificaría primero la columna vertical, quedando para el siguiente ciclo de
codificación esta pantalla:

0001204000
0000000000
0000000000
0000000000
0000000000
0001204000

Es decir, para codificar las 3 líneas originales hemos generado 1 ristra de bytes verticales, y ahora 4
horizontales, con sus bytes de X, Y y FIN DE CODIFICACION (255) para los 5 scanlines.
Sin embargo, hubiera sido mejor codificar primero los scanlines horizontales, aunque fueran de menor longitud
de que el vertical:

0001234000 --> 0000000000


0000010000 --> 0000010000
0000010000 --> 0000010000
0000010000 --> 0000010000
0000010000 --> 0000010000
0001234000 --> 0000000000

De esta forma codificamos las 3 líneas en 3 “ristras” (2 horizontales de 4 bloques y una vertical de 4 bloques),
utilizando 3*3 = 9 bytes de codificación de coordenadas y fin de scanline en lugar de los 15 del ejemplo
anterior.

Estos son los resultados actuales que obtiene el codificador para la anterior pantalla:

$ ./codificar_pantalla.py -h pantalla3.dat
; Flag codificacion: -h
; Resultado: 31 Bytes
DB 3 , 0 , 1 , 2 , 3 , 4 , 255 , 5 , 1 , 1 , 255 , 5
DB 2 , 1 , 255 , 5 , 3 , 1 , 255 , 5 , 4 , 1 , 255 , 3
DB 5 , 1 , 2 , 3 , 4 , 255 , 255

$ ./codificar_pantalla.py -v pantalla3.dat
; Flag codificacion: -v
; Resultado: 34 Bytes
DB 3 , 0 , 1 , 255 , 4 , 0 , 2 , 255 , 5 , 0 , 3 , 1
DB 1 , 1 , 1 , 3 , 255 , 6 , 0 , 4 , 255 , 3 , 5 , 1
DB 255 , 4 , 5 , 2 , 255 , 6 , 5 , 4 , 255 , 255

$ ./codificar_pantalla.py -m pantalla3.dat
; Flag codificacion: -m
; Resultado: 28 Bytes
DB 3 , 0 , 1 , 2 , 255 , 3 , 5 , 1 , 2 , 255 , 6 , 0
DB 4 , 255 , 6 , 5 , 4 , 254 , 5 , 0 , 3 , 1 , 1 , 1
DB 1 , 3 , 255 , 255

Y este es el coste que tendría la codificación con la detección de la situación que hemos comentado:

; Codificacion mixta con deteccion de situaciones T e I:


; Resultado: 22 Bytes
DB 3 , 0 , 1 , 2 , 3 , 4 , 255 , 3 , 5 , 1 , 2 , 3 , 4, 254
DB 5 , 1 , 1 , 1 , 1 , 1, 255 , 255

Estaríamos ganando unos pocos bytes adicionales sin cambios en nuestro programa: tan sólo mejorando el
codificador.

Agrupar 2 scanlines separados por 2 o menos huecos

Finalmente, podríamos modificar el script codificador para que detecte las situaciones en que tengamos 2
scanlines (horizontales o verticales) separados por 1 ó 2 bloques “vacíos” (transparentes).

00011101110

Codificar la anterior secuencia produciría 2 scanlines con sus bytes de COORD_X, COORD_Y y
FIN_SCANLINE por cada fila de tiles, con un total de 6 bytes de “datos de posicionamiento”.

; 12 bytes
DB 3, 0, 1, 1, 1, 255, 8, 0, 1, 1, 1, 255
Si codificamos el anterior scanline usando un código especial (ej: 252) como “tile transparente” a ser ignorado
por la rutina de impresión, nos ahorramos los bytes de posicionamiento del segundo scanline:

; 10 bytes
DB 3, 0, 1, 1, 1, 252, 1, 1, 1, 255

Si la separación entre 2 scanlines la forma 1 bloque “transparente” , añadimos 1 byte “252” pero eliminamos el
“255” del primer scanline y el COORD_X y COORD_Y del segundo, por lo que sumamos 1 bytes pero
restamos 3, ahorrando 2 bytes.

Si la separación entre 2 scanlines la forman 2 bloques “transparentes” , añadimos 2 bytes “252” pero
eliminamos el “255” del primer scanline y el COORD_X y COORD_Y del segundo, por lo que sumamos 2
bytes pero restamos 3, ahorrando 1 byte.

Para más de 3 bloques transparentes no se produce ningún ahorro, sino todo lo contrario.

La pega de esta técnica es que a cambio de 1 ó 2 bytes de ahorro en estas situaciones pasamos a poder utilizar
252 tiles (0-251) y se requiere un pequeño tiempo de procesado extra en la rutina para detectar y saltar los tiles
“252”.

Mapeados de diferentes tilesets

Cabe también la posibilidad de codificar las pantallas con datos de diferentes tilesets gráficos.

En ese caso, podemos añadir a todas las posibilidades vistas hasta ahora un “identificador de tileset” que
referencie a una tabla con los datos de los diferentes tilesets (datos gráficos, datos de atributo, ancho y alto):

Tabla_IDs_Tilesets:
DW dir_tileset_gfx_1
DW dir_tileset_attrib_1
DB ancho_tiles_tileset1, alto_tiles_tileset1
DW dir_tileset_gfx_2
DW dir_tileset_attrib_2
DB ancho_tiles_tileset2, alto_tiles_tileset2
DW dir_tileset_gfx_3
DW dir_tileset_attrib_3
DB ancho_tiles_tileset3, alto_tiles_tileset3

De esta forma podemos tener pantallas codificadas por codificación básica con tiles de diferentes tamaños:

Pantalla1:
DB ID_TILESET, X, Y, ID_TILE, ID_TILESET, X, Y, ID_TILE
DB ID_TILESET, X, Y, ID_TILE, ID_TILESET, X, Y, ID_TILE
DB (...)
DB 255 (fin de pantalla)

Podemos combinar este tipo de codificación con las técnicas de agrupación que ya hemos visto agrupando
siempre tiles de un mismo tipo.

En este caso las coordenadas X e Y de impresión deben de ser coordenadas de pantalla y no de “mapa” ya que
no todos los tiles tienen el mismo tamaño, por lo que resulta más óptimo que el mapa indica la posición exacta
de pantalla donde va cada tile.

Por otra parte, esta técnica añade 1 byte extra de ocupación a cada tile, por lo que las pantallas ocuparán más
que con tileset único.
La codificación e impresión de este tipo de mapas es mucho más compleja que utilizar un único tileset con tiles
de tamaños idénticos. No obstante, existirán situaciones donde resulta necesario disponer de 2 tilesets o
spritesets diferentes.

Mapeados de tiles de cualquier tamaño de bloque

Además de la posibilidad de disponer de mapeados basados en diferentes tilesets, podemos componer las
pantallas de nuestro juego a partir de todo tipo de gráficos y tiles de tamaños diversos, pertenezcan o no a
tilesets.

Para eso, debemos dejar de pensar en las pantallas como matrices de tiles y visualizarlas y definirlas como
“listas de sprites a dibujar” (sean tiles o no).

Primero debemos definir una tabla que almacene la información de todos los elementos gráficos que pueden
formar parte de una pantalla:

Tabla_Tiles:
DW tileset_1_gfx+(0*32) ; Cada tile ocupa 8*4=32 bytes en el tileset 16x16
DW tileset_1_attr+(0*4) ; Cada tile ocupa 4 atributos en el tileset 16x16
DB 16, 16
DW tileset_1_gfx+(1*32) ; Apuntamos a datos del tile 1 del tileset
DW tileset_1_attr+(1*4)
DB 16, 16
DW tileset_1_gfx+(2*32)
DW tileset_1_attr+(2*4)
DB 16, 16
DW tileset_2_gfx+(0*8) ; Cada tile ocupa 8 bytes en el tileset 8x8
DW tileset_2_attr+(0*1) ; Cada tile ocupa 1 atributo en el tileset 8x8
DB 8, 8
DW tileset_2_gfx+(1*8)
DW tileset_2_attr+(1*0)
DB 8, 8
DW logotipo_gfx
DW logotipo_attr
DB 32, 16
DW piedra_gfx
DW piedra_attr
DB 32, 32
DW muro_grande_gfx
DW muro_grande_attr
DB 64, 64
DW grafico_escalera_gfx
DW grafico_escalera_attr
DB 16, 32

A continuación definimos la pantalla en formato “codificación básica” utilizando identificadores de tile que
serán índices en la anterior tabla:

Pantalla:
DB X, Y, ID_EN_TABLA_TILE, X, Y, ID_EN_TABLA_TILE, (...), 255

La pantalla acabará en un valor 255 y los valores de X e Y deberán ser coordenadas exactas de pantalla. La
rutina de impresión recorrerá cada byte de la pantalla (hasta encontrar el fin de pantalla o 255) y trazará todos
los sprites llamando a la rutina genérica de DrawSprite_MxN.

Nuestra rutina de impresión puede acceder a los datos del tile mediante multiplicación de
ID_EN_TABLA_TILE por 2+2+2 e imprimir el sprite gfx/attr/ancho/alto leído desde la tabla.
Con este sistema se debe de diseñar y codificar manualmente la pantalla, pero nos permite tener una riqueza
gráfica que no siempre se puede conseguir sólo con tiles de tamaños fijos.

Por contra, nos obliga a utilizar la rutina de impresión genérica DrawSprite_MxN, que no es tan rápida como
las rutinas específicas. Para evitar esto, lo que podemos hacer es modificar la rutina genérica para que en caso
de que el ANCHO y el ALTO del sprite coincidan con el de alguna de las rutinas específicas disponibles hagan
la llamada a dicha rutina, y en caso contrario, utilicen el código genérico.

El código a añadir a la rutina genérica podría ser similar al siguiente:

DrawSprite_MxN_LD_extendida:

LD A, (DS_HEIGHT)
LD B, A
LD A, (DS_WIDTH)
LD C, A ; Obtenemos datos del sprite

;;; B = ALTO de Sprite


;;; C = ANCHO de Sprite

LD A, C ; A = ANCHO
CP 16 ; Comparar ancho
JR NZ, dspMN_no16 ; ¿es 16?

SUB B ; A = Ancho - alto


JR NZ, dspMN_generica ; Si no es cero, rutina generica
; Es cero, imprimir 16x16:
CALL DrawSprite16x16 ; Imprimir via especifica 16x16
RET

dspMN_no16:
LD A, C ; Recuperamos ancho
CP 8 ; ¿es 8?
JR NZ, dspMN_generica ; Si no es 8, ni 16, generica
SUB B ; A = Ancho - alto
JR NZ, dspMN_generica ; Si no es cero, rutina generica

CALL DrawSprite_8x8 ; Imprimir via especifica 8x8


RET

dspMN_generica:
;;; (resto rutina generica)
RET

De esta forma, utilizaremos la rutina genérica sólo para los tamaños de sprite de los que no dispongamos rutinas
de impresión específicas.

Tabla sólo de los tiles no estándar

Lo más normal es que el mayor porcentaje de gráficos de la pantalla se tomen desde un tileset y unos pocos
desde bitmaps de diferentes tamaños ajenos a éste. En ese caso nos podemos ahorrar la tabla de direcciones de
tiles si, por ejemplo, definimos estos gráficos especiales a partir de un determinado valor numérico.

Supongamos que tenemos 200 tiles y 20 gráficos de tamaño no estándar que queremos ubicar en nuestros
mapeados. En ese caso utilizamos los valores numéricos del 200 al 220 como identificadores de estos tiles y
construímos la tabla de direcciones sólo con los datos de estos gráficos:

Tiles_Extendidos:
DW logotipo_gfx
DW logotipo_attr
DB 32, 16
DW piedra_gfx
DW piedra_attr
DB 32, 32
DW muro_grande_gfx
DW muro_grande_attr
DB 64, 64
DW grafico_escalera_gfx
DW grafico_escalera_attr
DB 16, 32

Nuestra rutina de impresión de mapeado sería idéntica a las ya vistas con un pequeño cambio: Cuando la rutina
encontrara un identificador de tile menor de 200 (0-199), utilizaría la rutina de impresión basada en tileset, y
cuando encontrara un tile mayor o igual a 200, utilizaría los datos de esta tabla usando como índice el valor
(TILE-200). De esta forma se podría utilizar la rutina de impresión de sprites específica para nuestros tiles y la
genérica sólo para estos tiles “no estándar”. Además nos ahorramos 200*6 bytes (2 de la dirección de los
gráficos, 2 de la dirección de atributos y 2 de ancho y alto) en la tabla de “tiles extendidos”.

Este sistema permite extender el funcionamiento “estándar” basado en un tileset añadiendo la posibilidad de
incluir determinados gráficos “no estándar” en la pantalla, sin complicar las rutinas de impresión y añadiendo
sólo la necesidad de una sencilla tabla que ocuparía 6 bytes de datos por cada “tile no estándar” que queramos
incluir en el mapa del juego.

Fondos personalizados para cada pantalla

Todas las rutinas que hemos visto se basan en la impresión de tiles con contenido y la no impresión de tiles
considerados fondo o transparencia.

En ambos caso, el tile que se utiliza como fondo es un tile único (usualmente un tile sólido con el color del
fondo).

Si nuestro personaje es monocolor y no se va a producir attribute clash es posible que queramos un fondo no
sólido basado en un patrón repetitivo sobre el que nuestro personaje se pueda desplazar (con el mismo color de
tinta y papel que los sprites que se vayan a mover sobre él).

Lo que estabamos haciendo hasta ahora era borrar el área de pantalla donde íbamos a dibujar el tileset, y trazar
sólo aquellos tiles codificados en la pantalla y que eran contenido real (diferente del fondo).

Si incluímos ahora en el tileset uno o varios tiles específicos para el fondo y los codificamos dentro de las
pantallas, entonces dejará de ser posible la compresión ya que todos los tiles de la pantalla tendrían que ser
dibujados (ya no “ignoramos” los bloques de fondo sino que ahora hay que dibujarlos para trazar ese nuevo
fondo gráfico). Con esto, se esfuma nuestro ahorro de memoria.

Podemos utilizar una sencilla solución basada en tabla para disponer de fondos en las pantallas y continuar
ahorrando memoria con la codificación. La tabla alojará la dirección

FONDO_PIEDRA EQU 0
FONDO_BALDOSAS EQU 1
FONDO_HIERBA EQU 2

Fondos:
DW fondo_piedra_gfx
DW fondo_piedra_attr
DB 16, 16
DW fondo_baldosas_gfx
DW fondo_baldosas_attr
DB 64, 64
DW fondo_hierba_gfx
DW fondo_hierba_attr
DB 32, 32

De nuevo, para acceder a los datos de un fondo específico lo haremos mediante:

DIR_DATOS_FONDO = [ Fondos + (ID_FONDO * 6) ]

A continuación, definimos el mapa de forma que además de la dirección de memoria de la pantalla y las
conexiones con otras localidades incluya un identificador dentro de los nuestros fondos:

Mapa:
DW Pantalla_Inicio ; ID = 0
DB -1, 1 ; Conexiones izq y derecha ID 0
DB FONDO_BALDOSAS
DW Pantalla_Salon ; ID = 1
DB 0, 2 ; Conexiones izq y derecha ID 1
DB FONDO_PIEDRA
DW Pantalla_Pasillo ; ID = 2
DB 1, 3 ; Conexiones izq y derecha ID 2
DB FONDO_PIEDRA
(...)

(NOTA: Esto modificará la cantidad por la que hay que multiplicar para acceder a los datos de una pantalla ya
que ahora incluyen 1 byte adicional).

De esta forma, cada pantalla tiene un bitmap de fondo asociado que puede ir desde el tamaño de un simple tile
8×8 hasta 256×192 píxeles si así lo deseamos.

Finalmente, en lugar de realizar un borrado del área de pantalla donde vamos a imprimir el mapa sería
necesario programar una rutina que utilice el sprite Fondo (mediante su ID y la tabla de Fondos). La rutina
deberá “rellenar” vía impresión por repetición el área de juego con el fondo seleccionado. Para simplificar la
rutina es recomendable que todos los tamaños de los tiles de fondo sean dividores exactos del tamaño de la
pantalla, de forma que la rutina no tenga que calcular si la impresión del último tile que quepa en la misma ha
de ser dibujado total o parcialmente. Si el área de juego es de 256 píxeles, podremos utilizar tiles de ancho 8
(32 repeticiones), 16 (16 repeticiones), 32 (8 repeticiones), 64 (4 repeticiones), 128 (2 repeticiones) o incluso de
256 (1 repetición). Lo mismo ocurriría para la altura.

La rutina tendría que utilizar DrawSpriteMxN (ya que los fondos pueden tener cualquier tamaño) pero se
recomienda que emplee rutinas específicas en los tamaños de fondo para los que tengamos rutina de impresión
disponible.

De esta forma, rellenamos el área de juego con el fondo asociado a dicha pantalla y después llamamos a
DrawMap para dibujar sobre este “fondo” los tiles reales.

¿Qué ahorramos con este sistema? El ahorro consiste en que hemos definido el fondo de una pantalla con 1 sólo
byte (el Identificador de Fondo que asociado a cada pantalla en la estructura de mapa) y ya no es necesario
codificar los tiles de fondo en las pantallas. Los tiles del mapa que antes eran tiles con valores numéricos de
fondo ahora serán “vacíos” o “transparentes” con lo que la codificación los ignorará y las pantallas sólo
incluirán los datos de los tiles “reales”. Los tiles “transparentes”, al no dibujarse, permitirán ver en sus
posiciones el fondo predibujado.

Además esto nos permite cambiar el fondo asociado a una pantalla rápidamente (en la estructura de mapa) sin
tener que modificar todos los valores de tiles de fondo en una o más pantallas (y/o recodificar las pantallas). Y
por si fuera poco, nada nos obliga a que el fondo sea un tile de tamaño igual al de los bloques del tileset, lo que
puede dotar de mayor riqueza gráfica a nuestro juego.

Una versión más óptima de este sistema pero que requiere modificar las rutinas de impresión de mapeado
podría ser que la rutina de impresión, cuando encontrara un bloque vacío/transparente, utilizara las coordenadas
X e Y de impresión actuales como un “índice circular” en el gráfico de fondo para dibujar una porción del
mismo con tamaño de un tile y así rellenar el “tile transparente” que estamos considerando. Sería algo parecido
a un rellenado con patrón pero sólo en el espacio de un tile. Esta aproximación evitaría el dibujado de toda la
pantalla con el fondo transparente, incluídas porciones de pantalla sobre los que después dibujaremos tiles
gráficos.

Compresión de los datos de pantalla


Todavía podríamos arañar algunos bytes adicionales a las pantallas utilizando técnicas de compresión.

Compresión por repetición

La primera de las posibilidades de compresión se basa en repetición de tiles consecutivos en las técnicas de
agrupación, utilizando un tipo de compresión que utilice un byte de control “253”, de tal modo que si se
encuentra un valor de tile 253, a éste valor le siga el tile a repetir y el número de repeticiones (o al revés).

DB 253, NUMERO_REPETICIONES, TILE_A_REPETIR

Así, una secuencia:

; 9 bytes
DB 1, 2, 2, 2, 2, 2, 2, 3, 4

Se codificaría como:

; 6 bytes
DB 1, 253, 6, 2, 3, 4

En este caso, la rutina de impresión deberá expandir “253, 6, 2” como “6 veces 2” = 2, 2, 2, 2, 2, 2.

Por desgracia, este tipo de compresión no suele resultar muy efectiva porque requiere que haya un mínimo de 3
tiles consecutivos iguales (lo que permitiría ahorrar el mínimo de 1 byte). En los mapeados con riqueza gráfica,
las filas y columnas están formadas por diferentes tiles alternados y no se suele repetir el mismo tile de forma
consecutiva una y otra vez, ya que evidenciaría la repetición de un mismo gráfico.

En cualquier caso, para aplicar este tipo de técnica tendríamos que modificar el script codificador (en busca de
valores idénticos consecutivos) y la rutina de impresión (en busca del código de control 253 y con un bucle para
repetir N veces el valor del tile comprimido).

Compresión por patrones

El sistema de compresión por patrones es el método que, probablemente, producirá los mejores resultados de
reducción de tamaño de las pantallas en la mayoría de juegos.

Se basa en identificar “conjuntos de tiles” (no tienen por qué tener todos ellos el mismo valor) que se repitan a
lo largo de la pantalla y de otras pantallas. Estos patrones se almacenan en memoria y se referencian por medio
de una tabla que relaciona un “identificador de patrón” con la dirección donde está almacenado el patrón,
finalizado en 255.

Por ejemplo, supongamos un juego de plataformas donde hay gran cantidad de “suelos”, “plataformas en el
aire”, construcciones hechas con bloques, etc, que se repiten entre las diferentes pantallas. Podemos asociar
cada “patrón” con un identificador y después codificar la pantalla utilizando algún código de control y la
referencia al patrón.
Utilizaremos un código de control especial (253, por ejemplo) para codificar una ristra de datos “patrón” en
lugar de tiles reales. Para estos tiles basados en patrones usaremos el siguiente formato (independiente de que la
codificación del resto de tiles sea básica, horizontal, vertical o mixta):

Pantalla:
DB X_TILE, Y_TILE, 253, ID_PATRON

(También podemos agrupar X_TILE e Y_TILE en un mismo byte como hicimos en el script codificador).

Supongamos la siguiente pantalla ilustrativa:

000000000000000878787
001232400000000878787
000000001232400878787
000000000000000878787
012324000000000878787
000000000000000879987
123240012324000879987

Los tiles “1, 2, 3, 2, 4” representan en nuestro “ejemplo” una plataforma de suelo sobre la que el jugador puede
saltar, siendo el tile 1 un “borde izquierdo”, el tile 4 un “borde derecho” y los tiles 2, 3, 2 tiles que representan 3
bloques de suelo.

Los tiles “8, 7, 8, 7, 8, 7” representan bloques de “piedra” que forman parte de un castillo cuyo tile de “puerta”
es el “9”.

En la anterior pantalla encontramos los siguientes patrones:

CODIF_HORIZONTAL EQU 0
CODIF_VERTICAL EQU 1

Patron0:
DB CODIF_HORIZONTAL, 1, 2, 3, 2, 4, 255

Patron1:
DB CODIF_VERTICAL, 8, 8, 8, 8, 8, 8, 8, 255

Patron2:
DB CODIF_VERTICAL, 7, 7, 7, 7, 7, 7, 7, 255

Patron3:
DB CODIF_VERTICAL, 7, 7, 7, 7, 7, 255

Patron4:
DB CODIF_VERTICAL, 8, 8, 8, 8, 8, 255

A continuación creamos una tabla de direcciones de patrones relacionadas con su ID:

Tabla_Patrones:
DW Patron0
DW Patron1
DW Patron2
DW Patron3
DW Patron4
(...)

Mediante esta tabla podemos acceder a los datos de cualquier patrón como Tabla_Patrones +
ID_PATRON*2.

Si llamamos a estos patrones en el orden que los hemos visto, como A, B, C, D y E, obtenemos 1 patrón
horizontal y 4 verticales:
X 11111111112
Y 012345678901234567890
-------------------------
0 | 000000000000000BCEDBC |
1 | 00AAAAA00000000BCEDBC |
2 | 00000000AAAAA00BCEDBC |
3 | 000000000000000BCEDBC |
4 | 0AAAAA000000000BCEDBC |
5 | 000000000000000BC99BC |
6 | AAAAA00AAAAA000BC99BC |
-------------------------

(Nota: se ha numerado la coordenada X y la Y para facilitar la codificación manual que realizaremos a


continuación)

Vamos a codificar cada “patrón” en una línea de DBs diferente para simplificar su lectura. Nótese que los tiles
“9” no forman parte de ningún patrón y se han codificado como 2 scanlines horizontales de tiles:

Pantalla:
DB 2, 1, 253, 0
DB 8, 2, 253, 0
DB 1, 4, 253, 0
DB 0, 6, 253, 0
DB 7, 6, 253, 0
DB 15, 0, 253, 1
DB 19, 0, 253, 1
DB 16, 0, 253, 2
DB 20, 0, 253, 2
DB 17, 0, 253, 3
DB 18, 0, 253, 4
DB 17, 5, 9, 9, 255
DB 17, 6, 9, 9, 255

Nótese que las líneas de definición de patrón no necesitan acabar en 255 porque tienen un tamaño definido (4
bytes) y además el patrón en sí ya acaba en 255.

La pantalla original tenía un total de 67 tiles y se ha codificado con apenas 52 bytes. La codificación con
métodos sin compresión habría sido de 67 bytes más los datos de posicionamiento y fin de scanline porque hay
que almacenar esta información para cada “ristra”, es decir, 3 bytes por cada “scanline” (hay 11), lo que habría
sumado 11*3 = 33 bytes adicionales dando un total de 101 bytes para codificar la pantalla (100 más el byte 254
de cambio de codificación).

Puede que viendo una sóla pantalla no parezca un gran ahorro, pero en el global del mapa de juego cada nueva
pantalla que tenga repetido cualquiera de los patrones que tenemos en Tabla_Patrones permitirá codificarlo con
apenas 4 bytes (X, Y, 253, ID_PATRON), ó 3 bytes si se codifican en el mismo byte la coordenada X y la
coordenada Y.

Y cabe decir que dadas las limitaciones de memoria del Spectrum, los juegos tienen a repetir patrones de tiles
para los diferentes tipos de suelos, techos, paredes, etc:
Patrones codificables: suelo, paredes verticales, etc

Este sistema acaba consiguiendo ratios de compresión muy buenos pero basa toda su técnica en el programa
codificador: el script/programa debe analizar todas las pantallas del mapeado en una pasada (no vale con
analizar sólo la pantalla que estamos codificando).

A partir de esas pantallas debe de crear un diccionario de “patrones” formado por todas las combinaciones de
bloques que aparezcan, así como las mismas combinaciones de menor tamaño, y cuantificar cuántas veces
aparece cada “patrón potencial”. Finalmente, se utilizan las N entradas que producirían más sustituciones para
su uso como patrón. Es un algoritmo muy parecido a la compresión LZW.

Además de modificar la rutina mapeadora para añadirle la impresión de patrones es necesario realizar un
codificador específico más complejo que los que hemos visto en este capítulo.

Finalmente, es importante recordar que no estamos atados a utilizar una única técnica de las que acabamos de
ver: podemos utilizar, por ejemplo, codificación mixta con compresión y patrones, o con patrones pero sin
compresión, etc, mezclando las diferentes técnicas de codificación y de impresión en rutinas específicas a tal
efecto.

Propiedades de los tiles y el mapeado


A la hora de diseñar el mapa del juego tenemos que tener en cuenta las propiedades de cada uno de los tiles del
mapeado. ¿Qué tiles deben de ser sólidos de forma que el personaje no pueda atravesarlos? ¿Qué tiles, pese a
ser dibujados, deben de ser tomados como “parte del fondo” y el personaje puede pasar a través de ellos? ¿Qué
tiles soportan el peso del personaje y cuáles deben “deshacerse” cuando el personaje los pise? ¿Debe un tile
concreto permitir al personaje atravesarlo saltando desde debajo de él pero ser sólido al pisarlo?

Este tipo de características de los tiles se conoce como “propiedades o atributos”, y aunque no son usados por
las rutinas de impresión de mapeados, sí que son utilizados por el bucle principal del programa a la hora de
mover los personajes o enemigos para determinar si se puede atravesar una determinada zona de pantalla, si el
personaje debe morir por pisar un tile concreto, etc.

La primera distinción suele ser marcar qué tiles son “sólidos” y a través de cuáles puede pasar el jugador. En
muchos juegos se utiliza el tile 0 como tile “de fondo” y el resto de tiles como bloques sólidos (Manic Miner,
etc) pero es posible que necesitemos que algunos tiles sean dibujados y sin embargo nuestro personaje pueda
pasar a través de ellos. Todos los tiles serán iguales para la rutina de impresión, pero no lo serán para las rutinas
de gestión de movimiento de nuestro personaje y de los enemigos.
Nuestro personaje tiene que poder atravesar la columna

En ese caso, podemos utilizar un valor numérico como límite entre tiles no sólidos y tiles sólidos. Por ejemplo,
podemos considerar que los primeros tiles 0-99 son atravesables por el jugador y los tiles del 100 en adelante
serán sólidos. La rutina que gestione el movimiento de nuestro personaje deberá obtener del mapa el
identificador de tile y permitirnos pasar a través de él o no según el valor obtenido.

Para ciertos juegos es posible que ni siquiera necesitemos definir propiedades de solidez y que (según el tipo de
juego) baste con que el personaje se pueda mover sobre el color de fondo y que no pueda atravesar cualquier
color distinto de este.

Aparte de la típica clasificación entre bloque sólido y no sólido, puede sernos imprescindible otorgar ciertas
propiedades a los tiles y en ese caso nos resultará necesario disponer de algún tipo de estructura de datos que
almacene esta información. Por ejemplo, otro caso típico de propiedad de tile es el de indicar si un determinado
bloque de pantalla produce o no la muerte del jugador: como pinchos, lava, fuego, etc.

Se pueden definir estos atributos bien en los tiles (un mismo identificador de tile siempre cumple una
determinada propiedad) o bien en los mapas (es la posición de pantalla la que cumple esa propiedad).

En el primero de los casos (propiedades en los tiles), necesitaremos una tabla que relacione cada identificador
de tile con sus propiedades (bien usando un byte por propiedad o un simple bit en el caso de propiedades tipo
sí/no).

;;; Byte de propiedades de cada tile:


;;; Bit 0 -> Tile solido (1) o atravesable por el jugador (0)
;;; Bit 1 -> El tile se "rompe" al pisarlo el jugador (1), o no (0)
;;; Bit 2 -> Si 1, cuando el jugador toca el tile, muere.
;;; Bit 3 -> El tile es de tile "escalera" (permite subir y bajar)
;;; Bit 4 -> El tile es un teletransportador
;;; (etc...)
Propiedades_Tiles:
DB %00000000 ; Propiedades tile 0 (tile vacio)
DB %00000001 ; Propiedades tile 1 (bloque)
DB %00000010 ; Propiedades tile 2 (suelo que se rompe)
DB %00000001 ; Propiedades tile 3 (otro bloque)
DB %00000101 ; Propiedades tile 4 ("pinchos")

En el segundo de los casos (propiedades en un mapa alternativo), mucho más costoso en términos de
memoria, necesitamos una “copia” del mapa pero que en lugar de almacenar tiles almacene las propiedades de
ese punto del mapa. Esto permite que un gráfico determinado tenga un efecto en una zona de la pantalla, y otro
fuera de ella.

Por ejemplo, con un mapa de propiedades podemos conseguir que tiles que son sólidos en un lugar sean
atravesables en otro al estilo de las zonas secretas de juegos como Super Mario World. Podemos simular este
efecto utilizando propiedades de tiles si “duplicamos” el tile gráfico en cuestión y a uno le asignamos la
propiedad de solidez y al otro no. Tendríamos 2 identificadores de tile diferente a la hora de generar las
pantallas con el mismo gráfico, pero diferente comportamiento.

Asignar propiedades a Tiles resulta en general mucho menos costoso en términos de memoria que asignarlas a
posiciones de pantalla.

Objetos y enemigos en el mapeado


Por normal general, los enemigos, personajes y objetos del juego no se definen dentro del mapeado, sino que se
cargan desde estructuras de datos separadas. Estas estructuras pueden incluir por ejemplo la posición X, Y del
objeto, el ID de Pantalla en la que están esos objetos, así como referencias al sprite que lo representan.

Realmente, en los juegos es posible ubicar llaves, items de comida, salud, vidas u otros objetos en el mapeado,
pero esto tiene la desventaja de que su ubicación es la misma para todas las ejecuciones del juego.

Cuando tratemos el capítulo dedicados a estructuras de datos veremos ejemplos de cómo definir estructuras que
almacenen la información de posición, datos gráficos y características de objetos, enemigos, interruptores,
puertas, y otros items del juego.

Diseño y creación del mapeado


Conocemos la teoría y la práctica sobre el diseño de pantallas y mapas en cuanto a los mecanismos de
codificación e impresión, pero a la hora de programar un juego se hace palpable la necesidad de disponer de
una herramienta para diseñar las pantallas de forma visual en lugar de componerlas manualmente mediante los
identificadores de los tiles.

En la mayoría de los casos, lo más rápido puede ser diseñar un sencillo editor en nuestra plataforma de
desarrollo (por ejemplo, usando python y pygame o C++ y SDL) que cargue el tileset y nos permita seleccionar
bloques del mismo y “dibujar” en la pantalla utilizando el actual bloque seleccionado. A este programa le
podemos añadir funciones de grabación y carga de pantallas, además de la imprescindible opción de
“exportación” a formato ASM.

Al diseñar el editor específicamente para nuestras necesidades, podemos agregar no sólo gestión de las
pantallas sino del mapa en sí mismo, de tal modo que el editor nos permita definir las conexiones entre
pantallas y genere la estructura de mapa lista para usar. También podemos agregar algún tipo de gestión de los
atributos de cada tile en pantalla (si es sólido o no, etc). Otra función interesante sería la de permitir modificar
el orden de los tiles en el tileset alterando las pantallas para reflejar ese cambio, lo que haría más sencillo
reubicar tiles o añadir nuevos tiles por debajo de un valor numérico dado.
Si no tenemos el tiempo o los recursos para realizar un programa de estas características, siempre podemos
optar por utilizar alguno de los ya existentes para nuestra plataforma de desarrollo. Es imprescindible asumir
que deberemos programar algún tipo de script/programa de conversión del formato de pantalla utilizado por la
herramienta de mapeados al formato que nosotros deseamos, una ristra de “DBs” incluíble en nuestro
programa.

Alguno de los programas más conocidos para este tipo de tareas son:

• Mappy: http://www.tilemap.co.uk/mappy.php
• Mappy Linux : http://membres.multimania.fr/edorul/Mappy-1.0.tar.gz
• Map Editor: http://www.mapeditor.org/

Map Editor es el editor más moderno de los 3, y soporta exportación del mapa a formato XML lo que puede
facilitar la creación del script de conversión a formato ASM. Además, es una herramienta Free Software, por lo
que disponemos tanto del código fuente como del permiso para modificarlo y adaptarlo a nuestras necesidades.

Mappy es más antiguo y se diseñó para juegos basados en las librerías de PC “Allegro” y (actualmente) “SDL”.
El código fuente está basado en la librería Allegro y puede ser también modificado a nuestro antojo.

Mappy Linux es un pequeño programa que utiliza las bibliotecas de MappySDL para actuar como un editor de
mapeados. Al disponer del código fuente, puede ser directamente adaptado a nuestras necesidades. Como es un
editor realmente sencillo, su código no es tan complejo como pueda serlo el de Map Editor.

No obstante, es probable que resulte más rápido el crear un sencillo programa desde cero que el adaptar el
código de cualquiera de estos programas, sin olvidar que siempre nos queda la posibilidad de diseñar las
pantallas manualmente.

Recordemos por otra parte que nuestro editor de mapeados grabará normalmente la pantalla en formato “crudo”
y que debemos utilizar nuestro programa “codificador” para reducir el tamaño de cada pantalla. Si estamos
creando un programa propio, podemos aprovechar el script codificador llamándolo desde el propio editor para
exportar las pantallas directamente codificadas.
Mapeando sobre pantallas virtuales
Aunque el tiempo de generación de una pantalla por bloques es practicamente imperceptible por el usuario, si
queremos evitar que se vea la generación del mapa podemos utilizar una pantalla virtual como destino de la
impresión de los tiles y después realizar un volcado de la pantalla virtual completa sobre la videoram.

Esto implica la necesidad de 6912 bytes de memoria para evitar que el usuario vea la generación del mapa, por
lo que no es normal utilizar esta técnica a menos que esté realmente justificado.

Si utilizamos pantallas virtuales necesitaremos modificar todas las rutinas de impresión para que trabajen con
una dirección destino diferente en lugar de sobre $4000. Para que las rutinas sigan pudiendo utilizar los trucos
de composición de dirección en base a desplazamientos de bits que vimos en capítulos anteriores lo normal es
que busquemos una dirección de memoria libre cuyos 3 bits más altos ya no sean “010b” (caso de la videoram
→ $4000 → 0100000000000000b) sino, por ejemplo “110” ($C000). De esta forma se pueden alterar las
rutinas fácilmente para trabajar sobre un área de 7KB equivalente a la videoram pero comenzando en $C000.

Por otra parte, si nos estamos planteando el usar una pantalla virtual simplemente para que no se vea el proceso
de construcción de la pantalla, podemos ahorrarnos la pantalla virtual separando la rutina de impresión de
pantalla en 2: una de impresión de gráficos y otra de impresión de atributos. Así, rellenamos el área de pantalla
que aloja el mapa con atributos “cero” (negro), trazamos los datos gráficos (que no podrá ver el usuario) y
después trazamos los atributos. Estos atributos podemos trazarlos directamente en pantalla tras un HALT, o en
una “tabla de atributos virtual” de 768 que después copiaremos sobre el área de atributos. De esta forma
utilizamos una pantalla virtual de 768 bytes en lugar de requerir 6912.

Mapeando en posiciones de alta resolución


Todo el capítulo se ha basado en impresión de tiles en posiciones exáctas de bloque y con tiles múltiplos de
carácter, es decir, en impresión de tiles en baja resolución. Esta técnica es adecuada para prácticamente
cualquier juego sin scroll pixel a pixel de los tiles.

Para poder imprimir tiles en posiciones de alta resolución que no coincidan con caracteres exactos se tienen que
utilizar técnicas de rotación / prerotación de los tiles gráficos tal y como veremos en el próximo capítulo.
Compresión y Descompresión RLE
Como ya sabemos, la memoria disponible para datos en el Spectrum es bastante limitada. A los 48KB de
memoria base (si no usamos la técnica de paginación de memoria de los modelos de 128KB) hay que restarles
los casi 7KB de la VideoRAM (el área de memoria que representa lo que vemos en pantalla), el espacio de pila
y, finalmente, el espacio ocupado por nuestro programa (el código binario del mismo). El resultado de esta
operación nos dejará con una cantidad determinada de memoria en la que deben entrar los datos del programa
(gráficos, sonidos, músicas, tablas, áreas temporales, etc).

Ante esta tesitura, no nos queda otro remedio que aprovechar al límite el espacio disponible, y una de las
formas de hacerlo es mediante compresión de datos.

La compresión sin pérdida de datos consiste en coger unos datos de un determinado tamaño (gráficos, sonido,
una pantalla de carga, etc) y aplicarle un algoritmo que produzca como salida unos datos de un tamaño menor
que el inicial. Estos datos resultantes deben de permitir obtener a partir de ellos, aplicando una transformación a
los mismos en tiempo real, los datos iniciales.

Este proceso de compresión lo realizamos durante el desarrollo de nuestra aplicación o programa (por ejemplo,
en un PC), y utilizamos los “datos comprimidos” directamente en nuestro programa en lugar de utilizar los
“datos en crudo” o descomprimidos.

El resultado es, por ejemplo, que una pantalla gráfica de 7KB se reduzca a 2KB dentro de nuestro “ejecutable”,
y que podamos “descomprimirla” al vuelo sobre la VideoRAM desde los datos comprimidos. De esta manera,
ahorramos 5KB de memoria en una única pantalla.

En resumen:

• Proceso de compresión:

Datos (7KB) → COMPRESION EN PC → Datos_comprimidos (2KB)

• Proceso de descompresión:

Datos_comprimidos (2KB) → DESCOMPRESION AL VUELO → Datos (7KB).

Los procesos de COMPRESION y DESCOMPRESION son funciones que reciben como entrada los datos en
crudo o los datos comprimidos (respectivamente) y producen como salida los datos comprimidos o los datos
originales.

Hay infinidad de algoritmos de compresión, es decir, una gran variedad de transformaciones diferentes sobre
los datos originales y que producen datos comprimidos. En este capítulo veremos la compresión RLE, una de
las más básicas y que produce excelentes resultados con determinados tipos de datos (por ejemplo, los gráficos
que cumplan unas ciertas condiciones).

Fundamentos de la compresión RLE


La compresión RLE o Run Lenght Encoding se basa en la sustitución de elementos repetidos por “comandos”
que indican que un elemento está repetido, y cuántas veces lo está.
Lo que haremos será sustituir secuencias consecutivas de bytes por “bytes de repetición” seguidos del byte a
repetir.

Supongamos la siguiente secuencia de datos:

1, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, 6

Esta serie de 20 dígitos nos ocuparía en memoria 20 bytes. Pero si nos fijamos adecuadamente en los datos
repetidos, es posible comprimirla como la siguiente secuencia:

1, 2, 0, 3, "12 CEROS", 3, 4, 5, 6

Es decir, si sustituimos los doce ceros consecutivos por algún tipo de marcador que indique “esto son doce
ceros”, podemos ahorrar mucha memoria.

Un primer vistazo al problema podría sugerir el siguiente flujo de datos comprimidos:

1, 2, 0, 3, 12, 0, 3, 4, 5, 6

En este ejemplo, el 12 previo al cero nos indicaría que debemos repetir el cero un total de doce veces. Pero …
¿cómo distinguirá nuestra rutina descompresora si ese número 12 es un dato en sí mismo o un indicador de
repetición? La respuesta es, no puede saberlo, a menos que marquemos ese 12 de alguna forma especial.
Debemos pues determinar un marcador para que nuestro descompresor distinga los “datos” de los “bytes de
repetición”.

1, 2, 0, 3, *12*, 0, 3, 4, 5, 6

Ahora bien … ¿qué tipo de marcador podemos utilizar?

En el algoritmo RLE se utilizan como marcadores de repetición los 2 bits superiores del dato. Estos bits, el
número 7 y el número 6, tienen el siguiente valor:

7 6 5 4 3210
128 64 32 16 8 4 2 1

Bit 7 + Bit 6 = 128 + 64 = 192.

Así pues, nuestro “comando de repetición” será cualquier byte que tenga los bits 6 y 7 activos (con valor mayor
o igual que 192). Los bits del 0 al 5 nos indicará el número de veces que representa al elemento repetido.

7 6 5 4 3 2 1 0
RLE RLE Número de repeticiones

Por ejemplo, en nuestro bloque de datos anterior:

1, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, 6

Si sustituimos los 12 ceros por su “byte repetidor” seguido del dato a repetir, quedaría:

1, 2, 0, 3, 204, 0, 3, 4, 5, 6
¿De dónde ha salido ese 204? Pues es “192+12”, es decir, es un byte que tiene activos los bits 6 y 7 y que en los
bits 0 a 5 contiene el valor “12” (el número de repeticiones). Y al dato 204 le sigue el “0”, que es el dato que
hay que repetir 12 veces.

Nótese que nuestros datos en crudo ocupaban 20 bytes, y los datos comprimidos ocupan sólo 10 bytes. Es una
compresión del 50%. Si todos nuestros datos fueran de características similares, podríamos poner en nuestro
programa o juego el doble de niveles, de gráficos o de músicas. Obviamente no todos los bloques de datos son
susceptibles de ser comprimidos, pero la mayoría de gráficos, por ejemplo, lo son. Y poder meter más datos en
nuestro programa es algo que agradeceremos en muchas ocasiones.

Estos datos comprimidos (y no los originales) serían los que incluiríamos en nuestro programa, así:

Datos_comprimidos:
DEFB 1, 2, 0, 3, 204, 0, 3, 4, 5, 6

Después, nuestro programa debería ser capaz de descomprimirlos allá donde vayamos a usarlos, es decir, coger
los datos comprimidos, y desempaquetarlos obteniendo los datos originales previos a la compresión. La
descompresión puede hacerse nada más arrancar el programa (sobre bancos de memoria, zonas de trabajo, etc),
o directamente en el momento de necesitarlos (intros, pantallas finales de juegos, etc).

Para la descompresión sólo tenemos que asegurarnos de, byte a byte, copiar los datos “normales” y de “repetir
N veces” los datos comprimidos RLE. Distinguiremos los datos “comprimidos” porque son mayores de 192
(tienen los bits 6 y 7 activos).

Así, la rutina de descompresión se basaría en:


* Desde I=0 hasta LONGITUD_DATOS
* DATO = Datos[i]
* Si dato < 192 -> Es un dato normal
* A datos_comprimidos añado "dato".
* Si dato >= 192 -> Es un dato comprimido
* Repeticiones = dato - 192
* Dato_Real = al siguiente valor a Dato[i]
* A datos_comprimidos añado "Repeticiones" veces Dato_Real
* Fin_Desde

En el ejemplo anterior, la rutina de descompresión leería datos uno a uno y vería:

Comprimido = (1, 2, 0, 3, 204, 0, 3, 4, 5, 6)


Descomprimido = (vacío inicialmente)

• 1 → (<192) = No está comprimido


→ El 1 lo copio en Descomprimido.

• 2 → (<192) = No está comprimido


→ El 2 lo copio en Descomprimido.

• 0 → (<192) = No está comprimido


→ El 0 lo copio en Descomprimido.

• 3 → (<192) = No está comprimido


→ El 3 lo copio en Descomprimido.

• 204 → (>=192) = Sí está comprimido


→ Calculo REPETICIONES = 204-192 = 12
→ Leo el siguiente dato (un 0).
→ Copio a “Descomprimido” 12 ceros.

• 0 → (<192) = No está comprimido


→ El 0 lo copio en Descomprimido.

• 3 → (<192) = No está comprimido


→ El 3 lo copio en Descomprimido.

• 4 → (<192) = No está comprimido


→ El 4 lo copio en Descomprimido.

• 5 → (<192) = No está comprimido


→ El 5 lo copio en Descomprimido.

• 6 → (<192) = No está comprimido


→ El 6 lo copio en Descomprimido.

Nótese que dado que utilizamos los bits 0 al 5 para indicar el número de repeticiones (puesto que el 6 y el 7 son
marcadores RLE), eso quiere decir que un dato “comprimido” sólo puede representar un máximo de 63
repeticiones.

O, lo que es lo mismo, que 63 ceros seguidos se codificarían como 255, 0 (ya que 255-192 son 63).

Si tuvieramos que codificar 70 ceros, sería mediante 2 bloques RLE, es decir, codificaríamos 63 ceros en un
conjunto RLE, y los 7 restantes en otro:
255, 0, 199, 0

Los datos que son mayores de 191


La pregunta es … ¿qué pasa si nos encontramos con datos REALES en el array a comprimir que sean ya de por
sí (sin ser bytes de repetición) de valor 192 o superior?

Supongamos el siguiente flujo de datos:

0, 1, 200, 2

El valor 193 es, por sí mismo, un valor RLE (>=192), por lo que debemos encontrar una forma de indicar a
nuestra rutina descompresora que ese dato no es un dato comprimido, para evitar que (en el ejemplo anterior),
repita el siguiente valor, el 2, un total de 8 veces (200-192).

La solución es muy sencilla: cuando realicemos la compresión, codificaremos los valores mayores o iguales de
192 como un bloque RLE de longitud uno:

0, 1, 200, 2 -> 0, 1, 193, 200, 2

Nótese cómo se ha codificado el valor 200 como un RLE de 2 bytes: 193, 200. Esto implica, en nuestra rutina
de compresión, que los datos comprimidos han acabado ocupando 1 byte más que los originales.

El aumento del tamaño de los datos comprimidos sólo ocurre cuando el dato a comprimir es mayor que 192, y
además no está repetido, ya que si está repetido también nos beneficiaremos de la compresión RLE:

0, 1, 200, 200, 200, 200, 200, 200, 200, 2 -> 0, 1, 199, 200, 2

En el ejemplo anterior, los 7 datos 200 se han codificado como 199, 200, pasando de ocupar 7 bytes a ocupar 2.

Es decir, sólo perderemos ratio de compresión en aquellos datos que sean mayores que 192 y que no estén
repetidos de forma consecutiva. En el resto de casos nos mantendremos iguales (si no hay repeticiones) o
ganaremos espacio (en el caso de las repeticiones).

Obviamente, hay que seleccionar el mejor tipo de compresión para nuestros datos. La compresión RLE vendrá
muy bien, por ejemplo, para todas aquellas imágenes que tengan muchos colores iguales repetidos (imágenes
rellenadas con colores planos, por ejemplo), pero será nefasta en imágenes con mucha variación entre cada
pixel y el siguiente, si éstos valores son mayores o iguales que 192.

En una imagen de pantalla de Spectrum (aprox. 7KB) que tenga bastantes colores planos, podemos pasar a
obtener ratios de compresión que dejen la imagen comprimida en 2-3KB o a veces incluso menos. Las fuentes
de letras y los sprites también son en ocasiones buenos candidatos para la compresión.

Obviamente, podemos provocar mejoras en la compresión a la hora de diseñar las pantallas… si el dibujar una
determinada zona con un color de tinta o papel determinado produce valores mayores que 192, basta con
dibujar la misma zona invirtiendo la tinta y el papel (y los valores de los bits) para que esa zona no produzca
valores de compresión RLE.

Eso quiere decir que un bloque de 8 píxeles de valor 1111000 sin repetición se convertirá en 2 bytes (el 193 de
repetición, por ser mayor de 192, seguido de 1111000). Pero si invertimos tinta y papel en ese recuadro y lo
dibujamos como 00001111 ya no se producirán 2 bytes por este dato a comprimir.

Nada nos impide tampoco el comprimir aquello que produzca un buen ahorro, y dejar sin comprimir lo que no.
Resumen sobre la compresión
Antes de ver el pseudocódigo y las rutinas descompresoras y compresoras, vamos a resumir lo visto hasta
ahora.

• La compresión es un proceso que convierte una serie de datos de tamaño N en otra serie de datos de
tamaño M (siendo M menor que N), mediante un determinado algoritmo de procesado de los datos
originales.

• Los datos a comprimir pueden ser cualquier tipo: una pantalla de imagen de 6912 bytes, un sprite o
conjunto de sprites, música… En general se puede comprimir cualquier cosa que se pueda representar
como una “ristra” de bytes (es decir, todo).

• El algoritmo de compresión sin pérdida es un mecanismo de transformación de los datos que debe ser
reversible. Es decir, a partir de los datos comprimidos debemos ser capaces de obtener los datos
originales.

• El ratio de compresión nos indica el ahorro de memoria obtenido. Si una secuencia de 2000 bytes se
comprime en 1000 bytes, diremos que hemos obtenido un ratio de compresión del 50%. El ratio de
compresión variará según los datos a comprimir y el algoritmo de compresión empleado. En el caso de
RLE, obtendremos grandes ratios de compresión con series de datos que contengan muchos valores
repetidos y malos ratios con datos que contengan muchos valores mayores de 192 sin repeticiones.

• La descompresión es un proceso que obtiene, desde los datos comprimidos, la secuencia de datos
originales, mediante un determinado algoritmo de procesado.

• El algoritmo de descompresión es el mecanismo de transformación de los datos comprimidos que nos


permite obtener los datos originales.

• La compresión RLE es un algoritmo de compresión basado en la repetición de los datos. Consiste en


agrupar los datos consecutivos repetidos mediante un “marcador RLE” que indica el número de
repeticiones del siguiente dato de la secuencia.

• La descompresión RLE es un algoritmo de descompresión que, a partir de unos datos comprimidos con
RLE, obtiene los datos originales mediante la “expansión” de los datos “de repetición” formados por
“Marcador RLE con número de repeticiones”, y “Dato a repetir”.

Cómo trabajaremos con la compresión

A continuación vamos a ver el pseudocódigo y funciones ensamblador y C de descompresión RLE y


compresión RLE. Pero antes, es importante destacar la forma en que usaremos estas funciones:

• Compresión: La compresión RLE de los datos (imágenes, sonido, o cualquier otro tipo de datos) se
realiza en nuestro PC. Es decir, cogeremos un .SCR con una pantalla de 6912 bytes, o un .BIN con los
datos gráficos de un Sprite (o cualquier otro tipo de datos), y los comprimiremos con un programa que
nos dará como salida un fichero binario de datos comprimidos de (generalmente) mucho menor tamaño
que el original. El programa compresor no es, pues, un programa para Spectrum, sino para el sistema en
que estamos desarrollando de forma cruzada el juego. Así pues, la rutina de compresión la
programaremos (o la descargaréis) en C o como fichero ejecutable para Windows, MAC o Linux.

• Inclusión de los datos: Los datos resultantes de la compresión serán los que incluiremos en nuestro
programa, en lugar de los originales. Es decir, nuestra pantalla de fondo ya no serán los 6912 bytes del
.SCR original sino los (por ejemplo) 3102 bytes resultantes de la compresión. Los datos los incluiremos
con la directiva INCBIN de pasmo, o bien convirtiéndolos a ristras de bytes con bin2code o similar.

• Descompresión de los datos: La descompresión de los datos la realizamos en nuestro programa de


Spectrum, mediante una rutina programada en ensamblador de Z80 o C para Z88DK. La rutina es
pequeña y rápida, por lo que nos permitirá desempaquetar nuestra pantalla de 3102 bytes directamente
sobre la VideoRAM (por ejemplo). Los 3102 bytes de nuestra pantalla de ejemplo, expandidos, serán
los 6912 bytes originales del SCR inicial. Se dice pues que la rutina de descompresión trabaja al vuelo o
en tiempo real.

Rutina descompresora
La rutina descompresora de RLE debe recibir como parámetros los siguientes valores:

• Los datos comprimidos con RLE a descomprimir: Los recibiremos en forma de “puntero” o “dirección
de memoria” apuntando al principio de los datos.
• El tamaño de los datos comprimidos (necesario para el bucle de descompresión).
• Un puntero o dirección de memoria apuntando a dónde queremos descomprimir los datos.

La rutina deberá ir cogiendo cada byte del bloque de datos comprimido y decidir si es un dato “sin comprimir”
(<192) o un dato comprimido (>=192). Si es un dato sin comprimir lo copiará a la dirección de memoria
destino, y si está comprimido cogerá el siguiente byte y decidirá cuántas veces debe repetirlo.

Por ejemplo:

Datos leídos del array Significado del dato


10 Es menor de 192 → Es un dato sin compresión → 10
193, 200 Es mayor que 192, se repite 193-192=1 veces el 200 → 200
230, 5 Es mayor que 192, se repite 230-192=37 veces el 4 → 5, 5, 5, 5, 5 (…)
219, 240 Es mayor que 192, se repite 219-192=16 veces el 240 → 240, 240, 240 (…)

Así pues, nuestra rutina de descompresión debe hacer lo siguiente:

M = 0
N = 0
Desde N=0 hasta N=TAMAÑO_DATOS_COMPRIMIDOS

Cogemos valor=Comprimidos[N]
N = N + 1

# Si no es un dato comprimido, simplemente lo copiamos


Si valor < 192
Entonces
Descomprimidos[M] = valor
M = M + 1
Fin Si

# Si es un dato comprimido, lo descomprimimos:


Si valor >= 192
Entonces
repeticiones = valor - 192
dato_a_repetir = Comprimidos[N]

Repetir "repeticiones" veces:


Descomprimidos[M] = dato_a_repetiro
M = M + 1
Fin Repetir
Fin Si

Fin Desde

Empezaremos viendo la rutina en versión C, ya que en este formato es bastante comprensible y apenas ocupa
unas 20 líneas:

//-------------------------------------------------------------------------------------
void RLE_decompress_C( unsigned char *src, unsigned char *dst, int length )
{
int i;
unsigned char b1, b2, j;

for( i=0; i<length; i++)


{
b1 = *src++;
if( b1 > 192 ) // byte comprimido?
{
b2 = *src++;
i++;
for( j=0; j<(b1 & 63); j++ ) // sí, descomprime y escribe
*dst++ = b2;
}
else
*dst++ = b1; // no, es un byte de dato (escribe)
}
}

Nótese cómo:

• Usamos los punteros src y dst para leer y escribir en memoria (operador de indirección * ).
• Cogemos un valor desde src y analizamos si está comprimido o no (>= 192).
• Si no está comprimido, es que es un dato a guardar, de modo que lo escribimos directamente en dst.
• Si está comprimido, no es un dato a guardar sino un dato RLE. Obtenemos el número de repeticiones
reales (b1 en el ejemplo) y leemos el siguiente dato desde src. Ese dato (b2) tiene que ser escrito b1
veces.

Ahora trasladamos esa rutina a ensamblador de Z80, y nos queda lo siguiente:

;;
;; RLE_decompress
;; Descomprime un bloque de datos RLE de memoria a memoria.
;;
;; Entrada a la rutina:
;;
;; HL = dirección origen de los datos RLE.
;; DE = destino donde descomprimir los datos.
;; BC = tamaño de los datos comprimidos.
;;
RLE_decompress:

RLE_dec_loop:
LD A,(HL) ; leemos un byte

CP 192
JP NC, RLE_dec_compressed ; si byte > 192 = está comprimido
LD (DE), A ; si no está comprimido, escribirlo
INC DE
INC HL
DEC BC

RLE_dec_loop2:
LD A,B
OR C
JR NZ, RLE_dec_loop
RET ; miramos si hemos acabado

RLE_dec_compressed: ; bucle para descompresión


PUSH BC
AND 63 ; cogemos el numero de repeticiones
LD B, A ; lo salvamos en B
INC HL ; y leemos otro byte (dato a repetir)
LD A, (HL)

RLE_dec_loop3:
LD (DE),A ; bucle de escritura del dato B veces
INC DE
DJNZ RLE_dec_loop3
INC HL
POP BC ; recuperamos BC
DEC BC ; Este DEC BC puede hacer BC=0 si los datos
; RLE no correctos. Cuidado (mem-smashing).
DEC BC
JR RLE_dec_loop2
RET

Finalmente, para aquellos que utilizan ASM de Z80 integrado en el compilador Z88DK, veamos la rutina
descompresora integrada con el paso de parámetros de Z88DK:

int RLE_decompress_ASM( unsigned char *, unsigned char *, int );

//---------------------------------------------------------------------------
// RLE_decompress_ASM( src, dst, longitud );
//---------------------------------------------------------------------------
int RLE_decompress_ASM( unsigned char *src, unsigned char *dst, int length )
{

#asm
ld hl,2
add hl,sp

ld c, (hl)
inc hl
ld b, (hl)
inc hl // BC = lenght

ld e, (hl)
inc hl
ld d, (hl)
inc hl // de = dst
push de

ld e, (hl)
inc hl
ld d, (hl)
inc hl // de = src

ex de, hl
pop de // now de = dst and hl = src

// After this: HL = source, DE = destination, BC = lenght of RLE data

.RLE_dec_loop
ld a,(hl) // leemos un byte
cp 192
jp nc, RLE_dec_compressed // si byte > 192 = está comprimido
ld (de), a // si no está comprimido, escribirlo
inc de
inc hl
dec bc

.RLE_dec_loop2
ld a,b
or c
jr nz, RLE_dec_loop
ret // miramos si hemos acabado

.RLE_dec_compressed // bucle para descompresión


push bc
and 63 // cogemos el numero de repeticiones
ld b, a // lo salvamos en B
inc hl // y leemos otro byte (dato a repetir)
ld a, (hl)

.RLE_dec_loop3
ld (de),a // bucle de escritura del dato B veces
inc de
djnz RLE_dec_loop3
inc hl
pop bc // recuperamos BC
dec bc // Este DEC BC puede hacer BC=0 si los datos
// RLE no correctos. Cuidado (mem-smashing).
dec bc
jr RLE_dec_loop2
ret

#endasm

Rutina compresora
El algoritmo de compresión se basa en agrupar bytes repetidos iguales en “bloques comprimidos”, dejando sin
comprimir los datos cuando los siguientes bytes no son iguales a él.

La rutina de compresión recibe los siguientes parámetros:

• Los datos descomprimidos a comprimir: Los recibiremos en forma de “puntero” o “dirección de


memoria” apuntando al principio de los datos.
• El tamaño de los datos descomprimidos.
• Un puntero o dirección de memoria apuntando a dónde queremos dejar los datos comprimidos.

La descripción “en lenguaje natural” para la compresión RLE es la siguiente:

M = 0
N = 0
Desde N=0 hasta N=TAMAÑO_DATOS_DESCOMPRIMIDOS

Cogemos valor=Descomprimidos[N]
N = N + 1

Siguiente_valor = Descomprimidos[N]

# No se puede comprimir el dato porque el siguiente no es igual:


Si valor != Siguiente_valor
Entonces
# Si es menor que 192, lo copiamos al destino:
Si valor < 192
Comprimidos[M] = valor
M = M + 1
Fin Si

# Si es mayor/igual que 192, lo copiamos a destino como


# dato comprimido de repetición 1 (193):
Si valor >= 192
Comprimidos[M] = 192+1
M = M + 1
Comprimidos[M] = valor
M = M + 1
Fin Si
Fin Si

# Hemos encontrado repeticion de datos:


Si valor == Siguiente_valor
Entonces
repeticiones = Contar cuantas veces está repetido el byte
desde Descomprimidos[M] hasta un máximo de 63.
Comprimidos[M] = 192 + repeticiones
M = M + 1
Comprimidos[M] = valor
M = M + 1
Fin Si
Fin Desde

La rutina en C es la siguiente:

unsigned int RLE_compress( unsigned char *, unsigned char *,


char, unsigned int);

//-------------------------------------------------------------------------
unsigned int RLE_compress( unsigned char *src, unsigned char *dst,
char scanline_width, unsigned int length )
{
unsigned int offset, dst_pointer;
unsigned int bytecounter, width;
unsigned char b1, b2, data;

dst_pointer = offset = 0;
bytecounter = 1;
width = 0;

b1 = src[offset++];

do
{
b2 = src[offset++];
width++;

while ((b2 == b1) &&


(bytecounter < scanline_width-1 ) &&
(width < scanline_width-1))
{
bytecounter++;
b2 = src[offset++];
width++;
}
if (width >= scanline_width)
{
offset += scanline_width-width;
width = 0;
}
if (bytecounter != 1)
{
data = RLE_LIMIT+bytecounter;
dst[dst_pointer++] = data;
dst[dst_pointer++] = b1;
}

if (bytecounter == 1)
{
if (b1 < RLE_LIMIT)
{
dst[dst_pointer++] = b1;
}
else
{
data = RLE_LIMIT+1;
dst[dst_pointer++] = data;
dst[dst_pointer++] = b1;
}
}

bytecounter = 1;
b1 = b2;
}
while (offset <= length);

return(dst_pointer);
}

No necesitaréis la rutina de compresión en versión C de Spectrum ni Z80 de Spectrum, puesto que esta rutina la
debéis utilizar en vuestro entorno de desarrollo (PC, MAC) para comprimir los datos e incorporarlos en vuestro
programa.

Lo normal durante el desarrollo del juego será “descomprimir” esos datos, no comprimirlos.

En la sección de Ficheros y Enlaces tenéis disponible el compresor y descompresor “rlezx” para Linux y
Windows (con su código fuente). Con él se pueden comprimir y descomprimir bloques de datos para
incorporarlos en nuestros programas.

Ejemplo completo
Vamos a agrupar todo lo visto hasta ahora en un ejemplo completo comentado. En este ejemplo haremos lo
siguiente:

• Comprimir con un compresor RLE propio la pantalla SCR de carga de SOKOBAN.


• Incluir ese fichero RLE resultante en nuestros programas.
• Hacer un programa descompresor del RLE obtenido directamente sobre videoram.

Pantalla de carga del Sokoban


Veamos los diferentes pasos del proceso:

Compresión de la imagen

Descargamos la pantalla SCR de carga de Sokoban desde su ficha en World Of Spectrum. Como toda pantalla
de carga de Spectrum, ésta ocupará un total de 6912 bytes (256*192 pixeles en formato 8×1, y sus 32*24 bytes
de atributos):

[sromero@compiler:rlezx]$ ls -l sokoban.scr
-rw-r--r-- 1 sromero sromero 6912 2009-04-03 12:56 sokoban.scr

A continuación comprimimos la pantalla SCR con el compresor RLE, utilizando la opción “a” (comprimir):

[sromero@compiler:rlezx]$ ./rlezx a sokoban.scr sokoban.rle


Output size: 2571

Tras el proceso de compresión comparamos el tamaño de la imagen comprimida con el tamaño sin comprimir:

[sromero@compiler:rlezx]$ ls -l sokoban.scr sokoban.rle


-rw-r--r-- 1 sromero sromero 2571 2009-04-03 12:56 sokoban.rle
-rw-r--r-- 1 sromero sromero 6912 2009-04-03 12:56 sokoban.scr

Como vemos, el tamaño de la pantalla ha pasado de 6912 bytes a 2571, lo que supone un ratio de compresión
del 63%. La imagen ha ocupado finalmente casi 1/3 del tamaño original. Un ahorro de este tipo puede suponer
gran cantidad de espacio aprovechable a partir de las pantallas de presentación, introducción, menúes, game-
over o finales de juegos.

Inclusión de la imagen en nuestro programa

Ahora que ya tenemos el fichero binario de datos RLE comprimidos (sokoban.rle), debemos incluirlo dentro de
nuestro programa. Podemos hacerlo de 2 formas:

• Método INCBIN: Incluyendo el binario directamente en PASMO con la directiva “INCBIN”,


asociándole una etiqueta para poder hacer referencia a él:
• Datos_Comprimidos:
INCBIN "fichero.rle"

En este caso es importante no colocar los datos binarios en una zona de memoria que vaya a ser
ejecutada. Lo normal es ubicarlos al final del fichero, como en el ejemplo que veremos a continuación.

• Método BIN2CODE: Convirtiendo los datos binarios a “texto” con una utilidad como BIN2C o
BIN2CODE (las tenéis disponibles como descargas en la sección de ficheros). Estas utilidades para PC
(Linux, MAC, DOS/Windows) toman un fichero binario y lo convierten a datos listos para incluir en
nuestros programas:
• [sromero@compiler:~rlezx]$ bin2code sokoban.rle datos.asm a
• BIN2CODE v1.0 By NoP of Compiler SoftWare

• 2571 bytes from file sokoban.rle converted to datos.asm.


• [sromero@compiler:~rlezx]$ cat datos.asm
• ; File created with BIN2CODE v1.0 by NOP of Compiler SoftWare

• BINDATASIZE EQU 2571

• BINDATA LABEL BYTE
• DB 255,255,193,255,255,255,193,255,239,255,193,248,7,206,255,193
• DB 255,204,255,128,3,193,248,43,193,248,62,0,7,215,255,193
• DB 240,198,0,82,120,127,202,255,193,255,255,255,193,255,255,255
• DB 193,255,239,255,193,224,3,206,255,193,255,203,255,193,254,0
• DB 1,193,248,0,193,240,62,52,1,215,255,193,240,128,193,250
• DB 0,127,193,255,0,42,72,63,202,255,193,255,255,255,193,255
(etc...)

Es decir: el método 1 hace que PASMO incluya el binario directamente dentro de nuestro programa, y el
método 2 lo convierte a directivas de datos “DB” para que lo incluyamos con sentencias INCLUDE o
copiándolas y pegandolas dentro del código.

Programa de ejemplo de descompresión

Finalmente, sólo tenemos que juntar todas las piezas (esqueleto de programa básico, rutina de descompresión,
datos RLE comprimidos) y añadirle una espera de pulsación a tecla para hacer nuestro ejemplo completo.

El programa siguiente, ejemplo_rle.asm, toma la pantalla gráfica comprimida con RLE (sokoban.rle, de 2571
bytes), y llama a la rutina de descompresión RLE indicando como dirección de descompresión la ubicación de
la VIDEORAM (16384), con lo que estamos desempaquetando nuestros datos comprimidos directamente sobre
la pantalla. Tras eso, espera a la pulsación de una tecla y vuelve al BASIC.

; Prueba de descompresion RLE


; Desempaquetamos un SCR comprimido con RLE sobre la pantalla
ORG 32768

; Cargamos los datos y preparamos nuestra rutina


LD HL, Pantalla_Comprimida
LD DE, 16384
LD BC, 2571
CALL RLE_decompress

Wait_For_Keys_Pressed: ; Bucle para esperar pulsación de tecla


XOR A
IN A, (254)
OR 224
INC A
JR Z, Wait_For_Keys_Pressed

RET ; Fin del programa

;;
;; RLE_decompress
;; Descomprime un bloque de datos RLE de memoria a memoria.
;;
;; Entrada a la rutina:
;;
;; HL = dirección origen de los datos RLE.
;; DE = destino donde descomprimir los datos.
;; BC = tamaño de los datos comprimidos.
;;
RLE_decompress:

RLE_dec_loop:
LD A, (HL) ; leemos un byte

CP 192
JP NC, RLE_dec_compressed ; si byte > 192 = está comprimido
LD (DE), A ; si no está comprimido, escribirlo
INC DE
INC HL
DEC BC

RLE_dec_loop2:
LD A,B
OR C
JR NZ, RLE_dec_loop
RET ; miramos si hemos acabado

RLE_dec_compressed: ; bucle para descompresión


PUSH BC
AND 63 ; cogemos el numero de repeticiones
LD B, A ; lo salvamos en B
INC HL ; y leemos otro byte (dato a repetir)
LD A, (HL)

RLE_dec_loop3:
LD (DE),A ; bucle de escritura del dato B veces
INC DE
DJNZ RLE_dec_loop3
INC HL
POP BC ; recuperamos BC
DEC BC ; Este DEC BC puede hacer BC=0 si los datos
; RLE no correctos. Cuidado (mem-smashing).
DEC BC
JR RLE_dec_loop2
RET

; Aquí viene nuestra pantalla comprimida con RLE.


; Hay que darse cuenta de que está fuera de todo
; código ejecutable, es decir, el RET de la rutina
; principal y el RET de las subrutina de RLE_Decompress
; hacen que nunca se llegue a este punto para ejecución.

Pantalla_Comprimida:
INCBIN sokoban.rle

END 32768

Ensamblamos el programa con pasmo –tapbas ejemplo_rle.asm ejemplo_rle.tap y veremos que, al


ejecutarlo, aparece la pantalla de carga de Sokoban, que no es más que el bloque de datos RLE descomprimidos
directamente sobre la dirección de memoria en que empieza la videoram.

Aportaciones de nuestros lectores


El usuario Z80user, en los foros oficiales de Speccy.org, nos aporta una modificación de nuestra rutina original
y una rutina compresora nativa:

He leído el articulo del RLE, he reescrito el codigo de la rutina descompresora y he creado la rutina
compresora. He realizado la compresion de la rom de 48K, y posterior descompresión y salen identicas. He
utilizado un pequeño truquito con el LDI y el RET PO (un flash no muy usado, pero que usa LDI para indicar
BC=#0000) y me he ahorrado algunos bytes (compresora 62 bytes, descompresora 26 bytes).

;; Creada por Z80user


;; Compresor / Descompresor RLE
;; Comprime y Descomprime un bloque de datos RLE de memoria a memoria.
;;
;; Entrada a la rutina COMPRESORA:
;; HL = destino de los datos RLE.
;; IX = datos a comprimir
;; BC = tamaño de los datos a comprimir.
;; Salida
;; AF,DE desconocido
;; HL = HL+longitud de los datos comprimidos
;; IX = IX+BC
;;
;; Entrada a la rutina DESCOMPRESORA:
;; HL = dirección origen de los datos RLE.
;; DE = destino donde descomprimir los datos.
;; BC = tamaño de los datos a comprimir.
;; Salida
;; AF,DE desconocido
;; HL = HL+longitud de los datos descomprimidos
;; DE = DE+BC
;; 26 + 62 = 88
//-------------
org 16384
RLE_descompress
RLE_dec_loop
LD A,[HL] ; Leemos 1 byte
CP A,192
JR NC,RLE_dec ; si byte > 192 = está comprimido
test_end LDI ; Copiamos 1 byte en crudo
RET PO ; Volvemos si hemos terminado
JR RLE_dec_loop ; Repetimos el bucle
RLE_dec ; bucle para descompresión RLE
INC HL ; Nos colocamos en el valor
AND A,#3F
JR Z,test_end ; Si 192, es dato en crudo
PUSH BC
LD B,A ; B= numero de repeticiones
LD A,[HL]
bucle LD [DE],A ; \
INC DE ; Bucle de escritura B veces
DJNZ bucle ; /
POP BC
DEC BC ; Ajustamos el contador al usar RLE
JR test_end ; Copiamos 1 byte mas

//---------------
RLE_Comprimir
byte_1
LD E,[IX+#00] ; leer byte
INC IX ; incrementar posicion
DEC BC ; descontar contador
LD A,E ;
byte_2
CP A,#C0 ; Si es un codigo RLE
JR NC,RLE_compress ; tratar como RLE
CALL get_byte ; tomar el 2º byte
JR Z,ultimo_byte ; falta escribir el ultimo byte
CP A,E ;
JR Z,RLE_compress2 ; usar compresion RLE si son identicos
LD [HL],E ; son distintos, escribir el byte anterior
INC HL ;
LD E,A ; recuperar el ultimo byte leido
JR byte_2 ; continuar con la compresion
ultimo_byte LD [HL],E ; escribir el ultimo byte
INC HL ;
RET ; salir
RLE_compress2
LD D,#C1 ; eran identicos, empezar, con 2
JR RLE_Repetido
RLE_compress
LD D,#C0 ; era un valor RLE original
RLE_Repetido
CALL get_byte ; Obtener otro byte
JR Z,RLE_distinto ; Escribir el valor RLE si no hya mas bytes
CP A,E ; Comprobar si es identico
JR NZ,RLE_distinto ; Se encontro un byte distinto
INC D ; incrementar el contador de repeticiones
JR NZ,RLE_Repetido ; Otro byte identico
DEC D ; Se acabo el contador de repeticiones
RLE_distinto
LD [HL],D ; \
INC HL ; \ escribir valor RLE
byte_simple
LD [HL],E ; /
INC HL ; /
LD E,A ; Recuperar el ultimo byte distinto
JR byte_2 ; seguir comprimiendo
get_byte
LD A,B ; \
OR A,C ; Comprobar si es el ultimo byte
RET Z ; /
DEC BC ; descontar contador
LD A,[IX+#00] ; leer byte
INC IX ; incrementar posicion
RET

Agradecemos a Z80user su aportación, y aunque advertimos a los lectores que no podemos certificar el código,
lo listamos para quien le pueda resultar de interés.

En resumen

Hemos visto los fundamentos de la compresión y descompresión RLE, así como rutinas C y ASM para
implementarlos en nuestros programas. Mediante lo visto en este capítulo, podemos obtener un gran ahorro de
memoria en nuestros programas, pudiendo introducir más cantidad de gráficos en el mismo espacio. También
permite que pueda caber más código, más sonido o más texto, ya que aunque no apliquemos la compresión
sobre este tipo de datos, podremos aprovechar el espacio que dejen libre nuestros gráficos comprimidos.
MIME-Version: 1.0
Content-Type: application/octet-stream; name="Recursos.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Recursos.zip"

UEsDBAoAAAAAAFyb6U4AAAAAAAAAAAAAAAAJAAAAUmVjdXJzb3MvUEsDBBQAAAAIAAWV6U76pcX3
PQ4AAGZvAAAXAAAAUmVjdXJzb3MvMDJfb3Bjb2Rlcy5hc22V3E2P29YZxfF9gHwHLgrURrQQ30nM
6r6xkkFrVGkMjLxrAKPNolm0KdCPX2kUZ/icew6FDgJk8zNN/fV45pKXo8OhePtKf/3y9v+yqptd
8f71VPyt+Pcv//3t27dfi59/+a349T///Pnbv3784VDYP9d2yz/19ud+Lb798vd//Gb+2I8/fAeH
52PBvp6K7fZdzbHwYXM4EFUWr6/X/4z94MPHjctt9a72h3A9ZFEUuapBFVQ17yomqVr7GjYHqjrz
Ak5zwHP/Dvt3lV4LN23c9OdcDe/KxVjs5o0PuRrNqbnNLVquHLxMnszbZKGgKthjCRXNeQWRLNlk
J5lsWvylnw5fiz/9VOWqtLMWE521ks5aTGzWSpi1mIqiyBXMWiyoglkTys5a5OFKnDU89T/gYtY+
nVi2N5XPWky5ymbtGi1XMGsiGcxaKqiCWRPKzloSyXDWZLLJJDt83eTVnorKztpuprNW0Vk7HD5u
dnNmK7S3MOh+tzBxu4IqmDih7MTteL7KTlx0Kl9lJ47Vu6l84nZzrkaMfGuXKYfhYpLhYO7mgiqY
O6Hs3M0inJ27cGQHe4Mwd4HOXW3n7nykc1fLucu/x9V07s7kB/rVwtx92M0fiYK5E8rO3Q1tDrmy
c3cOU8G+rtDOXeBzV+dzdz7mKvtOl43dTdG5E+Fg7lxBFcydUBHOjs5dDXOnw03mcH5DFkFPRQPr
uA1dQjQlqEhVBYr+iGhqUDuqGlD0n32DK7jbtOWqA0X6X1VvVBC9BlC81wiK93KgeC8PivcKoHgv
XL7xXgkU72XnK/JeLazdeK+2BEV7tRUo2qutQdFebQOK9mpx1UZ7tR0o2qu185VErwEU7zWC4r0c
KN7Lg+K9AijeC5dsvFcCxXvZ+drxXh2s13ivrgRFe3UVKNqrq0HRXl0DivbqcI1Ge3UdKNqrs/M1
i14DKN5rBMV7OVC8lwfFewVQvBcuzXivBIr3mvJ1CiZ7KvptrvLL9b7MVcxVlav8cqyvc7XLVZOr
fKHdL+Zr5+aXgnxdVZcfK19P9na+HJ+vfgBF56vHhRidr96BovPVe1B0vvoAis5Xj0swOl99AkXn
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
kkMbBbfZtBQkpIjOJJNtCzdy4ab/YmLiNHB11GVwFBT///BjfJU58PYn+Hr14kvHjLd/BxFYj7dO
vHXhbQ3ePvkR3n0WqfHsJ3j3HLvrYW32fI13f4sU73of785GnlkFOz/jN7L/Tzj67k98e8Lj+Wc8
ed7XvBn+Bv/b/scamptubrW0vdZ+eNOOn+Z2XuyU78rdZd+1e9epXZN25+++Z/eh3R/vnrVn1Z77
9/xuT3DPgr3OvXv2vr03sUtnKbKstKy2HLK8ajll+djyjSXFOsM6z7rUWmo1Wp3WZutm6y7rM9ZX
rCetH1kvWcfZptuybLm2EtsaW52tyfaI7Snbr20v207Y/mwL2tLsKvsCu9ZebjfZvfbNjsccLzpO
Oj5yfOkYcijqsuoW1lXW1dS11t1bd7jujbov6i7VzXb+wKl2Fjhbnfc6n3e+7vzU+blT4RrvmuWa
6/K42ly7Xb2ul1yvuN51feC6zp3p/rn7Yfd295PuPvcr7nfdX7q/cad6pniu9yzy5HpKPLUei+c/
PO2exz2/8DznedHzqueU54LnK8847wTvdd5Mr9Zb6C3z3u7d4N3ofdS73XvA+4x3wPum9x3vH70y
31jfTN9s3xJfnm+V726fzbfR1+Z7zLfb1+t73veS77jvU9+g73v+W/xq/3J/qd/k3+rf6z/of8Hf
77/gv3r9hvW/Wv+b9S+v/98NH21Iqp9br6s31v+4vr5+W31nvbxpRtO8pqVNpU3GJmdTW9PWpr1N
zzW91nS6aUxzarOqeU5zTrOmeWVzTXN989fNOS2GltUt97fsaHmu5f9ahlqWtOa3elrbWne2/rb1
z61j2uxt9W0Z7Yvbl7Wb299tv2ZT8abyTWs21W7ZsOWLLQu3rtrWtu2hJwaeyOxc1Lmy8xed7U+d
35vcpe26u6u6y9e1sWtr17NdL3W91XWu6+9dQ12p3TO6r+2+vju7W9dd272p+/7uJ7r/u/uVbtCz
uMfZc2/P5p6BHnnv0t7be429j/W+2ZvSN7tvXt/avp19p/r+2HfVvvJ99+17eV9w37/mW2goKCgo
KCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgovnucTmzI/kNSEdyzP+zRuc6//uDLNQngH53H14Ad
g7bsmWKtuetAbrB/5+L+tEDoF3/15kn8zZsncxVvDuGvSdy/IHBX7tQT++HP1Uptqf/WI1egn6Zb
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
acziDrksC6EjJVxRtkMxrfSO3SThjak3KFjnpfVaV8jvwrIrH07/Lc/pyQTeqvhL+vh3v17s3sKx
KieT9N/7+kS1Uh7pZeqHwu4g3da7LnG8Fe/cpLvzjioU9wO/M3TFyQrFumw92rL1OC5bj2Oz9Tg2
W4+jh65YqlBckK3HimwVTZkVS2WDZUJxebYe12Sr2JDtdgxl6/GRbBUfyjbUnmw97s9WsTfbvvrL
bD0eztbj8Ww9vpqtx/ey9fj7bD2ez9bjp9l6PHcFPCo7+ZFsPZqzVTRdbqiKr6Ic1T1KL5TEk/Dt
yVeGqvj5e7SXSMq/E8VOShX3wjeORdLzGRx9TPZ1xYRiPldxU2ZFDVfx3nSKwqn0hN40yWzfLzOd
wzX9QLbJPJRtMg8PnszZdMk09iVMxy7FeKa3ZpvMtsyKaq5i1+DJ/Gy/IpnceDLz+zN2s55sk3ky
2y2ze/BkNvZljPnZbGPem+0G2JdZkR/qwcyKKq7iz7MN9VAaxQHU+1oswOJ0sRtPT6/9nN1v6v1A
+Pg5W9fAXtez15//iS3OIrSF/Ic6zyKJr1AKf0lt+gP7cpSC86lSX6Y2PTwH5Vwl/dLjEF5IJbbe
jHKGIbYTsD2A3XpiVRFf8uMv6vgL4f5bUc4IaU3iJdWB/KVA/jJwMTd3Apqc9sttAPD/j3vYruZl
ryG20zjZ3kBOoG4nJy+FGx9X3XWB/SqpjGuCDcGtwYHgZ8HhoYWh8tATG9ztH3Xu3GLa3r69d/vZ
7RN22Hbct+PIji93zNzZsLNr5/Gd+btu3eXbdXDXR7uu6TH0POLqcj3pes6V0zSqaVrT/CZz0/Km
xqbWpo6m7U37mg43RZvea/pzE3IXuae657lN7jr3GneL+353t3uv+xfuV92/d593x9xaj85T4rF4
VnjWeY54Bjx/8uR6R3uneIu9t3iNXod3k/d+73PePu8570Xv+ObJzZZma7OjeV3zc819zWeaLzRr
feN83/Xd5Fviq/J1+/b6jvn+w/eh7xNfrl/jX+qv9nf4n/Q/43/T/1/+v/rHB6YHZgYWBm4N2AL3
BB4M7AmcDHwc+CLwTSAviIMLgo3BruBvgl8Erw6tDf04dCr036FRLc6WTS0PtvS3vNByrOXNljMt
X7eoW6e39q83tjnaQm1PtL3bptlw9YZrNtzcYek43lH4wJ6H3npoUWeo80Tne52xztu2vNI9q8fc
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
prKAzAhaT46V1pPjnlpPThi5gZzUs5CcG1FIHo8tJP8qFJKf5heSp74qJOvOFVIpQzdSg356juLv
f54avHgzJcEt1IsbXqBeN7xE7XK8TL1hfYXy37YthDZsD4ngXg2JKtkRMvWdHd1+Aa+Hanu8HprQ
8/XQB7J2hubO3xm6asHOiGeW7NQtX71Tl//qTt2TF3bqVlp36VbZd+lWr9ile/bFXbq1X+3SFSTu
1m3I3a0rfGK3btOm3brNn+/WbeWKdC8+UqR7aXGRbltBke7Vk0W612L36HbO3qPbNX+Pbu/BPbo/
N+7R7Rvxhu6d9W/oSk+9oTsUt1d3MmGv7m9/2RuZe2Jv5OJ+b0Xm3f1W5OP1b0U+8e1bkctX7Yt8
sbA48pWpJZHbPtkf+dq5/frJ3AG9/NEB/ZSsd/RZ+lL9tN9L9YsOHtQvfu6Q3n7mPX3+1+/pn77y
nn5Tw3v6w9L7+uo17+td1GF9zbjDevfmw/raM4f1HtMHMWMWfRAz9oUPYu71H4nJHHM05r6UYzET
pWMx0/Yfi3mZK4vZtqUsZvebZTHF+8q4+yrKuInflHH3o7PcJIuTeyDDyT0438lNLnRy8gEnN6XG
yWV95+Smasu5adZybvr4ci77sXJuxuZybuahcm7WR+Xc7O/LuZyICm7OwAruofsruIeXVnBzX6rg
Hvmggpv3aQX36JUKbj5TyS1IreQWypVcLqzkFr1ayT1WVskddFZyn9dVcmd+q+S+iK3ivhSquL9l
V3H1K6u4r3ZXcV9XV3Fnv6zizqmqufPGau6b4dXchZxq7uIz1dy3b1ZzXk81993Zas4X7OL+bnZx
l0a5uH884uK+L3Bx/yxxcT984uIuX3RxP4bWcFf61nA/javhfl5YwzU8V8P98m4N9+upGpP+sxpT
zKUaU2wPtymuv9vE3uc2xee5TQkvuE3G990m7ozbZLrsNt3eq9Z0B19rMj9Ya+rtqDUlbqs1WY7W
mvp8VWvq21BruhONfr8hHpN1qseUtMJjSn7dY+pf4TEN+MZjGvjf7F17dFRFmq9+J4GEEGQVZOA2
eht0NDwWUZDB6YQADYHEJDCiDtDpdEJjuhv7AR0ViILgII6CKIPjKAo+1gFUxh0dj66jfY4eWJsA
ARzYsyN7PAK74zLuzujggPTeW7fqdtW93+3bMWF3/tj2mO6u+tWvvu9XVV897r3NxYw4UTgg3lh1
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
6OjoCfvox1vcna+PjU9Pz3KXfPDC+GcPjnaMj7z00llK4+NvdXVNNhjGx/mrO9hWl8ZGuqMjK2Hq
x4f5Y8th8XL3K32qtVubH+zgr8+5qzU9Tkixq1jvLv7S61GKyfE0KwEAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAACArx8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAMC/4UVtR6KGTmKYQvS88l1iZw9CZrJPG60uq6SlFSU1JTTvy93n
EB3b5tI7y2romhyH2byMlq+h69a7aWG+1SoqxsSQpGYNzbXyNj4crfMG67203LOebqdbM5OjhX31
fn7EW6f4gr7QpROZCFlXVVZaU72+knyFKWK7lM/J5rp9Yi5vbTRGfZt/+CNaZyRpImjTD56i9Vue
/vFlRjGJbZ7I6E5H/jK6YrnIzGamNRVlqxMZGUQQnyVfm6XRofgVbyNtbmj1Nfq0jEK0obnVF1Qa
NvkvP1E+9ayopE2WpYuWiSkDAYc1uVfnMJJJIijZ0uCwJXaaLLZFDpto1SbijbmscakWsZRVCsSp
aSG06YFWX53iD9KmcKihjk31QGDNitU1oSrbREig7oEq0bai5LKnW6Athye5HIllsomDc0g6u1ys
tKqkqqyaWicuBbvVqo0yh0xlIYlLqvJe6t0aDileuqlhc0NduFGhtiV5LOlpIlQ0NjSytgLHZi/f
K+clP3/tdBV/s58tcB27Vup8zZv+6XRtiQuz3tG4OC+xLrU2bXlrc40kg9CV5mxyX/ps6rpQpTOl
z55VSrZ8KXGnO06K40RiQ+awb5Nambhltm+Rc8xmqVrULEQS30IrZaLKpCJZWy6OqZIki1FETZKc
MjGbtbFlQtnWnoJcgTkFuWpX6nel2ZO5qym5u1Jyb0/JXZ+Suzsl9+JvZO7FV527+9rJ/b/vX77q
9Ckrb0hZeSll5eWUlZevgavOnZJ78bV61V1F7u6U3J0puev/k7nDNSt68+6bDZJkkGfqDMkm7UlB
myM/sbNd22nLvPoQmyNXO2Zz2JK7TW38WZsWIlq2O2y3WRoX25YszU8+61McuXlaiHjeWrVmVRnN
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
hsDXz+8LfPPavuCnNO8E68+8E/x09rvB2cEHgqf/cSC47ODB4Lnb3wuuuvV+cM0v7wcvl38QXOf9
QfCHUz4IPvXaB8GWoA+DGw0fBjft/DC4+daHwfSIQ6Gpyw+FTvzHodDH/D4Ozco+HDp5wpHQKVOO
hE43Hwl9NfZo6OtvHQ3dceho6N7DRzWTrxzVTGk/qnk82Kx54mGzZuo0s+bJJWbNU1vNGv1xs+bp
z8ya7A6zZhp1TDM94ZjGkHdMk7PimCZ35zFNXuMxjbHlmGaG+LgmX3tcM3P8cU1BwXHNrOePa2bv
Pq4pPHNcU3T9uKZYfkJTEnVCU5p6QjOn+ITGtP6Epmz/CU35xROag5dOaD779oTmX74nNZ/rTmq+
yDyp+bL8pKZl40nNv987qfnq6knN1z+c1FwLOKW5PvyU5j+Pn9LcmHdKc/PvpzTfHDqlaf3ilObb
26c0bUqL5ruRFs0tvUXzfbVF88MWi+bHoxbNT19bND/ftWh+CW3U3B7TqPnV0Ki5s7RR0769UfPb
yUbN3ZuN2uBvGrWhoiZtmLpJ2zepSUvlN2lVzzVpw//RpFXTTVrNrSat1rNZGxHZrO33aLM2srBZ
239tszaqoVkbfb5ZO+DnZu1Ab1o7aCCtHZxOa3VzaO2QF2htzD9p7dArtHZYO60d7ndaGxtzWjsi
67Q2bu5p7UMvndaOfP+09m+fndaO6jitnaE4o5054ox21tQz2sIFZ7TFr53Rln58RmtqOaMtF5/V
rgg6q1056qz22eyz2udqzmpXvXlW+/yxs9rV189q18jPaff3Pad9J/6c9t3cc9p/Lj+nPVB/TnvQ
ck77Xus57X/5nNf+ojmvvT3uvPbXmee1d1ad17a/fV772+nz2rvfn9f+7n9B+0f/C9qOlAtaa9GF
CLDuQoRo34UIjwsXIsS/XIiQBF6MCBx0MUKZcTGij+liRFDdxYjgAxcjQj65GBH628WIsJBLEX2H
XoqgJl+KUFVeigh/+VKE+oNLEZp/XYrQWi9FRKguR0wIv2z1HZ1O/yObVhrpl6cBQFvz6KUz6G8a
zQG15mv7VCsvw8ULnZENgBRm7s9jwS9PE4EhdPF0umxG4/oi84hDcKWt2nCZzp2h2morAJgCql2O
ggabQMQKNtgEPVjBJpvAhxW8ahP4OWgxltNCU6xa6cDrC2hxPjJm01zVMZQHEXRpNoOQQITOgKg/
cVTvC0fBNQcXqFodBT/aBH1YQbtN0IsVgCsONstwgU2xJQOam2mkWAKjm48NdPhpVIpJztID5lsO
iWrC4KqKLjLyRKMVtCycuRDDCz94oWpF1KprTAUyCvrMA6yvpRcY2QybSlsMQsyGFyCGyfXkmdoZ
ph9xVN2LiKndkUmA2bgJMVUYfcX0oRn01Hz6oWlMy6XDllNdsxX1gEW9ptN+BtSae0qhUY6qC2y8
JzsETDxyoeHeSYVl7X6qMLK9xBm2P8+cW8abnCE0OQRCJLzJb8z5603e0g2Tt3TN5C0OJj+GTG4l
t3K16S8wGXN+10hJ/b7CyA5yJ0qhcfFC4wLw9swt+yuN22K4B1IPhwJu7WQakcPzUc0ft1TKCKCl
qk9sMsDLkPU7y1DgffcNmDNffaXHaFUD4wgxoFdmi+BEMStbdYwpCV16R4y2hsysoGog6MFEQfjJ
58TwLYGhJWzlsOaB5U4TjK+Eizx1+ahNuhnH2ODOscgYrRg9u0vdbb2YWYjNU4H7pxffuGz/4ls6
g50ZsGa5IQZo2uLjhAhQNn/xEDEzQDCBPxsOANNnblSwsyLn1EwDO7UK+xxXeG8sI/Oz9cMvyP1Q
Nxf1Q/+tGE8Ik48JPBgBp0TRXHauVgG7EgyqF2f0p2LcaBE75HXIbvPbRnq2Dz8bIzX4b26QyriZ
1DZiA/ALf3b4UiAjMykjPjNpUhIVT02KT82Kz4SJ1HgKyibGDxo0iHPrSgfPIO1DKs3LKh2WOWxO
bKX5Jp4jtuVkVJofrsJyPGw5syrNq/EckS1nWaX5VhXy7KUT+OqjN2+PJ9HSCiMqcnWZL0DjESZH
Pusr4pKNq5ikrwxdr2aWbc8vNw+ax9N74PQ2xvYcXykqwNOK823ct1b4etq5ZVyya5yeOCebvFbr
K7XTeQroYOLwUZ7UqV17s6b7ypxJqzdhOkKrfT3tGVLnKtjsqfm+cm5h62iz7jWykp30SYG6gmYD
vO4vT/MEVFZyWtJIPhp44DiZHScBL8E/HiXlUS9PkzEZWJ7EnufL5zlAsPptsQQ3ZCHlyiYPx97X
G7cpgOWUA4qixhUUGwodF7owUwyoBANBVw9QXk4wTwoKjZCNzxEJa2HGLeVUC3fBjS84oX01kKEa
PJyiXJsrcjYX4BZ6EEce0vzJJwiao2U4prXN/FJjMRE9iYCWgpIKU66Rz5HZc6D1paaSWcbccrxR
OY08weLFMbohDzl1KqQs9TLRXx54n3jZqYF4vDNEikMkRAjAIVInSIXRwqtg26oyKxcUrbod86mk
slxTRbmhOK+Eml9SbCijcityCoymcpiCIZ+fShMc96YjHQSCPvIAAL2fukwPyTl/YZEgIwxmTGAy
GhepnmLWBhOYXlijZfLH2gL9ykVsnaoE54WZ+QixcKKt8JFFrH6qkYRVHSxTlHP1UYYCJoYscu1H
xkFu/oQDxpMj8BIGOudgj8JPZ0QyYN9sk4iw/swFKacOidXlheqqe5HCYoUYH0fU+lpHBeT3X4GN
m1wrsOEFRwW8/pQCXXZwj7+W3vuvpe/519L7dJ2+cyK+Eb3djYcM4XznCeIHJQ9y1Vmmpk9WsfWw
a42QXAaUAoDt+2+5vLQAG9PxNik/0ofgUusmaydY6xkrLl1CrI3IC57pAtZXhi+yuHws/MH437v8
Mh2ec/7kM469jHWvqpwBw+m8AmVXif2LNzHZD/BTBkI8knN1hSuCBAEBq8FIggbJSx0J/DvXIMxB
A7krgpF2AjbN6eQ13Xk/wO60Ni01f7zLAYvkoctcRHWnOTB5YnJicvzEsenUxMlJU+Bn8pSkVGps
EpWYnpYQn5XEdjX2mNdBC+Y4AK11G1aYNcVdrZAdKePh7iqNSmIv0qkpMJkpGENcORsV40BcarVa
naVLrs+YkY9JuXWD9Q8bljlEAoA7wL6yzGxeZjYWEfcRjjsV3CxkyZY3Ov/Hj1+FsGhnAaVrpGhV
ZJ+38Z0ep7ZtIVLOrDesaC/CdU+BDR7glb/TC/CsMIcs1PblzFrh/eX80OUrkAsGtDcx1Px4D1ie
d/M98HaGZZaO1nYrwOa86tTm5mZuIH+3Ah2UIQHnAHyQnlth4VkXcaxpANhOHJNzXeX2EuRGctJI
gZ5TOOkURk8HqXUXHmqtWcSwHMYxaAS80znpdAGWk1q3CHjjibxPCoP16nw+YxyXIQbA1lh1ajSi
OL73rfahJgTU5Tu5Mov7/pvAWY65CcTcyViXsufWYC7lq3fN7N9p7qhOcxM7ze1NzF3FfUs7zRXo
bP2Uc+hmwcyr4rA5gmar5qTLBd0hgtjwX1hJDLwuQTiD9SSHfUiA9eOwfgJpf662KwJ9ozhsFInB
+j2O5Wtjvu3YIszDdtsozGcZpP74JUfWQeyPdfn0RFsE5TlEfPALA692+idc54m7V1zSveLS7hX3
/JPFp9q8J+ueAvLuFffqXvEe3Svu3b3iPbtX3OdPFrf3fN/uKdCre8X9ule8d/eK+3eveED3ij/w
54qrDtrOns6wh1Ef2QQ17GHU6nz2pn9IruogszH4CN/EYNMPv3B0gol5mFOOxGWOtGvUngwMs2Kw
oxURNiv4G4j/iw1hTyc/vgro/zaqhtpsqLrOGDXAJjh8lREMZUiG2kiCmIM57riwppZONhpGcqd7
WvTJPmrCHdZNr1UNYITpjHCo4/EeSs7VA/rtYXSNA8LDjqjEEeyujsW9MwydLhqLHBcEUzpdjPG5
iZ3mjhXk8qvUuaQDAet3VsLaFTxMxN4WYGdx2MFd4OUXTBuI2J+I2MguYF/jsKld0IHHLiBifxZg
X+Cw64hYwTIKvE3S13qXWwdJBQsxfjFd0wUsvwWYjWNtCwjhPoLcQgc5bLygtt86O3gC3gLpDpK+
5Nqs3xJ5yzmGx4nHUT0EWO9ObfvW2mWskNfaStSMHy26LmimAJ0xVHSBoQeRoZ6TjuiCf28Trajj
GNIEDAGdeucBgXd+IfJu4LA5gr7zq7XrvGQsP7rj7jPvTKK+31m73qPkAp/Z9tALHXacDwmCq2Nu
Sqe5WkHubE5aJFCZ29Az33ajrUQpj71rJZlHlv4ukEqJWDSR87XapfjOwy6VAPvGzy4VEWvDzwed
NbtDtPh3EtahQT2IVnh0XQdbbUIr+L2iByB4BwCi1JOkGZPrbAXEErAeRKyUKBXyiogMvBUSor5S
9wzW30nesU0kUkDwpLA22zGVJ1EqJtUGAJFXRMIKe6rgEMKdtHOGrujQ0XUdbBGhgDhlDCdiyZPD
Bbw2250nwamN1dLplCE84RlK7NUzSfpaP+d4wwXYNRw2iXgqdlWgbxWHTRfwnrGS7n7x53XTcKzg
qDc+Fz/1XMzlpHLfQwRRl89N5r5jibn8krpHp7m+xFz+3qBHp7nsjsbxXKgXuz91eDbIA9hu1HBs
2ViPsftEhdlk91841wKNgv55iNg38kjTMwjmpAME2BCO96KAt5HIO41jeIJ0qijUDFAkK6yHiby8
VyOJ+g4E7jULIo4nfplSQZqbhfHKdibm5T622fp9Nc5ri8VEXmbBE+8Gu5STvijQt4Okgy1e/WF1
G+OBhDhji0jzrfUPYhwkb24WcPqOJ26EyMu134mrETL2DskKF8tLOegyL5B0NlOhGIS5B/Sw3RKc
8yYj6MDabn4YOhy3S2umQccx137szV4WEJLr+r0X53uoZ+z7Newuo5h9ESg516k6pg7ujiP9hgvl
wdYuKt9Fjd0oyamz6k3stSURc0rlyrtnhApKuuddiUvvDuYqiPgT3o3e6kL57G1dVL6LGrtRklPn
xlbVF07eXTqD/niXqpiTo3u3tOPjdhKb3ku2296pe2gabc1TFQtueDM3dwWDr0YB2BL8AwH8A+Uu
eaREnmfumUdO5Fnqiodz0ZZtLixfsMNlTSJiTavuk+Wr75Pla9xY/sp2F5ZX1LuqiZk2nGuqu0+W
b7xnHhmRZ5Mby+t2CCz3sFlesvMee/vr98nyLfepzd9wY/na+ns0cOd9MnDXfWrahnvmIdu1/555
JESed++TXe+54jGvZAPU60/Y7xiYptk8zcDYGw+MbHPXYB91DfaxW5iFfbhwtg/7usZU7mUOwuP+
9/R2A/ZcqSe4X+82eIBC0qP9kGUhs+9yeGysN0g0GQ3lBXONVGJJUVFJcRn+2KujLf/36sL/n1cX
eLv59U7vY0zk/ma3ICOG+5baHjBdu5vwUBO3dGLXS4/kXC1wx9Kym7g064Ry727VMSb0TtjttPHg
tlEc94o9fGXMd2+bbYv2oCcNjwlWeQ4vy7K18i3Xx/U7BqwTOZU0ezglvfc4PnfC9Wz2PTfvD7Db
xoKN/ijBBve5TjdVMYJNVWWnm0Dh0dCznfLqBLzzOuU9ZxU8GO2Jv6IHN3ZshuMjoKZp+OKa67sw
ArLuxm9T+RAfz+0FEtMnjs+Mz5rM/gLkJLyz+yMMfr7gK4wp3KvQPiA1nYI0k5LHT05OV9ELhA+r
+PEPPHiBqemTqceTJ6q459mxhiWEJMeHDxmOEC7wlRRTOfOoNEOmYRw1KX1cljBS+DkWCwBjjakV
TxipBGO5IctYVm40lY0kBhD+GEzMF1WCtIqyglxUXWb8uHgqNT0tiUqYPCkrHgsdwiJ9QIapJN9k
KBIWmhKfaYul/o5lgsGjhrkFVLqpvGA+FUHFFxpnGYrzTCWRSWmT+vOlHhDalSEMviEwgqfDuDyJ
ibZUBhtYIyuNOYMGDerPutzxV0jMc7exP+Cx2vYUPl2H3s7E3rHFdozuX2vwEExeTz5BmAJ7ALuW
hDnEyzYn4DOfowbYJPIgoJxmw2GDdFw84QcUP9wyiA8SA6yz2ofm3s4GN38T3uGpvjDhGCE5RgRe
JbgFk0qduZze9VU4lRK8PS51InZ+ANGzE3puDg4ASakU87O5mZlJmekUNT4+1dbx0TrfMpy/EijV
z0kptAxIzEpOTHd6ntD1YZKnUFHUN8ZRydRE9Ka2kwcdNPdjJ5oEiM2C/1KpJKe+ht9zldp9Iebz
BbO/2MUa0Bs4TGECpZyWISSKnoI5EfecM4dXJ/o69TnsAL8nWIr94Rq5LeojKLqUFtwG6IMFMvtw
ct2k/EB7VTCkpC71uC4cgg7PUUqESwEsYtlHQi/HTiQG6RTfC7n5U2KfP8Xso1MbZpDf2WGSMhd9
zgckTczKTKLSMxLhXDpSOOoEZvEG9LTPUEud/4QEvo5l/MCQ0VRycVm5qSI3F06JxjJhgd7CAhku
x7nDosAfxIymUpKmJqTHZ46lRj0Wn54x2rbCXYTen7A5lt0tOjwhiS01sDAkAU/NKDEhHof4LQUU
qoLPwQKlDFDjDUVwriZMEyLwtE2naptOtpe/7qF/jiN2MYWz88gDDbXdU/aVCLV4McWvRQSPjAvf
//Fz0T0zhDET76vO+1bCXs/V9k4EJjitd9gf1yGFrEyiw8fjod4mHUVoOKw2wT5wXqnRRHWuB4b3
Anl5BbnMvN65loI6jOWmEqrL+ksA7GB874ZBJHwXYYlkvvU++4gtKb5gTpZzgXzdOjyI21pgHVeN
GxYvwJF0j0YGOBrn6dq2rUc76U4B/8M2/O+kWblyJZFmJUtjwY+cipAz+wIKLqOc1lHMcoiikqfY
34FcyNzyyBD88gEWQjr5tRdmmTuR+/UlfjzyWHa+iID6J5WVGyhj2QAqF26h8gpmGE3G4twCwwCq
0IBsKyooKygh9Qcp6FdMFRlIWamgXxnzfw0oLMg15EEaA8WxzakwUuVGaqahjDIWz4A1lRvySqji
iuJcWCH8Ky6BcmquobgEXuZBbAVlJNXQF/SrKigvgZCBqBjcPeQZURWFBTlGkyGXrHEK0hgWmVFQ
XMAUg4HEgGpBJccbCg1VBeiykJpXkV/C5pugJhRDjQwylZaYDOy8OshhL+HqZRIF8nAxBUnLjdAf
DNlkUwVJvUDQbwBVhpxTjCrPKygrrYAbU2Lo6gGo4VQxjIOFxjLCBBoDKORHVC3n9GJEZSpBG6ly
Q35JcQFqdVQb/MsxIENJKuWDfogjtyTHZKRQByk3wVIVxUxbFhTPqSiAu+di2KDxyUwtvJ8Y79qd
xZgE2xq2N2ruUgOTb0LdDpaFzcF7Uyz0ptP7GdBsuKErLIGbO8rJbIfR1wNAzYsM80pMFNG2YaAf
oiktKWM7Q06hcRD8mgTpKdTvoH35xuICdMF4Kb+iwED2Uk/QD/VoNIwKyd3Vk+2unJn0asfDHHuY
wSZydvHltLkmL9jY96ozJqdOSqKykhJT46mM+Mx4dAiTlTxxcnym/cdMOjntlrhY2Qg2N7b9FnFz
4wOcdlhdrVJyL1WK3VUpfMv70FGz+Yj52j4mKifRHw5jSHrZfspTIMbephhDRmBvUywmI8R2RD4Z
IbEj7pIRUjviDzLC0474Dxkhc4UYxyO+J4sxJ/xORmBO6CAjMCdcJyMwJ3xBRmBO+JiM8HRroksn
jOcRVrIYc4ILhIdbhNgtAnNCFRmBOaGSjMCcMIaMwJwg7PcTyE6Y4NYJE9w6YYJbJ0wgOCGMjMCc
8B0ZgTnhdzICc4Kt31uEIybBbVB4xm1QOOk2KLgY0BJXfiIFBRfhC/OCp9ug0Jc8YhLcBoV4t0Hh
K7dBwcWAdukEUlBwEb4wJ0x3GxTukEfM4vsQFH5yGxSOuA0KLhCYE750GxQot0GBIo+YjvsQFM64
DQpL3QYFFwjMCTK3QUHiNihIhEEhnocAQcl4ghdkZATmhV5kBOaFvmSExC0C80IYGYF5IYiMwLwQ
KkAkkJ2QQHDCl2QE5oTvyQjMCXfICIlbBOaEG2QE5oSdZATmhE8EiEQe0ZcsFrlqo0SCE1wgxG4R
LifJRIITXHBgTggmI2SuEGN5xB2yWOSqjcYSnOACIXbVAmMJTogjI6Rua8GccIOMwJwwU4BI5hFW
sthlZEwmOAGQEWK3CIlbhNStHp5uETJXiDRyTEgjOMEFAnNCOBkhdsshcYuQukV4utVD5orjUbIT
HiU4QURGYE4QkxGYE2RkBOaEXmSEy5jwKMEJY8gIzAnCZVEKOTCmuA2MKW4DY4rbwJhCcIILhNQt
wtMtAnNCDwEilUfcIIsxJzSQEZgTXHCI3XJI3HJI3XJgTphJRshccTxGHg6PEZxQTUZgTsghIzAn
zCIjMCcUkxGYE6aTEZgTFpMRLmNCJtkJmW6dkElwwjgyAnPCFDICc8JcMgJzgicZgTmhg4wgOcFy
xmq48dW+0l+2Mp/Vu5nP1Abms3kr89lMf8V8fsfcSmfGEgDXnUVlziLovqne8kJG/zX+QCTmM3zQ
hx/skt8zyjR8xXwVMCW23mFeQdnazryB8h/mnvCb+xjAm/sRQnCBzmDkAMxj6N7bx9w0/699y/rO
TDm7+w7zCOTu9urAd7Vt5QA8OQqki0BM6P8DUEsDBBQAAAAIADmU6U61RJ2D1RcAAFRCAAAhAAAA
UmVjdXJzb3MvQ0hBVEFSUkVSTy1HQUxBQ1RJQ08udGFw1VoLXBNXuj9hTh6IIALRgIoT0t5GgjYg
ImVbLAK6PhALyq51BCNERB7BhJdWEfCBIk+FKL7Q9u51u7Xio9W2d9dfW7aPiNOlva1W27rLtXrX
m00fu1urdQv3nMkkmSGTkPr73b2/G2XmzHf+33e+73++85hHKAAg9ecpy1JI/PvZXBE4nXL3qbnD
QKQC/x2NfmT6YpIBZGWlZ2WS81MWp6QuW5CaSeLKQOmVB+DL2NlBAIwHIEYbBPAvJk6LS+/j0kxc
Z0OlxJmokI0Ks+JRIQEVnohDBQoVZuIqJUc/AReeRIXYuARUqsWlRFQIwRYZ/SEuaiY2RKKCBhsi
cBWWJAsbiH0CFUKx2ixUUDEgbHIz3zguPM01Pj0WmwpmwotFJQsAgUkHxoD7b2I/zhcCSWpmxtIF
i1OWLchcQpbrjGS+obScTM9KJ7fGPpEYH1gXradED4b9o/RU7QkyQFWrWqKad+L9vp7Ix9/tGAr+
qCdSldO/6e2eyP4fVEsksva6RLhwAmhXS+f3J/WHB/8w10JPza05sRFrYFNxwN9m/Eg+ddC2Uk8F
iW2nbdl66s6yVdOmhftjkUgM7DJr0HSE/LRPPvUWsjB4wjY3YHXRALJy6xN0/eGaYiS53hM5GEC/
qPW3KB8cvahSy3JVObNtzyr+nixCXqqsfWejcetnLjFta3I0uVakhY3QN3IVBH071xrASP2tJpVx
8J07GJUANAmiwbeY8jzNEqtC5gB/Mbi66C3bXC2Buh0rfHHHaWaDqtyCHZm7yNp3Jppu9QsCZCgd
SWiD5Hn0TOI7kQh8/21IkEgC+s5Hoxhp8VRkP6FBZVX+IU4E2ZgttkVVFEFrpn8dP3sCjK9LAaAu
zu84EZtIPz/dllqFuuD9K8HVlkEmul76fLTV/07ysELUy/bSslW4o06oRVZVaMREWcSpiVq/Xssk
TcR5dRC9L04ijjC/GaoKvj/bEhoaqkJdia4nRXS8OdtCv5mHIvl9HilNVMq0kYoE7X2ZNlYhd8i1
36pVitsYMFkbgVrrrVZVqxJEKnxCTVpwzZ+1t7EfQLxBFfwPRti2GikfX62GiZcVX8rU7yX+UdHn
EJJWmXog8QPFWfrIavV7ipfw6d8UF3tl9Leaa+jvk3euKYJ6n0TByZgrdMBZoB4rU/sNWmRa6J+W
IZMN1px5nwkqQPFdNJM+CIPatp1GiZew8an3a4emk36q2iGtmhia8eaa4iAxU6+ZZLW9ZmFp6xPN
cpT2xjtK6+NdqY947Zs0i3/9yIjr6bNcnUBf0jL96ZQgjH9fbyx7XXtCLVHVvltXbbGPpzOn37ei
nLLNtRo1Jk3t49WzLdxhpx6LTIZGvFsXMqkvWUtv0Q1oUJ1iDK/9iPNvJ2s/Ud3C9qzILBoKKK0H
W8d9AN5+UttTfWmmdtO5t2dqNctWJXSoEuqYnqu2fmhBqVSrWmuoOaGWbbGlGTbb0gqDOjafIGWv
LlYH6YuCdgdO0/6jX9Y/uX9K2hbkZVQXpeykqjXlmg2WT7AL4zDlttMfKpTM5Rj75aBiyi1REPAf
b8bD/LXByfR7OqsyAIQnz+mvtZKi3rjg/NoTZLFqHnZVMQVfnyfn4cvJJlUOOTbMSMogGaPRlKvQ
D0HHq2pfI3VMfLa55C9nKwpVtb+eWJuYobHS0jXWUEUsGroaTUKHYoI1Op9KqFcxSYr+oSsrnaJF
g00MwtGgiEgM19SeuAzWREyaqBVZ0bC4SMpUocH3ZitCJwLFuGgdFZVPIV6Ve6modZQseetnQ1MT
pclD6KT1k1n6npv0Y7/fzqQWKq6NSmql7t6La6eUHVTtuFO3l5/qv1BtoT+P6gvNpy88QmcEKk7T
q3PjDqBheQcX9lH4uJdKOkDl0XUFUZ1U/5P9U6x0V4FSJIoyI1hxnJmiT+VHm6ktv72UVJC4sqQw
ipH0JRVgFWt0J9Vry8Iqcfsp26K9FILWJU4eOq1VzH9nY1fmx7ei3koyU/W4wFbXdH7qVmVBJh3V
O5zqnXbMfr56V+J4tzpk5ynk6tsP8lEsGFg3P1E8dPpzVusA0pqPYfZKl7kDlN0RxA0iCaXUMol0
qlYU3q/qD8Hn4LuW3kLDNYnsGpbY1tuPEpDZr+wP7o/tD0ZzsMh/zsRrwTeu/W7gSYt99YjKoCy2
lRmUcxawnUbW2Vkcz/LRRmrzq5dC82cj9UDbIiMVnUFZGZDqlr1onUC+ZQxQ1Zwh72PVDAql061a
+l3NIIYTBEH/OZxvU4OnmuwMypZqpCy9Pyb8LGIwahVFB0iiV1GqO8kixV+S/RR/ZmS2KYyMKbeS
rrJlqh3rR4sm9GUEJgdedW8CTXIBEaFXFd9HZ1OaqF9QaFWzpc6l6K/1tkX1NfSA/g59Cf1NzaUR
n+Hr0HjJUTFrnTXxbxpmhbPcZFVti85SaP6dmvsOXRwYvYiqVQ3VqZ/pN6gVaOJu/GHS2lrVRO0D
PIMrvtTek4Qo/j5Rq5yYTCaKetE4RWs8wL1A+KMxaJ1frpq06Rx5X/HGkgSgmof+EEHYIPl11Aoq
IRBPM1E6anwcCEfr5kfXI6NfofCyfT3SlnqW+rDvYmEkUPhFkiZVAMN5FBN3rea8/Z+dB/uW4XpU
PtoKoJEzJEt8RB025I+2HMpWqvautDomJqb/K5xILVTtbWlmTO3te2cUY5mre2eq8SUDOYUHpLv+
HKZyqO/jeYzGHKz/3hlFSL8S/2Nk7zFW3mOAFx1W1OFDInUQ2v8gSP9FewUuDin+huz2Dwnizsf0
n2dxd/k4kToAYzqo/qMYgM433Gpa2ZoP0Nm2CHX/gN4afZZKSNbcF/2p8B2b6SlK9M2w/31R4lR0
caPwE4QD4G/RNwpxLqG+1qDUT5waHbEOjQCFchLOkxN4nWDzJFeTY1UQygNJUenUgz4iNKoY+M0v
BsTCYgDzioH4VjGQZJcAaX0JkBWXAv+KUjBWWwYClQYQNM0Axn1nAGFvlYPwlg0gMscI1FtMIPqk
CWi+MYHHL1SAOEklMJ2pBBVzq0BVWxWx/XQNsSNpE7Gz7Dmicc5mYteuLcTu01uIpr4txJ5VW4nm
/K1Ey4I6ohXUE22P1hPtM+qJvbfqiX1jGojO0AbiwIQG4qu4BuKbFQ3EX+saiL+faCDuftBA3Puu
gfhh8jbiH6nbIMjbBv12boPw5W1Q8vE2KHuwDY6J2g7HLtgOg8q3w5Dm7TDs3HY44fp2qBDtgBHq
HXBy5g4YWbUDkkd2wEde3wH/5U87oFq6E0bP2Aljlu+EMzbvhNoXdsK4Szthwu2dMDGwESbFN8In
VzbC5IZG+PSLjXDuQCNM+74R/jx0F1yYtAsu1u2CS3btgkt7d8GsK7vgsh93wZxHdsNnn94NqcLd
MLd1N1z96m645vPdsIBogmujm+C6Z5pgybomWNbUBMtPNkHjB02w4psmWBWyB9bE74GbcvbAXTV7
4O6De2DTb/fAPTf2wGbQDFuimmFrWjNsW9sM9zU0w84Xm2HXQDM0f98M90e2wAPpLbC7pAUe3NcC
D/e2wCNXWuDRH1tgzyOt8NiiVnjc2Aqf726FL7zZCk983gp/TbTBF6Pb4G+eaYMv1bTBkz1t8OV3
2+ApWxs8M64dno1th+ey2+Erle3w1f3t8Pzr7fDCZ+3wtR/bxaFD7eKw0g5x+Ocd4o22DvGmT/eJ
n+vpFG/e0CXekmoW194yi+tvm8VffGUW//HufvFg2QGx9eABsU3WLZnj3y0dCOuW/kdCt/Tjqm7p
1U+7pdc/65Zv/Uu3vE58UN6QfFC+vfqgfOfpg/LG1w/Kmz46KG8OOCRvTTskb99ySN6x85C8s+eQ
3PzpIfmB8YflBxcelh/KPiw/WnJYfuxXh+XP3zgs/9eJR+S/Io/If7PiiPzk3iPyU/1H5K/Ao/Lz
Tx2Vv1Z1VH5l49FhEAToKYX0+lzmPo/ujEKTLL2uEE+fNVRUFUUfWUtPaOjz70YSXG6rwsfSDnz8
vINu1DOqIqS6mVQeOUo35TICPyR4Q5ccTEujHLaTQx0XCJ0chi/O5SmAh5vZHXSX07Rd81yeFGSn
py7LzEpCdWxbN6fTbfogKX11LW0qdOCZ83R0A/zCUXrRmo+yS7CbzXmMGN1m0gt1TuQbOkfFFLcK
KYnaFIHHsCdteiUy1rjmuqRE+bujaDV2Mna5QPl7LOAEP8Zu6nIBJpI1qPw9rmY9qihVXsI6nttG
Vge4iFBhBDKpvMS0W/coUylBlarVdMvaS8WlKGoeREoqX+D5SOnc+optzs/V3DF7c5ZZLA1M9Mrf
OQ253LETdKDEkRQAAOdZSjJFKerMYG4aCOWEBJDzlqcvTnJkAHBVEYAkSW5msPJQQHJ/2amZWenC
+lpBfREg2fCOeOpL5c2R3X7HKTDbBZecgnF2wYBTMJYRJM+n38ml4/RMi110uD45jSMgkcDBHY8b
B5ESF5F+DiLP5fmBnh5HSBKXmAA9Ljl0yaWgh/mxATvHTnYe40QCvWu18hKvd1E+RetwSn1ncE8p
ewRdTqYd3ay8w9rYXnD5Mo1TzTWi2KbdgrcTOKJ1s24kce0YypCvvOMO7csxOmKZzY8lEkEiHbG8
Vf6/GwvP4w5PwdkHji+xPIFjGRDul5YN/8RY9uJYmMQe4Y7Z51gS+bGEcfslx/hPjGWfp1hG6Reh
mVdqjwKFsMVknyuUN91tugc3ctyKQOfIUCcz415507XiIfHmxThmXE2g6nsEXrfX5z5X+XbbKkYq
5kpVV8ckOxzhuAXti8AbOidys1OfjyT4SPus4R6LfV5jmO87aeLVznqUXU13MSSdNPG65jH73Dey
4+ezHc9EXE2ElB1wrFBjXCvUOACcjnF6C7MXw7R1ocIxrY6YSZ2J4rCIzwH2OscsHGLvlgCA9iYZ
6WRmTnoW6XTJtd0AHLqd0uHhYXdp/e21aws5Uj8W+6MD27ew2JmlGNAFgCNqkT1q+3ocbq8ex3pQ
4OgwdnGOWW2H9r3L9zQQbYwGmG3ImUq2x7npzy79jCcO2lDvs4g37JRLXRwBAEZoM3liT+cRJIYA
srt7cSaZmrkke8H85QsylUrHujvAbB6mVnKW3seQLQnTgXOrHJ6EskH4OTpwMkgvqyrSkfkluiq9
fSeZmpmWHqeNnU3qnFuAMPcVVOwacnKgrzQa8nWlpsqiCv3TG3XrDIYZepPdM0fLIfyWR2wgJqMN
SHlliUlPZi9NSU3HLxt0ZFFZUX6RzkiyMfZ91o13zbfN9tGzPnfL4suXLzv7kMc45KSVe2skas08
4jUQ5z1QF8m26L2NMOE23Pfs5/KCwbwFS9LSTaRRX2E0kGs28jZRTlCGLks3j8zOnLds61bUA4lO
L2qdXoQ5vAgSsxsP1pvgkT07CfVshVFPlhVV6UvIAj25vlJfaCDVsdPjpzk71m1DGTJiQ+cwL2XN
K1zBSrh7qGPOiCBXfNwpFnOn6OcdUn/W7ETBrZkIHB8JnCJAthPoclfOwuW8gZY8HsPdGtnvaMSP
C5EIQiRcCBSEQC5ELAgRcyFAEBLqzsz/owjcE4czdRAu+Bigjp1GLsnMSVmWObKrOZMkx82xQB03
jVywZEHqgpQ0p04AqyMTnKACgHrmNDL9l0vTs1zNBLIq/oIZHQLU8dPI7OVIJSN9fgq5NCszeuS+
AjiG2tMgGs3GKWRWytIFaSlkWjr6n43mlczFaLJempKVQj77y+nZS9HNd9byDNJUWa436soKDKS+
hB2d8W6c8cl13sqEjGxaAUgPP7eU4K3WEkEpFJSKBaU8JznTGASO1tH2cSm7Dmfn4a3ifu4tudi1
A5nr3N7hq/AVQLnfvjWMdCzyEQKySQKyJwRkSQKynwnIVgjInhWQrRwhaxTKJxbhiM6se6jEEoFo
LpFYEEtXrnZdzaLLV9MRzM7/9Ja+OZvt999MvWQKfWMW5rQ4kPuYiWP/XN5W1Fdp+jV6E1lYiVda
XQmpK6vQFRrKikwVOhKtGuW6/FJdWQypy6+o1JWU6ssq9KS+DNeUFOXrCnSmIiTPLzIwIvxn0peW
l+gNMSSG6UvKjUWleiNZUKTD9aZKsqxSX2UgK4y6Nbr1hhkouuTAq+g/cubfkTNLK/UItqFST5YU
lZYziz+6NupNRQWVBpO+RJ+P1k+0LTCYSPXxx489/vw0EntBmlCFwYicIk3Ic32pjiwzIKGOrNCV
ocar9MaKogJDvqHUQBpQ2yYS7XbKKipxCSuV60y6AkMMGpcG3J6+pASfy/Vl6KKiklxbiaAGcqN9
Ha0woAZLdGQWXsvn60p0NUU6XiAk6O62P2kj49FMlbp4+Yp0Misdf0yRviQ7RalU2jsV72aG2oMg
fyEfz3aSyNFJkSAhecEm1Obs5LT8dToyMTkFc0c+kZxiNBat0TmyJ4SvuJ4/00YCdTlDbqVJZyQX
Zq7IRnudRWR2URna+hUZpwnuCMLcTXK2UZFg6fLF2enk8iUp5LL01MUppH2yQ4GmL3k2JYuNsrkQ
R8fN7zDns8UZ9XQjfxuOpiD2MduIm1F2LuM8gxO7b5bbhQHs4zenLa/3Q+Gc+yH+BOYYdwhQz2ys
H61Ho8vCPrlbU+chyNCGkUEqnG6a/4+CDPM9yDENnCDn1/ctqHN4gVv8bTxzVQsAZ4rk17F377g4
7AXm54I95gVG+AaDvjUqdsHavMAkvlmTumAv8WAdXsjq8I2sDk9kfekFRvgGg741yiHrWy8wiW/W
OGR9woPt9ULWXk9kfeYFxiGr2wuM8OT3Xt/I2uuJrD4vMIlv1qSeIt3nhax9vmXWPk9keYMRvjkE
PVG/zxNZ3hqV+AbzSFanF7I6fSOr07fM6vQtszo9kfVfXmBi32AS3xr1SFaXF7K6fCOry7cJvsu3
Cb7Lt2HY5VtmdflGVpdvZJm9kGX2jSyzbxO82bcJ3uwbWWbfyDL7RpZ5dLLw9mSpO088MYeil4QR
HHauCiOIUW3AUW1w6OgVRggywUNwSOBH+3MHArgQQBjB4UMkjODwQQgjOHxIhREcPsYJIzh8TBVG
cPiYI4zg8LGVlzvHebkT5qWOQ8cMLzAOJ3leYBxiLnqBcdjZ4gXGoegHLzAOTxVeYByy1DxYNw8G
vNRxyJJ5gXHI8maN8M0a9A0m9g0m8Q0m9QQ7xoPVeanjkFXuBcYha50XGOGpk495IivJC4xDVqQX
GIesIC8wqacpJXvUaSlbgK3JwggOUQnCCEIoeB6CQ0+uMILDTLMwgkNKsDCCwwfpQFjohHy353LL
8x3SYY50Zf5IbC9XOiwa9oIdfmYYS53NOR6ZS3nNfeu1ueO85sZ5bS7P3pzygvPzmteZz2vs79W8
xsx7GO2RABdEkA3gfFrsmRouZARPHIjz7MHDH3338GXPHhK+e5jN85BgbROePbzvu4cvevZQ6ruH
v+B56Cf0QZnyAu8jgOXCaOCOlvGnjOXem+Hl0NOjN8TDJ/LwklHCCPbkmGSUiBQ/SZHnoXp0D3l4
LQ8PR4mI9OQYHCUi9U9S5HmoGN1DHp7k4cWjRKT15Jh4lIgSfVIU9DCYwVuCCPYRPKt4ia1+duS4
Zc7jeIPMgS0amcDOswv7xU+w+4WQ3eGhYSG7n7HYYh72W8FJQTi2UCCEHWClBZxo8j3Wini1V39S
Ld/yh6w0d2R3Cvh4jZWu5GE/EJR+LCh1WKAELdilFrvXVwD9nV7Z4Vw3a24z36k2OQWfXmEEHcx3
RB3OFI1gUnT8HeZ1QFU3na/XJdk/tVJudr1JxJLGNddzupVNjDCTEbqscPZbVRSgT8bTdSMQnP1W
NRfRgB1kcefi8deEC4vtrwrZj0to/KKQu6hC59uL1w9yvikCjs9PeF/Z1MmB49tEkq53fTfFURQL
KjaMrigTVNzmSdH+lkJ80EMwOw9xTIsETTc9bDDNDxtMi/dgPvIUTPZhl2lmZnA33fmwwZhHV5QK
Ku73HkzPIV4wfs5gyCOjptnRhw3m2MP2zHHvwZQcHtXnXz+sz7952A54aXRFYVfPjq4IBRVfeVhX
X/Wg2PeX7j8M69b99WZjQAlzfIE5NsW9zBxBPnOsYCaurxhz7EGEh6/fizeZOxoj88Lzhf9kTjXM
i/X7zE0x860GAzYBPxNTfZvZf6QhP+aLgHkpmtK+5lx88z9QSwMEFAAAAAgApJTpTmu91M7TFAAA
HwACABUAAABSZWN1cnNvcy9DcmFQT05HIS5zbmHsnX9sHFV+wN9u7HicxMkkzo9tSOHt2rrbi9e5
3Y3trHdnh8l4/ZNgL2uHhOyeOHtxlDWOHdkOrK97a3PXE1S00rU9IaFKNFxERSUk6Km9/NGKS2W4
NgRKe0WBcEQg0bgqsQm+EOAc/+j7vpk3uxs7P04oKVe+n1G8b76/3o958973DX9AmskhYpMiJJSw
EdkWTPTPE73r3G/jNnJrUBTye4aDXZxK1voFdsN6oOp6lssySta4jxAmyGRURV9ggsqsbvXTLGQI
KSJE1nVFcSiVQmV6sBoyGguuSBDWwVzSGcNCB3+FCxTdqJyXzKjLa0Bq+POgRDZDFkTj+kLHwmgG
ETdxm/2XiS6zG00mUo+uGf6aRprgvoXYmVqXdGbSJFcWVepcnw6ktUBk27YA0dm/wLbA00/rTW7d
7L8uPNwQif3qDo1EJHcTq9bw13XCbnQuYGU3EaUm0F5LA1LDnwclxAxZEI3rCx0Lo5n9l4gkytB/
CUZWiunG+OpKhrTB/RF4uhld0rIyFziM+JFIRIlEKI0QnUaKIlR+9ll7m6SLcMJDko3+290ZVqHU
BkMp+s+i2bmAlSUiSm2gvZaGS63+K8RrhiyIxvWFjoXRDNz5/Yem8vFsM59/xq6RGNy3gJWekdp1
GQSy6H86Yof+p0mG9T8N/VdikmqGU4WHBPMfmtpiJxFVikEoo0KdRVMVLshCu0QpZkRYVsPtM3nP
XzZDFkSz+p9TFUYT/XeI/vP5L0nwp6nS6H+EjUMP3EeIzNQRPpuZgIr5r0V06L9GNNZ/jcpPP631
SHYzoF14sBo0aKqW1SVFknpg4hjtO0B6JKJxASubKkMKLK8BqZbrvyQv0UbM96vQsTCa2WnVoeb3
X5UU9kd3mOMH6x/c69D/jM5WMxkEYv3TlIgSCNBtCut/oFLZtoYtnIoqFhdFeKjQdXBjvS+SVFZK
m/Ozkq2JJMsFeqVROS8pRv+X14BUs/SKFDBDFkTj+kLHwmhLoOYo0GXl4qegpOXbOW5m/5MDSyTL
lnOlG2kKBdfRyssJv+YUbKmwm2at/ZIDO3ZuA3VkLaXxnAu0eWpFMvT5vkrO13AraABXZ5UM91lQ
zKQkywwXshkiWudQ+HvJyxxToGd1LshLVLjayD2uQ8GWypcoWP8jYv/gO3ZuA3VrltLQF2jz1ODG
yfdtsnwddmsDzvk2wXKmkzQs6LqZlLD0Q5NZVN1snZutx2mzzHHrdhD0aJUy/PJEhfC0w2i+bAa6
FgWbJkTNGImO6D/s2LkNVMooQmms8QVaptbNG3Dj5Pu2Wb7uInODz/dtgwVWZ/ujAv031r8Me/Ry
Rid2s3WSnSUQcl7/JXsRCGIZhwy/PFGRedqR29CvR8GWylMUophSDuzYuQ1Usutm2ZxfBVpVsqvm
jdJi+uf56jHLtyW3AVu+oNZYWhAxnr/RbLuddV0z5z/0X2EJhFnmsA0FBG0a9L8gUTGi36j/BVuq
sZtqppQDO3ZuA5V0XSjN8c3Xsi7azRvNHL8C3x7LNytbG3DOF1rAXt+Imtd/eJVkeAnM1kmaLkXM
slGBJoOgSas0+g+JCuFpx831v2BLNXbTrLlJcmDHzm2gqiLKxMgPCrQkp86a6981fCXZ1F/lq7G1
MAIHGoUYSQlbFBU5q5Gs2To1q0gRs8xR2VDCC5HV+fzPS1S4GnIPcmPka5SvEuRKX1p7/dvrId9Q
8P8d2wK7yK2HHarVLGyp2R+wl6iSneoXdFi++BneeK0UQ7TkcG+lD9bODZ8U4GuCg5iGhORb59Tc
RcsuQEKQUXLJdI4Vc+witx52qJY02FK1CHuJKtl7KutaD8giRW4jrdAN0ZLDvZU+EJFXwCcF+Jrg
Ng0NvWWdU3MXTZPdsPbpmrmQ5VPyBbvIrUeCTRu21IzbDWdZ9qplMzEi8TO8kVYohmjJ4d5KH4jI
K+CTAnxNkIycIiKqMK1zau6iZWTJDscnRdLI1az7hF3k1iPF+PNva9FklZ/m4WG38ecvSy3cRDdE
Sw73VvpARF4BnxTga4JETENRhWGdU3MX9vwl9lboWfsy/b/rA3aRW4/E5zrbUjV5Feu9nfefvxPs
DG+ejw3RksO9lT5IIq+ATwrwNUEyDa0qDOuc2nSRJUh7rUQ2n7tPsovcetjSo2p8ZeLPX4H+w/qn
kaIiVax/XLTkcJ+XPoidW3dk4WuCahqKKkzrnJq7ZFmVWT7/l1n/xsbZRW4rVyVRAbI81z/c5308
kJcY5tQ33M8X+fVlCSBfb94MfKVpFmhXow+N9B/SAl+Sy6/a5seCifjH8iV/Ij49euWfZ069QeLn
tx+If3jqElNMXmRyZsNUExPE+c5KxwrnayWS4XV58qLhNjFZLM0wmRlEOiWT1lDJmxtXwzsWvd4L
uH9x6sJUlc1WSpwim1vFLtuKknV33T22uDD3xScfnBw3FK8EXgnM2xrrG+uP83+/tsMlSplA//y9
pGgfS43Cdtu/rTZsj9cLDfynvwD713WuhYQSP6o7GE98g60g9OgisVFaAZ8yNss2spKsZqbwe50l
SNHIxnO0a5G3dpESe0kp2dZX0/fzxD8mTifUg68lvtv33b6Z3hnS0weX7S8S8D2cGZLUr76/hTXx
uV37FzccgjxuC43ORm1SyRYb/GdK8nj7vb95/LGGi5v8D0G3x2uLLtXAn50/bF337k8J2be4uPhp
6fPyPZ+WvrKp87F1r2yK1cS/HY+OE5uDfNQw1B3taG920iOpdG8/PTI02NebHCkj9jXko+YO2tVB
/V6vt4ysqCfvetfyvgT/WxTO7mIFllMG3/OxAjtcBD/176yrDbCbvgTxBNbyLaCMbXz2K8HLL5hu
np3slz270HumIOSK9SaHertHUoMDdPAgHTnUS5tSQ8Mj9P7UQ72DNJ/m7sO9NDXAbVpSwyODQ6MP
3gBX6AU/1LSRVe2rYYW1UOW93bHuJtrZ0dRFx8ZYJ331rpDoRugFMGObraeO/a5kgnNC47IG7MD+
jiO9A52DR4eSva7QOdGXF3zQ7Q3M1+rdjRt41qwvrwXm0Hp8IFkNYep3cPZGWYfqmbC0QBswtJGO
fe1ML+R5Bl7DoLNrd6wLLOAplDEL/1r+WS50VhRcnamBZH93aoi2DY4Oj6SSD9Pk4OEj7PH09PeC
JwwKm+ses4mhs2KUfm2NUrS/e5QePUJHBqnfR4d7R4adrjJSPEBmRsO1MDB+Nllm0uFaqLQGyslw
ADr1AJSHw2ZbgjOpPPOhnHgwLObgTJ9V/GiUHmH19g756Ldpiqa6y4j0BfmNUOdPQfFsztayQnHB
wF9t4mKzriG6N0i9uVkY5dUwkStEK67pKV6PvNljuDfQIdpNo7SDtlM2k+gjvh0+QwPDa9jmpt6S
cLn3Jn8O0qjx7o6N8clcRko/sFnv5Wf+GujpZRZ3tMoH/quYdE60uFq8IME5Ub2pytnchEkuzGfm
u+9J3ZbazKco5nJwLtfdJW4+vj49A6Lq65hV+2phPv61EQ0exmfLRquD0jEzGjyhz5ePZtldGAib
g/O6rxaq/ytC3hAN/8znB9H3mcsA3EDAMX5zaWDpcltGVv0pmf2bsKvedXp2VPHVQOCfEHL6Go/b
rMVq3mfWxKR5DtU5B0t/tSd7jUerxOSa8oN6M7Rn42O8PQFojyoWioLmVP+uzam6yeZUW4Oy+cdk
NqmmfjmkmvrTsynFx9eRp6A1y83N67TFMr+pgUmFU7mB2QniO6FNjj8msylrRH6ZVFJ57aj+3dpR
dZPtqF6mHX8wXtCOIcV0uH2tKSN3/jn5qLWJplWf30t3t0doWvHt9NKulsZ22tAaa9jTSFvb72+M
dTZSXyjtSXr8QbqnsYumw+mqYaOYDCerhoLCuoMZ50xZ6KRSRztiNKmyN8yIC05D4Wrmozc2RukO
r8+z0xukRopTwxIc2kPesdrPI4m9xqh2xqjynXzVbFIxV+lXk6r1Mp+e4RW9zepY+72Kt1eFPbnh
d7eS2b91p6tEfpH8Vtis81Umrl4qPs02w+rh/GBiuMtI9SNkNq1ar8pptisOWnMvt9eZTyo0CEuP
qX7demjv+2uh1gVmaK6iUJm3fu33ykf+qc+SseVnii8vXqh5x2FWsyJinGZ7cN8yFfPV6w5Wc9+N
ahYLW37NPqh6Ha+6jPiqyJRIBKpEYbt7MOyHsJvY8PVZxW+VEf8qMpUbp4cOkNmc5enLVprygrCx
NlsXzBa6r7W9nc2o1k7Y+J3UFbRyCKswZSaxZWQgRpaLaD7KJRGje3Y/0Bjz3SBqyb+vY7n57ljz
btq0t7G9q7GzrOQ/ysh8bW19PWxLv3iX2fyKkpnDyXBtPcvQmSw9CWnUoXBtnXF/Emze+kNy4WD4
cPL1w8kqP/RyK6vmw+7gpwc93cFLB8tKzsyS//LB+K/Pva9s94XSv0JpJ+imYRpB2zphmkDX6lih
Ht6DBCT0oHLm+fPNToHtkdeZhZJIi3NP3bLaCYFo7mTgqQOBury/D/pfDsYwQSq4EUTMFMaGgpYf
u9rKW1l+xEqnYJjfDpPfJg8FP2WjI/KMl9kRZhe07JEE4QrzbTcUYPUoU8z0h08eTpaVvFNKrIc2
ymdlmleSJCRFyDBZXFiEQoiQIV4e5JX08WH6mGfmfLpwaTUpHi8bH+flHPx8x1uQY/wf2PmOrKgY
/0tM2X7PUrZxsvWM0+YoV22ONWc055M2R/GZqPMtW/MJd2m2YnLcPVvsuLz++Nxc8Jm5E6r/mbki
lW4ozzZXbChnys/Tk2N3bChXS4wvLKcusd/NW6bvcz618rn1I/NbJ8cWurcG7LS0AqyndznWFLml
7OR4hXvW8d6J9Gbud9+VjesC7Ex2qaf/fM/UyLz9O/1H7GvtM5ds6b+7MvEvF+VTFx2nLp768HK2
osJ1IF51cHBmInHe2XLeGT/vDJyfiM+PvXhpetSfiF8Jzz864Vzh2h93box/eH77/viHE/Gl34cK
Pg+Jb0PX+DR01Xeh7U/G/2ellD4b5m7Ok7bFu9XFR1++78VHX94z/dJZ+X3/E/G185sX7/yFrq6d
XpwedQQmXtwyEXBMvHnnRNFdE20TP10xsd0x8bxjonrVxPH1E++snHitZOK4NPGJdNEWOnU2/Z8L
jgVdLZkebRhdH3c+bNs//ZL6pv87cd9iXC1646Ft55xTF865frjqXN3qk+fq1jinqlSb60dx/+Nx
tZipnYuLrr9P2J6/cLF784UK1x8lqmzadEMg4fpZwqm46hLbf5a4f94ZTMyftk1eca52PRh/0d8d
9yfj/t64c8pVufhQ3LnK9ZO466m48+cJV0di/Z8U2zZdmH7pQpVrX6LCdU+ibrzC9UCijs0c4hCr
BYIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIg
CIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgyP8VJ0VBovIPiExIKdw8QQLsImQj
V1H6iG+Hj3JcIbq7i/o8dSHa2n5/4wbTO9bZSL0guofuClE91trc0sUErgP7O4TJkd6BzsGjQ8le
Gh0a7OtNjoyN+b2+ehcx/u/vhNQLP1+QRvd0dFF/Ta1ntMrnDdJIrNxoGNm9j3o91X4uY2WfR5S8
HlHRskruEhUmED3gSZmxC2IKk0L3gpjCpDA2xKz1+A07ywT6kAvjqw2Y5Wq/MCkwqPbV1ps3/po6
YZIfoc5rRajZ5fEKk7wIYNHUEaMD4QDt6qA+YVJbSzu7GqPUbzbW56/1DBhlq7k+/y6QtTfu76ID
QeN5BvlzESbwfMjysMdIyfXhD9Hnpa1NELvxgUoadtW7aFdLYzvIRhVfTTlZxU0DhnTJTGC9tJ60
Rww8zEWYg76QYVstTCyfnAX32tPIO+0++AT7W7JlXJuN2jaVbLE1k2ZyuwkUQG4zWDvWjrXfTrD2
r2ftyFeCH3/z2Ddlm61I3mizmSI6GmbpRZA2d0DG5PfnWTtu3uQmcjA0QZPbagK5dPGfzfJ8Orp7
LxzTgrRhTydliXOstb0LTnNej7ecSDyYcYSrtY5wvpBpEHJZZwRKG6J7g9RLLaL93aO9Q0ErL/ey
U2JFztU4Riw9MPpCLsO/gQ7RbhqlHbSdNjuFieve7lh3E+3saOqiY2OUHxbNhgkT0cAacR41wzYM
dQuTaEd7s5Oyc6h1/nQJU2HCj65wqg1APx/MR5g8uBSX0a8aYXLVmO3y+FhD6ncAwmTH3qhxfq7n
ygBX7oh07Gt3CROjIT6u9xr6zq7dsS7mJ0zAYKfHb1TvD7k6UwPJ/u7UEG0bFCajwyOp5MM0OXj4
SPdIqqe/1zy213l2CRPDnQ1b057dnS0waPAU6dEjdISZCKtB6vfR4d6RYaeLXIWD3CYkUTDW3mPH
jjUeu2SbH9u8Zfq+hajXt6B5v7Hg9d6xQL3yguwtdj5FHM4zsvNN4th6hjqJzVF+xutUbY41ZzTn
kzZH8Zmo8y1b8wl3abZictw9W+y4vP743HTns3PBZ+ZOqMT/zBwtUumG8mxzxYZyZvF5enLsjg3l
6rENagmrd3r0yilRP6tr5XPrR+a3To4tdG8N2Cfff5mWVoDT9C7HmiK3lJ0cr3DPOt47kd7Mfe+7
cspWbHxYWRcYOvpxc8tbr/ZMjcyz++4+WxGBHWkmThDk9hIgJQRBEARBEARBEARBEARBEARBEARB
EARBEARBEARBEARBEARBEARBEARBEARBEARBvmo01sP1SuB/25tjlAaCKAzAz00RS8+iBCKEMDwW
DQEt0gg2ioJFsLBMsYTcxc5C8CyWHsRKcWcT8BLfV8ww879/5n99Pf9quunnaIh3satrc/Rx9rhe
n/7nzz9XMf8ejacxGl9O3u/e7m8mMcvcZkZ02WV20Z9LyVnEps1sNxHb0pWyPexlyK9rPtT63vy4
N49omibrfbt4WbT9fSll6OXT7TCXD6vlRdZ+DnP1t/17uVoezrU3K7Omz+P3pKr/7OcBAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACg
9wdQSwMEFAAAAAgAopTpTpCT5A5ADQAAPxsAABUAAABSZWN1cnNvcy9DcmFQT05HIS50YXDNV3tQ
U1caP5cASYhEICqXoHBvbmcnTcTeRAyRKq6g+KA8Vqj2cQVijIBagyEQtDbQdvZRZ6rVKjrTTt3t
zpR9FdRW6rRdV00VY3qrUq2F7WMYiyObxq7Tan10w55z7k1IAlpn/KdhuPc73/l9v/N95/vO46oA
AEUOS0V52SKaon6VCUCP+oY2cwQQJPhPuKOxodW2nmp02NfarM5k6We3wTeGPCUAqQBMZ5UA/aYb
WSSdQtJM1BeAknkmFCqhMCsXCiYozDZCgYPCTNRFR9ibkDAHCgajCUpuJJmhkIYYsX0wEjUTEVFI
gG8J6kGKgvHtDbOhoELgWVBgMAgxbonmRsKvI7lzDIgqBUdngJIXgOT8vUng1lHkxt/qQGJReWnF
ksfmVy0pL6MaLQ7Kan+mkVq4bCHVZphtzk1u19k44s6IXGPj3J2UgnEzZUxx5ynPvqxHTu4IpvTt
y2KW+zYf35flu82UJcpeaTfHL50CXtFKF/nyfRkptwu9vKq6tXMTskBURiAPOPomZw8GnrZxyoTA
/kCljRuuWvnwwxlypCISgKDzK3Mg8nPP5OwhyDDYGShU1DachSxDF2D73Kp1UDOwL2tQwf+VlXvp
a64jjFZWzSzPCzxF/lBAQC8Zv+egDo1+4DQeW79cX+2HVoiE76smJfxAtV+BtXJ/E+MYPDGMUCag
NxGDx7BcrC/zk7IQ+MvB2oZjgUJWArOODL4cDtNsZBq9yJHCEr/ngI7fFqcElIrPkrDKyTX8TMl1
ggA/XktTEonA06ODMfIJ2ZDf9ALjp88YiXgxZm+gpIWT8Pqc73LzpsTnts8HoN0Y9yeJwcy/mRMo
aoEpOPVZiss7iKPr5nt0fvlwwQhJdItZqlqJEtWpJfyMSp0uU3els3Hd3ky9uker5F81JiaoO46q
mJRbeV6VSsXAVMJ2pnrH0Twv/24NjORwDSU10zI2izSxt2SsgZwc0rPXtAx5GQGmsmo4WreLcTEm
gkEvOKQX9VxhLyM/QMJGJuUnrHyxFhrvrtXGmz8mv5Fpe81fk56QkvLLtGfNn5AH+R212l7y7+j1
FnmkW8Zf0/fD/wsn+kll9xwYnAy34ANVgXaCTBs36JWx8fIFpTLZYOuBUzgoBXldh8sHYuDYgf1e
Ptu0ae4pdzCHimPcQVYrCc44umqdMgH36zP9gcNecdo8xKyQtDM3JK3NHS19OK+ezFnR7Ydi2jmz
RpPAn2ZxPsMaiJF7ug1i292pTWTcJ9tdXmE9Hdh/yg9rKlDod+ib9O5HXHneyGWnnQApVeqT7WmZ
ngKW32g5q4d9ZFLU+Oqe4wXsBWYI8fkhLVwKsKwHt038BByfw+5znZ7Jbn7n+ExWX7XStIMxtePM
ufznvLCU3Mwae2unVvZcYIF9S2BBnXLHlk5KdugxrdLWoHwp+WH2J5/MN9U3bcFz0EvNbo7exbn0
jfqN3gvIhYloygP7z5E0biYJzUFy2hChBPLUDrTMDw9O5T+w+GkFyCiY53P7KaLbGG91d1LrmGLk
KjkNtXuoYtSc2sQspyZMclCyeGq6Xt/IwB+EpjLuw5QFxxcopJ7II+sY91/S3eZSvZ+/Y/GrSANc
unq9aQc5xa+zcqbnGVyk8A+2/Px8Fi62BJABF4XanKF3d378g0Wdmc4SfrgsjlAyRpVyM49UpQNy
os7CaawcnFd6J6ep52QFbf8OZpulBUH4YuNkXs+zmf/zxf0u/2XOuJ3L38bduGl8haN3cO6JXZcf
7/K95/LyX2g8Uiv/3kN8aTK5n6+qNu6Fy3IYCa9y6LmTy9/L1fDO1ZpdnG+Ob5qff2k1TRCaDghb
Z+zg+Detug7uuQ9Pz1htfnp9nQZrPDNWIxO/bhfXHViGTIx7uEDJTg5C281Tg/tZctGJTbvLzw9p
juV3cM8jQexu3fX5mC4vpAx1/zZsvkvA7Ik2321OHdMHeeZCV49/Z4WxIGD7InNCcP8XotVeaLUI
wYTOUbq9nOAInBs4SbCkqhKl2SyR4WN8aeidcsPbXWfvT5T1I01grfBMBOU+2pfiM/hSAoUsIZ+X
3p/yVf8/z87xCqeHppTzBp4u5cK7QGA/ZBd3cbTL6xzclkOnpdY8aJ4cKHFwulLOj0HMkCD6p1DH
HAqm9QB1C5mWcrCchtz8Sf0ggkskEv5KRjSnHm01laVcoMjBebv/Z3pUPahZyfGKRN1KjhkuIMhv
C+LIK1gXmIZ1WN5GjcrebAEbxxNTPKXJBckXxw4BNzmFWnWR/FFXyek1Kzh4qgWKCjn+ki1Q0tzK
f2Qb5o/Af1U1D+czuR6ul+UMPuv85u/1+ITzXhJNAyUHObj/qqpP8OuSdSWcmwm2a3/js2tJuHH/
/vbENW4mnb2DdnDyG/ZmYhr5QzpLpxdQZqIbrlN4xgOUBYkcrkH/okYmc/M71C3y/TITYIrhP5wg
REh9p3mSMyWjbUZj4VKNIAOem30DWbp3OXRsD2QFig5y5zwH67IAGZdFNTEKPOdTcNxufY/wJ8yD
cGUYmGKFVwG4coIy80PaSUE5vHLQ2zj3Dalr+vTpvquokF7m3Jel5dPdl28eICfg1s0DLtTEkC60
IMfaz8OdQc/5YmwxD9n3HiDTfDT6w7pezNKLgUdCLNqMIKFVwvsPhPiOCB1IDJLfQ15fcFxcz3Rf
j4i7EY0jtAqE2cH53kAA+P5qTM82secT+A6UwPR/ZPPrDnKmAv0t4nzdiUDTXI7474j8FmHOho2+
ugsQB8D3ur46VEsw13pY+uZsnbIergCSnojqpBOdE2KdVOuX+0kJvTdfs5C745GoHPUgrrkeSN6q
B/FJ60DCznVA5l0P5Il2kHTWDibtaQRTWjcCstABMhRNgDrTBLRLnSDnqhPM2N4MDHwzMJ5tBqsX
t4ANb7dIzx1ukfYNtEjPq13Si1tc0s/bXCPA83aLEvCpdXx1Nb5F87s0sIaVUt63hq8TtGs5gN8y
2NueBXth4fLP2sIW1dX4/sxLqZCqQMVLNXxnzUqwzGZ12CzOBvsGyr6GctbbqOIGR5OTWt6w2man
In+LLM/YqIYNGLO4oclpd2yq+Zkf/5LgxCTRCaXgRGdNGii1LLMUU5XlxVVUWxtlZA2zQ2Ai5B5q
xIuWiYJlqH+S4P7k0Q+bp54ob7RtqLQ3O6y2EBMIIVEjTWQCIR8ocL/uQycKUkI8Ue5JRVJFiFQO
Zs/Av8crQtbyWIwCmAXMgvIVZbyIUsSiJgBWQFVWzV9WFYIli7C40fmIC3nXWTMVVDZssK63NDio
pfZNTc4G6zr8uQLzu2p9eFpSRA7pKEdibIRpAqEKVKy3bKKaGymnnTIaqCabs4lGPC/a6G4XPFCw
gRESHV1NHworcgXFB2HFk4LiWFgRJyh6Yzn4WMSnYQUQFP0xirCYnz1+5YPRMBPumkgQWx1TASz5
oorH8yl2dAmgybA5oCqiVvkt1N2Gk8YOJ5YtBQS6IspBWagKqpwqo9C3eYthhkHoieSvji7/qKKO
GiFiXatB5HqgKoTP+ra20Dp70Yb2Dp0V212HX+ndLt5djVtJcKguC7/EGuoWAxv5dgTw5WEtERFu
eSwWL/j7xMbwwj2M7v2lOJMQsdpGfbh+z9HeiNSOKEbuNZolarQb9+T9YxSv/J68tQKvOJ/4/dpo
KKlfu3h2VV9wQ8jsOTjnX+M5Hw2yLUKHhLJVA/yG8KmzqAYWGQFm81tq+efXnD5rjyigXXCkNFF9
nyUWN27kRGSlj2XYGskQhY2mEzaqCGNCGP7oas+exlAkZjGSPY0RQLRRpovqu0ey9YEj6XqQSLZa
hK0W6sSFk15L82EIEGLIsqAwWjdGrK2OcJ5aN97n6ruP6MYy3Hee8GFA946TJ0UT3RuTlaiI00IB
FjruHsnWB46k60Ei2XrXSKKTFY5F0fTLjiXWmD6EM/IBeoaJVliFa4HYeQx3dlnCtwMxjfyoHtLe
jWlUkYgnKkLxmlDmSjxzZ5qEe4RIW2sRrgmbmYtJczE6C6JvStCn4KEwQzyeFJHSFz6zeao2BrR1
XJA49FKncMmhj91l6KRxhr4e3meuOoULD/3p2FUQe6+R4TuBiLyUg870EF4cCG3081b1+Z1YEQQg
fKhtxl8J0C+V88O1Ya3gFzR6dtVAj9OzvTnCx6TwbrG9WbiD0f0/72Om4GP/z/q4vTnKR+m4Pk6M
8fEpwcgIQOgdMW+Thdz1xyhQngos0FvP+zZPon2MAYpwcUsojvBVODQF1aOXsc6aSYCqWryQWrGk
rGzhMmpJJbov0pR4v4q+lka1PM319xwgPjQAGTtAxWPzn1y4zHC/gyDFvwYAP2yjO8LX5tbL+B69
Paw4OoAVHXjRdYTnQ42Tmvo6zo/axa+wWfLp1zHoBfQUPjXp13EqbrfQ27GyHCs7YksDiS3wM/Uf
uXx7DCJuFOGKRPwBOSji3slFm2dz/ZkRy5VLMNPr8fPP+Anexk9wGT9b8bYKv+obAGjaNFYFRoIj
SHgUAAeW7djHtXjmruLvL5wIrM0BCUP/B1BLAwQUAAAACACglOlOL0EbTG8ZAAAfAAIAGQAAAFJl
Y3Vyc29zL0NyYVBPTkchX0VYRS5zbmHsnXtUU1e6wHdCIEEeHkElIsIJcc2kRJ2AikduTdNjBB/l
cYXa1xGNGBG1Akkg0aFHqGumU+8ardZSu9oZp71rDZ17p6J2ZHpL8ZVWjOlp1dpaaWe8jI+pN5Pa
Xq31MU3u3vskIQlqfTDYdWf/Ijn7fPvb397f3t9+4R+AR3/8cr5KXnzyvP2iHYAC7lgMqKgAr/dI
wA8D//UAg0QT/qYo9J0IKAMAPExTgKVkUZo8VgRYh6UABQaKFr9/bUtXy3k//IKet/xD/OfFB9sU
JWeZjAyGYQDjAAwFGAMDDAAKAAtkDBOluRQKHFiHBbBMhhEMCF2nuru6ftV13n/ts65nup/Z0915
6lQ3GGDYwIONlhtp2iijdEYjMCoB5TCCJh1tgOOv442R/hlkFUagM2IdXmegjPQA+b/n5LWurjf2
nPd/+9euzs7Ols7Okyc7wSD53+SgaQdPUch/GNRGBzBQNIPi3+iI9K+Er3BABazDUsxA+t+ytmvX
jvP+89D/Zzq/Q/4/AwYY/Q3i32igaYOxz38D8l9GgbEwGekfa6wwiP4b0OoA/efBgNB1snNP1391
+f0nYfzv6exC/u8BA4xUiWENUXKWz6B5PpFhjGj+Jxp5YGBoRyJQMjwfOf95voIHjBHrsIwjkaFl
YEBoOdmyJ7j+7WnpbOnuPnmqBQwwvOg/bwADB+P/Hm7NTHiJPbdXlDBYBOa6g++3gt5Wed7HN/nA
rSMeRuCXVDyl0KKYomgKDCoBr40suwjcCYXiw0Cx7O203AAcGRQDMoCUMcLzSIYGHk6WOmCKphgw
mATPTyw7F9wJs8UHf5v+8zojTcl0NJAajQZKR+sAbaxA5xWeMoLBJHR+YGeDOyHQa1KKb7od/6Vw
m6V4ioa7r5GhqID/8LySTTnAYMKK+4eRfbwQ3AmLAvsvxRpvx38W+W/E/vMy6L8G+e8wIv8NYDAJ
7J8Ofix7V/vv7cY/Y8xI5BkaJmTwyJExFs7/CgM8r/CJPPjnom+9M4D/7/CDOrjBFYlCYQoCG+7g
w4ImuM3wPO9D1zSeVw5WJ8DbnBHAWx269mvQzZ65J/4vYVgGXr9YePt+6SXAFmpYMDjA2xz0H93s
gFQB4G3QcE/8f1zGymAEsDAcX30VSGcrBsv/JooP+s8rAOOg7s34s3wTvJ5JWQP2n5+r0IPBwUDB
K60R/3LDoAAyAyUD9wLWaES/WgjEv2GRQgoGB0MiLwNw6iUygNfDK06i4x6NPwvHn+cNlLj+6Qdx
E2DuaFf1fw/gB77/FRUVyf+RFP1wMAYpujOMxruzYLwzA+xNKBooDvSn6B5w+/Ejv0vAPznf5ZcD
P8hOSlLHwrcUSgLiQAKQAPS88Rawt8cA/O+Dd3oe9oNP5P4tBRL5loIW+wl7B9fJubkL80/P/1/7
Rbuv0Qcu2tEHbOLQXkYDKTDt70jf2zPrwymPioujKo0u++8ylVyVJikCByLrGQJA82TZhUnoa+La
WUN7XgNgT4/ffzH+dWrOxfh3R5S3DH13xFyUKmsGEiX4n+kWU1lpSZGKrqtxmFfQdZbaZeYqW5L8
k6vgdO6UZACGATBOl4ytj8vTodRBlJqI8rwwxUyEiXKYmDwJJvJhYmoeTHAwMRFlqcLK56PE/TCR
m5efjH9BNC6XgYkUZBGX94VrTUSGaJSAzxiUgwT665fPnQoTqUh5MkyosRKy2BRpGyUM4bbH5yJT
FPYuF6ZcACQVbBkCruxFzfiPahA3vbS4bNZDD1bMKi2h60wWuqr2yTp6xtwZ9JrcqcykpOYcMye5
5o/PNnN8G52g5tUl6sK2g86tmT85sNFHHd2aqZ7nXr1/a6b7qrokTvFcMyObPRI8p5EXuQvco6ir
rEtIrXS0rUIlkKk8EO+1HB2R1et9wswlx3q3e8vN3LmK+ffdNyoeiSSxQJR5ksdDzU+dI7LOQAu9
bV42YWHNYWjlzMfw/cii5VDSszWzN0H4nS7epfravlutUVSq503xPq68qJfAVqo9zp05qPYdh3Dd
2nnaSg8shYwIRyuVMUJPpScBS+M9VrWl971zSCsfaPMlvftwulBb4lEqgsp/6l1Ys8/L6mLgqKMC
fzoXMlOvrnOhhrBzPM4dOcJ6aTKgU4XMGF3yiAXCxJhvJBLw7dcpyZI44OzIgT4KsVnQfv7Tao/q
wzyJLOCzyzunkYsRtOPPT5oyUjap+UEY8HnSV2NyGeG18d7pjXAIDn5C2V292Lt2oSPHE39O71dK
2gOjVDEfDVSbRuJRp6anKdK3pemk7a7R2vQOTbLwfF5cbHrr3lQ1dWWKKzU1VQ2HEr6PTt+4d4pL
+MMC6MlbC2g5o1LoMpX5uisKXa5yRFCu+1qjVp5FChm6dFhbu11tV+dL1OgBq3ShnC90Z1E7QGy9
mvo7Fq5dCAu/sFAjY95XnlZoupmTSmdQSHsUmsPMB8qdwsaFmm7lf6LHb5W72xXC19oT8Ofj904o
k9vvh84p8Bv8QlGgSVRopL0uhU4WbyxWKHodOw5ipxKU3+Tg8IE6sG7vdpeQlb9q2kHeN56Wqnmf
ThPjm7B30fLkWJyvHe3xvuUKdJtTMjmY2jQpmFo2qS/0Yb86R0+OfB8b9T5+ct8gCId0eDxDEqgT
72zPDbzzbZo4NX+g2e4S59OO7Qc9MKa8rMeitWr5n9inuMKnnSYRmkxNP9CcMtqp1wn1psNamKcc
ElF/esd+ve5j9RlkzwPNwqkAw7p3/dAPwP77dVvthybqVr+5f6JOWzE/f6M6vxmPnN1zxAVDiVcv
qXW0aRRPeY21TV5jdfLGpjZaseshTbK5JvnZpPt0f3cr3BnuMcanYCuzX+BUmzm7tk5b7/oYNWEo
6nLv9iNKFX4dIr72KseckSSD+GGtaJq/1ZshdJo8qgQwSv+Am/fQkvY8WRXfRi9XF6KmKseg9w66
EL1mWNXz6MThFloho8dptXVqCFQdpubfok3YPy9LPzpFWa3mX0/jmWKtR7hm8qQqc+HU1WrzNypH
enKquPwWNQ5S+IFvHuFBHZxssWAUnBTpzCgt3/b+RVP66DSdxAOnxW5aoU6lLk9RpqYB5dAcE5dd
xcF+VW3ispdyCv2az3xZjFzvgw+dVOFy/nT0d27pzwt+yeVt4ArWc5cu5z3HqTZy/NBtZx/e5v6j
3SV8nu2UVwl/HCsUJym3CxWVeVvgtDyHEs9z6HsTV7CFWyDYFmdv5tz3u8d4hGcXqySS7Faotjyv
lRNeq8pp5Z5659CExcwTK6qzscQ5YTEq4snZzLV756IieS9y3jmbOKjazGT4tuuURe+teqH02Jns
fQWtXAtKBLIdmz/tl+WCJoPZPwsV3yzqvBhZ/AVmWL88aGcabOr+81XQF6TYXMTE+rZ/Hii1BZYq
QmpiZp+5LZzYENg3sJNgSFXEybN0klFutTsFPalLrvbq2hNxihNI4l0mfseBUrfKTblz3ZSX1Uni
H0g7Qf35RNfh+13i7pFdzLm8TxRzoVXAux1aD6ziaJXPsXBNuw7Jq6bA4kneORYup5jzYCX1GTHp
GUnvsySoHTvoK6hoMQfD6QwvHND2IvWYmBjhi1GRNrVoqSkv5rzTLZyr/bv8f0nvzZ7PCQlxOfM5
9Tm9RPk3vVT5BZZ5x2AZTq+n+9KuLFFXKkhGOouT9EnH+1cBF7mE9NTjym9zyjlt9iMc3NW801lO
OGX2zmlwCO+azwm74U9qpQD7M2kpnC/z1Hiv8zAXtHiHc50KFPXO2cnB9Te18j1heVLOHI5X+5o1
/+qu1Sjhwv3M1aFLeHWa7hpawZWndZfjUpQX03SqND3NSNrhPIV7PECjEBMP56CnqE49evWb9BXl
2yX5QF0If2AHIYP0+ezHuPwktMxkm7hheWAU3DeP9mTm/IFD23ZPpnf6Tu6Ic2d1JlBKM2mrOgH3
+UjsN6/tED9iP4hHhp6RVfAoAGeOT8GM1Qz3xcMjh2o9x1+S28eNG+f+EgXSLzn+rLx0HH/28g5l
In67vMOOXrHKNjQh+5d/AGf6nMcKcYkHUPnuHcoUtwp9sKwbW+nGiruDVjSjfBJNMjz/QBX3bjED
JX3KC9Cu23ddvY5x7o6A3qVIPYkmAels5Ny/Rgrw+ed+OesDOR/Ap3cOHP53zZ6cnVy+XntFcqz6
Pa91Gif5yh9/RcJkwZej1R9Dvd/3XMg5Wo1iCY61FoY+k5WTvBTOAKVqKIqTNrRPBOKkUjvPo4xR
bSnInsFdc8akWpYCacNSEPPbpUA2ZDmI3bQcKFwrQHxcLRhyuBYMf7EOjHTUAyVrAaMSrID+0Ao0
s21g/Jc2MGFDA8gVGkDe4QaweGYjWPlGo/zIW43yoz2N8mPpdvnxJrv80zV2P3C+0ZgMhGHVQmUl
PkULm7NhDCfLBfcSoVqULuMAfipgbnMmzIWBK/zUHCpRWYnPz4KcDor0qYI8W2hbMB/MNVdZzCZb
Te1KunYJbVtqpgtrLFYbPa9msbmWDqfI9KSZrlmJdWbWWG21llULvgfhWbERwwONSBYb0bYgBRSb
5poK6fLSwgp6zRo6T5c7NagsCTYPvcgCJePEksH84WLzR4DQ/eXxR0vrzCvLaxssVeagJRDURC8p
AUsg2AYa3GrzYSP0VNBORPPkAaMJQaPxYOoEzMNlwdLx0ToJgBF1jKWPlAS1EqK1EoFO1CqveHBu
RVAtKaAm7esPabB1bQsyQHnNyqoVphoLPbt2ldVWU7UcX1fg+C5aEeoWKmBD3mcjLtrDFNFgKihb
YVpFN9TRtlo6L5e2mm1WFbKz1qxqt8MNBRfIg4b2LlbtCgkmiYLOkOAxUbAvJJCKgu5oG0K0xkch
ARAFJ6IEoWRB1vUjH/S5GXvDgQTR0ZEBYMhPL3u4gNb1TQHUGWYLFIXFqtBE36g6eXR1gbClgWhu
Om2hTXQZXUqX0DCE6cbcCbliTrj9ysjwjwjqiBrC5nU6CJ8PdJl4rV+zJjjP1prR2pFThct9A2/p
7XaBr8RvQ2BV20zCrKpgdsAx/9/8QCgNSSVh7pZG6+IJf4u6UXbhGqbq/qE0JjZstvW14Zub1vbr
cKk/wX+z2kwRtV26qd3fRNiNv6ndhaLdQH/i58t9rgw7aRd0i476VgaLPQX7/CTu8z4n14TJUKJk
UY+wMrTrFC2AQSYBU4WmhULLkkOHa8MCaDOsKSUgvsUQk17Xc0l4pPe3sC7cQoRupDlxoQorLBGr
37vY+WJd0BMm4MmLdWGKaKFMC4hv7Mm6u/Zk2914ss4kLrVQFpg4aQtVQkgFiD5kmpAbjvqwudUa
GidH/S3Ovlvwrr+FWx4nvBmouq8zTglWVXfUqER4nBJ0kLXc2JN1d+3JtrvxZN0NPYkcrJAvCdYf
ti/RhVW78Ih0ou+QoUeqxGNBIHMfztxmCp0OAsMo9Mmh2RtZ6hPE4Y4KE7wshnky7rkPreI5ImB2
oUk8JqxWHx8yDWtnQu3LMegquCtkQYY7JWDSHdqzBXphlNK66yoFqp5tEw85qn03qHrIdar+JrTO
fGkTDzyqj/rPguhzjQKfCQKap8ajPT2oH6gILfQPLDrqsWGBD4DQprYa3xJgu1Jt7ywLScV2wUI/
XdTTYXNuaAhr45DQarGhQTyDqU58fxtHi2088b1t3NAQ0Ub5dds4NKqNj4uF8gAIPsP6bYQ4diei
BGic9CbYWufbZmdcbb8CyMOZjUE/QkfhYBdU9h3G2hYMB3TFzBn0I7NKSmbMpWeVo/Oiig6cryKP
pRFvzoalN61AFqxAGV1B2UMPPjZjbu6tVoIEe3qAcM6sag0dmx1n8Tl6Q0iwtwcLWvGkaw31Rzoe
1GGv4PFJtwuPmE0Fqlew0tPoW7xqql7BQ3G1UbUBC0uxsDU6NFCyEV5Tfz9JaI7SkPZp2MM1foEa
GNB7cxJaPBuWfug34f+iWYHd+PcvcOPfOIXfzgLwPPwBm86CIKtwUDjwhaQK30KsuL4aLLfgdC3W
XIa/vwT9aE5qDqTw/306kPFhoO/eSf8IxnhHuqjC1+6q7aw9XRtXN7NuTd3P6l6q21r3et3+utN1
P6qfX19Vv7r+3+o31m+r/6r+x5bxFqPlYctqyzuWC5YrljFWtXWStcBqsD5knWt91FptfdLKWzdY
f2dttx6x/sV6ySq3jbXl2B6yVdlqbM/b2m2nbV/Z5A1JDVMayht+0fBqw86Gow1vN7obP2/8S+OF
Roldbk+1Z9g19tn2FXaLPegEgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQ
CAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIBAKBQCAQCAQCIchn0s+kTcyxmGKw
cw4AwfRFOwDn7RVAn/Wr5UxWZIlj1UDvd/5mqpPiQ//wH+79FP/d3k/18o98+I+stufxnH7k8Xb4
yVRML3NMc6ehz9PT8jZwqg38HP64Oom+Sv11+gj6lArMrJif2m6X+Id6t3uGgf9r735j2zgLOI4/
dt31wrLixmH12kg8dqLNQl51dtPk2hjLcc9JmrWxZ3sdBARyM5d5dLGVeJMrhUs1CWkSb3iF1DdT
KrT3vCuvoG/7gu3FpkgMBG8grwgaqhCifwzP85yPsaCKgCAC7ftJ4sd393v+3UXROXqk+2pIhHd/
dO9Y59CpI6GbQj+29MF3fh9tf9DXD0LdDQl/l3mJ/zwmMmdjIWdKHLiooh/ANDHhb094E+pbCCuf
yn8ub4krr1jRZ+dULhX9kntO7R883T9iKREhpG5Aqh2jVt4aVaWlSsvPWH44H/QVj6uXcMTSeW9u
sXrFU/tSqh2V8Iphr+DpcjhcVKUVTy14qh2vsGG2VVk0pa4f1TnP3y4W82HPzMR8m0J3oGJWxO/e
DEBtWaZexNKPDPIWL3/D7083JMRGUbW/obcLBd1uzy0W3Z7pt1DwBmXBHF/Sx001VS9v+eMPh8Nm
PO5CZ8FV+wuazl1dNrnileriXNH0Z3K6N7+9YnVxsK3rqZNg5tuPmisjioO8Libifrmsro/aiE+o
/FtuyuQ8fVpV7qgez1E134I08+/oZjpq+k7d5Hx/EWPJntR54UQ8V5VqI5nU22NSyjF1+tRu1xx3
e3oew9G4X1/vjjhqfj3XdfXptqNRRz+rJxKJqAHKlG2nJvzfq+iwyr9aV/QTY1yTV+25ruNv9wqF
QXtP+P3q3yOn4ER6eryD+u4g77qplO7fVc2ldJmKplxd+uMRG2r+G3r+tpm/Zej5D5v5j6bMxL3i
zVu3bhY9AQAA/m8cu/PRbm3q6wmR80b6OXko743Exjf6BfnxXQEAAAAAAAAAAPAf4S84CpYd/SJ8
76n3nvxN5b0nf/vpA3eWC+onsrx+9N5TieMfN3/60Y/fdL4odqPnxZD47lh0rHryzQ+/FcuL3J71
KDm9HiX3+PUoOb0eJfeY9Si5f74eJacXlOQ+WY+S0+tRcp+sR8kVcuFibs96lJxwhC0AAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAMDBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4Nd4I3lgy/
JcJDIqw33haO+hJiVP1kZLV0SZ5fa1TKS/MJ2Wn1mtdkZ639WnOlK54RIRXJyvmyrJdl1rbtf+jh
GRGuVUrn69WXLj1mDGLIvJ6WldlKqSrtc7JYrrr+uwtLL8hptSMmDplQ9cL8Ql1mzslK+YWSzJ6e
OuOkHb+VuHmdlOcv1qQ6Xr2wVJezdWmnT8/Iol8vJiImZM8kq82VtWaj22qvyvZV2X21Kedaa+td
OTIY0+XWK822/HvzjdebsrUqu0FE1VlorXfba9e/uVcQ2bM7OaNHlLXTmcmZIJK81Kg25mStPFeX
m5vqYOZscjDgIKImbCpOpqdm1Bm5XKrWSmpXUl2UIOJfnOWvlDvN1Vr7jbWVZjKIBpGStE0rGSet
5r+f4Q6GrK/BZBAZDC0Y0nQ6owZy9pQWRE69VPEnetYcdMzBU2755aVkEPEHkjHHbf94rT5brat6
QUQHTqezfvfZmWSttbpyrdFak4vtIHJ9vdta+bZcab/eUVfyyrWm321mKj0dRPzq6rTNXZytLeiT
VrnWuC7f6PiXMXX1bfV65PiNwv1K6AtHjofmxbw4aM6niANG7/RO7weJ3j+bveN/wvef23oubFmR
6GgoNNjVltmMXG921xPJven4/iNpdb/qVmdfVrecmang/fPZyem0HdyO/C3wvEnMlaty9cuOvnfO
BJEzZ2StXqrIrLqHvVhWdzPZM+lV/z0RIv9ixHze+dWf9eu+Psrt4xMWn40+G5+NxP7+6v3XBX+k
hT/Ora2t0ta90KPNp4/vvtiv2Jl+wX62b9sn+9KO9qP24cQPRDyxHU28L+IntmVChOKxbTuRD8WH
twuJ74Xih7criQ9D87dTQ974zo3U/cPxPx774cPd2q2H5955eDsvsu88lJG8HIl58+MjMZX4U29n
8+RILL81kj+i+t29/uBu0L/q64l3j3UfndjZ7DdOOOGdX/9EDo3rSrvT8eFIyvJ2boyn7sd/ebv3
tKn74oO7ocP+P1Y+7wz+L/Kz33Ufqe3Ga6EhM9k/fE0AB+uvUEsDBBQAAAAIAPGa6U6zMQvTmRAA
ADsRAAAYAAAAUmVjdXJzb3MvZWplbXBsb19ybGUuemlwddh3MFx92wfwRYjeQkRsRO/hVqJsRCyi
r2ir7mLZ1TdEXdF7L9GzWF0Q0aK3hAiiLYIIEi0sokQJiWBfyT3z3s/z3vOeM78558yc+f73nc9c
l74O2SUWAABACeh/GA7qxEUh90kBgB8UAAADgBWAckah3V3drD1cUeIITzTUmApAtvZjSuv3gWIu
AdbJ1skioWi32b8Yz3HWj26CB/U5TmT1BcED0+jyFzMlkF9hQMKlHeCdl2nahwtbgrq/qE7r8iAT
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
IAAAAAAAAQAYAI5Lvih5NtUBrkM6gns21QGuQzqCezbVAVBLAQIfABQAAAAIAM2Z6U4arZW11QgA
AFMbAAAdACQAAAAAAAAAIAAAAH7WAgBSZWN1cnNvcy9nZngzX3Nwcml0ZTE2eDE2LmFzbQoAIAAA
AAAAAQAYAG0mwcJ5NtUBLqQ8gns21QGuQzqCezbVAVBLAQIfABQAAAAIAM6Z6U55Urx4PgEAAFcB
AAAdACQAAAAAAAAAIAAAAI7fAgBSZWN1cnNvcy9nZngzX3Nwcml0ZTE2eDE2LnRhcAoAIAAAAAAA
AQAYABZhN8R5NtUBLqQ8gns21QEupDyCezbVAVBLAQIfABQAAAAIAM+Z6U4FUi91MAoAACskAAAi
ACQAAAAAAAAAIAAAAAfhAgBSZWN1cnNvcy9nZngzX3Nwcml0ZTE2eDE2X21hc2suYXNtCgAgAAAA
AAABABgASEEbxnk21QEupDyCezbVAS6kPIJ7NtUBUEsBAh8AFAAAAAgA0ZnpThKLmlqXAQAAVQIA
ACIAJAAAAAAAAAAgAAAAd+sCAFJlY3Vyc29zL2dmeDNfc3ByaXRlMTZ4MTZfbWFzay50YXAKACAA
AAAAAAEAGAAQvBvIeTbVAS6kPIJ7NtUBLqQ8gns21QFQSwECHwAUAAAACADFmelOVPLwyDkHAABe
EwAAGwAkAAAAAAAAACAAAABO7QIAUmVjdXJzb3MvZ2Z4M19zcHJpdGU4eDguYXNtCgAgAAAAAAAB
ABgA0IKQuXk21QEupDyCezbVAS6kPIJ7NtUBUEsBAh8AFAAAAAgAxpnpTmgl1s8GAQAADAEAABsA
JAAAAAAAAAAgAAAAwPQCAFJlY3Vyc29zL2dmeDNfc3ByaXRlOHg4LnRhcAoAIAAAAAAAAQAYAJ7I
V7t5NtUBLqQ8gns21QEupDyCezbVAVBLAQIfABQAAAAIAMqZ6U62O7NYmgcAAAcVAAAgACQAAAAA
AAAAIAAAAP/1AgBSZWN1cnNvcy9nZngzX3Nwcml0ZTh4OF9tYXNrLmFzbQoAIAAAAAAAAQAYABJy
+L95NtUBWwY/gns21QFbBj+CezbVAVBLAQIfABQAAAAIAMyZ6U40i9OEFAEAABsBAAAgACQAAAAA
AAAAIAAAANf9AgBSZWN1cnNvcy9nZngzX3Nwcml0ZTh4OF9tYXNrLnRhcAoAIAAAAAAAAQAYAPcj
ZcF5NtUBWwY/gns21QFbBj+CezbVAVBLAQIfABQAAAAIAMiZ6U4C2OTvRAcAAHMTAAAeACQAAAAA
AAAAIAAAACn/AgBSZWN1cnNvcy9nZngzX3Nwcml0ZTh4OF9vci5hc20KACAAAAAAAAEAGACiO+i8
eTbVAVsGP4J7NtUBWwY/gns21QFQSwECHwAUAAAACADJmelOGdmXyAcBAAANAQAAHgAkAAAAAAAA
ACAAAACpBgMAUmVjdXJzb3MvZ2Z4M19zcHJpdGU4eDhfb3IudGFwCgAgAAAAAAABABgAnGFqvnk2
1QGJaEGCezbVAYloQYJ7NtUBUEsBAh8AFAAAAAgACprpTujsQxsUEwAAu1IAABcAJAAAAAAAAAAg
AAAA7AcDAFJlY3Vyc29zL2dmeDRfNjRjb2wuYXNtCgAgAAAAAAABABgADP1/Bno21QGJaEGCezbV
AYloQYJ7NtUBUEsBAh8AFAAAAAgAC5rpTgmQeQL4AwAAPwUAABcAJAAAAAAAAAAgAAAANRsDAFJl
Y3Vyc29zL2dmeDRfNjRjb2wudGFwCgAgAAAAAAABABgA3BhfCHo21QGJaEGCezbVAYloQYJ7NtUB
UEsBAh8AFAAAAAgA9JnpTkdlHwuzCAAA7xwAABkAJAAAAAAAAAAgAAAAYh8DAFJlY3Vyc29zL2dm
eDRfY2hhcnNldC5hc20KACAAAAAAAAEAGABN6VfveTbVAYloQYJ7NtUBiWhBgns21QFQSwECHwAU
AAAACAD2melON1DwHCUCAAAZAwAAGQAkAAAAAAAAACAAAABMKAMAUmVjdXJzb3MvZ2Z4NF9jaGFy
c2V0LnRhcAoAIAAAAAAAAQAYAM5AMvF5NtUBiWhBgns21QGJaEGCezbVAVBLAQIfABQAAAAIAPiZ
6U5zKm+WKwYAAEYSAAAcACQAAAAAAAAAIAAAAKgqAwBSZWN1cnNvcy9nZng0X2NoYXJzZXRyb20u
YXNtCgAgAAAAAAABABgA9ROS83k21QGJaEGCezbVAYloQYJ7NtUBUEsBAh8AFAAAAAgA+ZnpToRX
AbMJAQAAGQEAABwAJAAAAAAAAAAgAAAADTEDAFJlY3Vyc29zL2dmeDRfY2hhcnNldHJvbS50YXAK
ACAAAAAAAAEAGADCBYn1eTbVAb7KQ4J7NtUBvspDgns21QFQSwECHwAUAAAACAD7melOECg581cI
AAAsHwAAGQAkAAAAAAAAACAAAABQMgMAUmVjdXJzb3MvZ2Z4NF9lc3RpbG9zLmFzbQoAIAAAAAAA
AQAYALGVffd5NtUBvspDgns21QG+ykOCezbVAVBLAQIfABQAAAAIAPyZ6U78ZBHChgEAAAUCAAAZ
ACQAAAAAAAAAIAAAAN46AwBSZWN1cnNvcy9nZng0X2VzdGlsb3MudGFwCgAgAAAAAAABABgAzwvv
+Hk21QG+ykOCezbVAb7KQ4J7NtUBUEsBAh8AFAAAAAgABprpTvcmyNA9EwAA3FMAABcAJAAAAAAA
AAAgAAAAmzwDAFJlY3Vyc29zL2dmeDRfaW5wdXQuYXNtCgAgAAAAAAABABgAaXuUAno21QG+ykOC
ezbVAb7KQ4J7NtUBUEsBAh8AFAAAAAgACJrpThvZDmBpAgAACwMAABcAJAAAAAAAAAAgAAAADVAD
AFJlY3Vyc29zL2dmeDRfaW5wdXQudGFwCgAgAAAAAAABABgAgY3QBHo21QG+ykOCezbVAb7KQ4J7
NtUBUEsBAh8AFAAAAAgAA5rpTiZUuTKkGAAAJ3IAABgAJAAAAAAAAAAgAAAAq1IDAFJlY3Vyc29z
L2dmeDRfcGFyYW1zLmFzbQoAIAAAAAAAAQAYAKk4hf55NtUBvspDgns21QG+ykOCezbVAVBLAQIf
ABQAAAAIAAWa6U6sC4wXdwMAALIEAAAYACQAAAAAAAAAIAAAAIVrAwBSZWN1cnNvcy9nZng0X3Bh
cmFtcy50YXAKACAAAAAAAAEAGAANxZgAejbVAe4sRoJ7NtUB7ixGgns21QFQSwECHwAUAAAACAD9
melODqKOgG4QAAAARgAAFwAkAAAAAAAAACAAAAAybwMAUmVjdXJzb3MvZ2Z4NF90ZXh0by5hc20K
ACAAAAAAAAEAGABycE36eTbVAe4sRoJ7NtUB7ixGgns21QFQSwECHwAUAAAACAABmulOAIYrFCgC
AACFAgAAFwAkAAAAAAAAACAAAADVfwMAUmVjdXJzb3MvZ2Z4NF90ZXh0by50YXAKACAAAAAAAAEA
GADfHKb8eTbVAe4sRoJ7NtUB7ixGgns21QFQSwECHwAUAAAACABcmulOaz7JiPwKAACyLAAAGAAk
AAAAAAAAACAAAAAyggMAUmVjdXJzb3MvZ2Z4NV9tYXAxNmguYXNtCgAgAAAAAAABABgA3V7hY3o2
1QHuLEaCezbVAe4sRoJ7NtUBUEsBAh8AFAAAAAgAXZrpTodLjQJSAgAAtgMAABgAJAAAAAAAAAAg
AAAAZI0DAFJlY3Vyc29zL2dmeDVfbWFwMTZoLnRhcAoAIAAAAAAAAQAYAGgNbWV6NtUB7ixGgns2
1QHuLEaCezbVAVBLAQIfABQAAAAIAHSa6U4GQ9quiQ4AAOE5AAAaACQAAAAAAAAAIAAAAOyPAwBS
ZWN1cnNvcy9nZng1X21hcGFycmF5LmFzbQoAIAAAAAAAAQAYAKvEz316NtUBOo9Igns21QE6j0iC
ezbVAVBLAQIfABQAAAAIAICa6U5E8IuvBgMAABEGAAAaACQAAAAAAAAAIAAAAK2eAwBSZWN1cnNv
cy9nZng1X21hcGFycmF5LnRhcAoAIAAAAAAAAQAYACCl0ol6NtUBOo9Igns21QE6j0iCezbVAVBL
AQIfABQAAAAIAGKa6U6NMRGXKw0AAPstAAAaACQAAAAAAAAAIAAAAOuhAwBSZWN1cnNvcy9nZng1
X21hcGRpZl9iLmFzbQoAIAAAAAAAAQAYAN3NNGl6NtUBOo9Igns21QE6j0iCezbVAVBLAQIfABQA
AAAIAGSa6U41d5F2awIAAPYCAAAaACQAAAAAAAAAIAAAAE6vAwBSZWN1cnNvcy9nZng1X21hcGRp
Zl9iLnRhcAoAIAAAAAAAAQAYAJe9Smt6NtUBOo9Igns21QE6j0iCezbVAVBLAQIfABQAAAAIAGWa
6U7FktsxeQ0AADgvAAAaACQAAAAAAAAAIAAAAPGxAwBSZWN1cnNvcy9nZng1X21hcGRpZl9oLmFz
bQoAIAAAAAAAAQAYAE3f62x6NtUBYPFKgns21QE6j0iCezbVAVBLAQIfABQAAAAIAGea6U6oYUox
XQIAAOoCAAAaACQAAAAAAAAAIAAAAKK/AwBSZWN1cnNvcy9nZng1X21hcGRpZl9oLnRhcAoAIAAA
AAAAAQAYAMgPv256NtUBYPFKgns21QFg8UqCezbVAVBLAQIfABQAAAAIAGya6U4fqiGUaQ4AAEQy
AAAaACQAAAAAAAAAIAAAADfCAwBSZWN1cnNvcy9nZng1X21hcGRpZl9tLmFzbQoAIAAAAAAAAQAY
APyUtHR6NtUBYPFKgns21QFg8UqCezbVAVBLAQIfABQAAAAIAG2a6U5SV8hNcQIAAPwCAAAaACQA
AAAAAAAAIAAAANjQAwBSZWN1cnNvcy9nZng1X21hcGRpZl9tLnRhcAoAIAAAAAAAAQAYAEkra3Z6
NtUBYPFKgns21QFg8UqCezbVAVBLAQIfABQAAAAIAHCa6U7mFg3ouQ4AAFQyAAAcACQAAAAAAAAA
IAAAAIHTAwBSZWN1cnNvcy9nZng1X21hcGRpZl9teHkuYXNtCgAgAAAAAAABABgAtn0xeXo21QFg
8UqCezbVAWDxSoJ7NtUBUEsBAh8AFAAAAAgAcZrpTnZPzKJvAgAA9QIAABwAJAAAAAAAAAAgAAAA
dOIDAFJlY3Vyc29zL2dmeDVfbWFwZGlmX214eS50YXAKACAAAAAAAAEAGAB00Sp7ejbVAWDxSoJ7
NtUBYPFKgns21QFQSwECHwAUAAAACABpmulOEubp0IoNAAAqLwAAGgAkAAAAAAAAACAAAAAd5QMA
UmVjdXJzb3MvZ2Z4NV9tYXBkaWZfdi5hc20KACAAAAAAAAEAGACUdctwejbVAZhTTYJ7NtUBmFNN
gns21QFQSwECHwAUAAAACABqmulOCsdmuVYCAADhAgAAGgAkAAAAAAAAACAAAADf8gMAUmVjdXJz
b3MvZ2Z4NV9tYXBkaWZfdi50YXAKACAAAAAAAAEAGABCBcByejbVAZhTTYJ7NtUBmFNNgns21QFQ
SwECHwAUAAAACAAEmelOZBYu0ZgBAAAZBAAAFwAkAAAAAAAAACAAAABt9QMAUmVjdXJzb3MvZ2Z4
X2JvcmRlci5hc20KACAAAAAAAAEAGAA7skzheDbVAR25MIJ7NtUB2lYugns21QFQSwECHwAUAAAA
CAAFmelOy7DesowAAACaAAAAFwAkAAAAAAAAACAAAAA69wMAUmVjdXJzb3MvZ2Z4X2JvcmRlci50
YXAKACAAAAAAAAEAGACVSAPjeDbVAR25MIJ7NtUBHbkwgns21QFQSwECHwAUAAAACACXmOlOi4f6
tEIGAADmEgAAGAAkAAAAAAAAACAAAAD79wMAUmVjdXJzb3MvaW1faXNyY2xvY2suYXNtCgAgAAAA
AAABABgA8pjPaXg21QGYU02CezbVAZhTTYJ7NtUBUEsBAh8AFAAAAAgAmZjpTulVTL4BAQAAFgEA
ABgAJAAAAAAAAAAgAAAAc/4DAFJlY3Vyc29zL2ltX2lzcmNsb2NrLnRhcAoAIAAAAAAAAQAYAOJ1
0mt4NtUBmFNNgns21QGYU02CezbVAVBLAQIfABQAAAAIAKCY6U4fAvEL4wcAANIWAAAaACQAAAAA
AAAAIAAAAKr/AwBSZWN1cnNvcy9pbV9pc3JnZW5lcmljLmFzbQoAIAAAAAAAAQAYAEw9hXF4NtUB
mFNNgns21QGYU02CezbVAVBLAQIfABQAAAAIAKGY6U7e3k1OOwEAAGcvAAAaACQAAAAAAAAAIAAA
AMUHBABSZWN1cnNvcy9pbV9pc3JnZW5lcmljLnRhcAoAIAAAAAAAAQAYAHsyXXN4NtUBzrVPgns2
1QHOtU+CezbVAVBLAQIfABQAAAAIAJuY6U4SFAhZ9QIAAJgIAAAUACQAAAAAAAAAIAAAADgJBABS
ZWN1cnNvcy9pbV93YWl0LmFzbQoAIAAAAAAAAQAYABrVHG54NtUBzrVPgns21QHOtU+CezbVAVBL
AQIfABQAAAAIAJyY6U6HzQCPwQAAANEAAAAUACQAAAAAAAAAIAAAAF8MBABSZWN1cnNvcy9pbV93
YWl0LnRhcAoAIAAAAAAAAQAYAHkMsm94NtUBzrVPgns21QHOtU+CezbVAVBLAQIfABQAAAAIAOea
6U73oyVTGwQAAE4OAAAQACQAAAAAAAAAIAAAAFINBABSZWN1cnNvcy9ybGV6eC5jCgAgAAAAAAAB
ABgAemKh/Xo21QFhGFKCezbVAc61T4J7NtUBUEsBAh8AFAAAAAgADZjpTpWgi4ncEwAAbE8AABkA
JAAAAAAAAAAgAAAAmxEEAFJlY3Vyc29zL3JvbV9rZXlib2FyZC5hc20KACAAAAAAAAEAGAAzCw7O
dzbVAWEYUoJ7NtUBYRhSgns21QFQSwUGAAAAAH4AfgBZNAAAriUEAAAA

También podría gustarte