Atari Assembler – Llamadas a subrutinas BASIC / Assembler

Nuestro primer programa en Assembler será muy sencillo.  Este programa hará NADA.  De qué sirve hacer nada? Sirve para ver lo mímino que necesita un programa en assembler para ser ejecutado desde BASIC.

Antes de entrar al código en Assembler, debemos entender cómo funciona una subrutina en BASIC.  Si ya sabes lo que es, puedes saltar directamente a la siguiente sección.

Una subrutina es lo que en un lenguaje de alto nivel conocemos como función (método, procedimiento, etc).  Por ejemplo este es un sencillo ejemplo de una subrutina en Atari BASIC

10 for i = 1 to 2
20 gosub 100
30 next i
40 end
100 REM esta es la subrutina
110 print "En la subrutina"
120 return

Este simple ejemplo es un codigo bastante tonto, pero sirve para explicar la idea de subrutina.  Entre las lineas 10 y 40 tenemos el programa principal, en donde se ejecuta un ciclo 3 veces.  Según la linea 20, por cada ciclo se llama a una subrutina o función que se encuentra entre la linea 100 y la 120.  Aunque aquí no se puede apreciar por la simpleza del código, el aporte de una subrutina es que puede ser llamada por distintas partes del programa. Permite reutilizar código o al menos organizarlo en una forma más conveniente.

Cuando el intérprete BASIC pasa por la linea 20, se encuentra con la instrucción gosub y lo hace saltar a la linea 100 (gosub = go subroutine).  La linea 100 sólo es un comentario para facilitar la lectura del código, no hace nada. Luego en la linea 110 hace su tarea y al llegar a la linea 120 y encontrarse con la instrucción RETURN,  ésta le indica a BASIC sabe que tiene que retornar el control a lo que estaba haciando antes de saltar.  En este caso volvería a la linea 20 para seguir ejecutando el programa principal de ahí en adelante.

Si vieramos la secuencia de ejecución de las lineas de código, sería así:

10, 20, 100, 110, 120, 30, 20, 100, 110, 120, 30, 40
        | subrutina |         | subrutina |

Veamos la siguiente modificación a este código:

10 for i = 1 to 2
20 gosub 100
30 next i
40 gosub 200
50 end
100 REM esta es la subrutina
110 print "En la subrutina"
120 gosub 200
130 return
200 REM esta es otra subrutina
210 print "en la segunda subrutina"
220 return

Ahora tenemos dos subrutinas, una que comienza en la linea 100 y otra que comienza en la linea 200.  Si seguimos la traza del código vemos que al pasar por la linea 20, se salta a la linea 100, y luego al pasar por la linea 120 vuelve a saltar, esta vez a la linea 200.  Al llegar a la linea 220 debe retornar, pero a donde? Según BASIC tendría que retornar a la linea 120 porque es ahí donde iba antes de saltar.  En la linea 130 vuelve a retornar y a donde debería ir? A la linea 20 que es donde se encontraba originalmente.

Cuando termina el ciclo, se ejecuta la linea 40 en donde vuelve a saltar a la linea 200.  Cuando se ejecuta el return de la linea 200 debe volver a la linea 40 para finalizar el programa.

Así es como se vería la ejecución de cada linea de este programa:

10, 20, 100, 110, 120, 200, 210, 220, 130, 30, 20, 100, 110, 120, 200, 210, 220, 130, 30, 40
        |----------subrutina------------|          |-----------subrutina-----------|
                       |-subrutina-|                              |-subrutina-|

Pila o Stack

La pregunta a resolver es: Cómo sabe el interprete BASIC hacia donde tiene que volver cada vez que se encuentra con la sentencia RETURN ?

Lo que hace BASIC es ir anotando en donde estaba junto antes de saltar.  Cada vez que salta por GOSUB, genera una nueva anotación.  Luego cada vez que se encuentra con un RETURN retira la última anotación que hizo.  Este comportamiento es lo que se conoce como un pila o stack, porque los datos se van «apilando» uno sobre otro.  Cuando digo que se anota un valor, este queda anotado en la pila, cuando se anota el siguiente valor, este se pone sobre el anterior.  Un tercer valor se pondría sobre el segundo y así sucesivamente.  Al encontrarse con los RETURN va sacando los valores desde el que está más arriba en adelante, primero el tercero, luego el segundo y finalmente el primero. En ciencias de la computación a esto se le llama comportamiento LIFO (Last In First Out) o el último que entra es el primero que sale.

Por ejemplo, y en una forma muy simplificada:  Cuando BASIC llega a la linea 20 y se encuentra con el GOSUB, internamente anota el valor 20 en la pila y ésta queda así:

[20]

Al llegar a la linea 120 y encontrar otro GOSUB para saltar a la linea 200, la pila queda así:

[20, 120]

