Introducción y conceptos básicos
Todos aquellos que hayáis programado en BASIC conoceréis sin duda las limitaciones de
este lenguaje de alto nivel: a cambio de su sencillez pagamos una penalización enorme en
velocidad. BASIC es un lenguaje interpretado, lo que quiere decir que el "sistema
operativo" (más bien el intérprete BASIC integrado en la ROM) del Spectrum tiene que
leer línea a línea nuestro programa, decodificar lo que estamos diciendo en lenguaje
BASIC y ejecutarlo.
Eso implica que cada vez que se ejecuta el programa, para todas y cada una de las líneas,
no sólo se está ejecutando nuestro programa sino que debajo de él tenemos a la CPU del
Spectrum (que no es especialmente potente) ejecutando un intérprete de BASIC que nos
roba tiempo de ejecución y hace que un programa diseñado e implementado de una forma
elegante se ejecute con una lentitud que no podemos salvar.
LOS LÍMITES DE BASIC
BASIC tiene una serie de trucos más o menos conocidos para acelerar su ejecución:
escribir muchas instrucciones en una sóla línea BASIC, poner las rutinas que más
velocidad necesitan en las primeras líneas de programa, reducir el nombre (en longitud)
de las variables, etc. Pero al final llegamos a un punto en que no podemos mejorar
nuestros programas en cuanto a velocidad. Sin duda, BASIC es un comienzo prácticamente
obligado para programar, pero no debería ser el final. Dejando de lado que sigue siendo
una herramienta muy útil para programar en el Spectrum, para muchos llega la hora de dar
el siguiente paso.
|
Lenguaje BASIC y su intérprete |
Una de las primeras posibilidades que se nos plantea más allá del Intérprete BASIC del
Spectrum es la utilización de un compilador de BASIC, como por ejemplo MCODER: seguimos
programando en BASIC, pero lo hacemos dentro de un entorno de desarrollo (dentro del
mismo Spectrum) que cuando terminamos de introducir nuestro programa, actúa como el
intérprete BASIC, sólo que en lugar de ejecutar el programa lo compila y lo graba
directamente en el formato que entiende el Z80. A partir de un programa en BASIC
obtenemos (por ejemplo en cinta) un ejecutable que podremos cargar directamente desde el
cassette. La labor de interpretación se hace igualmente, pero se hace antes, ya que en
lugar de ejecutar, el resultado de la interpretación se graba en cinta. Un programa en
BASIC compilado y ejecutado de este modo es muchísimo más rápido que el mismo programa
ejecutado en el intérprete de BASIC del Spectrum.
MCODER es una buena solución, y para muchos puede ser suficiente para muchas de sus
creaciones. Nuestra querida DINAMIC realizó sus primeros juegos en BASIC y los compiló
con MCODER: hablamos de Babaliba, Saimazoom, o la utilidad Artist. La pega es que MCODER
tiene unas limitaciones que no tienen porqué ser especialmente problemáticas si las
conocemos, las aceptamos, y realizamos nuestros programas teniéndolas en cuenta. Por
ejemplo, no podemos utilizar vectores (creados con DIM en BASIC), y el manejo de cadenas
sufre algunos cambios de sintaxis, entre otros.
ALTERNATIVAS A BASIC
Aparte de compilar BASIC existen 3 alternativas más para programar juegos y aplicaciones
que expriman al máximo nuestra máquina:
Para empezar, como primera opción, podemos realizar pequeñas rutinas en ensamblador y
utilizarlas desde nuestros programas en BASIC. Posteriormente veremos todo lo necesario
sobre el lenguaje ensamblador, pero como muchos de vosotros ya sabéis, se trata del
lenguaje más cercano a lo que es el código binario que entiende directamente un
microprocesador. El lenguaje ensamblador es de bajo nivel, es decir, está más lejos del
lenguaje humano de lo que está BASIC, y a la vez está muy cerca del lenguaje que
entiende el microprocesador de nuestro Spectrum.
En BASIC, una instrucción es traducida por el Intérprete BASIC a una serie más o menos
larga de comandos en lenguaje máquina. Por ejemplo, 10 PRINT "HOLA", se traduce como una
serie de comandos en lenguaje máquina que podrían ser algo como "para cada una de las
letras de la palabra HOLA, realiza todas las operaciones necesarias para mostrar en
pantalla todos los píxels que forman dichas letras, actualizando la posición del cursor
y usando tal y cual color". Una instrucción BASIC equivale a muchísimas instrucciones en
código máquina. Por contra, una instrucción en lenguaje ensamblador equivale a una
instrucción en lenguaje máquina: hablamos directamente el lenguaje de la máquina, sólo
que en vez de hacerlo con unos y ceros, lo hacemos en un lenguaje que tiene unas
determinadas reglas de sintaxis y que el "programa ensamblador" se encarga de traducir a
código máquina. Es por eso que programar en ensamblador es de "bajo nivel": hablamos
directamente al nivel de la máquina, y por eso mismo los programas son más complicados
de escribir, de leer y de mantener que un programa en BASIC, donde se habla un lenguaje
más natural y que es traducido a lo que la máquina entiende.
; Listado 1
; Ejemplo de rutina de multiplicacion en ASM.
; El registro HL obtiene el valor de H*E .
; por David Kastrup (Z80 FAQ).
LD L, 0
LD D, L
LD B, 8
MULT: ADD HL, HL
JR NC, NOADD
ADD HL, DE
NOADD: DJNZ MULT
|
Así, realizamos una rutina o un conjunto de rutinas en ensamblador. Mediante un programa
ensamblador, traducimos el código ASM a código que entiende directamente la máquina
(código binario) y lo salvamos en cinta (o si es corto, anotamos sus valores para
meterlos en DATAs) y mediante una serie de procedimientos que veremos más adelante,
metemos ese código binario en memoria y lo llamamos en cualquier momento desde
BASIC.
Por otro lado, nuestra segunda opción: aprender lenguaje C, y realizar programas
íntegramente en C que son compilados (al igual que hace MCODER) y trasladados a código
binario que ejecutará el Spectrum. Podemos ver el lenguaje C (en el Spectrum) como una
manera de realizar programas bastante rápidos saltándonos las limitaciones de BASIC. No
llega a ser ensamblador, y desde luego es mucho más rápido que BASIC (y que BASIC
compilado). C es un lenguaje muy potente pero tal vez sea demasiado complejo para mucha
gente que quiere hacer cosas muy concretas de forma que C se convierte en algo así como
"matar moscas a cañonazos". Para quien ya conozca el lenguaje C y se desenvuelva bien
con él, utilizar un compilador cruzado como Z88DK será sin duda una de las mejores
opciones. Programando en C se puede hacer prácticamente cualquier aplicación y un gran
número de juegos. Además, se puede embeber código ensamblador dentro de las rutinas en
C, con lo cual se puede decir que no estamos limitados por el lenguaje C a la hora de
realizar tareas que requieren un control muy preciso de la máquina. Para quien se decida
por esta opción, nada mejor que Z88DK tal y como os estamos mostrando mes a mes en el
curso de "Programación en C con Z88DK para Spectrum" de MagazineZX.
Finalmente, la tercera y última opción: nos hemos decidido y queremos hablarle a la
máquina directamente en su lenguaje, ya que queremos controlar todo lo que realiza el
microprocesador. Con BASIC compilado y con C, es el compilador quien transforma nuestros
comandos en código máquina. Con Ensamblador, nosotros escribimos directamente código
máquina. La máquina hará exactamente lo que le digamos, y nadie hará nada por nosotros.
En este caso tenemos que programar la máquina en ensamblador (assembler en inglés, o ASM
para abreviar). La diferencia de este modo con el primero que hemos comentado (integrar
ASM con BASIC) es que no existe ni una sóla línea de BASIC (como mucho el cargador que
lanza el programa) y realizamos todo en ensamblador.
Es importante destacar que el desarrollo de un programa en ASM requiere mucho más tiempo,
un mejor diseño y muchos más conocimientos del hardware (muchísimos más) que utilizar
cualquier otro lenguaje. Un programa en BASIC sencillo puede tener 1000 líneas, pero el
mismo programa en ASM puede tener perfectamente 5000, 10000, o muchas más líneas. En
ensamblador no tenemos a nadie que haga nada por nosotros: no existe PRINT para imprimir
cosas por pantalla, si queremos imprimir texto tenemos que imprimir una a una las
letras, calculando posiciones, píxeles, colores, y escribiendo en la videomemoria,
nosotros mismos. Podemos apoyarnos en una serie de rutinas que hay en la ROM del
Spectrum (que son las que utiliza BASIC), pero en general, para la mayoría de las
tareas, lo tendremos que hacer todo nosotros.
Un ejemplo muy sencillo y devastador: en BASIC podemos multiplicar 2 números con el
operador "*". En ensamblador, no existe un comando para multiplicar 2 números. No existe
dicho comando porque el micro Z80 tiene definida la operación de suma (ADD), por
ejemplo, pero no tiene ninguna instrucción para multiplicar. Y si queremos multiplicar 2
números, nos tendremos que hacer una rutina en ensamblador que lo haga (como la rutina
que hemos visto en el apartado anterior).
Sé que estoy siendo duro y poniendo a la vista del lector un panorama desolador, pero esa
es la realidad con el ensamblador: cada instrucción en ensamblador se corresponde con
una instrucción de la CPU Z80. Si quieres hacer algo más complejo que lo que te permite
directamente la CPU, te lo has de construir tú mismo a base de utilizar esas
instrucciones. Una multiplicación se puede realizar como una serie de sumas, por
ejemplo.
Visualmente, en BASIC para construir una casa te dan paredes completas, ventanas,
escaleras y puertas, y combinándolos te construyes la casa. En ASM, por contra, lo que
te dan es un martillo, clavos, un cincel, y madera y roca, y a partir de eso tienes que
construir tú todos los elementos del programa.
Obviamente, no tendremos que escribir miles de rutinas antes de poder programar cualquier
cosa: existen rutinas ya disponibles que podemos aprovechar. En Internet, en revistas
Microhobby, en libros de programación de Z80, en la ROM del Spectrum (aprovechando cosas
de BASIC), encontraremos rutinas listas para utilizar y que nos permitirán multiplicar,
dividir, imprimir cadenas de texto, y muchas otras cosas.
POR QUÉ APRENDER ASM DE Z80
Está claro que cada lenguaje tiene su campo de aplicación, y utilizar ensamblador para
hacer una herramienta interactiva para el usuario (con mucho tratamiento de textos, o de
gráficos) o bien para hacer un programa basado en texto, o una pequeña base de datos o
similar es poco recomendable.
Donde realmente tiene interés el ASM es en la creación de determinadas rutinas, programas
o juegos orientados a exprimir el hardware de la máquina, es decir: aquellos programas
orientados a escribir rápidamente gráficos en pantalla, reproducir música, o controlar
el teclado con gran precisión son los candidatos ideales para escribirlos en ASM. Me
estoy refiriendo a los juegos.
Ensamblador es el lenguaje ideal para programar juegos que requieran gran velocidad de
ejecución. Como veremos en el futuro, dibujar en pantalla se reduce a escribir valores
en memoria (en una zona concreta de la memoria). Leer del teclado se reduce a leer los
valores que hay en determinados puertos de entrada/salida de la CPU, y la reproducción
de música se realiza mediante escrituras en otros puertos. Para realizar esto se
requiere mucha sincronización y un control total de la máquina, y esto es lo que nos
ofrece ensamblador.
En MagazineZX hemos pensado y creado este curso con los siguientes objetivos en
mente:
- Conocer el hardware del Spectrum, y cómo funciona internamente.
- Conocer el juego de instrucciones del Z80 que lleva el Spectrum.
- Saber realizar programas en lenguaje ASM del Z80.
- Aprender a realizar pequeñas rutinas que hagan tareas determinadas y que luego
usaremos en nuestros programas en BASIC.
- Con la práctica, ser capaces de escribir un juego o programa entero en ASM.
Este pequeño curso será de introducción, pero proporcionará todos los conceptos
necesarios para hacer todo esto. El resto lo aportará el tiempo que nos impliquemos y la
experiencia que vayamos adoptando programando en ensamblador. No se puede escribir un
juego completo en ensamblador la primera vez que uno se acerca a este lenguaje, pero sí
que puede uno realizar una pequeña rutina que haga una tarea concreta en un pequeño
programa BASIC. La segunda vez, en lugar de una pequeña rutina hará un conjunto de
rutinas para un juego mayor, y, con la práctica, el dominio del lenguaje se puede
convertir para muchos en una manera diferente o mejor de programar: directamente en
ensamblador.
Queremos destacar un pequeño detalle: programar en ensamblador no es fácil. Este curso
deberían seguirlo sólo aquellas personas con ciertos conocimientos sobre ordenadores o
programación que se sientan preparadas para dar el paso al lenguaje ensamblador. Si
tienes conocimientos de hardware, sabes cómo funciona un microprocesador, has realizado
uno o más programas o juegos en BASIC u otros lenguajes o sabes lo que es binario,
decimal y hexadecimal (si sabes cualquiera de esas cosas), entonces no te costará nada
seguir este pequeño curso. Si, por el contrario, no has programado nunca, y todo lo que
hemos hablado no te suena de nada, necesitarás mucha voluntad y consultar muchos otros
textos externos (o al menos aplicarte mucho) para poder seguirnos.
Un requerimiento casi imprescindible es que el lector debe de conocer fundamentos básicos
del sistema de codificación decimal, hexadecimal y binario. Como ya sabéis, nosotros
expresamos los números en base decimal, pero esos mismos números se pueden expresar
también en hexadecimal, o en binario. Son diferentes formas de representar el mismo
número, y para distinguir unas formas de otras se colocan prefijos o sufijos que nos
indican la base utilizada:
DECIMAL HEXADECIMAL BINARIO
-------------------------------------------------
64d ó 64 $40 ó 40h %01000000
255d ó 255 $FF ó FFh %11111111
3d ó 3 $03 ó 03h %00000011
Para seguir el curso es muy importante que el lector sepa distinguir unas bases de
codificación de otras y que sepa (con más o menos facilidad) pasar números de una base a
otra. Quien no sepa esto lo puede hacer con práctica, conforme va siguiendo el
curso.
En realidad, intentaremos ser muy claros, máxime cuando no vamos a profundizar al máximo
en el lenguaje: utilizaremos rutinas y ejemplos sencillos, prácticos y aplicados, y los
ejecutaremos sobre emuladores de Spectrum o debuggers.
Y tras este preámbulo, podemos pasar a lo que es el curso en sí.
EL LENGUAJE ENSAMBLADOR
Como ya hemos comentado, el lenguaje ensamblador es un lenguaje de programación muy
próximo a lo que es el código máquina del microprocesador Z80. En este lenguaje, cada
instrucción se traduce directamente a una instrucción de código máquina, en un proceso
conocido como ensamblado.
Nosotros programamos nuestras rutinas o programas en lenguaje ensamblador en un fichero
de texto con extensión .asm, y con un programa ensamblador lo traducimos al código
binario que entiende la CPU del Spectrum. Ese código binario puede ser ejecutado,
instrucción a instrucción, por el Z80, realizando las tareas que nosotros le
encomendemos en nuestro programa.
Este mes no vamos a ver la sintaxis e instrucciones disponibles en el ensamblador del
microprocesador Z80 (el alma de nuestro Sinclair Spectrum): eso será algo que haremos
entrega a entrega del curso. Por ahora nos debe bastar conocer que el lenguaje
ensamblador es mucho más limitado en cuanto a instrucciones que BASIC, y que, a base de
pequeñas piezas, debemos montar nuestro programa entero, que será sin duda mucho más
rápido en cuanto a ejecución.
Como las piezas de construcción son tan pequeñas, para hacer tareas que son muy sencillas
en BASIC, en ensamblador necesitaremos muchas líneas de programa, es por eso que los
programas en ensamblador en general requieren más tiempo de desarrollo y se vuelven más
complicados de mantener (de realizar cambios, modificaciones) y de leer conforme crecen.
Debido a esto cobra especial importancia hacer un diseño en papel de los bloques del
programa (y seguirlo) antes de programar una sóla línea del mismo. También se hacen
especialmente importantes los comentarios que introduzcamos en nuestro código, ya que
clarificarán su lectura en el futuro. El diseño es CLAVE y VITAL a la hora de programar:
sólo se debe implementar lo que está diseñado previamente, y cualquier modificación de
las especificaciones debe resultar en una modificación del diseño.
Así pues, resumiendo, lo que haremos a lo largo de este curso será aprender la
arquitectura interna del Spectrum, su funcionamiento a nivel de CPU, y los fundamentos
de su lenguaje ensamblador, con el objetivo de programar rutinas que integraremos en
nuestros programas BASIC, o bien programas completos en ensamblador que serán totalmente
independientes del lenguaje BASIC.
CÓDIGO MAQUINA EN PROGRAMAS BASIC
Supongamos que sabemos ensamblador y queremos mejorar la velocidad de un programa BASIC
utilizando una rutina en ASM. El lector se preguntará: "¿cómo podemos hacer esto?".
La integración de rutinas en ASM dentro de programas BASIC se realiza a grandes rasgos de
la siguiente forma: escribimos nuestra rutina en ensamblador, por ejemplo una rutina que
realiza un borrado de la pantalla mucho más rápidamente que realizarlo en BASIC, o una
rutina de impresión de Sprites o gráficos, etc.
Una vez escrito el programa o la rutina, la ensamblamos (de la manera que sea:
manualmente o mediante un programa ensamblador) y obtenemos en lugar del código ASM una
serie de valores numéricos que representan los códigos de instrucción en código máquina
que se corresponden con nuestro listado ASM.
|
Parte de una tabla de ensamblado manual |
Tras esto, nuestro programa en BASIC debe cargar esos valores en memoria (mediante
instrucciones POKE) y después saltar a la dirección donde hemos POKEADO la rutina para
ejecutarla.
Veamos un ejemplo de todo esto. Supongamos el siguiente programa en BASIC, que está
pensado para rellenar toda la pantalla con un patrón de píxeles determinado:
10 FOR n=16384 TO 23295
20 POKE n, 162
30 NEXT n
|
Lo que hace este programa en BASIC es escribir un valor (el 162) directamente en la
memoria de vídeo (en la próxima entrega veremos qué significa esto) y, en resumen, lo
que consigue es dibujar en pantalla con unos colores determinados. Si ejecutamos el
programa en BASIC veremos lo siguiente:
|
Salida del programa BASIC de ejemplo |
Teclead y ejecutad el programa. Medid el tiempo necesario para "pintar" toda la pantalla
y anotadlo. Podéis también utilizar el fichero "ejemplo1-bas.tap" que os ofrecemos ya
convertido a TAP.
A continuación vamos a ver el mismo programa escrito en lenguaje ensamblador:
; Listado 2: Rellenado de pantalla
ORG 40000
LD HL, 16384
LD A, 162
LD (HL), A
LD DE, 16385
LD BC, 6911
LDIR
RET
|
Supongamos que ensamblamos este programa con un programa ensamblador (luego veremos cuál
utilizaremos durante el curso) y generamos un TAP o TZX con él. Ejecutad el programa y
calculad el tiempo (si podéis). Es en ejemplos tan sencillos como este donde podemos ver
la diferencia de velocidad entre BASIC y ASM.
Para ensamblar el programa lo que hacemos es teclear el código anterior en un fichero de
texto de nombre "ejemplo1.asm" en un ordenador personal. A continuación, el proceso de
ensamblado y ejecución lo podemos hacer de 3 formas:
- a.- Con un programa ensamblador generamos un fichero bin (o directamente un fichero
TAP) y con bin2tap generamos un TAP listo para cargar en el emulador.
- b.- Con un programa ensamblador generamos un fichero bin (que no es más que el
resultado de ensamblar el código ASM y convertirlo en códigos que entiende el
microprocesador Z80), los pokeamos en memoria en BASIC y saltamos a ejecutarlos.
- c.- Con un programa ensamblador generamos un fichero bin, lo convertimos a un tap
sin cabecera BASIC y lo cargamos en nuestro programa BASIC con un LOAD "" CODE
DIRECCION. Tras esto saltamos a la DIRECCION donde hemos cargado el código para que
se ejecute.
La opción a.- es la más sencilla, y lo haremos fácilmente mediante el ensamblador que
hemos elegido: PASMO. Sobre las opciones b.- y c.-, el ensamblado lo podemos hacer
también con PASMO, o mediante una tabla de conversión de Instrucciones ASM a Códigos de
Operación (opcodes) del Z80, ensamblando manualmente (tenemos una tabla de conversión en
el mismo manual del +2, por ejemplo).
Supongamos que ensamblamos a mano el listado anterior. Ensamblar a mano consiste en
escribir el programa y después traducirlo a códigos de operación consultando una tabla
que nos dé el código correspondiente a cada instrucción en ensamblador. Tras el
ensamblado obtendremos el siguiente código máquina (una rutina de 15 bytes de
tamaño):
21, 00, 40, 3e, a2, 77, 11, 01, 40, 01, ff, 1a, ed, b0, c9
O, en base decimal:
33, 0, 64, 62, 162, 119, 17, 1, 64, 1, 255, 26, 237, 176, 201
Esta extraña cadena tiene significado para nuestro Spectrum: cuando él encuentra, por
ejemplo, los bytes "62, 162", sabe que eso quiere decir "LD A, 162"; cuando encuentra el
byte "201", sabe que tiene que ejecutar un "RET", y así con todas las demás
instrucciones. Un detalle: si no queremos ensamblar a mano podemos ensamblar el programa
con PASMO y después obtener esos números abriendo el fichero .bin resultando con un
editor hexadecimal (que no de texto).
A continuación vamos a BASIC y tecleamos el siguiente programa:
10 CLEAR 39999
20 DATA 33, 0, 64, 62, 162, 119, 17, 1, 64, 1, 255, 26, 237, 176, 201
30 FOR n=0 TO 14
40 READ I
50 POKE (40000+n), I
60 NEXT n
|
Tras esto ejecutamos un RANDOMIZE USR 40000 y con eso ejecutamos la rutina posicionada en
la dirección 40000, que justo es la rutina que hemos ensamblado a mano y pokeado
mediante el programa en BASIC.
Lo que hemos hecho en el programa BASIC es:
- Con el CLEAR nos aseguramos de que tenemos libre la memoria desde 40000 para arriba
(hacemos que BASIC se situe por debajo de esa memoria).
- La línea DATA contiene el código máquina de nuestra rutina.
- Con el bucle FOR hemos POKEado la rutina en memoria a partir de la dirección 40000
(desde 40000 a 40015).
- El RANDOMIZE USR 40000 salta la ejecución del Z80 a la dirección 40000, donde está
nuestra rutina. Recordad que nuestra rutina acaba con un RET, que es una instrucción
de retorno que finaliza la rutina y realiza una "vuelta" al BASIC.
Siguiendo este mismo procedimiento podemos generar todas las rutinas que necesitemos y
ensamblarlas, obteniendo una ristra de código máquina que meteremos en DATAs y
pokearemos en memoria. También podemos grabar en fichero BIN resultante en cinta
(convertido a TAP) tras nuestro programa en BASIC, y realizar en él un LOAD "" CODE de
forma que carguemos todo el código binario ensamblado en memoria. Podemos así realizar
muchas rutinas en un mismo fichero ASM y ensamblarlas y cargarlas de una sola vez. Tras
tenerlas en memoria, tan sólo necesitaremos saber la dirección de inicio de cada una de
las rutinas para llamarlas con el RANDOMIZE USR DIRECCION_RUTINA correspondiente en
cualquier momento de nuestro programa BASIC.
Para hacer esto, ese fichero ASM podría tener una forma como la siguiente:
; La rutina 1
ORG 40000
rutina1:
(...)
RET
; La rutina 2
ORG 41000
rutina2:
(...)
RET
|
También podemos ensamblarlas por separado y después pokearlas.
Hay que tener mucho cuidado a la hora de teclear los DATAs (y de ensamblar) si lo hacemos
a mano, porque equivocarnos en un sólo número cambiaría totalmente el significado del
programa y no haría lo que debería haber hecho el programa correctamente pokeado en
memoria.
Un detalle más avanzado sobre ejecutar rutinas desde BASIC es el hecho de que podamos
necesitar pasar parámetros a una rutina, o recibir un valor de retorno desde una
rutina.
Pasar parámetros a una rutina significa indicarle a la rutina uno o más valores para que
haga algo con ellos. Por ejemplo, si tenemos una rutina que borra la pantalla con un
determinado patrón o color, podría ser interesante poder pasarle a la rutina el valor a
escribir en memoria (el patrón). Esto se puede hacer de muchas formas: la más sencilla
sería utilizar una posición libre de memoria para escribir el patrón, y que la rutina
lea de ella. Por ejemplo, si cargamos nuestro código máquina en la dirección 40000 y
consecutivas, podemos por ejemplo usar la dirección 50000 para escribir uno (o más)
parámetros para las rutinas. Un ejemplo:
; Listado 3: Rellenado de pantalla
; recibiendo el patron como parametro.
ORG 40000
; En vez de 162, ponemos en A lo que hay en la
; dirección de memoria 50000
LD A, (50000)
; El resto del programa es igual:
LD HL, 16384
LD (HL), A
LD DE, 16385
LD BC, 6911
LDIR
RET
|
Nuestro programa en BASIC a la hora de llamar a esta rutina (una vez ensamblada y pokeada
en memoria) haría:
POKE 50000, 162
RANDOMIZE USR 40000
|
Este código produciría la misma ejecución que el ejemplo anterior, porque como parámetro
estamos pasando el valor 162, pero podríamos llamar de nuevo a la misma función en
cualquier otro punto de nuestro programa pasando otro parámetro diferente a la misma,
cambiando el valor de la dirección 50000 de la memoria.
En el caso de necesitar más de un parámetro, podemos usar direcciones consecutivas de
memoria: en una rutina de dibujado de sprites, podemos pasar la X en la dirección 50000,
la Y en la 50001, y en la 50002 y 50003 la dirección en memoria (2 bytes porque las
direcciones de memoria son de 16 bits) donde tenemos el Sprite a dibujar, por ejemplo.
Todo eso lo veremos con más detalle en posteriores capítulos.
Además de recibir parámetros, puede sernos interesante la posibilidad de devolver a BASIC
el resultado de la ejecución de nuestro programa. Por ejemplo, supongamos que realizamos
una rutina en ensamblador que hace un determinado cálculo y debe devolver, tras todo el
proceso, un valor. Ese valor lo queremos asignar a una variable de nuestro programa
BASIC para continuar trabajando con él.
Un ejemplo: imaginemos que realizamos una rutina que calcula el factorial de un número de
una manera mucho más rapida que su equivalente en BASIC. Para devolver el valor a BASIC
en nuestra rutina ASM, una vez realizados los cálculos, debemos dejarlo dentro del
registro BC justo antes de hacer el RET. Una vez programada la rutina y pokeada, la
llamamos mediante:
Con esto la variable de BASIC A contendrá la salida de nuestra rutina (concretamente, el
valor del registro BC antes de ejecutar el RET). Las rutinas sólo pueden devolver un
valor (el registro BC), aunque siempre podemos (dentro de nuestra rutina BASIC) escribir
valores en direcciones de memoria y leerlos después con PEEK dentro de BASIC (al igual
que hacemos para pasar parámetros).
CÓDIGO MAQUINA EN MICROHOBBY
Lo que hemos visto hasta ahora es que podemos programar pequeñas rutinas y llamarlas
desde programas en BASIC fácilmente. Todavía no hemos aprendido nada del lenguaje en sí
mismo, pero se han asentado muchos de los conceptos necesarios para entenderlo en las
próximas entregas del curso.
En realidad, muchos de nosotros hemos introducido código máquina en nuestros Spectrums
sin saberlo. Muchas veces, cuando tecleabamos los listados de programa que venían en la
fabulosa revista Microhobby, introducíamos código máquina y lo ejecutábamos, aunque no
lo pareciera.
Algunas veces lo hacíamos en forma de DATAs, integrados en el programa BASIC que
estábamos tecleando, pero otras lo hacíamos mediante el famoso Cargador Universal de
Código Máquina (CUCM). Para que os hagáis una idea de qué era el CUCM de Microhobby, no
era más que un programa en el cual tecleabamos los códigos binarios de rutinas ASM
ensambladas previamente. Se tecleaba una larga línea de números en hexadecimal agrupados
juntos (ver la siguiente figura), y cada 10 opcodes se debía introducir un número a modo
de CRC que aseguraba que los 10 dígitos (20 caracteres) anteriores habían sido
introducidos correctamente. Este CRC podía no ser más que la suma de todos los valores
anteriores, para asegurarse de que no habíamos tecleado incorrectamente el listado.
|
Ejemplo de listado para el CUCM de MH |
Al acabar la introducción en todo el listado en el CUCM, se nos daba la opción de
grabarlo. Al grabarlo indicábamos el tamaño de la rutina en bytes y la dirección donde
la ibamos a alojar en memoria (en el ejemplo de la captura, la rutina se alojaría en la
dirección 53000 y tenía 115 bytes de tamaño). El CUCM todo lo que hacía era un
simple:
Esto grababa el bloque de código máquina en cinta (justo tras nuestro programa en BASIC),
de forma que el juego en algún momento cargaba esta rutina con LOAD "" CODE, y podía
utilizarla mediante un RANDOMIZE USR 53000.
PASMO: ENSAMBLADOR CRUZADO
El lector se preguntará: "Y si no quiero ensamblar a mano, ¿cómo vamos a ensamblar los
listados que veamos a lo largo del curso, o los que yo realice para ir practicando o
para que sean mis propias rutinas o programas?". Sencillo: lo haremos con un ensamblador
cruzado, es decir, un programa que nos permitirá programar en un PC (utilizando nuestro
editor de textos habitual), y después ensamblar ese fichero .asm que hemos realizado,
obteniendo un fichero .BIN (o directamente un .TAP) para utilizarlo en nuestros
programas.
Los programadores "originales" en la época del Spectrum tenían que utilizar MONS y GENS
para todo el proceso de desarrollo. Estos programas (que corren sobre el Spectrum)
implicaban teclear los programas en el teclado del Spectrum, grabarlos en cinta,
ensamblar y grabar el resultado en cinta, etc. Actualmente es mucho más comodo usar
programas como los que usaremos en nuestro curso.
Por ejemplo, PASMO. Pasmo es un ensamblador cruzado, opensource y multiplataforma. Con
Pasmo podremos programar en nuestro PC, grabar un fichero ASM y ensamblarlo cómodamente,
sin cintas de por medio. Tras todo el proceso de desarrollo, podremos llevar el programa
resultante a una cinta (o disco) y ejecutarlo por lo tanto en un Spectrum real, pero lo
que es el proceso de desarrollo se realiza en un PC, con toda la comodidad que eso
conlleva.
Pasmo en su versión para Windows/DOS es un simple ejecutable (pasmo.exe) acompañado de
ficheros README de información. Podemos mover el fichero pasmo.exe a cualquier
directorio que esté en el PATH o directamente ensamblar programas (siempre desde la
línea de comandos o CMD, no directamente mediante "doble click" al ejecutable) en el
directorio en el que lo tengamos copiado.
La versión para Linux viene en formato código fuente (y se compila con un simple make) y
su binario "pasmo" lo podemos copiar, por ejemplo, en /usr/local/bin.
Iremos viendo el uso de pasmo conforme lo vayamos utilizando, pero a título de ejemplo,
veamos cómo se compilaría el programa del Listado 2 visto anteriormente. Primero
tecleamos el programa en un fichero de texto y ejecutamos pasmo:
pasmo ejemplo1.asm ejemplo1.bin
Con esto obtendremos un bin que es el resultado del ensamblado. Es código máquina directo
que podremos utilizar en los DATAs de nuestro programa en BASIC. Podemos obtener el
código máquina mediante un editor hexadecimal o alguna utilidad como "hexdump" de
Linux:
$ hexdump -C ejemplo1.bin
00000000 21 00 40 3e a2 77 11 01 40 01 ff 1a ed b0 c9
Ahí tenemos los datos listos para convertirlos a decimal y pasarlos a sentencias DATA. Si
el código es largo y no queremos teclear en DATAs la rutina, podemos convertir el BIN en
un fichero TAP ensamblando el programa mediante:
pasmo --tap ejemplo1.asm ejemplo1.tap
Este fichero tap contendrá ahora un tap con el código binario compilado tal y como si lo
hubieras introducido en memoria y grabado con SAVE "" CODE, para ser cargado
posteriormente en nuestro programa BASIC con LOAD "" CODE.
Los programas resultantes pueden después cargarse en cualquier emulador para comprobarlos
(como Aspectrum, FUSE). De este modo el ciclo de desarrollo será:
- Programar en nuestro editor de textos favorito.
- Ensamblar el programa .asm con pasmo.
- Cargar ese código máquina en memoria, bin con DATA/POKE o bien cargando un tap con
LOAD "" CODE.
- Realizar nuestro programa BASIC de forma que utilice las nuevas rutinas, o bien
directamente programar en ensamblador.
Este último paso es importante: si estamos realizando un programa completo en ensamblador
(sin ninguna parte en BASIC), bastará con compilar el programa mediante "pasmo --tapbas
fichero.asm fichero.tap". La opción --bastap añade una cabecera BASIC que carga el
bloque código máquina listo para el RANDOMIZE USR.
Si, por contra, estamos haciendo rutinas para un programa en BASIC, entonces bastará con
generar un BIN o un TAP y grabarlo tras nuestro programa BASIC. Para eso, escribimos las
rutinas en un fichero .ASM y las compilamos con "pasmo --tap fichero.asm fichero.tap".
Después, escribimos nuestro programa en BASIC (con bas2tap o en el emulador). Tras esto
tenemos que crear un TAP o un TZX que contenga primero el bloque BASIC y después el
bloque código máquina (y en el bloque BASIC colocaremos el LOAD "" CODE DIRECCION,
TAMANYO_BLOQUE_CM necesario para cargarlo). Esto, sin necesidad de utilizar emuladores
de por medio, sería tan sencillo como:
Linux: cat programa_basic.tap bloque_cm.tap > programa_completo.tap
Windows: copy /b programa_basic.tap +bloque_cm.tap programa_completo.tap
EN RESUMEN
En esta entrega hemos definido las bases del curso de ASM de Z80, comenzando por las
limitaciones de BASIC y la necesidad de conocer un lenguaje más potente y rápido. Hemos
visto qué aspecto tiene el código en ensamblador (aunque todavía no conozcamos la
sintaxis) y, muy importante, hemos visto cómo se integra este código en ensamblador
dentro de programas en BASIC. Por último, hemos conocido una utilidad que nos permitirá,
a lo largo del curso, ensamblar todos los programas que realicemos (y probarlos en un
emulador, integrado en nuestros programas BASIC).
A lo largo de los siguientes artículos de este curso aprenderemos lo suficiente para
realizar nuestras propias rutinas, gracias a los conceptos y conocimientos teóricos
explicados hoy (y en la siguiente entrega).
LINKS