cursos:ensamblador:rutinas_rom

Rutinas de la Rom y variables del Sistema


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

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 128K/+2

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:


  • Nuestro Spectrum de 16KB de RAM nos deja apenas 8KB de RAM libres para nuestros programas y juegos (lo cual da aún más mérito a los juegos que funcionaban en Spectrums 16KB como “3D Deathchase” o “JetPac”), y unos 40KB disponibles en el modelo de 48KB.
  • Gracias a la documentación sobre la ROM del Spectrum, podemos hacer uso de sus rutinas (que fueron escritas para el Sistema Operativo) y aprovecharlas en nuestros juegos. Nos referiremos a partir de ahora a esto como llamar a rutinas de la ROM.
  • La ROM del Spectrum se apoya en una serie de Variables del Sistema. Las Rutinas de la ROM utilizan esas variables como almacén de información o como parámetros. Las variables del sistema no son más que “celdillas de memoria” normales y corrientes. Y gracias a la documentación existente sobre ellas, podemos leer y escribir esas variables en nuestros programas para conseguir diferentes efectos, principalmente en conjunción con llamadas a rutinas de la ROM.


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


 ROM_ATTR_P

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:


  • Hace incompatibles nuestros programas con los Timex Sinclair americanos y con clones que usan ROMs modificadas.
  • No se puede usar en juegos para cartuchos ROM externos.
  • No es recomendable usarla si utilizamos el modo im 2 de interrupciones.
  • Hacen uso de los registros IY y HL' sin preservarlos, por lo que no podremos usarlos nosotros.
  • Aunque algunas rutinas concretas pueden ser óptimas, en general son rutinas lentas porque están diseñadas para su uso por BASIC.
  • Usan y actualizan variables del sistema entre $5b00 y $5cca.


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:

  1. FLASH 0
  2. BRIGHT 0bb
  3. PAPER 111b = 7 = blanco (“gris”)
  4. INK 000b = 0 = negro

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:

  • La coordenada Y se manda primero, y después la X, en 2 rst 16s separados, tras el AT.
  • Ambas coordenadas empiezan en 0, siendo (0,0) la parte superior izquierda de la pantalla.
  • La coordenada X puede variar entre 0 y 31 (las 32 columnas de la pantalla).
  • La coordenada Y puede variar entre 0 y 21, ya que en el Channel 2 sólo nos permite imprimir en la parte superior de la pantalla. No podemos imprimir en las 2 líneas inferiores (Y=22 e Y=23). Si lo intentamos, aparecerá el mensaje: 5 Out of screen.
  • Si queremos imprimir en Y=22 ó Y=23, podemos hacerlo, si primero cambiamos el Canal de salida al 1 (mediante la rutina CHAN-OPEN). Tras hacerlo podremos imprimir en 22 y 23, pero no en 0 a 21.
  • Si intentamos imprimir en coordenadas fuera de rango (>31 o >23) nos aparecerá el error B Integer out of range.

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:

  • ZF a 0 ⇒ más de dos teclas están siendo pulsadas, o dos teclas pero ninguna de ellas es Shift.
  • ZF a 1 ⇒ una tecla pulsada, o dos y una de ellas es shift. En ese caso se devuelven las teclas en el registro DE.
  • Si se pulsan los 2 Shifts, entonces DE vale $2718.
  • Si se pulsa Shift junto a otra tecla, entonces D identifica el Shift y E el “código” de la otra tecla.
  • Si se pulsa sólo una tecla, sin Shift, entonces D vale $ff y E identifica la tecla.
  • Si no se pulsa ninguna tecla, entonces DE = $ffff.
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:

  • Puede usarse como semilla o valor para combinar con otros datos (como el registro R) y generar números aleatorios. De hecho, RANDOMIZE (en BASIC, y en la ROM) la usan.
  • Nos puede permitir temporizar cosas, ya que si vamos consultando y almacenando su valor, cada incremento de 50 nos indicará que ha transcurrido un segundo. De esta forma, podemos implementar “tiempo” en nuestros juegos que usen la ROM.

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.


  • cursos/ensamblador/rutinas_rom.txt
  • Última modificación: 21-01-2024 17:32
  • por sromero