RAM de color

En una entrada anterior hemos visto cómo pintar caracteres en pantalla, bien con las instrucciones “lda”/”sta”, bien con la rutina del Kernal “chrout”.

Pues bien, no sólo se pueden pintar caracteres. También se puede cambiar su color. Y para esto existe la RAM de color, que es una zona de 1000 posiciones RAM que determinan, una por una, el color de las 1000 posiciones correspondientes de la RAM de pantalla.

La RAM de color va desde la posición $d800 hasta la posición $dbe7. Por tanto, para fijar el color del carácter en la posición $0400, se debe fijar el color en $d800, y así sucesivamente.

Para fijar el color hay que almacenar en la posición elegida un byte con cualquiera de los 16 colores soportados por el C64 (en realidad sólo se utiliza el nibble menos significativo):

$00 – Negro $01 – Blanco $02 – Rojo $03 – Cyan (turquesa)
$04 – Morado $05 – Verde $06 – Azul $07 – Amarillo
$08 – Naranja $09 – Marrón $0a – Rojo claro $0b – Gris oscuro
$0c – Gris medio $0d – Verde claro $0e – Azul claro $0f – Gris claro

 
Colores


Programa de ejemplo: Prog29

Revisión de la situación

Hasta el momento hemos tratado los siguientes aspectos:

  • Introducción y objetivos del blog. Referencias.
  • El equipo de trabajo. CBM prg Studio y VICE.
  • Sistemas de numeración decimal, binario y hexadecimal.
  • El hardware del C64. La memoria y los registros del microprocesador.
  • Los modos de direccionamiento.
  • Las instrucciones del código máquina/ensamblador del 6510.
  • Subrutinas, macros e interrupciones.
  • Rutinas del Kernal.

En definitiva, nos hemos centrado en cómo es el código máquina/lenguaje ensamblador del C64 o, lo que es lo mismo, del microprocesador 6510.

A partir de ahora la temática cambia porque, una vez conocida la herramienta básica, se trata de aplicarla para hacer cosas: gráficos, sonido, entrada/salida, etc. Y estas capacidades las aportan los otros circuitos integrados del C64: el VIC (gráficos), el SID (sonido) y las CIAs (entrada/salida).

Como ya se comentó en su momento, a efectos del microprocesador, el resto de circuitos integrados no son más que posiciones de memoria. Por tanto, se manejan con instrucciones de lectura y escritura de datos (“lda”, “sta” y similares). Pero para poder usarlos, hay que conocer sus registros, qué hace cada uno de ellos, y en qué direcciones están.

Y esta será la temática principal a partir de ahora…

Códigos de pantalla vs caracteres PETSCII

A la hora de imprimir caracteres en pantalla tenemos dos opciones:

  • Hacerlo en una posición conocida, es decir, en cualquiera de las 1000 posiciones de la RAM de pantalla ($0400 – $07e7).
  • Hacerlo donde esté el cursor actualmente.

Para la primera opción usaremos directamente las instrucciones “lda” y “sta”, o sus variantes para los registros X e Y. Y para la segunda opción usaremos la rutina “chrout” del Kernal.

Pues bien, según la técnica que estemos usando debemos usar códigos de pantalla (“screen codes”) o la tabla de caracteres PETSCII. Como se podrá ver son tablas diferentes, porque en el primer caso el código correspondiente a la “A” es 1 y en el segundo caso es 65.

Códigos de pantalla

Cuando usamos “lda” y “sta” para poner un carácter en la pantalla (posiciones $0400 – $07e7) lo que estamos haciendo es exactamente lo mismo que cuando hacemos un “sta” con cualquier otra posición de memoria, es decir, simplemente almacenar ahí un byte.

Para los que sepan BASIC estamos hablando de hacer lo mismo que un “POKE 1024, 1”.

Lo que ocurre es que, si la posición utilizada pertenece a la RAM de pantalla, el VIC interpreta eso como que tiene que pintar determinado carácter en esa posición. Y el VIC pinta un carácter u otro en función de la tabla de códigos de pantalla.