Cuando encuentra el RETURN de la linea 220, ve que lo último que se anotó en la pila es el valor 120, por lo tanto vuelve a esa linea y la pila queda así:

[20]

Luego llega a la linea 120 y se encuentra con un nuevo RETURN y sabe que debe volver a la linea 20 y la pila queda vacía:

[ ]

Cuando llega a la linea 40 en donde hay un nuevo GOSUB, la pila queda así:

[40]

Y finalmente al encontrar el RETURN en la linea 220, la pila le dice que debe volver a la linea 120, quedando nuevamente vacía

[ ]

Primera subrutina en Assembler, ahora sí

Toda esta explicación es para poder entender cómo son las llamadas desde BASIC a código de máquina, y así poder implementar nuestra rutina en código de máquina que hace NADA.

Atari BASIC tiene una sentencia similar al GOSUB pero en vez de saltar a código BASIC salta a código de máquina.  Esta instrucción que se llama USR (User SubRoutine) hace que el procesador deje de ejecutar el código del interprete BASIC para saltar a código de máquina propio. Una vez que el procesador se encuentra con la instrucción equivalente a RETURN en código de máquina, se retorna el control al intérprete BASIC.

USR requiere al menos un parámetro, pero el usuario puede pasar varios más.  El parámetro obligatorio corresponde a la dirección de memoria en donde comienza el código de máquina que queremos ejecutar.  Por ejemplo, la siguiente linea en BASIC ejecuta el código de máquina que comienza en la dirección de memoria 1536 y asigna el resultado en variable A

A = USR(1536)

El resultado es cualquier número que querramos entregarle a BASIC como resultado de la llamada, igual que una función.  Si no queremos entregar nada, el valor que BASIC toma por omisión es cero.

Una llamada USR también puede pasar parámetros al código de máquina propio, por ejemplo:

A = USR(1536, 29)

En este caso, de alguna forma el intérprete BASIC le tendrá que pasar el valor 29 a nuestro código.

Cuando el interprte BASIC se encuentra con la instrucción USR, anota en la pila la dirección del código propio del intérprete BASIC hacia donde el 6502 debe volver cuando termine de ejecutar nuestro código, pero además debe anotar el número de parámetros y sus valores para que el código de máquina los pueda recoger. Por ejemplo veamos como queda la pila con esta llamada:

A = USR(1536)
Pila = [valores anteriores, dirección de retorno a interprete BASIC, 0]

A diferencia de la pila que vimos anterioremente, esta no es la pila del intérprete BASIC, sino que la pila propia del 6502.  Ésta se encuentra en la memoria RAM desde la dirección 256 a la 511, pudiendo almacenar un máximo de 255 valores.  Como el intérprete BASIC es complejo, y éste a su vez fue invocado desde el sistema operativo, la pila del 6502 tendrá las direcciones de todas las subrutinas de código de máquina que se han ejecutado en el sistema operativo y en el intérprete BASIC hasta que llegó al USR.  Eso son los «valores anteriores» que ya estaban en la pila.  Luego encontramos la dirección a la que se retornará cuando se termine de ejecutar nuestro código y finalmente un cero que indica que no hay parámetros.

En Assembler, la instrucción equivalente a RETURN es RTS (ReTurn from Subroutine), y se codifica con el valor 96, por lo que uno esperaría a que nuestra rutina que no hace nada, quede así de simple:

RTS

Pero como podemos ver, el último valor de la pila no es la dirección de retorno, sino que el número de parámetros (cero en este caso).  Por lo que lo primero que debemos hacer es retirar ese valor de la pila. Si ejecutáramos sólo RTS sin retirar el valor de la pila, el 6502 creería que cero es parte de la dirección de memoria a la que debe retornar, volviendo a cualquier parte menos al interprete BASIC, causando resultados impredecibles.

Para retirar un valor de la pila se utiliza la instrucción PLA (PuLl to Acumulator).  Esta instrucción toma el último valor de la pila y lo deja en el registro Acumulador del 6502.  Este registro Acumulador o simplemente A es como una variable interna de 8 bits en donde se realizan la mayoría de las operaciones.  Se trata de una celda de memoria que se encuentra al interior del chip, operando a máxima velocidad.

Agregando el PLA nuestra subrutina que no hace nada se transforma en:

PLA
RTS

Si lo vieramos paso a paso, esta llamada ocurriría así:

  1. BASIC anota la dirección de retorno al intérprete en la pila
  2. BASIC anota la cantidad de parámetros en la pila (cero)
  3. BASIC hace que el procesador salte a nuestro código almacenado en la dirección 1536 (USR)
  4. PLA: Retira el valor de la pila y lo deja en A. Por lo tanto A = 0
  5. RTS: El 6502 recupera la dirección de memoria anotada en la pila, volviendo código del intérprete BASIC

En la siguiente sección, veremos cómo codificar nuestra subrutina que hace nada y la llamaremos desde BASIC