> ÍNDICE DE REVISTAS <
Número 12 - Septiembre 2005 ZX Certified webmaster speccy.org
 

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
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
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
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:

 LET A=USR 40000

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
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:

  SAVE "" CODE 53000, 115

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



SROMERO
 
Volver arriba
> ÍNDICE DE REVISTAS <
2003-2009 Magazine ZX