Básicamente lo que hace el VIC es que lee el byte ahí almacenado (ej. 1), consulta su mapa de definición de caracteres, que está en ROM, y pinta el carácter (matriz de pixels) que corresponde a ese valor (ej. “A”).

El mapa de caracteres del C64 tiene 2 subconjuntos (a matizar en breve):

  • Las letras mayúsculas, los números y otros caracteres gráficos. Códigos 0 hasta 127.
  • Las versiones invertidas del punto anterior. Códigos 128 hasta 255.
Screen codes 1 Screen codes 2

Lo anterior (letras mayúsculas) es la opción por defecto al arrancar, pero se pueda cambiar pulsando SHIFT y la tecla Commodore (SHIFT y la tecla Windows en VICE). Eso sí, o las letras son todas mayúsculas, o todas minúsculas, pero no se pueden mezclar. Por eso es suficiente con un byte para codificar 512 caracteres (mayúsculas, mayúsculas invertidas, minúsculas y minúsculas invertidas).

Screen codes 1 Screen codes 2

Caracteres PETSCII

En este caso la historia es distinta porque vamos a través de la rutina “chrout”. A esta rutina, por diseño, le deben llegar caracteres y no códigos de pantalla.

El equivalente en BASIC sería hacer un “PRINT ‘A’”. Lo que pasa es que en BASIC hay números y hay cadenas de caracteres (variables numéricas y alfanuméricas), pero en ensamblador todo vienen a ser bytes.

El C64 no sigue el estándar ASCII (caracteres de 7 bits), pero tiene un estándar equivalente llamado PETSCII. Además, la tabla PETSCII es compatible con ASCII en lo fundamental, es decir, usa los mismos valores que ASCII para identificar letras, números y algunos caracteres de control. A esto el C64 añade sus muy característicos caracteres gráficos.

PETSCII

No conozco los detalles internos de la rutina “chrout” (se podrían consultar desensamblando el código a partir de la posición $f1ca), pero me puedo imaginar que lo que hace es determinar la posición de pantalla en función de la posición del cursor, obtener el código de pantalla que corresponde al carácter PETSCII recibido como parámetro, almacenarlo en esa posición de memoria, y mover el cursor.

CBM prg Studio y text

En CBM prg Studio, y en cualquier ensamblador, es posible definir el contenido de determinadas posiciones o zonas de memoria con directivas como “text” (para textos), “byte” (para bytes), “word” (para palabras, es decir, dos bytes), etc.

En particular, “text” vale para definir textos que luego van a ser leídos e impresos en pantalla mediante un programa en ensamblador. Por ejemplo:

texto1 text “En un lugar de La Mancha…”
texto2 text ‘En otro lugar de La Mancha…’

Pues bien, si lo que desea el programador es que CBM prg Studio almacene ahí códigos de pantalla, entonces debe usar comillas simples. Por el contrario, si lo que quiere el programador es que CBM prg Studio almacene caracteres PETSCII, entonces debe usar comillas dobles.


Programa de ejemplo: Prog28

Rutinas del Kernal

El sistema operativo del C64 básicamente consta de dos piezas:

  • El intérprete de BASIC.
  • El Kernal.

El intérprete de BASIC es un programa en ROM que interpreta, es decir, traduce y ejecuta, los programas en BASIC. En este blog estamos más interesados en el ensamblador y el código máquina del C64, básicamente porque el BASIC es fácilmente accesible para casi todo el mundo. El BASIC nos interesa tangencialmente, principalmente porque desde BASIC se puede ejecutar (comando SYS) e interactuar con el código máquina.

El Kernal (hoy en día diríamos “Kernel”) es un conjunto de rutinas en código máquina, también ubicadas en ROM, que dan servicios básicos al programador. El listado de estas rutinas se puede consultar en la dirección http://sta.c64.org/cbm64krnfunc.html.

Como se puede observar en el listado anterior, cada rutina aparece descrita con:

  • Nombre.
  • Descripción de la función.
  • Parámetros de entrada.
  • Parámetros de salida.
  • Registros modificados (ya se explicó en su momento que, salvo que se tomen medidas para evitarlo, todas las rutinas en general pueden modificar los registros).
  • Dirección.
  • Dirección real.

