Z88DK v1.8 y SP1 (SPLIB3)
Desde la última entrega de nuestro curso de Z88DK ha llovido mucho, y hay bastantes
novedades que contar. La principal de ellas es que el 9 de Marzo del 2008 se liberó la
versión 1.8 de Z88DK.
Una de las novedades más destacables de la versión 1.7 fue la integración de SPLIB en
z88dk, rebautizando a splib3 como SP1. En nuestro anterior artículo comenzamos a
utilizar SP1 a través de ejemplos que mostraban la instalación del compilador y la
utilización de esta librería de funciones para juegos.
En este artículo mostraremos los diferentes módulos de SP1 y otras bibliotecas
interesantes de Z88DK para su utilización en la creación de programas y juegos.
La nueva librería SP1
SP1 es una librería de sprites por software (puesto que el Spectrum no dispone de chip
hardware para el tratamiento de Sprites), que como el lector sabe, está diseñada para
minimizar la cantidad de dibujados de bloques gráficos en pantalla.
SP1 es la nueva versión de la antigua librería SPLIB. En esta versión (SPLIB3), la
biblioteca se divide en diferentes módulos para que el programador pueda elegir cuáles
de ellos desea usar y cuales no. Hasta ahora, si un programador quería usar las
funciones de teclado o de modo IM2 de interrupciones de SPLIB, se veía obligado a
incluir la biblioteca completa, con la consiguiente pérdida de memoria, espacio
"ejecutable" y tiempo de carga. Ahora, es posible incluir o no cada módulo
individualmente.
Librerías de Z88DK
A continuación se detallan algunas de las librerías incluídas con Z88DK. Algunas son
parte del compilador desde sus inicios, y otras son, como ya hemos comentado, diferentes
módulos de la antigua SPLIB2, ahora integrada con el nombre de SP1.
Tipos Abstractos de Datos
- Fichero de cabecera a incluir: #include
<atd.h>
- Librería a enlazar: -ladt.
Esta librería incluye funciones genéricas para algunos tipos abstractos de datos, como
Listas Enlazadas (Linked Lists), Pilas (Stacks), y Colas (Queues). Por el momento, estos
tipos de datos son dinámicos (se reserva y libera memoria con su uso), aunque están
previstas también implementaciones estáticas. Otros tipos de datos (árboles, buffers
circulares, etc.) serán implementados en el futuro.
Veamos algunas funciones de ejemplo (en este caso, de Pilas / Stacks) que muestran "el
aspecto" de la librería:
adt_StackCreate();
adt_StackDelete();
adt_StackPush();
adt_StackPop();
adt_StackPeek();
adt_StackCount();
|
La Página de documentación de libadt muestra detallados ejemplos de uso de la
librería.
Peticiones de memoria
- Fichero de cabecera a incluir: #include
<malloc.h> e #include
<balloc.h>
- Librería a enlazar: -lmalloc y -lballoc.
Esta librería incluye funciones para pedir y liberar memoria dinámicamente, en lugar de
utilizar arrays vacíos estáticos (que aumentan el tiempo de carga).
Veamos algunas funciones de ejemplo:
mallinit();
calloc();
malloc();
free();
realloc();
|
La Página de documentación de malloc y balloc muestra detallados ejemplos de uso de la
librería.
Modo 2 de Interrupciones
- Fichero de cabecera a incluir: #include
<im2.h>
- Librería a enlazar: -lim2.
Esta librería incluye funciones de gestión del modo 2 de Interrupciones. Estas funciones
nos permitirán enlazar la ejecución de código con el modo 2 de interrupciones, para así
poder implementar temporizadores y funciones sincronizadas con IM2.
Algunas de las funciones de que provee esta librería son:
im2_init();
im2_InstallISR();
im2_EmptyISR();
im2_CreateGenericISR();
|
La Página de documentación de im2.h muestra información detallada sobre los modos de
interrupciones y ejemplos de uso de la librería.
Teclado, joystick y ratón
- Fichero de cabecera a incluir: #include
<input.h>.
- Librería a enlazar: No es necesaria.
La biblioteca input.h incluye las funciones necesarias para la lectura del teclado en
nuestros juegos y programas. Algunas de las funciones más útiles dentro de esta librería
son las siguientes:
in_GetKeyReset();
in_GetKey();
in_InKey();
in_LookupKey();
in_KeyPressed();
in_WaitForKey();
in_WaitForNoKey();
in_Pause();
in_Wait();
in_JoyKeyboard();
|
Por otra parte, input.h también contiene todas las funciones necesarias para acceder a
ratón y joystick, con funciones como las siguientes. Concretamente, la anterior función
in_JoyKeyboard() emula la lectura de los joysticks con el mismo formato que la lectura
de teclas, con in_LookupKey(). Para hacer uso de la emulación de joystick, será
necesario incluir el fichero de cabecera
.
Como muestra, el siguiente ejemplo tomado de la documentación de la librería input, que
combina el uso de in_JoyKeyboard() con las llamadas a in_LookupKey():
#include <input.h>
#include <spectrum.h>
// example for ZX Spectrum target which supplies
// the following device specific joystick functions:
// in_JoyKempston(), in_JoySinclair1(), in_JoySinclair2()
uchar choice, dirs;
void *joyfunc; // pointer to joystick function
char *joynames[] = { // an array of joystick names
"Keyboard QAOPM",
"Kempston",
"Sinclair 1",
"Sinclair 2"
};
struct in_UDK k;
// initialize the struct_in_UDK with keys for use with the keyboard joystick
k.fire = in_LookupKey('m');
k.left = in_LookupKey('o');
k.right = in_LookupKey('p');
k.up = in_LookupKey('q');
k.down = in_LookupKey('a');
// print menu and get user to select a joystick
printf("You have selected the %s joystick\n", joynames[choice]);
switch (choice) {
case 0 : joyfunc = in_JoyKeyboard; break;
case 1 : joyfunc = in_JoyKempston; break;
case 2 : joyfunc = in_JoySinclair1; break;
default: joyfunc = in_JoySinclair2; break;
}
...
// read the joystick through the function pointer
dirs = (joyfunc)(&k);
if (dirs & in_FIRE)
printf("pressed fire!\n");
...
|
Librería de Sonido
- Fichero de cabecera a incluir: #include
<sound.h>.
- Librería a enlazar: No es necesaria.
Esta librería de sonido contiene funciones muy básicas de reproducción sonora, así como
algunos "efectos especiales" estándar.
bit_open();
bit_close();
bit_click();
bit_fx(N);
bit_fx2(N);
bit_fx3(N);
bit_fx4(N);
bit_synth();
bit_beep();
bit_frequency();
bit_play();
|
Más información sobre sound.h en la página oficial de la librería.
Librería de Sprites SP1
- Fichero de cabecera a incluir: #include
<sprites/sp1.h>.
- Librería a enlazar: -lsp1.
La librería sprites/SP1 contiene todo el código dedicado a Sprites de la antigua SPLIB:
creación de sprites, movimiento, borrado, etc.
Algunas de las funciones que se detallan a continuación ya fueron mostradas en nuestro
anterior entrega, incluyendo ejemplos de uso:
sp1_CreateSpr();
sp1_AddColSpr();
sp1_DeleteSpr();
sp1_MoveSprAbs();
sp1_MoveSprRel();
sp1_TileEntry();
sp1_PrintAt();
sp1_GetTiles();
sp1_PutTiles();
sp1_ClearRect();
|
Más información sobre SP1 en la página de SP1 en el wiki de Z88DK.
Esta biblioteca debe compilarse desde los fuentes de Z88DK para poder utilizarla. Esto se
hace entrando en el directorio sp1 y ejecutando el comando make sp1-spectrum:
$ cd $Z88DK
$ cd libsrc/sprites/software/sp1/
$ make sp1-spectrum
|
Librería general "spectrum.h"
- Fichero de cabecera a incluir: #include
<spectrum.h>.
- Librería a enlazar: No es necesaria.
La librería spectrum.h contiene funciones varias, como acceso directo a Joystick y Ratón,
identificar el modelo de Spectrum que ejecuta nuestro programa, detectar la existencia
de periféricos conectados al Spectrum, funciones de cinta (save/load), cambio de color
del borde, cálculo de direcciones de atributos, etc.
A continuación se muestran algunas de las funciones que podremos utilizar mediante la
inclusión de spectrum.h:
zx_type();
zx_model();
zx_soundchip();
zx_kempston();
zx_basemem();
tape_save();
tape_load_block();
tape_save_block();
in_JoyFuller();
in_JoyKempston();
in_JoySinclair1();
in_JoySinclair2();
in_MouseAMXInit();
in_MouseAMX();
zx_border();
zx_attr();
zx_screenstr();
x_cyx2saddr(); (y derivados)
|
La biblioteca define también gran variedad de constantes para su uso en nuestros
programas, como colores (BLACK, BLUE, RED, MAGENTA, etc.), identificadores de los tokens
del BASIC, etc.
Se puede consultar la página de spectrum.h para más información.
Otras librerías
Otras bibliotecas de las que podemos hacer uso:
- Funciones de E/S: stdio.h.
- Funciones de reloj y tiempo: time.h.
- Funciones de acceso al puerto serie: rs232.h.
- Implementación del algoritmo A*: algorithm.h.
- Funciones de cadena: string.h.
Puede encontrarse la documentación de cada una de ellas en la página principal de
Z88DK.
Integrando ASM de Z80 en Z88DK
Una de las cosas más interesantes de Z88DK es que nos permite utilizar ensamblador
en-línea dentro de nuestro código en C. Gracias a esto, podemos identificar las rutinas
más críticas en necesidades de velocidad (por ejemplo, rutinas gráficas, de sonido,
etc), y reescribirlas en ensamblador si es necesario.
Esto permite acelerar el ciclo de desarrollo y depuración, ya que es posible inicialmente
escribir el programa o juego íntegramente en C (simplificando mucho la estructura
general del programa), para después pasar a convertir en ensamblador, una a una,
aquellas rutinas importantes y críticas.
El código en lenguaje ensamblador se inyecta dentro de nuestro código C con la directiva
asm.
Para incluir más de una línea de código ensamblador consecutiva, se recurre a las
directivas #asm y #endasm. El siguiente código vacía el contenido de la pantalla:
#asm
ld hl, 16384
ld a, 0
ld (hl), a
ld de, 16385
ld bc, 6911
ldir
#endasm
|
La principal utilidad será, habitualmente implementar el código ensamblador en funciones
C para llamarlas desde otras partes de nuestro programa:
void BORDER_BLACK( void )
{
#asm
ld c, 254
out (c), a
ld hl, 23624
ld a, 0
ld (hl), a
#endasm
}
|
Pero para poder aprovechar la integración entre C y ASM, necesitamos conocer los
siguientes mecanismos:
- Creación y referencia de etiquetas ASM.
- Acceso a variables de C desde bloques ASM.
- Definición de variables en ASM utilizables desde C.
- Lectura desde bloques ASM de los parámetros pasados a las funciones.
- Devolución de valores desde bloques ASM llamados como funciones.
A continuación se detalla la forma de realizar esto desde Z88DK.
Creación y referencia de etiquetas ASM
Si tenemos que realizar rutinas medianamente largas, necesitaremos utilizar etiquetas
para las estructuras condicionales. A continuación se muestra un ejemplo de declaración
de una etiqueta y la referencia a la misma:
#asm
LD B, 8
.mirutina_loop ; la etiqueta lleva punto delante
LD A, (HL)
(...)
LD C, A
AND 7
JR Z, mirutina_loop ; la referencia a la etiqueta, no.
#endasm
|
Como puede verse, las etiquetas se definen con un punto delante y se referencian sin el
punto.
Nótese que hemos utilizado el nombre de la rutina dentro de la etiqueta. Esto es así
porque las etiquetas son 2 globales y no podemos llamar a 2 etiquetas de igual forma en
2 funciones diferentes. Por eso, en vez de "loop", hemos usado "mirutina_loop", de forma
que en una segunda rutina usaremos "mirutina2_loop".
Accediendo a variables de C desde ASM
Dentro de los bloques de ASM podemos acceder a las variables globales definidas en el
código de C. Esto se mediante el símbolo de subrayado antes del nombre de la
variable:
char borde;
void BORDER( void )
{
#asm
(...)
// Leer variable:
ld hl, 23624
ld a, (_borde)
ld (hl), a
// Escribir variable:
ld (_borde), a
// Escribir variable (forma 2):
ld hl, _borde
ld a, (hl)
#endasm
}
|
Es de vital importancia, a la hora de leer y escribir valores en variables, que
respetemos el tipo de variable en que estamos escribiendo. Si estamos tratando de
modificar una variable de 1 sólo byte (apuntada por HL), utilizaremos una operación del
tipo LD (HL), A para que, efectivamente, se escriba un sólo byte en la posición de
memoria apuntada por la variable. Si escribimos más de un byte, estaremos afectando al
byte siguiente al de la variable en cuestión, con lo que modificaremos el valor de la
siguiente variable o bloque de código en memoria.
Recuerda para esto que:
- signed y unsigned char -> 1 byte.
- signed y unsigned short -> 2 bytes.
- signed y unsigned int -> 2 bytes.
- signed y unsigned char * -> 2 bytes (es una dirección de memoria).
Finalmente, cabe destacar que sólo se pueden acceder a variables globales: no se podrá
acceder desde esta forma a variables definidas dentro de las funciones, puesto que son
locales y están localizadas en la pila. Tampoco se podrá acceder de esta forma a los
parámetros pasados a las funciones.
Es decir, los siguientes ejemplos no son válidos:
void BORDER( unsigned char borde )
{
char varlocal;
#asm
ld a, (_varlocal) // MAL
(...)
ld hl, 23624
ld a, (_borde) // MAL
ld (hl), a
#endasm
}
|
Uso de variables temporales desde ASM
También es posible definir "variables" y bloques de datos dentro de lo que es el "binario
ejecutable" del programa, y refenciar a ellos después desde ensamblador. Esto es
habitual a la hora de utilizar "variables temporales" o de almacenamiento en rutinas
ensamblador cuando tenemos necesidades de almacenamiento que hacen a los registros
insuficientes.
Como no vamos a referenciar las variables desde C, no tenemos que utilizar el carácter de
subrayado antes del nombre de la variable.
Recuerda, a la hora de guardar los datos dentro de las etiquetas que hacen referencia a
ellas, que debes guardar el número de bytes adecuado para no machacar datos o código que
vaya tras ellos:
#asm
ld a, (hl)
ld (valor_temp), a ; guardamos un byte (A)
inc hl
ld c, (hl)
inc hl
ld b, (hl)
inc hl ; construimos BC como un word
ld (valor_int), bc ; guardamos un word (2 bytes)
(...)
ret
valor_temp defb 0
valor_int defw 0
#endasm
|
Definición de variables en bloques ASM
Acabamos de ver cómo acceder en los bloques de ASM a variables definidas desde C. A
continuación veremos el proceso inverso: definiremos variables (incluso bloques de datos
contiguos) dentro de bloques de ASM que después podrán ser referenciadas desde código
C.
Para ello, definimos al principio del código C las variables tal y como las
referenciaremos desde C. Es importante indicar el tipo de dato adecuado (char, int, o
char *):
extern unsigned char caracter;
extern unsigned int dato_int;
extern unsigned char sprite0[];
|
Después, en otra zona del programa (normalmente al final del código o en un fichero .c/.h
aparte), se define el dato o array de datos al que referencian las variables que hemos
indicado como extern. Para ello utilizaremos las directivas DEFB, teniendo en cuenta que
el tipo de la variable C nos indica la longitud que debe tener el dato.
#asm
._caracter
DEFB 52
._dato_int
DEFB 0, 0
._sprite0
DEFB 199,56,131,124,199,56,239,16
DEFB 131,124,109,146,215,40,215,40
(...etc...)
#endasm
|
En realidad, dentro del bloque #asm sólo estamos definiendo etiquetas a direcciones de
memoria (igual que pueda ser la de un bucle) que coincidirán, para el compilador, con
las variables (que también son etiquetas a direcciones) definidas como externs del mismo
nombre.
Pasando parámetros a funciones ASM
Hemos visto que no es posible acceder directamente desde los bloques #asm a las variables
locales de una función, así como a los parámetros de llamada, ya que en realidad están
almacenados en la pila. Pero aunque no se pueda acceder de forma directa a estos
valores, sí que podemos acceder a ellos leyéndolos mediante la ayuda del puntero de pila
SP.
Sin utilizar la pila, la mejor forma de pasar parámetros a las funciones es usar algún
conjunto de variables globales que utilicemos en las funciones:
// Definimos unas cuantas variables globales que usaremos
// como parametros para nuestras funciones que usen ASM:
char auxchar1, auxchar2, auxchar3, auxchar4;
int auxint1, auxint2, auxint3, auxint4;
// A la hora de llamar a una función, lo haríamos así:
auxchar1 = 10;
auxchar2 = 20;
MiFuncion();
// De este modo, la función podria hacer lo siguiente:
void MiFuncion( void )
{
#asm
ld a, (_auxchar1)
ld b, a
ld a, (_auxchar2)
(etc...)
#endasm
}
|
Aunque este método es fáctible (y rápido), resulta más legible el evitar la utilización
de este tipo de variables (que haría complicada la creación de funciones recursivas o
anidadas). Para ello, utilizaremos el sistema de paso de argumentos basado en la
pila.
En C (y en otros lenguajes de programación) los parámetros se insertan en la pila en el
orden en que son leídos. La subrutina debe utilizar el registro SP (mejor dicho, una
copia) para acceder a los valores apilados en orden inverso. Estos valores son siempre
de 16 bits aunque las variables pasadas sean de 8 bits (en este caso ignoraremos el byte
que no contiene datos, el segundo).
Veamos unos ejemplos:
int jugador_x, jugador_y;
jugador_x = 10;
jugador_y = 200;
Funcion( jugador_x, jugador_y );
(...)
//------------------------------------------------
int Funcion( int x, int y )
{
#asm
ld hl, 2
add hl, sp ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (el valor de Y)
ld c, (hl)
inc hl
ld b, (hl)
inc hl ; Ahora BC = y
ld e, (hl)
inc hl
ld d, (hl)
inc hl ; Ahora, DE = x
; ahora hacemos lo que queramos en asm
; utilizando DE y BC, que son los valores
; pasados como X e Y
#endasm
}
|
No tenemos que preocuparnos por hacer PUSH y POP de los registros para preservar su valor
dado que C lo hace automáticamente antes y después de cada #asm y #endasm.
El problema es que conforme crece el número de parámetros apilados, es posible que
tengamos que hacer malabarismos para almacenarlos, dado que no podemos usar HL (es
nuestro puntero a la pila en las lecturas). Veamos el siguiente ejemplo con 3
parámetros, donde tenemos que usar PUSH para guardar el valor de DE y EX DE, HL para
acabar asociando el valor final a HL:
int Funcion( int x, int y, int z )
{
#asm
ld hl, 2
add hl, sp ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (valor de Z)
ld c, (hl)
inc hl
ld b, (hl)
inc hl ; Ahora BC = z
ld e, (hl)
inc hl
ld d, (hl)
inc hl ; Ahora, DE = y
push de ; Guardamos DE
ld e, (hl)
inc hl
ld d, (hl)
inc hl ; Usamos DE para leer el valor de x
ex de, hl ; Ahora cambiamos x a HL
pop de ; Y recuperamos el valor de y en DE
; (ahora hacemos lo que queramos en asm)
#endasm
}
|
La manera de leer bytes (char) pulsados en C es de la misma forma que leemos una palabra
de 16 bits, pero ignorando la parte alta. En realidad, como la pila es de 16 bits, el
compilador convierte el dato de 8 bits en uno de 16 (rellenando con ceros) y pulsa este
valor:
int Funcion( char x, y )
{
#asm
ld hl,2
add hl,sp ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (y)
ld a, (hl) ; Aquí tenemos nuestro dato de 8 bits (y)
ld b, a
inc hl
inc hl ; La parte alta del byte no nos interesa
ld a, (hl) ; Aquí tenemos nuestro dato de 8 bits (x)
ld c, a
inc hl
inc hl ; La parte alta del byte no nos interesa
; (ahora hacemos lo que queramos en asm)
#endasm
}
|
En ocasiones, es posible que incluso tengamos que utilizar variables auxiliares de
memoria para guardar datos. En este caso, utilizamos memoria adicional pero evitamos el
uso de la pila.
int Funcion( int x, int y, char z )
{
#asm
ld hl, 2
add hl, sp ; Ahora SP apunta al ultimo parametro metido
; en la pila por el compilador (Z)
ld c, (hl)
inc hl
ld b, (hl)
inc hl ; Ahora BC = y
ld (valor_y), bc ; nos lo guardamos, BC libre de nuevo
ld c, (hl)
inc hl
ld b, (hl)
inc hl
ld (valor_x), bc ; Nos lo guardamos, BC libre de nuevo
ld a, (hl)
ld (valor_z), a ; Nos guardamos el byte
inc hl
inc hl ; La parte alta del byte no nos interesa
(ahora hacemos lo que queramos en asm)
RET
valor_x defw 0
valor_y defw 0
valor_z defb 0
#endasm
}
|
Devolución de valores por funciones
Por contra, para devolver valores no se utiliza la pila (dado que no podemos tocarla),
sino que se utiliza el registro HL. Al finalizar la función C, el valor que contenga el
registro HL será el devuelto y asignado en la llamada.
En tal caso, cuando realicemos una llamada a una función que contenga un bloque ASM, el
valor indicado en HL en el momento de retorno será el devuelto por la misma.
Supongamos pues la siguiente llamada:
valor = MiFuncion( a, b, c);
|
En este caso, a valor se le asignará el contenido del registro HL. Como tipos de
variables de devolución podremos usar int, short o char (en este último caso sólo se
asignará la parte baja de HL).
A continuación, como ejemplo, se muestra una función que dadas unas coordenadas X, Y de
pantalla (con X entre 0 y 31 e Y entre 0 y 24), devuelve la dirección de memoria donde
se puede alterar el atributo correspondiente a dichas coordenadas. El ejemplo muestra
cómo recibir variables por la pila, realizar cálculos con ellas, y devolver un valor en
HL:
//
// Devuelve la direccion de memoria del atributo de un caracter
// de pantalla, de coordenadas (x,y). Usando la dirección que
// devuelve esta función (en HL, devuelto en la llamada), podemos
// leer o cambiar los atributos de dicho carácter.
//
// Llamada: valor = Get_LOWRES_Attrib_Address( 1, 3 );
//
int Get_LOWRES_Attrib_Address( char x, char y )
{
#asm
ld hl, 2
add hl, sp ; Leemos x e y de la pila
ld d, (hl) ; d = y
inc hl ; Primero "y" y luego "x".
inc hl ; Como son "char", ignoramos parte alta.
ld e, (hl) ; e = x
ld h, 0
ld l, d
add hl, hl ; HL = HL*2
add hl, hl ; HL = HL*4
add hl, hl ; HL = HL*8
add hl, hl ; HL = HL*16
add hl, hl ; HL = HL*32
ld d, 0
add hl, de ; Ahora HL = (32*y)+x
ld bc, 16384+6144 ; Ahora BC = offset attrib (0,0)
add hl, bc ; Sumamos y devolvemos en HL
#endasm
}
|
En resumen
La intención con este artículo final del curso de Z88DK era cerrar un conjunto de
contenidos básicos y necesarios a la hora de desarrollar aplicaciones y juegos con este
fantástico compilador cruzado de C. En la anterior entrega vimos su instalación y la
utilización de la librería SP1, y en esta se han detallado los diferentes módulos de SP1
y la integración de ASM dentro de nuestros programas en C.
Ambos capítulos, unidos, deberían permitir a un lector con conocimientos de C comenzar su
andadura en el desarrollo de aplicaciones o juegos en C para Spectrum, incluyendo la
posibilidad de utilizar ensamblador en aquellas partes del programa que lo
requieran.