El Spectrum lleva su “Sistema Operativo” embebido en un chip ROM (sólo lectura) de 16KB. Este chip, como ya hemos visto, está conectado al bus de direcciones de tal forma que su contenido está accesible en los primeros 16K del espacio de direccionamiento del procesador, lo que viene a decir que todos los datos entre 0000 ($0000) y 16383 ($3fff) son bytes pertenecientes a la ROM, y como tales, no se pueden escribir.
Cuando arrancamos nuestro Spectrum, el registro PC vale 0 por lo el primer opcode que lee, decodifica y ejecuta es el que está en la posición 0, es decir, el primer byte de la ROM. Nuestro Spectrum recién encendido lo que hace es ejecutar la ROM, que no deja de ser un programa en código máquina de 16KB de tamaño.
Existen webs y libros dedicados al Sistema Operativo del Spectrum, porque el código de ese programa ha sido desensamblado y documentado de forma que tenemos detalladas todas las rutinas del mismo, qué variables de entrada tienen, qué salida producen y cómo se relacionan entre ellas.
El famoso libro “"The Complete Spectrum ROM Disassembly"” de Ian Logan y Frank O’Hara, publicados en 1983, detalla el código fuente de la ROM, asignando etiquetas de texto legibles a las diferentes direcciones de memoria con rutinas útiles (CLS
, PRINT_FP
, BORDER
, BEEP
, etc), y esto tan sólo 1 año después de la salida del Spectrum. A día de hoy, tenemos no sólo este libro sino también múltiples sitios web con referencias cruzadas de todas las rutinas para darnos toda la información que necesitemos para utilizarlas, tal y como hicimos en el capítulo de introducción al código máquina llamando a las rutinas para borrar la pantalla, cambiar el color del borde, imprimir caracteres en la posición de cursor o imprimir valor numéricos.
A partir de ahora nos referiremos a este Sistema Operativo, a este programa, como “la ROM del Spectrum”
La ROM del Spectrum está, como su propio nombre indica, grabada en un chip ROM (Read Only Memory, memoria de sólo lectura), por lo que ninguno de los primeros 16384 bytes del mismo puede ser modificado. Intentar escribir con el Z80 en cualquier dirección menor de 16384 no dará ningún error, pero no producirá ningún cambio en la memoria, ya que no puede ser alterada.
Como cualquier “Sistema Operativo”, la ROM del Spectrum necesita variables donde almacenar el estado del Sistema: cuál es el color actual del borde, qué tinta/papel actual debe usar el sistema para imprimir caracteres, cuál ha sido la última tecla pulsada, todo el código BASIC que ha tecleado el usuario… todos estos datos se tienen que almacenar en algún sitio, y no puede ser en el área de 16KB entre 0 y 16383. Y no sólo eso, sino que también necesitamos algún lugar donde guardar los “datos gráficos” que luego se convierte en una imagen en la pantalla (la “videoRAM”).
Este lugar es, evidentemente, en la RAM.
Después de esos primeros 16KB de ROM, que tenemos “mapeados” de 0 a 16383, tenemos mapeados los chips de RAM del Spectrum.
Si tenemos un modelo de 16KB, tendremos esos 16KB de RAM “mapeados”, “disponibles”, desde la celdilla 16384 hasta la celdilla 32767.
Si tenemos un modelo de 48KB, tendremos esos 48K de RAM “mapeados”, “disponibles”, desde la celdilla 16384 hasta la celdilla 65535 (que es el valor máximo direccionable por un bus de direcciones de 16 bits).
Mapa de memoria del Spectrum
Y si tenemos un modelo de 128KB, como el procesador no puede direccionar más de 65535 a la vez, tendremos la misma disposición de memoria del modelo de 48K, pero podremos “intercambiar” el bloque de 16KB final por otros bloques, aunque sólo podremos ver uno al mismo tiempo (pero podremos ir cambiando entre cualquiera de ellos para acceder a sus datos).
Paginación de los modelos de 128K
En el capítulo dedicado a la paginación 128KB entraremos en detalle sobre la memoria disponible y la paginación, pero volvamos por ahora a la ROM del Spectrum.
Los primeros 16KB de la RAM, son pues la ROM del Spectrum: no se puede escribir en ella, es el “Sistema Operativo” del Spectrum, lo que se ejecuta cuando arranca, y contiene tanto el intérprete de BASIC, como todas las rutinas que éste utiliza para interactivar con el usuario, cargar datos desde cinta, ejecutar programas BASIC, etc.
Desde el byte 16384 para arriba (hasta 32767 en los modelos 16KB, y hasta 65535 en el resto), tenemos la RAM disponible para el usuario.
Pero no toda la RAM está en realidad disponible para el usuario, ya que parte de la RAM se tiene que dedicar a ciertas tareas.
Como ya vimos en el capítulo dedicado a la Arquitectura del Spectrum, sección “Partes del mapa de Memoria del Spectrum”, parte de nuestra RAM es “requerida” por el sistema para ciertas tareas:
Primero tenemos la “VideoMemoria” (o “pantalla”, “fichero de imagen y de atributos”, “videoRAM”…): es un área de 6912 bytes que está situada a partir del primer byte de la RAM (16384 o $4000) y hasta el byte 23295 (o $5aff). Escribir cualquier valor en este área producirá cambios en la pantalla: hará aparecer píxeles y cambiar el color de zonas de la pantalla. El chip gráfico del Spectrum lee esta zona de memoria y convierte los valores de la misma en “colores” que enviar al haz de electrones de la TV.
Después, y ahora volvemos a enlazar con la ROM y nuestro “Sistema Operativo”, tenemos una zona de memoria desde la dirección 23296 ($5b00) hasta 24576 ($6000) la cual contiene las variables que usa nuestro Spectrum para funcionar: la zona de Variables del Sistema (que incluye tanto el buffer de impresión como zonas utilizadas por BASIC para la edición).
Así pues, nos damos cuenta de lo siguiente:
Ya hemos hecho uso de la ROM del Spectrum en capítulos anteriores del curso con CLS o la impresión de números, y también accediendo al valor de RAMTOP para visualizarlo. Veamos de nuevo un ejemplo que combina ambas:
ORG 32768 call ROM_CLS ld bc, (SYSVAR_RAMTOP) call ROM_STACK_BC call ROM_PRINT_FP ret SYSVAR_RAMTOP EQU $5cb2 ROM_CLS EQU $0daf ROM_STACK_BC EQU $2d2b ROM_PRINT_FP EQU $2de3 END 32768
El programa anterior imprimirá (usando las rutinas de la ROM) el valor 32767, que el valor contenido en la dirección de memoria $5cb2 (RAMTOP
). Ese valor se puso a 32767 cuando el cargador BASIC del ejecutable que acabamos de compilar hizo un CLEAR 32767
.
Por otra parte, la rutina CLS de la ROM (que borra la pantalla y abre el canal 2 para RST, que es la parte superior de la pantalla) es una rutina que podemos encontrar documentada en la ROM, la cual pone las coordenadas de pantalla a (0,0) (en la variable del sistema COORDS
que se aloja en la dirección $5c7d), luego abre el canal de la pantalla, y después la limpia totalmente.
También estamos utilizando rutina de la ROM cuando llamamos a rst 16
(rst $10
) para imprimir caracteres, ya que rst $10
es un opcode cuyo equivalente es un call $0010
, que es la rutina de la ROM para imprimir un carácter en la posición del cursor.
Por otra parte, las rutinas de la ROM no sólo actualizan “variables del sistema” como resultado de su ejecución, también utilizan en ocasiones variables de sistema como parámetros o, más bien, como “variables de configuración actuales”.
Por ejemplo, la variabla del sistema ATTR_P
(23692 o $5c8d) almacena el valor del ATRIBUTO DE COLOR a utilizar para cualquier al borrar la pantalla con CLS. El valor por defecto de esta variable cuando arrancamos el Spectrum es $38, que se corresponde con FLASH 0: BRIGHT 0: PAPER 7: INK 0
.
ORG 32768 ld hl, SYSVAR_ATTR_P ld a, 1*8 + 7 ; INK white, PAPER blue ld (hl), a call ROM_CLS ld a, '*' rst 16 ret SYSVAR_ATTR_P EQU $5c8d ROM_CLS EQU $0daf END 32768
Efectivamente, al ejecutar nuestro programa, la rutina CLS de la ROM tomará de la variable del sistema ATTR_P el color de tinta/papel que debe utilizar para el borrado de la pantalla, y en este caso veremos el fondo azul y nuestro asterisco impreso en blanco o gris (más adelante veremos cómo especificar los colores para estas rutinas).
Así que, como hemos visto, disponemos de una serie de rutinas y variables útiles para usar en nuestros programas… hasta cierto punto, como veremos a continuación.
Disponemos en la ROM pues de una serie de rutinas que, como veremos, nos permiten hacer todo tipo de tareas: imprimir texto, imprimir valores numéricos, leer el teclado, borrar la pantalla, cambiar el color del borde, entre otros.
Y, sin embargo, estamos planteando la pregunta de “¿Debemos usar las rutinas de la ROM?”
La respuesta es, “depende de dónde y cuándo”.
En general, no se recomienda por los diferentes motivos que vamos a explicar a continuación:
im 2
de interrupciones.
Para empezar, no se recomienda utilizarlas por compatibilidad con los modelos Timex y con Spectrums con ROMs modificadas. La ROM es estándar entre los modelos “clásicos” es decir, las rutinas de la ROM, y las variables del sistema, están en las mismas direcciones de memoria en los modelos de 16K, 48K, y 128K, pero no lo están en los ordenadores Spectrum “Timex Sinclair” que se comercializaban en USA. La ROM de estos sistemas americanos tiene cambios y por tanto podemos estar llamando a rutinas que no existen en dicha ROM. De la misma forma, si alguien tiene una ROM modificada podría no funcionarle nuestro programa. Mucha gente que escribe software para Spectrum no se preocupa de esta compatibilidad porque se centran en los modelos Europeos PAL, pero es algo a tener en cuenta.
El segundo motivo para no usar las rutinas de la ROM es que si estamos programando un juego para un cartucho ROM (Interface 2), no tendremos disponible la ROM, ya que el cartucho se carga en 0000, sustituyéndola. Es decir, entre 0 y 16384 ya no hay ROM sino que está nuestro juego.
El tercer motivo a tener en cuenta es que la mayoría de las rutinas de la ROM nos van a servir mientras no utilicemos lo que se conoce como “Modo 2 de Interrupciones”, que veremos más adelante en el capítulo dedicado a las “Interrupciones del Z80”, por ejemplo porque necesitemos utilizar variables del sistema como LAST_K
o FRAMES
.
El Spectrum, al arrancar, funciona en el “Modo 1 de Interrupciones”, que viene a decir que 50 veces por segundo, el Z80 llama a la rutina $0038 la cual principalmente lee el teclado (actualizando determinadas variables del Sistema).
La rutina en $0038 también aumenta un contador (también otra variable) llamada FRAMES
que nos permite saber cuántas interruciones se han ejecutado desde que se encendió el Spectrum. Como se ejecutan 50 interrupciones por segundo (El sistema PAL funciona a 50Hz), dividiendo este número por 50 podemos saber el número de segundos transcurrido desde que se encendió el ordenador (ya que FRAMES vale 0 cuando se enciende y se aumenta cada 1/50 segundos o 50 veces por segundo).
Si estamos desarrollando un juego, y queremos hacer cosas como reproducir música sin que se detenga la ejecución del juego (“música de fondo”) o tareas similares, necesitaremos poner nuestra propia Rutina de Interrupción para ejecutarla 50 veces por segundo. En im1 la rutina que se ejecuta es $0038, que está en la ROM y por tanto no podemos cambiar, de modo que para poder hacer que el Z80 llame a nuestra propia “rutina de interrupciones” hay que cambiar al modo 2. En este modo, se dejará de llamar a $0038 y por tanto se dejarán de ejecutar las tareas y de actualizar las variables del sistema relacionadas.
Un caso práctico: si usamos una rutina de la ROM para leer el teclado, y cambiamos a im2 en nuestro juego, se dejará de actualizar la variable del Sistema con las teclas pulsadas ya que no hay nadie actualizándolas. Es cierto que desde nuestra Rutina de Interrupción en im2 podríamos hacer un call $0038
(o rst $38
) pero sirva como ejemplo de por qué no es habitual utilizar las rutinas.
El cuarto motivo para no usar las rutinas de la ROM es que hacen uso interno del registro IY y del registro shadow HL' sin preservarlo, por lo que no podremos usarlo en nuestros programas. En el caso de IY, la ROM lo inicializa apuntando al principio de la zona de memoria donde están las variables del sistema, y luego accede a ellas mediante (IY+offset). Por eso, si queremos poder usar el registro IY en nuestros programas no podemos llamar a rutinas de la ROM, lo cual incluye también no utilizar el modo im1 de interrupciones (en realidad, estando en im2 sí podríamos usarlas haciendo una copia del valor de IY en una variable al empezar nuestro programa, y restaurar el valor de IY antes de llamarlas, pero no es práctico).
Finalmente, cabe destacar que estas rutinas están diseñadas para su uso desde BASIC, y utilizan lo que se conoce como “el stack de BASIC” o “la pila de la calculadora” para trabajar con números, así como limitaciones impuestas artificialmente como por ejemplo no permitirnos fácilmente escribir o dibujar en las últimas 2 líneas de la pantalla.
Por ejemplo, cuando en nuestros primeros programas llamábamos a la siguientes rutinas:
call ROM_STACK_BC call ROM_PRINT_FP
Lo que hacíamos era introducir BC en la pila de la calculadora (con el primer call) y llamar a la rutina de BASIC que imprimir el número actual en la calculadora por pantalla (la segunda).
Esto dista mucho de ser eficiente, ya que aunque nosotros estemos trabajando con números enteros (0-65535) al pasar por la pila de la calculadora la rutina lo que hace es usar un sistema de punto flotante para permitir números decimales. Esto es muchísimo más lento que trabajar con enteros y no podemos utilizarlo para imprimir en un juego el marcador o las vidas de un jugador si necesitamos que el código sea lo más rápido posible.
En otras ocasiones, como veremos en el ejemplo de BORDER
, examinando el código fuente de la rutina podremos encontrar puntos de entrada a la misma a los cuales saltar con el parámetro introducido en algún registro, saltándonos la porción de código que extrae el valor de la pila de BASIC.
Además, las rutinas de la ROM usan (y actualizan) variables del sistema alojadas entre $5b00 y $5cca, por lo que si mezclamos código propio con llamadas a rutina de la ROM, tendremos que trabajar también con esas variables y actualizarlas nosotros en nuestro código propio cuando queramos llamar a alguna rutina que la usa. Por ejemplo, la rutina CLS
que tenemos en nuestro esqueleto de programa, borra la pantalla usando el valor de ATTR_P
($5c8d) que hemos definido en nuestra constante CLS_COLOR
.
Sin embargo, incluso aunque en algunos casos tengamos que pasar por la pila de BASIC, hay algunos contextos donde nos interesa conocer las rutinas de la ROM y variables del sistema y usarlas:
1.- Todos aquellos programas que creemos para aprender ensamblador, pruebas, código de ejemplo, etc, es perfecto para utilizar las rutinas de la ROM ya que no nos importa lo rápidas que son, sino hacer programas sencillos sin tener que añadir código propio para imprimir un carácter o un número.
2.- Programas sencillos basados en texto, o juegos muy sencillos y básicos… piensa que los juegos realizados en BASIC no sólo usan las rutinas de la ROM sino que lo hacen a través de BASIC. Los juegos programados en BASIC, convertidos a ensamblador usando las rutinas de la ROM para realizar las mismas tareas que realizan desde BASIC, serían muchísimo más rápidos.
Por eso, vamos a ver en este capítulo una serie de rutinas de la rom, de variables del sistema y de rutinas propias apoyadas en ambas para sacarle provecho a la ROM en todas las pruebas que necesitemos realizar. Conforme vayamos realizado bibliotecas de funciones propias de texto o gráficos, es muy probable que dejemos de usarlas, al principio nos resultarán muy útiles e incluso en algunas ocasiones puede que nos vengan mejor (por venir ya en la ROM) que incluir rutinas externas adicionales.
Empezaremos por CLS
, una rutina que ya hemos utilizado, y que se ubica en $0daf.
El nombre de la rutina es CLS_ALL
porque en la ROM existe otro punto de entrada a CLS
que usa diferentes parámetros (como CLS
en $0d6b, que no utilizamos aquí porque no abre el canal de la pantalla).
Para borrar la pantalla, CLS_ALL
utiliza como tinta y papel el valor alojado en la variable del sistema ATTR_P
($5c8d o 23693), donde se codifica el color así:
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
FLASH | BRIGHT | PAPER_BIT2 | PAPER_BIT1 | PAPER_BIT0 | INK_BIT2 | INK_BIT1 | INK_BIT0 |
Tenemos en el Bit 7 si queremos FLASH o no (1/0), en el bit 6 si queremos Brillo o no (1/0), y luego 3 bits para el PAPER y 3 bits para el INK.
Con 3 bits podemos generar las siguientes combinaciones y por tanto colores:
Valor | Color |
---|---|
0 | BLACK |
1 | BLUE |
2 | RED |
3 | MAGENTA |
4 | GREEN |
5 | CYAN |
6 | YELLOW |
7 | WHITE |
Para definir el valor del PAPEL dentro del “byte” en ATTR_P
, tenemos que desplazar los 3 bits del color a la izquierda.
El valor por defecto de ATTR_P
cuando encendemos el Spectrum es $38, o 00111000b, que es:
Resultado: negro sobre blanco, sin flash ni brillo.
Si queremos establecer color AZUL (1) con tinta ROJA (2), el valor sería “(1«3) | 2”, o lo que es lo mismo (1*8 + 2). Si además queremos activar el Flash, debemos activar el bit 7 sumando 128.
Podemos establecer los valores tanto con desplazamientos de bits (color_deseado_paper « 3) como con multiplicaciones equivalentes (color_deseado_paper * 8). Lo mismo ocurre con la activación de FLASH y BRIGHT (usando 1«7 o su equivalente +128).
Esta información nos permite definir nuestros primeros EQU
's para colores y ver cómo funcionan:
ORG 32768 ld hl, SYSVAR_ATTR_P ld a, cOLOR_FLASH + (COLOR_YELLOW << 3) + COLOR_BLUE; ld (hl), a call ROM_CLS ret SYSVAR_ATTR_P EQU $5c8d ROM_CLS EQU $0daf COLOR_BLACK EQU 0 COLOR_BLUE EQU 1 COLOR_RED EQU 2 COLOR_MAGENTA EQU 3 COLOR_GREEN EQU 4 COLOR_CYAN EQU 5 COLOR_YELLOW EQU 6 COLOR_WHITE EQU 7 COLOR_BRIGHT EQU 64 COLOR_FLASH EQU 128 END 32768
Cuando ejecutemos este código, tendremos una horrible pantalla parpadeante cambiando entre amarillo y azul.
Recordemos que tenemos disponibles la rutina CLS
y la variable CLS_COLOR
en la librería utils.asm
que creamos en los primeros capítulos del curso.
En la ROM tenemos disponible una rutina para actualizar el color del borde al color que le especifiquemos como parámetro. Veamos su código y vamos a demostrar con él por qué es importante tener acceso a una versión desensamblada y documentada de la ROM con el objetivo de utilizar estas rutinas de la forma más eficiente posible.
BORDER: $2294 call FIND_INT1 $2297 cp $08 $2299 jr nc,REPORT_K $229b out ($fe),a $229d rlca $229e rlca $229f rlca $22a0 bit 5,a $22a2 jr nz,BORDER_1 $22a4 xor $07 BORDER_1: $22a6 ld ($5c48),a Set the system variable (BORDCR) as required and return. $22a9 ret
La rutina empieza en $2294 con 3 líneas que hacen lo siguiente: Primero en $2294 se llama a una rutina que extrae de la pila de la calculadora del BASIC el parámetro que hemos introducido en la instrucción BORDER x
, y que la devuelve en el registro A. Después, en $2297 y $229 se comprueba que el valor del color (ahora en A) no es mayor que 7 (los colores disponibles van del 0 al 7). Si es mayor de 7, salta a REPORT_K
que es la rutina responsable de imprimir el famoso error “K Invalid colour”.
A partir de este punto, en $229b, es donde se escribe el valor del borde usando OUT
en el puerto correcto y después se guarda su valor en la variable del sistema (BORDCR
) para su futuro uso por la ROM.
A nosotros, desde ASM, no nos interesa llamar a esta rutina teniendo que introducir el color del borde deseado en la pila de BASIC. Implicaría una serie de operaciones para meter el número en la pila de BASIC y después regresar al mismo punto sólo para que la rutina lo saque de allí y lo introduzca en el registro A.
Mirando la rutina, vemos que podemos saltar directamente a $229b con el valor del color metido en A, evitando así el uso de la pila de la calculadora. Por eso, nuestra librería utils.asm
y los ejemplos que hemos usado hasta ahora utilizan $229b como el punto de entrada para call:
BORDER EQU $229b
Los colores para el borde siguen el mismo formato que los vistos en ATTR_P
, y para los cuales ya tenemos “constantes” definidas en la librería como _BLACK, _RED, etc.
_BLACK EQU 0 _BLUE EQU 1 _RED EQU 2 _MAGENTA EQU 3 _GREEN EQU 4 _CYAN EQU 5 _YELLOW EQU 6 _WHITE EQU 7
Sabemos que tenemos en la ROM una serie de rutinas que nos permiten imprimir caracteres (RST
), cadenas de texto e incluso números (las veremos en detalle a continuación). Estas rutinas en el Spectrum trabajan sobre lo que la ROM conoce como el “canal de salida” (output channel).
El canal de salida por defecto al arrancar el Spectrum es el 1, que son las 2 líneas inferiores de la pantalla, donde introducimos los comandos. Si queremos escribir en las otras 22 líneas, las de la parte superior de la pantalla, tenemos que cambiar al canal 1. La rutina de CLS
que hemos visto anteriormente ya hace esto por nosotros, por si por algún motivo no queremos borrar la pantalla, podemos utilizar las siguientes líneas en ensamblador al principio de nuestro programa:
ld a, 2 call $1601
Recordemos que aunque en este capítulo vamos a ver los ejemplos usando los valores numéricos de llamada de las rutinas, se recomienda crear constantes (EQU
) en nuestros programas, como por ejemplo:
ROM_CHAN_OPEN EQU $1601
La rutina OUT_NUM_1
(“REPORT and lINE NUMBER PRINTING”) recibe como parámetro en BC un número entre 0 y 9999, y lo imprime en pantalla en la posición actual del cursor.
ld bc, numero call $1a1b
Esta rutina está limitada a números menores de 9999 porque es la que imprimir el número de línea en BASIC, de modo que su código está diseñado para imprimir hasta ese valor.
Si BC contiene un valor superior a 9999, no obtendremos ningún error al llamarla pero el valor impreso no será correcto.
En el apartado anterior hemos visto la rutina OUT_NUM_1
, la cual permite imprimir en la posición del cursor números entre 0 y 9999.
Si necesitamos imprimir números de 16 bits (desde 0 a 65535), podemos utilizar las rutinas STACK_BC
y PRINT_FP
, que ya hemos utilizado.
La primera, en $2d2b, coge el valor numérico en BC y lo introduce en la pila de la calculadora de BASIC.
La segunda, en $2de3, es una rutina que extrae el valor de la parte superior de la pila de la calculadora BASIC, y lo imprimir por pantalla.
Combinándolas, podemos imprimir el número en BC, así:
ld bc, numero call $2d2b call $2de3
Esta rutina está limitada a números menores de 9999 porque es la que imprimir el número de línea en BASIC, de modo que su código está diseñado para imprimir hasta ese valor.
Esta rutina se utiliza para imprimir por pantalla en la posición del cursor el carácter ASCII establecido en el registro A.
El opcode rst 16
(o rst $10
) se traduce en una llamada a la rutina de la ROM ubicada en $0010, la cual simplemente hace:
PRINT_A_1: $0010 jp PRINT_A_2 ; Saltar a $15ef
Ejecutar rst $10
tiene el mismo efecto que call $0010
y que call $15ef
. El RST ocupa sólo 1 byte, pero realiza 2 saltos, mientras que el call 15EF
ocupa 3 pero salta directamente a la rutina de impresión.
Visto esto, podríamos editar nuestra librería utils.asm y hacerle una pequeña optimización cambiando:
PrintChar EQU $0010 ; $0010 = rst $10 = rst 16
por:
PrintChar EQU $15ef ; $15ef = PRINT_A_2
El funcionamiento de esta rutina es muy sencillo: ponemos en A un valor numérico o un carácter ASCII, llamamos a rst 16
, y éste aparecerá por pantalla en la posición actual del cursor y con los atributos de papel y tinta actuales:
ld a, '*' rst 16
Pero la rutina PRINT_A
es mucho más compleja que simplemente escribir caracteres, también soporta códigos de control para establecer el color de tinta, de papel, flash, brillo, cambiar a una coordenada X e Y, salto de línea, etc:
Por ejemplo, para cambiar la tinta para todo lo que imprimamos desde ese momento, mandamos el código de “INK” (16) y después mandamos el color al que queremos cambiarlo:
ld a, 16 ; INK rst 16 ld a, 2 ; Rojo rst 16
De igual forma, si queremos cambiar el cursor a unas coordenadas X e Y concretas, mandamos el código de “AT” (22) y después hacemos 2 RSTs más con la coordenada Y y la coordenada X:
ld a, 22 ; AT rst 16 ld a, 13 ; Y=13 rst 16 ld a, 2 ; X=2 rst 16
Para no tener que recordar estos códigos, podemos crearnos constantes EQUs tal y como tenemos en nuestra librería utils.asm:
_CR EQU 13 _INK EQU 16 _PAPER EQU 17 _FLASH EQU 18 _BRIGHT EQU 19 _INVERSE EQU 20 _OVER EQU 21 _AT EQU 22 _TAB EQU 23 _BLACK EQU 0 _BLUE EQU 1 _RED EQU 2 _MAGENTA EQU 3 _GREEN EQU 4 _CYAN EQU 5 _YELLOW EQU 6 _WHITE EQU 7
Estos colores representan la gama cromática que se puede conseguir en Spectrum, dando lugar a los siguientes tonos según si BRIGHT es 0 ó 1:
Nótese al utilizar el código de control _AT varias peculiaridades:
Por otra parte, utilizar un código de color mayor que 7 tras un _INK, o _PAPER, o mayor que 1 tras _FLASH, _BRIGHT, etc, mostrará el error K Invalid colour.
Supongamos que no queremos imprimir un sólo carácter, sino que queremos imprimir una cadena de texto completa.
Para ello, podemos usar la rutina PR_STRING la cual imprime en la posición actual del cursor la cadena de texto apuntada por el registro DE (es decir, que DE tiene que apuntar al inicio de la cadena). La rutina necesita en BC el número de caracteres a imprimir.
PR_STRING: $203c ld a,b $203d or c $203e dec bc $203f ret z $2040 ld a,(de) $2041 inc de $2042 rst $10
Como vemos en el código original de la rutina, simplemente carga caracteres desde (DE) en A, un total de BC veces, incrementando DE cada vez. Como la rutina usa rst $10
para imprimir los caracteres, eso implica que podemos utilizar “códigos de control” para cambiar colores o la posición del cursor en PR_STRING
.
La cadena la podemos definir en memoria con la directiva DEFB o DB:
cadena1 DB "Esto es una cadena" cadena2 DB "Esto es una cadena", 13, "y otra linea" cadena3 DB "Tambien soporta", _INK, _RED, "colores"
Ahora bien, como vemos, es una incomodidad el hecho de tener que especificar el número de caracteres en BC, ya que debido a esto tenemos que andar contando la longitud de las cadenas (lo cual incluye contar también los códigos de control como caracteres de la misma):
ld de, cadena1 ld bc, 18 ; longitud de la cadena call $203c
Para no tener que contar longitudes manualmente, podemos utilizar al propio ensamblador y el símbolo $
el cual se refiere a la posición actual donde aparezca:
ld de, cadena ld bc, posicion_fin_cadena-cadena ; longitud call $203c ret cadena DB "Esto es una cadena" posicion_fin_cadena EQU $
Cuando el ensamblador procesa el fichero, sustituye $
en el EQU
por la posición exacta de la memoria donde aparece la definición, es decir, después de la 'a' final de 'cadena'. Por eso, si a $
le restamos cadena
(que representa la dirección donde empieza la cadena, es decir, la dirección en memoria de “E”), obtenemos la longitud en bytes.
No obstante, es molesto estar añadiendo etiquetas EQU de fin para cada cadena del programa, de modo que la mejor solución es probablemente utilizar una rutina propia que imprima la cadena usando RST pero no basándose en la longitud de la misma, sino en un carácter de fin de cadena.
Recuperemos la rutina PrintString
de nuestra librería utils.asm:
;----------------------------------------------------------------------- ; PrintString: Imprime en la pantalla una cadena acabada en un byte de ; valor $ff usando rst 16. ; Entrada: DE = Dirección de la cadena a imprimir. ; El valor de DE no se preserva (se incrementa) ;----------------------------------------------------------------------- PrintString: ld a, (de) ; Leemos el caracter apuntado por DE CP _EOS ; chequeamos si es $ff (fin de cadena) ret z ; Si lo es, se activa el ZEROFLAG => SALIR rst 16 ; No lo es, no hemos saltado, imprimirlo inc de ; Avanzar al siguiente caracter de la cadena jr PrintString ; Repetir hasta que alguno sea 0 y salgamos _EOS EQU $ff ; EOS = End Of String
Esta rutina, en lugar de repetirse decrementando BC hasta que valga 0, lo que hace es repetirse hasta que se encuentre un byte de valor $ff (_EOS).
Así, podemos imprimir cadenas sin tener que indicar su longitud. Y como utiliza rst $10
, podemos usar códigos de control embebidos:
ld de, cadena3 call PrintString ret cadena1 DEFB 'Esto es una cadena', $ff cadena2 DEFB 'Esto es una cadena con salto', _CR, _EOS cadena3 DEFB _CR, _CR, 'Codigos:', _AT, 13, 3, 'posicion,' DEFB ' ', _INK, _RED, _PAPER, _YELLOW, 'color', _CR, _CR DEFB ' ', _FLASH, 1, 'FLASH ON', _FLASH, 0, ' OFF', _EOS
Como ya explicamos, aunque lo habitual es usar 0 como carácter terminador de las cadenas, en nuestro caso los códigos de control pueden ser 0 (color negro, y también el 0 que desactiva BRIGHT, FLASH o INVERSE), por lo que hemos elegido $ff como código de fin de cadena o _EOS.
Si quisiéramos reescribir la rutina pero sin que modifique el valor de AF, no podríamos salir por ret z
sino que tendríamos que saltar a un punto final en la rutina para poder hacer un pop af
y el RET
.
PrintString: push af print_string_loop: ld a, (de) ; Leemos el caracter apuntado por DE CP _EOS ; chequeamos si es $ff (fin de cadena) jr z, end_print_string ; Si lo es, se activa el ZEROFLAG => SALIR rst 16 ; No lo es, no hemos saltado, imprimirlo inc de ; Avanzar al siguiente caracter de la cadena jr print_string_loop ; Repetir hasta que alguno sea 0 y salgamos end_print_string: pop af ret
De la misma manera que hemos creado PrintString
, recordemos que tenemos funciones en utils.asm para imprimir valores en binario PrintBin
), en hexadecimal de 8 y 16 bits (PrintHex
y PrintHex16
), hacer cambios de coordenadas del cursor (CursorAt
) y otras funciones varias (PrintSpace
, PrintCR
, etc).
PAUSE_1
realiza una pausa (espera) del tiempo especificado en BC, o hasta que se pulse una tecla.
El tiempo no está especificado en segundos sino en “interrupciones”. Como se producen 50 interrupciones por segundo en un sistema PAL (60 en ordenadores Timex NTSC), para esperar un número N de segundos, debemos poner en BC el valor N*50, y después llamar a $1f3d.
Si el usuario pulsa una tecla durante el tiempo de espera, la rutina se interrumpe y establece a 1 el bit 5 de la variable del sistema FLAGS
($5c3b ó 23611).
Esta rutina multiplica HL por DE, y deja el resultado en HL. Si el resultado excede 65535, el cálculo no será correcto, por lo que podemos usarlo para multiplicar valores pequeños (típicamente cálculos de coordenadas, velocidades, posiciones en pantalla, etc).
Hace uso de BC, pero lo preserva (no perdemos su valor).
Por ejemplo:
ORG 33500 call CLS ld hl, 4 ld de, 1024 call MULT_HLDE ld b, h ld c, l call PrintNum ; Aparece por pantalla 4096 ret INCLUDE "utils.asm" END 33500
La rutina PLOT
nos permite dibujar un pixel en pantalla en las coordenadas (X,Y) deseadas, indicadas en el registro BC (B=Y, C=X).
Con esta rutina vamos a poder ver ciertas limitaciones y el por qué es recomendable crear nuestras propias rutinas.
Para empezar, el valor de X puede estar entre 0 y 255 (como esperábamos, ya que el ancho de la pantalla en el Spectrum es de 256 píxeles) pero sin embargo el valor de Y ha de estar entre 0 y 175, ya que la rutina no nos permite escribir en la parte inferior de la pantalla (las 2 líneas o 16 píxeles del área de mensajes de estado o introducción de comandos de BASIC). Si llamamos a esta rutina con un valor superior a 175, no dibujará ningún pixel.
Además, así como la coordenada X=0 está en la parte izquierda de la pantalla, la coordenada Y=0 no está en la parte superior de la pantalla, sino en la parte inferior (algo que en general suele ser contraintuitivo en la programación en otros sistemas ya que se suele considerar (0,0) la parte superior izquierda de la pantalla).
La manera de utilizar la rutina es introducir en BC las coordenadas, llamar a $22d5, y veremos cómo aparece un pixel en la posición indicada, usando la tinta y color actuales.
La rutina PIXEL_ADDR
nos permite dibujar el pixel manualmente. Indicamos las coordenadas de pantalla de la misma forma y con el mismo formato que en PLOT
(en BC y con las mismas limitaciones) y nos devolverá HL apuntando al bloque de videomemoria que contiene nuestro píxel, y en A la posición del BIT de nuestro pixel en los 8 apuntados por HL.
Esta rutina es llamada por PLOT
para calcular la dirección en que ha de dibujar el pixel, y es la que implementa la limitación de cálculo hasta 175:
PIXEL_ADD: 22AA ld a,$af ; Test that the y co-ordinate (in B) is not greater than 175. 22AC sub b 22AD jp c,REPORT_B_3 ; Show ERROR if y>175 22B0 ld b,a ; B now contains 175 minus y. 22B1 and a (...) 22CA ret
Veamos un ejemplo muy sencillo de uso. Para poder referenciar las coordenadas Y desde la parte superior de la pantalla, las podemos pasar como (175-Y). Por ejemplo, si queremos calcular la dirección de X=50 e Y=30 para nuestro sistema de coordenadas mental donde (0,0) es la esquina superior izquierda, haremos:
ORG 33500 call ROM_CLS ld b, 175-30 ; Y = 30 ld c, 50 ; X = 50 call ROM_PLOT ; PLOT ld b, 175-20 ; Y = 20 ld c, 200 ; X = 200 call ROM_PIXEL_ADDR ld a, %10101010 ld (hl), a ; Imprimir 8 pixeles ret ROM_CLS EQU $0daf ROM_PLOT EQU $22e5 ROM_PIXEL_ADDR EQU $22aa END 33500
El anterior programa imprimirá un píxel en pantalla con PLOT
y después escribirá 8 pixeles en la dirección calculada por PIXEL_ADDR
:
Como puede verse, las rutinas de la ROM están pensadas para su uso desde el “sistema operativo” (el intérprete de BASIC), y si queremos tener control total de la pantalla necesitaremos crearnos nuestras propias rutinas sin limitaciones.
En el caso de PIXEL-ADDRESS
, como veremos en los capítulos dedicados a gráficos, podemos saltarnos la limitación del área inferior de la pantalla entrando a ella en $22b1. En este punto de entrada no sólo nos saltaríamos la limitación de 175 líneas sino que la coordenada Y se expresarían relativa a la parte superior izquierda.
Tenemos diferentes posibilidades en cuanto a rutinas de la ROM y variables del sistema para leer el teclado y poder responder a las pulsaciones del usuario. La mayoría de métodos se basan en dejar que el “sistema operativo” del Spectrum lea el teclado por nosotros, procese el estado de la matriz de filas y columnas de las teclas, y actualice ciertas variables del sistema que indican si se está pulsando alguna tecla, y qué tecla está pulsada, ya decodificada en un código ASCII.
La manera más sencilla de leer el teclado usando la ROM del Spectrum es ver el valor del bit 5 de la variable del sistema (no confundir con el registro F) FLAGS
($5c3b). Si está a 1, el ASCII pulsado en ese momento estará disponible en la variable del sistema LAST-K ($5c08). Una vez detectada la pulsación, hay que poner a 0 el bit 5 de FLAGS para informar a la ROM de que la tecla ha sido leída. La rutina de la ROM habrá decodificado las filas y columnas del teclado y habrá calculado el còdigo ASCII de la tecla pulsada y lo dejará en LAST-K
listo para que nosotros lo tomemos.
La variable del sistema LAST-K
seguirá conteniendo el código ASCII de la última tecla pulsada, aunque se haya liberado la tecla, pero nosotros podremos saber que no es una tecla pulsada mediante el valor bit 5 (0 = no pulsada, 1 = pulsada).
Otra opción muy sencilla es aprovechar la variable KSTATE5
. En la dirección de memoria $5c05 (23557) tendremos un valor 5 (%00000101) si hay alguna tecla pulsada en este momento y distinto de 5, si no está pulsada ninguna tecla. Podemos utilizar esa condición para saber si el valor de LAST_K
es una tecla que esté siendo pulsada por el usuario en el momento en que comprobamos si KSTATE5
vale 5.
También podemos poner a 0 el valor de LAST_K
, y llamar manualmente a la rutina de la ROM KEYBOARD
($02bf). Si hay alguna tecla pulsada, el valor de LAST_K
cambiará, y si no, se mantendrá en 0.
Finalmente, se puede llamar a KEY_SCAN
en $028e (que es la rutina que utiliza internamente KEYBOARD
para actualizar el valor de LAST_K
) a cuyo retorno tendremos en el registro DE el código de tecla pulsada (que no su ASCII), pudiendo identificar si Shift está pulsado o no además de la tecla pulsada.
En el registro de Flags (F), el flag de Zero (Z) estará a 0 si están pulsando más de 2 teclas (o 2 teclas pulsadas sin que ninguna de ellas sea Shift), o a 1 si la lectura ha sido correcta y DE contiene la(s) tecla(s) pulsadas.
La lógica es:
Tecla | Código (Hex) | Código (Dec) | Código (Bin) |
---|---|---|---|
1 | $24 | 36 | %00100011 |
2 | $1c | 28 | %00011100 |
3 | $14 | 20 | %00010100 |
4 | $0c | 12 | %00001100 |
5 | $04 | 4 | %00000100 |
6 | $03 | 3 | %00000011 |
7 | $0b | 11 | %00001011 |
8 | $13 | 19 | %00010011 |
9 | $1b | 27 | %00011011 |
0 | $23 | 35 | %00100011 |
Q | $25 | 37 | %00100101 |
W | $1d | 29 | %00011101 |
E | $15 | 21 | %00010101 |
R | $0d | 13 | %00001101 |
T | $05 | 5 | %00000101 |
Y | $02 | 2 | %00000010 |
U | $0a | 10 | %00001010 |
I | $12 | 18 | %00010010 |
O | $1a | 26 | %00011010 |
P | $22 | 34 | %00100010 |
A | $26 | 38 | %00100110 |
S | $1e | 30 | %00011110 |
D | $16 | 22 | %00010110 |
F | $0e | 14 | %00001110 |
G | $06 | 6 | %00000110 |
H | $01 | 1 | %00000001 |
J | $09 | 9 | %00001001 |
K | $11 | 17 | %00010001 |
L | $19 | 25 | %00011001 |
ENTER | $21 | 33 | %00100001 |
Caps Shift | $27 | 39 | %00100111 |
Z | $1f | 31 | %00011111 |
X | $17 | 23 | %00010111 |
C | $0f | 15 | %00001111 |
V | $07 | 7 | %00000111 |
B | $00 | 0 | %00000000 |
N | $08 | 8 | %00001000 |
M | $10 | 16 | %00010000 |
Symbol Shift | $18 | 24 | %00011000 |
Espacio | $20 | 32 | %00100000 |
La ROM tiene rutinas que transforman estos códigos de teclas en ASCII (como K_DECODE
, en $0333) para su almacenamiento en LAST_K
.
De nuevo, la documentación de la ROM es de valor incalculable para el uso de cualquier rutina del sistema.
A continuación podemos ver un programa para explorar algunos de los métodos descritos, y cuya ejecución y estudio puede ayudar al lector a comprender su uso y funcionamiento en programas:
ORG 33500 call ROM_CLS ld de, cadena call PrintString ; Imprimir textos loop: ; Línea KSTATE5 ld de, 17*256 + 0 ; X=17, Y=0 call CursorAt ld a, (SYSVAR_KSTATE5) ; KSTATE5 call PrintBin ; Imprimir valor (bin) ; Línea FLAGS ld de, 17*256 + 2 ; X=12, Y=4 call CursorAt ld a, (SYSVAR_FLAGS) ; FLAGS call PrintBin ; Imprimir valor (bin) ld hl, SYSVAR_FLAGS res 5, (hl) ; Limpiar BIT 5 de FLAGS ; Línea KEY_SCAN ld de, 17*256 + 4 ; X=17, Y=4 call CursorAt call ROM_KEY_SCAN ; Leer teclado ld a, _FLAG_Z call PrintFlag ; Imprimir Z Flag call PrintSpace ld a, d call PrintHex call PrintSpace ld a, e call PrintHex ; Imprimir DE ; Línea LAST_K ld de, 17*256 + 6 ; X=17, Y=6 call CursorAt ld a, (SYSVAR_LAST_K) ; LAST-K call PrintChar jr loop ; repetir en bucle (no hay ret) ROM_CLS EQU $0daf ROM_KEY_SCAN EQU $028e SYSVAR_FLAGS EQU $5c3b SYSVAR_KSTATE5 EQU $5c05 SYSVAR_LAST_K EQU $5c08 cadena DEFB "KSTATE5 ($5c05): ", _CR, _CR DEFB "FLAGS ($5c3b): ", _CR, _CR DEFB "KEYSCAN (ZF-DE): ", _CR, _CR DEFB "LAST-K ($5c08): ", _EOS ; Incluimos nuestra "libreria" de funciones INCLUDE "utils.asm" END 33500
La ejecución del programa producirá un output similar al siguiente (según las teclas que pulsemos):
Así, si quisiéramos hacer una determinada acción según la tecla pulsada, podríamos comparar el valor de LAST_K
con un ASCII o de E con un scancode, y actuar en consecuencia. Por ejemplo, para salir del programa anterior si se pulsa 'q', podríamos añadir esta comprobación al final:
; Línea LAST_K ld de, 17*256 + 6 ; X=17, Y=6 call CursorAt ld a, (SYSVAR_LAST_K) ; LAST-K cp 'q' ret z ; Si LAST-K es 'q', salir a BASIC call PrintChar jr loop ; repetir en bucle (no hay ret)
En este programa hemos aprovechado utilísimas funciones de la librería utils.asm, como la impresión de un flag concreto con PrintFlag
.
La variable del sistema FRAMES
es muy útil e interesante.
Es una variable de 3 bytes alojada en $5c78. Este valor de 3 bytes es incrementado (de una forma que ahora veremos) por el código alojado en rst $38
($0038) el cual es llamado normalmente por el Z80 a razón de 50 veces por segundo, cada 20 segundos.
Decimos normalmente porque como veremos en el capítulo dedicado a las Interrupciones del Z80, el Spectrum arranca en lo que se conoce como im1 (Modo de Interrupciones 1), en el cual se generan 50 interrupciones por segundo, y esas interrupciones provocan que el microprocesador interrumpa la ejecución del programa para saltar a $0038, ejecutar el código que está allí (Rutina de Servicio de Interrupción o ISR) y después continuar con la ejecución del programa hasta que la próxima interrupción vuelva a, valga la redundancia, interrumpirlo.
En la rst $38
se realizan principalmente 2 tareas: leer el teclado (y actualizar variables como LAST-K, entre otras), e incrementar el valor de FRAMES
.
Concretamente, rst $38
incrementa los 2 primeros bytes (los más bajos en memoria, $5c78 y $5c79) como un valor de 16 bits. Si este valor de 16 bits alcanza 0, entonces se incrementa el tercer valor, el byte en $5c7a.
Esta variable del sistema proporciona pues no sólo el tiempo que ha transcurrido desde que se encendió el Spectrum, sino que también sirve para otras funcionalidades. Principalmente:
RANDOMIZE
(en BASIC, y en la ROM) la usan.Podemos ver su funcionamiento con el siguiente programa:
; Comprobacion de valor de FRAMES ORG 35000 call CLS loop: ld de, 0 call CursorAt ; Nos vamos a (0,0) ld hl, $5c7a ; Apuntamos HL a FRAMES+2 ld a, (hl) ; Cogemos su valor call PrintHex ; Imprimir y luego espacio call PrintSpace dec hl ; Bajamos a FRAMES+1 ld a, (hl) call PrintHex ; Imprimir call PrintSpace dec hl ; Bajamos a FRAMES ld a, (hl) call PrintHex ; Imprimir jr loop ; Repetir INCLUDE "utils.asm" END 35000
El programa imprime el valor de 3 bytes de Frames colocando en HL el valor más alto (alojado en el tercer byte, en $5c7a) y muestra los 3 valores decrementando HL para cada impresión. Al ejecutarlo, veremos cómo el byte menos significativo aumenta 50 veces por segundo, hasta pasar $ff, en cuyo momento se incrementa el siguiente byte.
En el Spectrum tenemos una serie de caracteres imprimibles (con cualquiera de las rutinas de la ROM) que va desde el número 32 (que se corresponde con el Espacio) hasta el 143, que es el código del último carácter imprimible después de las diferentes letras, números y símbolos y que se corresponde con el “símbolo gráfico” de “bloque relleno”:
Colocando en el registro A cualquier valor entre 32 y 143, podemos imprimir dicho carácter. Si intentamos imprimir cualquier valor desde 163 (en modo 128) o desde 165 (en modo 48K) en adelante (163-255), por pantalla aparecerá el valor del TOKEN de BASIC asociado a dicho código (“CLEAR”, “FOR”, “PLAY”, etc).
Como se puede ver, los últimos caracteres de la tabla tienen una serie de caracteres “de bloques”, en el rango del 128 al 143, que podemos utilizar para dibujar gráficos primitivos, pero evidentemente no es suficiente para realizar juegos desde BASIC.
Sinclair solucionó esto dejando un hueco de 21 caracteres (en 48K) o 19 caracteres (en 128K) que van entre el 144 y el 161 que son “configurables” por el usuario. Estos gráficos definidos por el usuario se conocen por sus siglas en inglés: UDG (User Defined Graphics).
Podemos modificar estos 19 (o 21) caracteres para que tengan el aspecto que nosotros queramos, y que así al imprimir los valores entre 144 y 161 se imprima el UDG correspondiente.
Si no “redefinimos” los UDGs y imprimimos el ASCII correspondiente a estos valores, veremos que por defecto se corresponden con letras mayúsculas (ABCDE…).
Para que al imprimir un ASCII “144” aparezca nuestro gráfico en vez de esa “A”, tenemos que definir los UDGs y decirle a BASIC dónde puede encontrarlos.
Cada UDG está formado por 8 bytes. Cada uno de esos bytes representa el estado (1/0) de los 8 píxeles que forman cada línea del mismo. Por ejemplo, el siguiente “marciano”:
Se podría representar de la siguiente forma:
00011000 00111100 01111110 11011011 11111111 00100100 01011010 10100101
O por sus valores en decimal:
24, 60, 126, 219, 255, 36, 90, 165
Para definir los UDGs, debemos dibujarlos en alguna herramienta de dibujado de UDGs de las muchas existentes, para que nos proporcione los valores en decimal, o bien podemos dibujarlos a mano en binario con unos y ceros en nuestro programa. También podemos exportarlos como un bloque de datos binarios e incluirlos con INCBIN
en cualquier punto de nuestro programa ASM.
Una vez tenemos los UDGs en memoria, le tenemos que decir a la ROM dónde están ubicados. Así, cuando le pidamos a sus rutinas de cadenas (rst 16
, etc) que impriman un valor entre 144 y 162, aparecerán nuestros gráficos UDG en vez de esos caracteres “ABCDE…”.
Para definir la dirección de los UDGs, tenemos que modificar la variable del sistema UDG
($5c7b), que por defecto apunta al final de la memoria ($ff58) y apuntarlo a la dirección de memoria donde tengamos la definición de nuestros 19 (ó 21) UDGs de 8 bits.
Veamos un ejemplo que muestra todo lo que hemos explicado:
; Prueba de UDGs ORG 33500 call CLS ld de, cadena call PrintString ; Imprimimos UDG por defecto ld hl, Mis_UDGs ld (SYSVAR_UDG), hl ; Asignamos UDGs a nuestros datos ld de, cadena call PrintString ; Ahora vemos nuestros graficos ld de, willy call PrintString ; Pintar las 2 partes de Willy ret cadena DEFB 144, 145, 146, 147, 148, 149, 150, 151 DEFB 152, 153, 154, 155, 156, 157, 158, 159 DEFB 160, 161, 162, _CR, _CR, _EOS ; 163 y 164 solo disponibles en modelos 48K ; en 128KB imprimen "SPECTRUM" y "PLAY" willy DEFB 145, _CR, 146, _EOS Mis_UDGs DEFB %00011000 ; UDG 144: Marciano DEFB %00111100 DEFB %01111110 DEFB %11011011 DEFB %11111111 DEFB %00100100 DEFB %01011010 DEFB %10100101 DEFB %00000110 ; UDG 145: Cabeza de Willy DEFB %00111110 DEFB %01111100 DEFB %00110100 DEFB %00111110 DEFB %00111100 DEFB %00011000 DEFB %00111100 ; UDG 146: Cuerpo de Willy DEFB 126, 126, 247, 251, 60, 118, 110, 119 ; UDG 147: Patrón %01010101 DEFB 85, 85, 85, 85, 85, 85, 85, 85, 85 ; Podriamos seguir definiendo UDGs aqui... ; Resto de UDGs: se pintaran los opcodes y datos ; que hay en memoria despues de este punto... SYSVAR_UDG EQU $5c7b INCLUDE "utils.asm" END 33500
Este código imprime primero los UDGs sin redefinirlos, por lo que podremos ver arriba de la pantalla qué aspecto tienen los UDGs por defecto, que están al final de la memoria RAM porque BASIC los copia allí al inicializarse.
Después, redefimos los UDGs apuntando UDG
a Mis_UDGs
, una dirección de memoria donde hemos definido 3 UDGs, que podemos ver en la parte final del programa.
Nótese que no estamos obligados a redefinir los 19 UDGs y tener una tabla de 19*8 ó 21*8 bytes dentro de nuestro código. Si sólo vamos a usar 3 UDGs, definimos 3, y el resto de los UDGs tendrán como valor los datos/opcodes que haya en la memoria detrás de los datos que hemos definido. Serán valores aleatorios de la RAM, pero si no vamos a usarlos, da igual.
Los UDGs los podemos definir como queramos: en binario, en decimal, incluidos con INCBIN
… lo único que tenemos que saber es que cada bloque de 8 bytes se corresponde con un UDG, empezando en 144.
En nuestro caso, hemos definido en el 144 el marciano de la imagen anterior, y en el 145 y 146 las 2 partes del minero Willy, que es de 8×16 pixels y por tanto necesita 2 UDGs para poder dibujarse:
Al ejecutar el programa, veremos lo siguiente:
Veamos la salida del programa anterior:
Nótese que una vez cambiamos UDG, y volvemos a imprimir los mismos UDGs que habíamos impreso en la línea superior, ahora aparecen los gráficos que hemos definido (junto a basura a partir del cuarto UDG).
Después imprimimos las 2 partes del Minero Willy (ASCIIs 145 y 146) uno encima de otro para mostrar cómo componer un gráfico más grande a partir de UDGs.
En BASIC, en lugar de cambiar UDG, lo que se hacía era definir los gráficos en DATAs y después POKEarlos en memoria, en la zona apuntada por UDG (en lugar de cambiar UDG). Eso era necesario porque los números en los datas estaban en el código del programa y no podían usarse para cambiar UDG y apuntarlo a ellos, ya que hay TOKENs BASIC en medio. En nuestro caso eso es innecesario porque ya tenemos los gráficos en memoria en el formato correcto, así que en vez de copiar estos gráficos a la dirección apuntada por UDG (al final de la RAM), lo que hacemos es cambiar UDG para apuntar a nuestros gráficos.
De nuevo, recordamos que no es necesario que definamos todos los UDGs. En este programa hemos impreso los 19 y por eso vemos todos los “UDGs” no definidos (o definidos con los bytes de la RAM que no hemos establecido), pero si vamos a tener controlado qué UDGs imprimimos, sólo necesitamos definir aquellos que vayamos a usar.
Si nuestro juego es complejo, podemos usar más de 21 UDGs cambiándolos por ejemplo entre el menú y el juego, o entre fase y fase (manteniendo siempre al protagonista por ejemplo) y así tener más variedad en las diferentes pantallas.
No obstante, como es evidente, no es la manera más eficiente de hacer gráficos, ya que se basa en impresión de caracteres y cadenas.
De la misma forma que podemos cambiar los 19 (ó 21) ASCIIs entre 144 y 162, también podemos redefinir la fuente de letras usada en el Spectrum (tanto en el Sistema, como en nuestros programas y juegos).
Podemos conseguir esto cambiando la variable del sistema CHARS
($5c36), y apuntarla a la definición de un juego de caracteres alternativo para los símbolos entre 32 (espacio) y 127 (símbolo de copyright).
Concretamente, debemos apuntar CHARS a X siendo X la dirección de memoria donde está alojada la fuente, menos 256.
Esto permite preparar en memoria una fuente de letras alternativa (con formato similar a los UDGs, es decir, cada carácter son 8 bytes y cada byte son los 8 píxeles de una línea del carácter), empezando en el carácter 32 y hasta el 127.
El hecho de que nuestra fuente alternativa empiece en 32 es el motivo por el cual en CHARS se introduce la dirección de la fuente menos 256 (los primeros 32 caracteres, es decir, 32*8 = 256 bytes, no son imprimibles), y hace que una fuente alternativa ocupe un máximo de 768 bytes (96*8) para sus 96 caracteres imprimibles. En estos 768 bytes, podemos encontrar cada carácter en “CHARS + (8*ASCII)”.
De nuevo, basta con hacer un DEFB
de datos gráficos o un INCBIN
de un fichero de 768 bytes con datos de fuentes para cambiar la fuente para nuestros programas.
Existen diferentes repositorios de Webs que contienen múltiples fuentes descargables, como por ejemplo ZX Origins.
Veamos un ejemplo descargando la fuente ZX Times (cogemos el fichero .ch8 de la carpeta Spectrum):
; Prueba de CHARS ORG 33500 call CLS ld hl, font-256 ld (SYSVAR_CHARS), hl ; Asignamos CHARS a nuestra fuente ld de, cadena call PrintString ret cadena DB _CR, "PRUEBA DE CAMBIO DE FUENTE", _CR, _CR DB "0123456790", _CR, _CR DB "abcdefghijlmnopqrstuvwxyz", _CR, _CR DB "ABCDEFGHIJLMNOPQRSTUVWXYZ", _EOS ;; Font from ZX-ORIGINS website "zxtimes.ch8" Font: INCBIN "zxtimes.ch8" SYSVAR_CHARS EQU $5c36 INCLUDE "utils.asm" END 33500
Como hemos visto, esto nos permite personalizar la fuente de letras muy fácilmente.
Además, estas fuentes son muy útiles porque también podemos utilizarlas desde ensamblador, sin pasar por la ROM del sistema, con rutinas propias.
Dirección | Nombre | Efecto |
---|---|---|
$0010 | RST $10 | Imprime por pantalla el valor presente la pila de la calculadora de BASIC. |
$0038 | MASK_INT | Esta es la rutina que se ejecuta 50 veces por segundo (por interrupciones). Incrementa la variable (FRAMES) y actualiza el teclado. Si la llamamos nosotros, FRAMES no será exáctamente incrementado 50 veces por segundo lo que afectaría a las rutinas que la usen para temporizar. |
$028e | KEY_SCAN | Rutina que lee el teclado y deja en DE el código de la tecla pulsada o $ffff si no hay nada pulsado, y en Z un flag indicativo. |
$02bf | KEYBOARD | Rutina de la ROM que lee el teclado y actualiza LAST-K. |
$0556 | LD-BYTES | Se utiliza para cargar datos desde cinta. La veremos en un capítulo dedicado a ella. |
$0daf | CLS_ALL | Borra la pantalla con el atributo de ATTR_P, abre el canal de la pantalla y resetea las coordenadas del cursor. |
$0dfe | SCROLLING | Desplaza la pantalla hacia arriba una línea, pero si afectar a la posición del curso actual. |
$0e9e | LINE_ADDRESS | Si especificamos en A un número de línea (0-23), devuelve en HL la dirección del pixel (0,0) de esa línea en concreto en la VideoRAM (es decir, dónde empieza la línea, su primer carácter, en videomemoria). |
$15ef | PRINT_A | Rutina interna que usa rst $10 para imprimir el carácter ASCII en el registro A. |
$1601 | CHAN-OPEN | Abre el canal de salida indicado en A. 1 = las 2 líneas inferiores de la pantalla. 2 = las 22 líneas de la parte superior |
$1a1b | OUT_NUM_1 | Imprime el número contenido en BC. Limitada a números entre 0 y 9999. |
$1a28 | OUT_NUM_2 | Imprimir el número contenido (0-9999) alojado en la dirección de memoria apuntada por HL, con padding de espacios por la izquierda. |
$1e94 | UNSTACK-A | Extrae de la parte superior de la pila de la calculadora BASIC el número presente y lo asigna a A. |
$1e99 | UNSTACK-BC | Extrae de la parte superior de la pila de la calculadora BASIC el número presente y lo asigna a BC en el rango 0-65535. |
$1f3d | PAUSE_1 | Pausar BC/50 segundos o hasta que se pulse una tecla. Si se pulsó una tecla, nos encontraremos a 1 el bit 5 de la variable del sistema FLAGS ($5c3b). |
$1f54 | BREAK_KEY | Chequear si la tecla BREAK está pulsada. La rutina volverá con el Carry Flag a 0 si la tecla BREAK está siendo pulsada, y 0 si no lo está siendo (Esto comprueba si CAPS SHIFT y SPACE están siendo pulsadas simultáneamente). |
$2032 | PRINTSTACK | Extrae el valor de la parte superior de la pila de la calculadora y lo imprime en el canal actual. |
$203c | PR_STRING | Imprimir en pantalla la cadena apuntada por DE de BC caracteres de longitud. |
$229b | BORDER | Cambia el color del borde al valor contenido en el registro A. |
$22e5 | PLOT_SUB | PLOT pixel en B,C (y,x). Y está limitada a 0-175 empezando en la parte inferior de la pantalla como 0. |
$22aa | PIXEL_ADDR | Calcula HL = dirección y A = número de bit del píxel en las coordenadas BC = Y,X con Y invertido y máximo 175. |
$22b1 | PIXEL_ADDR2 | Punto interno de PIXEL_ADDR pero sin limitación para Y y con origen en la parte superior izquierda. |
$2cb3 | STACK-DE | Introduce en la parte superior de la pila de BASIC el valor del registro DE (“last value” = DE). |
$2d28 | STACK-A | Introduce en la parte superior de la pila de BASIC el valor del registro A (“last value” = A). |
$2d2b | STACK-BC | Introduce en la parte superior de la pila de BASIC el valor del registro BC (“last value” = BC). |
$2da2 | FP_TO_BC | Extrae el valor de la parte superior de la pila ('last-value') en BC. Si el valor es demasiado grande, se activa el Carry Flag. Si es negativo, se activa el Zero Flag. La parte baja del resultado se copia además en el registro A. |
$2de3 | PRINT_FP | Imprime por pantalla el valor (0-65535) presente la pila de la calculadora de BASIC (“last value”). |
$30a9 | HL=HL*DE | Calcula HL=HL*DE. Si el resultado excede 65535, el cálculo no será correcto. |
$03b5 | BEEPER | Reproduce una nota de sonido de longitud DE (f*t) y pitch HL. DE = duración_en_segundos * frecuencia HL = (437500 / frecuencia) - 30.125 Ejemplo: Nota “DO” (f=261.6) durante 2 segundos ⇒ DE = $020b y HL = $066a. Consultar Anexo al final del libro sobre equivalencia de notas, frecuencias y duraciones. |
$25f8 | S_RND | Rutina RND llamada por BASIC. |
Dirección | Nombre | Efecto |
---|---|---|
$5c08 | LAST-K | Contiene el código ASCII de la última tecla pulsada (incluso si ya no se está pulsando). |
$5c3b | FLAGS | Utilizada por BASIC para varias tareas. A nosotros nos puede interesar el bit 5: bit 5- A 1 si una nueva tecla ha sido pulsada. |
$5c7d | COORDS (2 bytes) | Las coordenadas X,Y del último punto dibujado ($5c7d = X y 5C7E = Y) |
$5c8d | ATTR_P | Es el valor de “Atributos Permanentes” que se establece con las llamadas globales tipo INK X o PAPER X en BASIC.Almacena un byte de atributos (FLASH*128 + BRIGHT*64 + (PAPER*8) + INK) para CLS. Su valor por defecto es $38 (tinta negra sobre blanco). |
$5c8f | ATTR_T | Es el valor de los “Atributos Temporales” que se establecen cuando indicamos un INK/PAPER en una sentencia (por ejemplo en un PRINT . Se aplican en la sentencia en curso, y después se vuelve a usar los “Permanentes”. |
$5c05 | KSTATE5 | Contiene el valor 5 si se está pulsando alguna tecla en este momento, y distinto de 5 si no. |
$5c36 | CHARS | Dirección (-256) de la fuente de letras a utilizar. Por defecto vale $3c00 (ubicación de la fuente estándar en la ROM). |
$5c76 | SEED (2 bytes) | La semilla de números aleatorios para RND. Usada por RANDOMIZE y RND. |
$5c78 | FRAMES (3 bytes) | Contiene el número de interrupciones ejecutadas desde que se encendió el Spectrum. Cada 50 interrupciones (en sistemas PAL) habrá transcurrido un segundo. Es un valor de 3 bytes alojado en $5c7b, $5c7c y $5c7d. Los 2 primeros bytes (los más bajos) se incrementan pues pues cada 20ms y el byte más alto se incrementa cuando el valor de los 2 primeros bytes es 0. |
$5c7b | UDG (2 bytes) | Por defecto vale $ff58 y apunta al primer carácter UDG para los ASCIIs entre 144 y 162. |