Hay dos direcciones, una “dirección sin más” y una “dirección real”, porque el Kernal utiliza una tabla de saltos o “jump table”.

Así, para llamar la rutina “chrout”, por ejemplo, los programadores normalmente harán “jsr $ffd2”, o “jsr chrout” con la constante chrout = $ffd2, que es la dirección de entrada a la “jump table”. Pero si se analiza el contenido de la posición $ffd2 y siguientes se verá que es así:

$FFD2 6C 26 03 JMP ($0326)

Es decir, se hace un salto (“jmp”) a la dirección apuntada por el vector o puntero $0326 – $0327 que, mediante los comandos BASIC PRINT PEEK(806) y PRINT PEEK(807), se puede deducir que apunta a $f1ca (recuérdese el orden «Little endian»), que es precisamente la “dirección real” que aparece en la ficha de “chrout”.

Chrout

En resumen, podemos llamar a la rutina de forma directa (dirección $f1ca) o a través de la “jump table” (dirección $ffd2). Al hacerlo de esta segunda manera tenemos la ventaja (en realidad teníamos) de que, si un día Commodore cambiaba la ubicación de las rutinas en futuros modelos, los programas seguirían funcionando.

Por lo demás, el Kernal tiene muchas rutinas. Se anima al lector a revisarlas y probarlas. Dos de las más utilizadas son:

  • CHROUT. Sirve para imprimir un carácter en la posición del cursor.
  • GETIN. Sirve para leer un carácter del teclado.

Getin


Programa de ejemplo: Prog18

Otras instrucciones

Según mis cuentas, las últimas instrucciones del 6510 que nos quedan por revisar son:

  • “bit”.
  • “nop”.

La instrucción “bit” es similar a la instrucción “and”, pero con algunas diferencias. “and” hace el AND bit a bit del acumulador y de una posición de memoria (o un valor en el modo inmediato), altera los flags del registro de estado consecuentemente, y almacena el resultado de ese AND en el acumulador. Pues bien, “bit” viene a hacer básicamente lo mismo (acumulador AND posición de memoria) pero sin almacenar el resultado del AND en el acumulador.

En el fondo “bit” viene a ser una manera de comprobar el contenido de una posición de memoria, o de ciertos bits de esa posición de memoria. Con “and” se podría conseguir el mismo efecto, pero si no interesa el resultado del AND, sino sólo ver cómo se alteran los flags, la forma más directa es usar “bit”.

De hecho, por ese motivo no se ha considerado “bit” como una instrucción para hacer operaciones lógicas (como “and”, “or” y “eor”), porque no da acceso al resultado del AND.

“bit” soporta menos modos de direccionamiento que “and”, ya que sólo soporta los modos absoluto y página cero. Modifica los flags S – sign, V – overflow, y Z – zero, si bien sólo Z – Zero se deriva del resultado del AND, ya que S – Sign y V – overflow se derivan directamente de los bits 7 y 6 de la posición de memoria referenciada. Sus detalles se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#BIT.

Por último, la instrucción “nop” es la más sencilla de todas, ya que no hace nada. Vale para rellenar zonas de memoria con algún valor conocido o para dejar inutilizado código máquina previamente existente.

La instrucción “nop” sólo utiliza el modo implícito y no modifica ningún flag. Sus detalles se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#NOP.


Programa de ejemplo: Prog27

Interrupciones

Una “interrupción” consiste en dejar de ejecutar el programa que está en curso para pasar a ejecutar una “rutina de interrupción”. Tras ejecutarse la rutina de interrupción continúa la ejecución del programa original allí donde se interrumpió.

Las rutinas de interrupción típicamente son rutinas del sistema (por ejemplo, para hacer que parpadee el cursor de BASIC, o para leer el teclado, etc.), pero como veremos más adelante el usuario puede modificarlas o incluso sustituirlas por otras propias.

Para que ocurra una interrupción, es necesario que se active una señal sobre uno de los dos “pines” (patillas) del microprocesador 6510 que tienen que ver con las interrupciones:

  • El pin IRQ.
  • El pin NMI.

Algunos chips del C64 (ej. CIAs) generan interrupciones de forma periódica, y otros (ej. VIC) cuando ocurre algún evento de interés para el programador.

Interrupciones IRQ por hardware

IRQ significa “interrupt request” (petición de interrupción). Cuando se activa este pin, si el flag I – interrupt del registro de estado está desactivado (instrucción “cli”), deja de ejecutarse el programa actual, se ponen a salvo los registros en la pila, y se ejecuta la rutina de interrupción.

Si, por el contrario, el flag I – interrupt estuviera activado (instrucción “sei”), la interrupción IRQ no sería atendida. Por ello a las interrupciones de tipo IRQ también se les llama “enmascarables”.

Interrupciones IRQ por software

Igual que es posible generar interrupciones IRQ por hardware, también es posible hacerlo por software. Para ello, hay que ejecutar la instrucción “brk”.

Cuando se ejecuta la instrucción “brk” ocurre básicamente lo mismo que cuando se activa el pin IRQ, con las diferencias de que se activa el flag B – break en el registro de estado, y que las interrupciones “brk” no son enmascarables mediante el flag I – interrupt.

La instrucción “brk” utiliza el modo de direccionamiento implícito, y sólo afecta al flag B – break. Sus detalles se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#BRK.

Rutina de interrupción IRQ

Más en detalle, al ejecutarse una interrupción IRQ hardware o software ocurre lo siguiente:

  • El contador de programa se guarda en la pila. Primero el MSB y luego el LSB.
  • El registro de estado se guarda en la pila.
  • Se ejecuta el programa apuntado por las posiciones $fffe y $ffff.

Las posiciones $fffe y $ffff conforman el “vector de interrupción”. Estas posiciones están en ROM, y contienen los valores $48 y $ff respectivamente. Es decir, apuntan a la dirección $ff48, que también está en ROM.

El programa que empieza en $ff48 se llama “rutina de interrupción”, y resumidamente hace esto:

  • Guarda el acumulador en la pila.
  • Guarda el registro X en la pila.
  • Guarda el registro Y en la pila.
  • Analiza si la interrupción IRQ es hardware o software mediante el flag B – break.
  • En el caso software salta a la dirección apuntada por $0316 y $0317.
  • En el caso hardware salta a la dirección apuntada por $0314 y $0315.

Por defecto, las posiciones $0316 y $0317 apuntan a $fe66, y las posiciones $0314 y $0315 apuntan a $ea31. Pero las posiciones $03xx están en RAM y, por tanto, su contenido se puede modificar, modificando así el algoritmo de la rutina de interrupción o incluso sustituyéndolo por otro completamente nuevo.

Esta modificación hay que hacerla con cuidado, porque podrían tener lugar interrupciones en mitad de la modificación. Por ello, suele hacerse dentro de un trozo de código que empieza por “sei” (para inhabilitar temporalmente las interrupciones) y termina con “cli” (para volver a habilitarlas ya con la nueva rutina de interrupción).

De hecho, muchas veces en vez de llamar “vector de interrupción” a $fffe – $ffff, se suele llamar “vector de interrupción” a $0314 – $0315, y en vez de llamar “rutina de interrupción” al programa del sistema esbozado arriba (el que empieza en $ff48), se suele llama “rutina de interrupción” al programa de usuario apuntado por $0314 – $0315. Esto se hace así porque, en el fondo, lo que interesa al usuario son las posiciones $0314 – $0315 y el programa apuntado por este vector. Lo demás es código del sistema que no se puede modificar.

Instrucción “rti”

La rutina de interrupción, ya sea la rutina de interrupción del C64, o una propia del usuario, tiene que terminar con la instrucción “rti”.

Cuando se ejecuta esta instrucción ocurre lo siguiente:

  • Se recupera desde la pila el registro de estado.
  • Se recupera desde la pila el contador de programa. Primero el LSB y luego el MSB.
  • Continúa la ejecución en ese contador de programa, es decir, donde tuvo lugar la interrupción.

La instrucción “rti” usa el modo de direccionamiento implícito, y afecta a todos los flags porque recupera el registro de estado desde la pila. Sus detalles pueden consultarse en http://www.6502.org/tutorials/6502opcodes.html#RTI.

Adicionalmente, antes de la instrucción “rti” lo correcto sería recuperar los registros Y, X y acumulador desde la pila, dado que sólo así podrá continuar la ejecución en exactamente las mismas condiciones que cuando se produjo la interrupción.

Lo anterior es cierto si la “rutina de interrupción de usuario” es una rutina de interrupción completa, que sustituye a la del sistema. Sin embargo, en ocasiones se ven rutinas parciales que ejecutan un “trozo de código” adicional antes que la rutina habitual del C64, y que terminan con un “jmp $ea31” (es decir, con el resto de la rutina habitual del sistema). En este caso se puede confiar en que la recuperación de registros y el “rti” serán ejecutados por la rutina habitual del sistema.

Todo esto quedará más claro con los ejemplos…

Interrupciones NMI

NMI significa “non-maskable interrupts”, es decir, interrupciones no enmascarables. Son interrupciones hardware igual que las IRQ, pero con algunas diferencias:

  • Se activan con el pin NMI, no con el pin IRQ.
  • No son enmascarables, es decir, no les afecta el flag I – interrupt.
  • El vector de interrupción es $fffa – $fffb, no $fffe – $ffff.

Programas de ejemplo: Prog25 y Prog26

Instrucciones de manejo de la pila

La pila ya se describió en una de las entradas dedicadas a los registros del microprocesador (porque hay un registro llamado el “puntero de la pila”). No obstante, a modo de resumen, podemos decir que la pila es una estructura de datos de tipo LIFO (last in – first out) que ocupa la página uno de la memoria, desde la $01ff hasta la $0100 (porque se va llenando de arriba abajo). Permite guardar datos y recuperarlos en el orden contrario, es decir, primero se recuperan los últimos datos que se han guardado. Y se utiliza básicamente para tres cosas:

  • Para llevar la pista de las llamadas a subrutinas y las direcciones de retorno para continuar la ejecución una vez han terminado.
  • Para salvaguardar y recuperar los registros del microprocesador cuando ocurre una interrupción. Las interrupciones se verán más adelante.
  • Como estructura de datos para que el programador guarde y recupere datos.

Los dos primeros usos son automáticos, el programador no necesita hacer nada para que ocurran. Ya se encargan el microprocesador, en el caso de las llamadas a rutinas, y el sistema operativo, en el caso de las interrupciones.

Para el último uso, es decir, para utilizar la pila como almacén de datos, el programador tiene que utilizar las siguientes instrucciones:

  • “pha”.
  • “pla”.
  • “php”.
  • “plp”.

La primera instrucción guarda el valor del acumulador en la pila, y la segunda recupera el último valor de la pila y lo mete en el acumulador.

Estas instrucciones, usadas conjuntamente con las ya presentadas “tax”, “txa”, “tay” y “tya”, permiten meter y sacar de la pila el acumulador, el registro X y el registro Y, en los dos últimos casos pasando antes por el acumulador.

Análogamente, la tercera instrucción guarda el registro de estado en la pila, y la última recupera el último valor de la pila y lo mete en el registro de estado.

Todo lo anterior puede ser útil, por ejemplo, para evitar que una rutina modifique estos registros. Esto se conseguiría guardando todos los registros (acumulador, X, Y y estado) en la pila al comienzo de la rutina, y recuperándolos al final. Precisamente esto es lo que ocurre cuando tiene lugar una interrupción, como se verá más adelante.

Todas estas instrucciones utilizan el modo de direccionamiento implícito, y no modifican los flags del registro de estado, salvo “plp” que, al cargar el último valor de pila en el registro de estado, lógicamente sí modifica sus flags.

Los detalles de estas instrucciones se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#PHA.

Adicionalmente, también guardan relación con la pila las instrucciones:

  • “tsx”.
  • “txs”.

La primera de ellas carga el puntero de la pila en el registro X. Es decir, carga la dirección de la primera posición libre de la pila, en el registro X. Y más concretamente su byte menos significativo (LSB), ya que el más significativo (MSB) siempre es $01 en el caso de la pila (página uno).

La segunda hace lo contrario: carga el valor del registro X en el puntero de la pila.

Estas dos instrucciones también utilizan el modo de direccionamiento implícito y tampoco modifican los flags del registro de estado. Sus detalles se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#TXS.


Programas de ejemplo: Prog23 y Prog24

Subrutinas vs macros

Para terminar de entender mejor las similitudes y diferencias entre subrutinas y macros, lo mejor es hacer una comparación de diferentes aspectos:

Subrutina Macro
Objetivo Reutilizar código y crear niveles de abstracción. Reutilizar código y crear niveles de abstracción.
Código ensamblador Empieza con una etiqueta (opcional, pero cómodo) y termina con “rts”. Empieza con “defm” y termina con “endm”.
Llamada Se hace con “jsr etiqueta” o “jsr dirección”. Se hace con el nombre de la macro y los parámetros separados por comas.
Parámetros de entrada Se pasan mediante los registros o mediante posiciones de memoria. Se especifican en cada llamada, tras el nombre de la macro. Se usan con /1, /2, …
Parámetros de salida Se pasan mediante los registros o mediante posiciones de memoria. El mecanismo de macros como tal no tiene parámetros de salida.
Código máquina Empieza en una posición de memoria y termina con “rts”. El código sólo aparece una vez en memoria. En cada “llamada” a la macro, se copia el código de la macro y los parámetros de entrada se sustituyen por sus valores.

Supongamos que tenemos un programa que tiene que imprimir por pantalla varios caracteres. Si la función para imprimir un carácter X la hacemos con una subrutina, el código ensamblador quedaría así:

Subrutina

Y el código máquina quedaría así:

Subrutina_CM

En cambio, si la función la hacemos con una macro, el código ensamblador quedaría así:

Macro

Y el código máquina quedaría así:

Macro_CM

Nótese que cada llamada a la macro es sustituía por una copia del código de la macro (defm … endm), y que en cada copia los parámetros son sustituidos por el valor con que se hizo la “llamada” (2 en este caso). De la macro como entidad independiente no queda ni rastro; no es más que una plantilla de código que se reutiliza.

Desde una subrutina es posible llamar a otra subrutina (así creamos diferentes niveles de abstracción) y también usar macros. Desde una macro es posible llamar a una subrutina, pero, en cambio, no es posible usar una segunda macro; el ensamblador se haría lío con tanta sustitución de código. Al menos con CBM prg Studio…


Programa de ejemplo: Prog22

Macros

Las macros valen para lo mismo que las subrutinas: factorizar código y crear diferentes niveles de abstracción. Ahora bien, así como las subrutinas son una característica del código máquina del 6510 (y de muchos otros lenguajes de programación), las macros son una construcción de los ensambladores modernos.

Una macro es un trozo de código en ensamblador que tiene que tener un nombre e ir definido entre las directivas “defm” y “endm” (esto es para el caso de CBM prg Studio; en el caso de otros ensambladores la directiva será diferente). Una macro puede tener parámetros. Posteriormente, la macro será “llamada” mediante su nombre y parámetros desde diferentes puntos del código ensamblador.

Ahora bien, la “llamada” tiene poco que ver con la llamada a una subrutina. En el caso de una subrutina, al ejecutar “jsr” el contador de programa pasa a tomar el valor de la primera dirección de la subrutina, y al ejecutar “rts” el contador de programa pasa a tomar el valor de la instrucción siguiente al “jsr”, que previamente se guardó en la pila.

Pues bien, en el caso de una macro, lo que realmente ocurre es que cada “llamada” a la macro se sustituye por una copia del código de la macro, es decir, por una copia del código que hay entre “defm” y “endm”. Y el “paso de parámetros” se consigue gracias a que, en cada copia del código de la macro, los parámetros se sustituyen por el valor que se ha suministrado en la llamada.

En definitiva, subrutinas y macros valen para lo mismo, pero las primeras son un mecanismo del código máquina del 6510, y las segundas son como un “truco” del ensamblador (por ejemplo, CBM prg Studio). Lo que hace el ensamblador es sustituir cada “llamada” a la macro por una copia de su código, y en cada copia del código los parámetros son sustituidos por los valores especificados en la “llamada”.


Programa de ejemplo: Prog21

Subrutinas

Las subrutinas son una herramienta de programación muy potente. Permiten sacar “factor común” de aquellos trozos de código que se ejecutan muchas veces y reutilizarlo, además de permitir la creación de diferentes niveles de abstracción.

Las instrucciones relacionadas con subrutinas son:

  • “jsr”.
  • “rts”.

La instrucción “jsr” salta a la subrutina, es decir, la ejecuta. La instrucción “rts” termina la ejecución de la subrutina, es decir, devuelve la ejecución a la instrucción siguiente al “jsr”. Esto es posible gracias a que al ejecutar “jsr” se guarda la dirección de retorno en la pila.

Si un programa en código máquina se ejecuta desde BASIC con SYS 49152 (o la dirección que proceda), entonces “rts” también se utiliza para terminar ese programa en código máquina y volver a BASIC.

Una cuestión importante es cómo pasar información o parámetros a las subrutinas. Las dos formas principales son:

  • Mediante los registros del microprocesador.
  • Mediante posiciones de memoria.

Por ejemplo, las rutinas del Kernal utilizan con frecuencia los registros del microprocesador (acumulador, registro X o registro Y) para recibir los parámetros con los que trabajar. Por ejemplo, la rutina “chrout”, que pinta un carácter en pantalla, recibe el código de ese carácter mediante el acumulador.

Otra posibilidad es utilizar posiciones de memoria. El programa principal, que es el que llama a la subrutina, deposita la información en una o varias posiciones de memoria (que pueden ser de memoria general o de la página cero en particular), y la subrutina obtiene la información de esas mismas posiciones.

A mí particularmente me gusta más esta segunda opción (posiciones de memoria), porque los registros del microprocesador me parecen herramientas para trabajar de forma temporal o transitoria, cargando la información de una posición de memoria, procesando esa información, generando otra, devolviéndola a memoria, y así sucesivamente.

Los razonamientos anteriores (cómo pasar información o parámetros) aplican tanto a los parámetros de entrada, es decir, la información que el programa principal pasa a la rutina, como a los parámetros de salida, es decir, la información que la rutina podría devolver al programa principal.

Y por supuesto una subrutina podría llamar a otra, y así sucesivamente, mientras no se agote la pila. Precisamente esta es la forma de crear diferentes niveles de abstracción, desde rutinas más básicas, que hacen funciones sencillas, hasta rutinas que, a base de combinar otras, hacen funciones más complejas.

Y todo lo anterior nos lleva a otro debate importante: una rutina puede modificar los registros del microprocesador y/o posiciones de memoria. Es decir, salvo que el programador tome medidas para evitarlo (por ejemplo, salvaguardar los registros en la pila o en posiciones de memoria y recuperarlos al final de la rutina), lo más probable es que una rutina modifique los registros. Por tanto, con carácter general, el programador no puede confiar en que las rutinas que usa, que pueden ser propias o de terceros, dejen los registros del microprocesador como estaban antes de la llamada.

De hecho, cuando se documentan rutinas es muy habitual documentar el nombre, la función, la dirección de llamada (directa o a través de una “jump table”), cómo se pasan los parámetros de entrada y salida, y qué registros se modifican. Como ejemplo se puede consultar esta página que documenta las rutinas del Kernal: http://sta.c64.org/cbm64krnfunc.html.

“jsr” utiliza el modo de direccionamiento absoluto, porque debe ir acompañada de la dirección de la subrutina (primero el LSB y después en MSB). “rts”, en cambio, utiliza el modo de direccionamiento implícito ya que, al obtenerse la dirección de retorno de la pila, no necesita ningún operando.

Ninguna de estas instrucciones modifica los flags del registro de estado de forma directa. Ahora bien, igual que ocurre con el acumulador, el registro X o el registro Y, la ejecución de una rutina (digamos, el cuerpo de la misma) por supuesto que puede modificar los flags del registro de estado. De hecho, lo más habitual será que lo haga.

Los detalles de estas instrucciones pueden consultarse en http://www.6502.org/tutorials/6502opcodes.html#JSR y http://www.6502.org/tutorials/6502opcodes.html#RTS.


Programa de ejemplo: Prog20