Idioma

Subrutinas del lenguaje de máquina para usar con ATARI

UBICANDO EN LA MEMORIA LOS PROGRAMAS EN LENGUAJE DE MÁQUINA


Cuando comenzamos a escribir subrutinas, debemos decidir dónde queremos ubicarlas. Hay dos tipos de programas en Lenguaje de Máquina: reubicables y fijos. Los programas fijos son aquellos que utilizan direcciones específicas dentro del programa; estas direcciones no pueden cambiar. Por ejemplo, supongamos que nuestro programa tiene estas líneas:


30  *= $600
45  LDA ADDR1
50  BNE NOZERO
55  JMP ZERO
60 NOZERO RTS
70 ZERO   SBC #1
80  RTS
90 ADDR1 .BYTE 4	

En este fragmento, utilizamos varias referencias a direcciones dentro del programa que son fijas: no pueden cambiar sin estropear completamente el programa. Esto se ve más fácilmente después de que usamos el ensamblador para ensamblar este programa, produciendo un resultado similar a este:


ADDR  ML    LN LABEL   OP    OPRND
----------------------------------
0000        30         *=    $600
0600 AD0C06 45         LDA   ADDR1
0603 D003   50         BNE   NOZERO
0605 4C0906 55         JMP   ZERO
0608 60     60 NOZERO  RTS
0609 E901   70 ZERO    SBC   #1
060B 60     80         RTS
060C 04     90 ADDR1   .BYTE 4	

Esta salida está muy bien formateada en columnas. La primera columna enumera las direcciones hexadecimales en las que se encuentran las instrucciones en Lenguaje de Máquina traducidas a partir de los mnemónicos. La segunda columna enumera el código en Lenguaje de Máquina que resulta de esa traducción. Por ejemplo, la instrucción RTS en la línea 80 generó el código de Lenguaje de Máquina 60, que se encuentra en la ubicación de memoria $060B. La tercera columna enumera los números de línea del programa en lenguaje Ensamblador. La cuarta columna contiene las etiquetas que estaban presentes en el programa original y la quinta columna contiene los mnemónicos del programa. La sexta columna contiene el operando. En este ejemplo, no hay una séptima columna, que habría estado presente si el programa original hubiera contenido algún comentario.


Volviendo al problema de las direcciones fijas discutido anteriormente, la primera de las direcciones del problema, ADDR1, se puede encontrar en las líneas 45 y 90. Miremos por un momento el código en Lenguaje de Máquina que el Ensamblador produjo para la línea 45. Tres bytes fueron producidos; AD, 0C y 06. AD es el código de Lenguaje de Máquina para el modo de direccionamiento Absoluto de la instrucción LDA. Como sabemos que el modo de direccionamiento Absoluto de la instrucción LDA requiere 3 bytes, sabemos por qué el Ensamblador produjo 3 bytes. El segundo y tercer byte, 0C y 06, constituyen la dirección desde la cual cargar el Acumulador, en el orden estándar de byte menos significativo-byte más significativo del 6502. Por lo tanto, la dirección desde la que cargar el Acumulador es $060C, que es la dirección de la línea que contiene la etiqueta ADDR1. Cuando escribimos LDA ADDR1, el ensamblador tradujo esto como LDA $060C, ya que esa era la dirección asignada a ADDR1.


Ahora podemos entender por qué cualquier intento de ejecutar este programa en algún otro lugar de la memoria está condenado al fracaso. Cuando se ejecuta la línea 45, el microprocesador buscará la dirección original, $060C, para cargar el Acumulador; espera encontrar ADDR1 allí, ya que esta fue la ubicación asignada para ADDR1 en el momento del ensamblaje. Sin embargo, la lógica del programa se estableció para realizar ciertas funciones basadas en el valor almacenado en $060C solo si $060C era igual a ADDR1. Si movemos la rutina en la memoria, ADDR1 estará en algún lugar distinto de $060C y la lógica del programa ya no será válida.


La segunda dirección fija a la que se hace referencia en este programa está en la línea 55. Cada instrucción JMP tiene como destino una dirección fija. Podemos ver esto examinando el código en Lenguaje de Máquina generado para la línea 55: 4C, 09, 06. El byte 4C es el código en Lenguaje de Máquina para una instrucción JMP absoluta, una instrucción de 3 bytes. Los siguientes 2 bytes son la dirección a la que saltar, $0609 (Recuerde: el byte menos significativo primero). Ahora podemos ver que cuando se ejecuta la línea 55, el programa saltará a $0609, independientemente de a qué parte de la memoria hayamos movido este programa. Sin embargo, si movemos este programa a otro lugar de la memoria, la instrucción que pretendíamos ejecutar en $0609 (el SBC #1 en la línea 70) ya no estará allí. De hecho, probablemente no habrá ninguna instrucción válida en $0609, por lo que el programa fallará.


Observe un momento la línea 50. Recuerde que todas las instrucciones de bifurcación utilizan la forma de direccionamiento Relativo. Si miramos el código en Lenguaje de Máquina para esta instrucción, encontraremos D0, 03. D0 es el código en Lenguaje de Máquina que significa bifurcar en valor distinto de cero (BNE), pero ese 3 no parece una dirección. No lo es. Simplemente le dice al microprocesador 6502 que avance 3 bytes en la memoria contando desde la ubicación actual del Contador de Programa. Cuando se ejecuta la línea 50, el Contador de Programa apunta al inicio de la siguiente línea. En este caso, apunta al 4C de la instrucción JMP en la línea 55. Al avanzar 3 bytes, apuntará a 60, la instrucción RTS en la línea 60. Es decir, cuando se ensambla y ejecuta BNE NOZERO, esta instrucción le dice al 6502 que avance 3 bytes, pasando la instrucción JMP a la dirección NOZERO ($0608). Dado que la instrucción de bifurcación simplemente dice "Bifurcación hacia adelante, tres bytes", en lugar de "Bifurcación hacia adelante a $0608", se puede ubicar en cualquier lugar de la memoria y la bifurcación terminará en NOZERO, independientemente de en qué parte de la memoria se encuentre NOZERO. NOZERO siempre estará 3 bytes por delante de la instrucción de bifurcación, por lo que todo estará bien.


¡¡¡TENGA EN CUENTA!!! Esto nos enseña una lección importante: las bifurcaciones se pueden incluir en el código reubicable, pero los JMP y las direcciones específicas dentro del programa, no.


¿Por qué se escribe tanto sobre el código reubicable? Por una razón muy sencilla: si el código no es reubicable, entonces necesitamos encontrar algún lugar seguro en la memoria para almacenarlo. Puede que esto no siempre sea fácil, ya que compartimos la computadora con BASIC y no siempre podemos estar seguros de qué ubicaciones usará BASIC. Si nuestro código es reubicable, podremos colocarlo en cualquier lugar. ¿Pero dónde?


Repasemos por un momento cómo BASIC maneja las cadenas. Cuando se desea utilizar una cadena en ATARI BASIC, esta se debe acotar. Cuando se hace esto, la computadora reserva espacio suficiente para la cadena en la memoria. Si por alguna razón necesita parte de ese espacio, simplemente mueve la cadena a otro lugar, pero BASIC entonces es responsable de recordar dónde está la cadena y también es responsable de proteger su espacio. ¡Ajá! ¡Ahora hemos salido de la situación en la que tenemos que proteger algún área de la memoria del BASIC y hemos entrado en una situación en la que el BASIC hace la asignación y la protección por nosotros! Luego podemos almacenar nuestro programa en Lenguaje de Máquina como una cadena en BASIC y acceder a él usando la forma de comando USR(ADR(ourstring$)).


Para ser justos, normalmente habrá espacio para una breve rutina en la Página 6. Recuerde que se garantiza que la Página 6 siempre estará libre para el uso del programador. Bueno, casi siempre. Debe tener en cuenta que existe una condición frecuente en la que la Página 6 puede no ser segura. Como se mencionó en el Capítulo 3, su ATARI utiliza el espacio de $580 a $5FF (la mitad superior de la Página 5) como buffer (un lugar para almacenar información temporalmente). Si ingresa información desde el teclado, este buffer de entrada puede desbordarse hacia la parte inferior de la Página 6. Este desbordamiento sobrescribirá todo lo almacenado entre $600 y $6FF, dependiendo de cuánto desbordamiento se haya producido. Para los propósitos de este libro, asumiremos que tal desbordamiento no ocurrirá y que los programas que no pueden escribirse para ser reubicables generalmente tendrán su origen en $600. Si no funcionan en alguna aplicación específica que pueda tener, verifique que no se esté desbordando el buffer de la Página 5.


Otros lugares donde se pueden ubicar programas no reubicables son en la parte superior de la Memoria o debajo de LOMEM. Ambos lugares generalmente están a salvo de las interferencias del BASIC si se tiene cuidado en su uso. Además, si tiene una aplicación que nunca usará la grabadora, puede usar el buffer de la cinta, ubicado entre $480 y $4FF, para almacenar su programa. También se pueden colocar rutinas muy pequeñas en el extremo inferior de la Pila, desde $100 hasta aproximadamente $160, ya que sólo aplicaciones atípicas utilizarán la Pila a esta profundidad. Sin embargo, esto es extremadamente riesgoso y no se pueden ofrecer garantías sobre el rendimiento seguro de los programas que utilizan este espacio.


Ya que estamos en el tema de las grabadoras, una nota final sobre la organización de este libro. Todos los programas se escriben asumiendo la presencia de una unidad de disco y un DOS residente. Si está utilizando un sistema basado en cinta, consulte el manual del Ensamblador para obtener instrucciones sobre cómo realizar determinadas operaciones. Por ejemplo, si carga archivos en Lenguaje de Máquina desde una unidad de disco usando la opción L del DOS, la misma operación usando una grabadora probablemente usará alguna forma de los comandos LOAD o BLOAD, dependiendo del ensamblador que esté usando.


UN EJEMPLO SIMPLE DE SUBRUTINA PARA BORRAR MEMORIA


Comencemos a construir nuestra biblioteca de subrutinas con un ejemplo muy simple. Recuerde, si no desea escribir todos los programas, están disponibles en disco en MMG Micro Software.


En BASIC, frecuentemente necesitamos borrar un área de la memoria poniéndole ceros. Esto ocurre, por ejemplo, cuando se utilizan gráficos de Player/Missile o cuando se utiliza la memoria como un bloc de notas o incluso simplemente cuando es necesario borrar una pantalla o un dibujo. Recuerde que si queremos almacenar datos cerca de la parte superior de la memoria, el Display List y la memoria de visualización deben reubicarse debajo de esta área de la memoria, o de lo contrario esta rutina borrará la imagen en la pantalla de nuestro televisor. Esta reubicación se puede lograr fácilmente, usando el siguiente código en BASIC:


10 ORIG=PEEK(106):REM Guarda la parte superior de la memoria original
20 POKE 106,ORIG-8:REM Baja la parte superior de la memoria en 8 páginas
30 GRAPHICS 0:REM Restablece el Display List  y la memoria de la pantalla
40 POKE 106,ORIG:REM Restaurar la parte superior de la memoria como antes.	

Por supuesto, podemos realizar la operación de borrado de memoria en BASIC. Si necesitamos borrar las 8 páginas superiores de la memoria, podemos hacerlo con el siguiente programa:


10 TOP=PEEK(106):REM Encuentra la parte superior de la memoria
20 START=(TOP-8)*256:REM Calcula donde comenzar la limpieza
30 FOR I=START TO START+2048:REM Area a limpiar
40 POKE I,0:REM Borrar cada ubicación
50 NEXT I:REM Todo terminado	

Este programa funciona tal como queremos, pero tarda aproximadamente 13,5 segundos en ejecutarse. Si necesitamos realizar esta operación varias veces durante el transcurso de un programa, o si tenemos un programa que no puede permitirse los 13,5 segundos necesarios para hacerlo en BASIC, entonces tenemos un buen candidato para una subrutina en Lenguaje de Máquina.


Primero tendremos que pensar en dónde ubicar nuestra subrutina. La Página 6 es un lugar tan bueno como cualquier otro. Luego necesitaremos saber cómo encontrar la parte superior de la memoria, para saber qué parte de ella queremos borrar. Hay una ubicación de memoria que siempre realiza un seguimiento de dónde está la parte superior de la memoria, en páginas, en un ATARI: la ubicación 106. Por lo que esa parte es fácil. Finalmente, este tipo de programa suele ser un buen candidato para el direccionamiento "Indirecto, Y", por lo que necesitaremos dos ubicaciones de Página Cero para guardar nuestra dirección indirecta. Escribamos lo que tenemos hasta ahora, en lenguaje Ensamblador:


100 ; ********************************
110 ; Configurar condiciones iniciales
120 ; ********************************
130  *= $600 ; tenemos que ensamblarlo en algun lugar
140 TOP = 106 ; aqui es donde se encuentra TOP
150 CURPAG = $CD ; donde almacenamos la página actual que esta siendo borrada	

Tenga en cuenta que solo hay cuatro ubicaciones de memoria que son seguras en la Página Cero, $CC a $CF, y no pueden ser cambiadas ni por BASIC ni por el cartucho Assembler/Editor: para este programa, usaremos dos de ellas: $CC y $CD. Es posible encontrar otras ubicaciones en la Página Cero que pueden estar a salvo de ser modificadas por el cartucho Assembler/Editor, pero ATARI garantiza que estas siempre serán seguras, por lo que las usaremos.


Ahora pensemos en lo que nos gustaría que hiciera la rutina. Primero debemos usar el PLA necesario para extraer de la Pila la cantidad de parámetros pasados por BASIC en la llamada USR que escribiremos. Una vez que hayamos encontrado la parte superior de la memoria actual, necesitaremos comenzar 8 páginas por debajo, borrando toda la memoria poniéndole ceros. El código para encontrar en qué parte de la memoria comenzar a borrar es bastante sencillo, tal como se muestra a continuación:


160 ; *************************
170 ; Comenzar con los cálculos
180 ; *************************
190  PLA     ; saque # de parametros de la Pila
200  LDA TOP ; busque TOP
210  SEC     ; preparese para restar
220  SBC #8  ; busque la primera pagina a borrar
230  STA CURPAG ; la necesitaremos
240  LDA #0  ; para insertarla en memoria posteriormente
250  STA CURPAG-1 ; el byte bajo de un # de pagina siempre es cero	

Hemos configurado la dirección indirecta del primer lugar en la memoria a borrar y la hemos almacenado en CURPAG-1 (byte bajo) y CURPAG (byte alto). También cargamos cero en el Acumulador, por lo que estamos listos para almacenar un cero en cada ubicación de memoria que necesitemos borrar.


A continuación, necesitamos un contador para realizar un seguimiento de cuántas ubicaciones de memoria se han borrado en cada página. Si usamos el registro Y para esto, puede actuar como contador y como desplazamiento para el sistema de direccionamiento que estamos usando. Todo lo que tenemos que hacer es configurar el contador, almacenar el cero del Acumulador en la primera ubicación y disminuir el registro Y en 1, volviendo a realizar el almacenamiento. Como comenzamos con el contador en cero y vamos disminuyendo en 1 a medida que limpiamos cada ubicación, siempre y cuando el registro Y aún no haya llegado a cero nuevamente, todavía nos queda por limpiar en esta página, ya que hay 256 ubicaciones por página de memoria. Veamos cómo lucirá el código:


260 LDY#0 ; para uso como contador
270 ; ***************************************
280 ; ahora ingresaremos al ciclo de limpieza
290 ; ***************************************
300 LOOP STA (CURPAG-1),Y ; se borra el primer byte
310  DEY ; disminuir el contador
320  BNE LOOP ; si > cero, la pagina aun no ha terminado	

Eso fue bastante simple. Almacenamos el cero que estaba en el Acumulador en la dirección señalada por la dirección indirecta CURPAG-1, CURPAG (byte bajo, byte alto) compensado por Y, que era cero la primera vez que se recorre el ciclo. Luego disminuimos el registro Y en 1 y retrocedimos para almacenar un cero en la ubicación de memoria señalada por la misma dirección indirecta, pero esta vez compensada en 255, por lo que borramos el byte superior de la página. La próxima vez, Y es igual a 254, por lo que borramos el siguiente byte inferior de memoria, y así sucesivamente, hasta que Y es igual a cero, cuando se borra toda la página y nuestro contador vuelve a cero, listo para la siguiente página.


Muy bien, ahora hemos borrado 1 página. ¿Cómo conseguimos que borre las otras 7 páginas? Recuerde que la dirección indirecta que configuramos en la Página Cero contiene 1 byte para el byte bajo de la dirección indirecta que apunta a la página que se va a borrar y un byte para el byte alto de esta dirección. Si simplemente aumentamos el byte alto en 1, esta dirección indirecta apuntará a la siguiente página superior, así:


330 INC CURPAG ; vamos a la siguiente pagina	

Realmente no podría ser más fácil, ¿verdad? Ahora todo lo que tenemos que hacer es averiguar cuándo terminamos.


En este caso, sabemos que hemos terminado cuando la página que estamos borrando está por encima del límite superior de la memoria. Es bastante fácil determinar si esta condición es verdadera, de la siguiente manera:


340  LDA CURPAG ; necesitamos ver si hemos terminado
350  CMP TOP ; ?es CURPAG > TOP?
360  BEQ LOOP ; no, !la ultima pagina esta por llegar!
370  BCC LOOP ; no, siga borrando
380  RTS ; volver a BASIC	

Recuerde: Si CURPAG es igual a TOP, todavía tenemos que borrar la última página. Sólo hemos terminado si CURPAG es mayor que TOP.


Ahora que hemos escrito nuestro programa, necesitamos convertirlo desde lenguaje Ensamblador a Lenguaje de Máquina. Para hacer esto, utilizamos la parte del cartucho Assembler/Editor que se encarga de ensamblar, a la que se puede acceder simplemente escribiendo ASM seguido de RETURN. Esto iniciará el proceso de ensamblaje y, después de una breve pausa, aparecerá la siguiente información en su pantalla:


Listado 7-1


ADDR  ML    LN   LABEL  OP   OPRND    COMMENT                   
            0100 ; ********************************
            0110 ; Configurar condiciones iniciales
            0120 ; ********************************
0000        0130        *=   $600     ; tenemos que ensamblarlo en algun lugar
006A        0140 TOP    =    106      ; aqui es donde se encuentra TOP
00CD        0150 CURPAG =    $CD      ; donde almacenamos la página actual que esta siendo borrada
            0160 ; ***************************
            0170 ; Comenzar con los cálculos
            0180 ; ***************************
0600 68     0190        PLA           ; saque # de parametros de la Pila
0601 A56A   0200        LDA  TOP      ; busque TOP
0603 38     0210        SEC           ; preparese para restar
0604 E908   0220        SBC  #8       ; busque la primera pagina a borrar
0606 85CD   0230        STA  CURPAG   ; la necesitaremos
0608 A900   0240        LDA  #0       ; para insertarla en memoria posteriormente
060A 85CC   0250        STA  CURPAG-1 ; el byte bajo de un # de pagina siempre es cero
060C A000   0260        LDY  #0       ; para uso como contador
            0270 ; ***************************************
            0280 ; Ahora ingresaremos al ciclo de limpieza
            0290 ; ***************************************
060E 91CC   0300 LOOP   STA  (CURPAG-1),Y ; Se borra el primer byte
0610 88     0310        DEY           ; Disminuir el contador
0611 D0FB   0320        BNE LOOP      ; Si > cero, la pagina aun no ha terminado
0613 E6CD   0330        INC CURPAG    ; Vamos a la siguiente pagina
0615 A5CD   0340        LDA CURPAG    ; Necesitamos ver si hemos terminado
0617 C56A   0350        CMP TOP       ; ?Es CURPAG > TOP?
0619 F0F3   0360        BEQ LOOP      ; No, !la ultima pagina esta por llegar!
061B 90F1   0370        BCC LOOP      ; No, siga borrando
061D 60     0380        RTS           ; Volver a BASIC	

Hemos ensamblado el programa y lo hemos almacenado en la memoria. La siguiente tarea es almacenarlo en nuestro disco para que podamos usarlo en nuestro programa BASIC. Esto se puede hacer de dos maneras. La primera es directamente desde el cartucho Assembler/Editor, utilizando el comando SAVE de la siguiente manera:


SAVE #D:PROGRAM<0600,061F	

Este comando crea un archivo en el disco llamado PROGRAM y almacena en él todo el contenido de la memoria desde $0600 hasta $061F. Tenga en cuenta que hemos almacenado algunos bytes adicionales; generalmente es una buena idea.


La segunda forma de almacenar esta información es ir a DOS y guardar el contenido de la memoria usando la opción K. Esto puede hacerse de la siguiente manera:


PROGRAM,0600,061F	

Cualquiera de estos métodos sirve para almacenar los resultados del ensamblaje.


Ahora puede cambiar el cartucho del Assembler/Editor por el cartucho BASIC. Después de iniciar la computadora, escriba DOS, y cuando aparezca el menú de DOS, use la opción L para cargar el archivo llamado PROGRAM que acabamos de crear. Luego escriba B para volver a BASIC.


Nuestro programa ahora se encuentra en la Página 6 y podemos acceder a él si lo deseamos. Sin embargo, el siguiente paso debería ser ponerlo en un formato que no requiera el uso de DOS para cargarlo. Podríamos simplemente escribir una línea de código BASIC en modo directo para extraer esta información de la Página 6; Por ejemplo,


FOR I=1 TO 30:?PEEK(1535+I);" ";:NEXT I	

Sin embargo, ya que estamos usando una computadora, ¿Por qué no escribir un programa de propósito general que extraiga los datos de la memoria y los configure en una forma que podamos convertir fácilmente en líneas DATA en un programa BASIC? Un programa de este tipo se detalla a continuación:


10 FOR J=1 TO 30 STEP l0:REM Longitud de los datos en la memoria
20 FOR I=J TO J+9:REM Obtendremos lineas DATA de 10 bytes de longitud
30 PRINT PEEK(I+1535);",";: REM Imprime los datos en la pantalla
40 NEXT I:REM Termina la linea
50 PRINT:PRINT:REM Dejar lineas en blanco para facilitar el trabajo
60 NEXT J:REM !Todo listo!	

Si ahora escribimos este programa y lo ejecutamos con RUN, nuestra pantalla mostrará lo siguiente:


104,165,106,56,233,8,133,205,169,0,
133,204,160,0,145,204,136,208,251,230,
205,165,205,197,106,240,243,144,241,96,	

Ahora es muy sencillo mover el cursor hasta estas líneas, eliminar las comas finales y convertirlas a lo siguiente:


10000 DATA 104,165,106,56,233,8,133,205,169,0
10010 DATA 133,204,160,0,145,204,136,208,251,230
10020 DATA 205,165,205,197,106,240,243,144,241,96	

Ahora podemos borrar las líneas de la 10 a la 60, de modo que el programa en la memoria conste solo de las líneas 10000 a 10020. En este punto, debemos guardar el programa en disco, para no tener que volver a realizar todo este procedimiento si es que falla la energía. Podemos incorporar esta rutina a un programa BASIC corto para probarla, de la siguiente manera:


10 FOR I=1 TO 30:REM Numero de bytes
20 READ A:REM Obtener cada byte
30 POKE 1535+I,A:REM Hacer POKE con el byte en la ubicación correcta
40 NEXT I:REM Terminar de hacer POKE con los datos
50 ORIG=PEEK(106): REM Ahora reubique el Display List, como arriba
60 POKE 106,ORIG-8
70 GRAPHICS 0
80 POKE 106,ORIG:REM Restaurar la parte superior de la memoria
90 POKE 20,0:REM Poner el temporizador en cero
100 X=USR(1536):REM Llamar nuestra rutina de Lenguaje de Maquina
110? PEEK(20)/60:REM ?Cuantos segundos tomo?
120 END:REM Separar los DATA del programa
10000 DATA 104,165,106,56,233,8,133,205,169,0
10010 DATA 133,204,160,0,145,205,136,208,251,230
10020 DATA 205,165,205,197,106,240,243,144,241,96	

La línea 90 primero pone en cero el reloj interno en tiempo real y luego la línea 110 lee la hora en jiffis (Sexagésimos de segundo). Esto medirá el tiempo que le tomó a nuestra rutina en Lenguaje de Máquina borrar las 8 páginas de memoria. Tardó 0,0333 segundos, por lo que esta rutina en Lenguaje de Máquina, que parece tan larga y que consume mucho tiempo, es más de 400 veces más rápida que el programa BASIC que hizo el mismo trabajo. Vale la pena el esfuerzo, ¿no?


Por supuesto, todo el tiempo dedicado a programar esta rutina no fue en vano, ya que ahora tenemos una rutina que podemos usar siempre que necesitemos limpiar la parte superior de la memoria, como por ejemplo para los gráficos Player/Missile.


El programa que escribimos tiene un inconveniente: el código reside en la Página 6 y por lo tanto no puede usarse en un programa que necesita la Página 6 para su propio uso. Ahora, aquí es donde entra en juego la naturaleza reubicable del código. Miremos nuevamente el programa que escribimos en lenguaje Ensamblador. Tenga en cuenta que no utilizamos ningún salto ni hicimos referencia a ninguna dirección dentro del programa, excepto en las instrucciones de bifurcación. Este programa no está vinculado a ninguna ubicación de memoria específica; puede residir en cualquier lugar de la memoria y seguir funcionando. Aprovechemos eso y convirtamos el programa en una cadena. Este proceso es bastante simple. Todo lo que necesitamos hacer es agregar la línea 5 y cambiar las líneas 30 y 100, de la siguiente manera:


5 DIM CLEAR$(30):REM Configurar la cadena
30 CLEAR$(I,I)=CHR$(A):REM Insertar byte en la cadena
100 X=USR(ADR(CLEAR$)):REM Nueva ubicacion del rutina de limpieza	

Ahora tenemos una rutina reubicable para borrar la memoria, que será mucho más versátil que la vinculada a ubicaciones específicas. De hecho, si la cadena que creamos no contiene caracteres de control, simplemente podemos producir una subrutina de 1 línea que contendrá toda la información que necesitemos. Podemos hacer esto ejecutando el programa y luego imprimiendo CLEAR$ en la pantalla. Luego podemos mover el cursor hasta la cadena de Lenguaje de Máquina y convertirlo a una sola línea BASIC, de la siguiente manera:



¡Esto no puede ser más sencillo! Ahora, cuando necesitemos borrar la parte superior de la memoria, podemos simplemente incluir esta línea de código y acceder a la subrutina para borrar la memoria.


Puede escribir sus propias rutinas en Lenguaje de Máquina para programas en BASIC, o puede usar otras rutinas más sofisticadas. En el número de septiembre de 1983 de la revista ANTIC, Jerry White publicó una rutina muy buena para producir subrutinas como cadenas. Esta rutina lee los datos del Lenguaje de Máquina directamente desde el disco y escribe el código BASIC nuevamente en el disco. Esto evita el problema de imprimir dichas cadenas en la pantalla; si la cadena contiene un carácter que no se imprime, se requiere una buena cantidad de trabajo para asegurarse de que la cadena sea correcta. Una forma de verificar sus rutinas con bastante facilidad para detectar tales problemas es simplemente contar el número de caracteres impresos en la pantalla cuando imprime su cadena y comparar ese número con el número de bytes contenidos en su rutina de Lenguaje de Máquina. Si los números difieren, será mejor que averigüe qué carácter se ha omitido e insértelo en el lugar apropiado usando esta secuencia de teclas:


ESC - Tecla CTRL

lo que le permitirá imprimir caracteres que normalmente no se imprimen. Tomemos un ejemplo sencillo. Suponga que ha escrito una rutina en Lenguaje de Máquina para algún propósito y cuando imprime en la pantalla la cadena que contiene, esta rutina es 1 byte más corta de lo que debería ser. Además, escuchará un sonido de campana cada vez que imprima esta cadena. Al revisar sus líneas DATA, descubre que el byte 15 de su rutina de Lenguaje de Máquina es 253. Cuando intenta imprimir el carácter correspondiente al ATASCII 253, sonará el timbre, ya que este es el código para el timbre del teclado, pero el carácter no se imprimirá en la pantalla. Para resolver este problema, imprima la cadena en la pantalla y luego coloque el cursor sobre el byte número 15 de la cadena. Presione la tecla CTRL y la tecla INSERT simultáneamente, y a partir del carácter 15 de la cadena se moverá 1 posición hacia la derecha, dejando espacio para el carácter que falta. Ahora presione la tecla ESC y luego presione simultáneamente la tecla CTRL y la tecla 2, y el carácter correcto se insertará en el byte 15 de su cadena. Luego se puede agregar un número de línea y el resto de la información requerida, como se muestra arriba, y tendrá su rutina en una sola línea.


Para rutinas breves y únicas, la forma más sencilla de solucionar este problema es no utilizar cadenas en líneas individuales, sino insertar los caracteres en una cadena utilizando instrucciones DATA, como ya se mostró. Sin embargo, cuando se desean cadenas de una sola línea (por ejemplo, cuando el espacio es escaso), cuanto más cauteloso sea el programador, mejores serán los resultados.


SUBRUTINA PARA REUBICAR EL CONJUNTO DE CARACTERES


Una de las características más interesantes de las computadoras ATARI es la facilidad con la que el conjunto de caracteres estándar (normalmente las letras mayúsculas, minúsculas e inversas, números y símbolos que usamos todos los días) se puede alterar para cualquier propósito que deseamos. Por ejemplo, uno de los juegos más populares de ATARI, SPACE INVADERS, fue programado utilizando caracteres redefinidos para los invasores atacantes. Luego, simplemente se imprimen en la pantalla en el lugar apropiado. Al imprimirlos todos una posición más a la derecha en cada ciclo del juego, parecen marchar por la pantalla, en su siniestro estilo.


Sin embargo, como sabemos, el conjunto de caracteres ATARI normal reside en la ROM, comenzando en la ubicación 57344 ($E000 hexadecimal). Para modificar cualquiera de los caracteres estándar, necesitamos mover el conjunto de caracteres a la RAM, donde podemos acceder a él. Por supuesto, esto se puede hacer en BASIC. A continuación, se proporciona un programa BASIC muy simple para lograr esto:


10 ORIGINAL=57344:REM Donde el conjunto de caracteres esta en ROM
20 ORIG=PEEK(106):REM Donde se encuentra la parte superior de la RAM
30 CHSET=(ORIG-4)*256:REM Donde el conjunto sera reubicado 
40 POKE 106,ORIG- 8:REM Le haremos espacio
50 GRAPHICS 0:REM Configurar nuevo Display List
60 FOR I=0 TO 1023:REM Ahora transferiremos todo el conjunto
70 POKE CHSET+I,PEEK(ORIGINAL+I)
80 NEXT I
90 END :REM Eso es todo	

Este programa reserva 8 páginas de memoria cerca de la parte superior de la RAM, al igual que lo hizo en el ejemplo anterior. Sin embargo, en este caso no es necesario borrar esta área, ya que llenamos 4 páginas con el conjunto de caracteres de la ROM. El ciclo de las líneas 60 a 80 en realidad logra la transferencia del conjunto de caracteres, que tiene una longitud de 1024 bytes (8 bytes por carácter multiplicado por 128 caracteres). El programa funciona bien y si no nos importa dedicar 14,7 segundos a realizar esta transferencia, no necesitamos el lenguaje Ensamblador en absoluto.


Sin embargo, si queremos ir más rápido, necesitaremos una subrutina en Lenguaje de Máquina para realizar la transferencia. La subrutina que escribimos para borrar un área de memoria contiene las técnicas que usaremos en dicho programa. Sin embargo, necesitaremos agregar dos funciones nuevas. La primera permitirá que BASIC pase a nuestra subrutina la ubicación en la que nos gustaría que resida nuestro juego de caracteres en la RAM, utilizando el paso de parámetros discutido en el Apéndice 1. La segunda función almacenará diferentes valores en cada ubicación de memoria, en lugar de almacenar el mismo carácter en cada ubicación. Para hacer esto, necesitaremos dos direcciones indirectas configuradas en la Página Cero. Además, en esta rutina emplearemos la nomenclatura más habitual, definiendo nuestra etiqueta como la menor de las dos ubicaciones de Página Cero y refiriéndonos a la ubicación superior como etiqueta + 1, en lugar de definir la superior y referirnos a la ubicación inferior como etiqueta - 1. Se presentan ambos métodos para demostrar la flexibilidad de la programación en lenguaje Ensamblador. Ahora, comencemos con la configuración:


        0100 ; ***************************
        0110 ; Configurar condiciones iniciales
        0120 ; ***************************
0000    0130       *=  $600
00CC    0140 FROM  =   $CC
00CE    0150 TO    =   $CE	

A partir de este momento, usaremos la salida del Ensamblador para todos los programas mostrados. Para escribir estos programas usted mismo, simplemente escriba el número de línea, la etiqueta (si está presente), el mnemónico, el operando y los comentarios, y compárelo usted mismo. Cuando se muestre en su pantalla, la salida de su Ensamblador debe verse como la salida que se proporciona aquí. Al presentar los programas de esta manera, podemos hacer referencia al código en Lenguaje de Máquina generado por el Ensamblador así como al código en lenguaje Ensamblador que escribimos.


Como puede ver, hemos reservado dos áreas diferentes de la Página Cero para nuestras direcciones indirectas. Hemos definido el menor de cada par de bytes, $CC y $CE, de modo que la dirección indirecta del lugar del que obtendremos el conjunto de caracteres se almacenará en $CC y $CD, y la dirección indirecta del lugar al que moveremos el conjunto se almacenará en $CE y $CF. Hemos nombrado inteligentemente estas ubicaciones FROM y TO. Para ambos conjuntos de ubicaciones, el byte bajo de la dirección indirecta se almacenará en la ubicación inferior de las dos y el byte alto se almacenará en la superior, utilizando la convención típica del microprocesador 6502.


Ahora que hemos reservado espacio para las direcciones indirectas, la siguiente tarea es llenarlas correctamente con las direcciones que necesitamos. Recuerde que vamos a pasar la dirección TO desde el BASIC, pero el Sistema Operativo fija la dirección FROM en $E000. Veamos cómo se ve esta parte del programa.


0160 ; ***********************************************
           0170 ; Inicializar y configurar direcciones indirectas
           0180 ; ***********************************************
0600 68    0190    PLA        ; Sacar # de parametros de la Pila
0601 68    0200    PLA        ; Obtener byte alto del destino
0602 85CF  0210    STA TO+1   ; Guardarlo en el byte alto de TO
0604 68    0220    PLA        ; Obtener byte bajo del destino
0605 85CE  0230    STA TO     ; Guardarlo en el byte bajo de TO
0607 A900  0240    LDA #0     ; Limite de pagina par LSB=0
0609 85CC  0250    STA FROM   ; Byte bajo de direccion indirecta
060B A9E0  0260    LDA #$E0   ; Pagina del juego de caracteres en ROM
060D 85CD  0270    STA FROM+1 ; Completar direcciones indirectas	

Analicemos por un momento las líneas 190 a 230. La línea 190 es nuestra vieja amiga, utilizada para extraer desde la Pila la cantidad de parámetros pasados ​​por BASIC, y mantenerla en orden. Tenga en cuenta que las líneas 200 y 220 son instrucciones PLA. Este es el método utilizado para pasar parámetros desde el BASIC. BASIC divide el número que se va a pasar en byte alto y byte bajo, y los coloca en la Pila: primero el byte bajo y luego el byte alto. Por lo tanto, el primer número que sacamos de la Pila es el de arriba: el byte alto. Lo almacenamos apropiadamente en TO+1: el byte alto de la dirección indirecta que hemos configurado en la Página Cero. De manera similar, almacenamos el byte bajo pasado desde BASIC en TO y con esto hemos completado la configuración de la primera de las dos direcciones indirectas que necesitaremos.


Ahora sólo nos queda hacer la parte fácil. Sabemos que cualquier límite de página tiene una dirección con el byte bajo igual a 0, por lo que podemos almacenar un cero en FROM sin dificultad. Sabemos que el byte alto es $E0, y no olvide el signo #, para que el Ensamblador sepa que queremos almacenar el número $E0 en FROM+1, y no cualquier número que esté en la ubicación de memoria $E0.


Ahora todo lo que tenemos que hacer es escribir el ciclo que realizará la transferencia por nosotros. Sabemos que necesitamos transferir 1024 bytes, 4 páginas de información, por lo que necesitaremos un contador para realizar un seguimiento de cuánto hemos progresado. Para ello utilizaremos el registro X. También necesitaremos un contador para realizar un seguimiento de qué página que estamos transfiriendo y para ello usaremos el registro Y. Veamos el resto del programa para realizar esta transferencia:


             0280 ; ***********************************
             0290 ; Ahora transferimos todo el conjunto
             0300 ; ***********************************
060F A204    0310       LDX #4       ; 4 paginas en el juego de caracteres
0611 A000    0320       LDY #0       ; Inicializar contador
0613 B1CC    0330 LOOP  LDA (FROM),Y ; Obtener un byte
0615 91CE    0340       STA (TO),Y   ; Y reubicarlo
0617 88      0350       DEY          ; ?Esta terminada la pagina?
0618 D0F9    0360       BNE LOOP     ; No, siga reubicando
061A E6CD    0370       INC FROM+1   ; Si, byte alto
061C E6CF    0380       INC TO+1     ; Byte alto, para la pagina siguiente
061E CA      0390       DEX          ; ?Hemos hecho las 4 paginas?
061F D0F2    0400       BNE LOOP     ; No, continue
0621 60      0410       RTS          ; Si, entonces regrese al BASIC.	

Solo hay dos diferencias entre esta parte del programa y la parte correspondiente del programa que escribimos anteriormente. El primero es el uso del registro X para determinar cuándo terminamos. La línea 310 establece el registro X para el número de páginas que se transferirán. Las líneas 390 y 400 determinan si hemos terminado, disminuyendo el registro X y retrocediendo para continuar la transferencia si el valor del registro X aún no ha llegado a cero.


La segunda diferencia, por supuesto, es que no vamos a almacenar el mismo valor en todas las ubicaciones, por lo que necesitamos cargar el Acumulador usando la misma técnica que usamos para almacenarlo, indexando la ubicación indirecta de la Página Cero con el registro Y. Tenga en cuenta que cuando Y es igual a 1, cargaremos desde la segunda ubicación del conjunto de caracteres de la ROM en la línea 330 y almacenaremos en la segunda ubicación de la RAM establecida en la línea 340, y así sucesivamente. Recuerde que las líneas 370 y 380 incrementan en una página ambas direcciones indirectas, ya que en ese punto del programa habremos terminado una página y estaremos listos para comenzar otra.


Todo lo que queda es convertir este programa en una subrutina en Lenguaje de Máquina para BASIC. Con la misma técnica que usamos para el primer programa discutido, guardamos el código en Lenguaje de Máquina, colocamos el cartucho BASIC, cargamos nuestro código nuevamente y producimos líneas DATA examinando los valores almacenados en las direcciones de la 1536 a la 1569. Estas líneas DATA luego se pueden usar en un programa como el que se muestra a continuación:


10 GOSUB 20000:REM Configurar rutina de lenguaje de Maquina
20 ORIG=PEEK(106):REM Parte superior de la RAM
30 CHSET=(ORIG-4)*256:REM Lugar para el juego de caracteres reubicado
40 POKE 106,ORIG-8:REM Hacer espacio
50 GRAPHICS 0:REM Configurar nueva Display List
60 POKE 20,0:REM Configurar temporizador
70 X=USR(ADR(RELOCATE$),CHSET):REM Reubicar todo el conjunto
80 ? PEEK(20)/60:REM ?Cuanto tiempo tomo?
90 END :REM Tomo 0.03 segundos
20000 DIM RELOCATE$(34):REM Configurelo como una cadena
20010 FOR I=1 TO 34:REM Configure la cadena
20020 READ A:REM Obtenga un byte
20030 RELOCATE$(I, I)=CHR$(A):REM Metalo en la cadena
20040 NEXT I:REM Repita hasta que la cadena este lista
20050 RETURN :REM Todo listo, regrese
20060 DATA 104,104,133,207,104,133,206,169,0,133
20070 DATA 204,169,224,133,205,162,4,160,0,177
20080 DATA 204,145,206,136,208,249,230,205,230,207
20090 DATA 202,208,242,96	

La subrutina de la línea 20000 a la línea 20070 coloca cada byte de la rutina en Lenguaje de Máquina en su lugar apropiado en una cadena que hemos llamado RELOCATE$. Para acceder a esta rutina, utilizamos la línea 70, que pasa el parámetro CHSET a nuestra rutina de Lenguaje de Máquina. Recordemos que CHSET, definido en la línea 30, es la dirección en la que nos gustaría localizar el juego de caracteres en la RAM. Este programa se ejecuta casi 500 veces más rápido que el programa BASIC descrito anteriormente, lo que demuestra nuevamente la velocidad de las rutinas en Lenguaje de Máquina.


SUBRUTINA PARA TRANSFERIR CUALQUIER ÁREA DE MEMORIA


Con algunas modificaciones menores al programa que acabamos de escribir, podemos hacerlo mucho más versátil. Escribámoslo de tal manera que permita la transferencia de cualquier área de la memoria a otra. Al observar el programa anterior, vemos que solo es necesario cambiar dos partes del código. La primera es la dirección absoluta FROM, que está establecida en 57344, y la segunda es el número de páginas almacenadas en el registro X, que está establecida en 4. Si pudiéramos usar aquí variables en lugar de constantes, nuestra rutina sería mucho más versátil. Es fácil convertir la rutina de esta manera: simplemente pasemos la dirección FROM y el número de páginas a transferir como parámetros desde el BASIC. Aquí está el programa completo en lenguaje Ensamblador para esta subrutina:


Listado 7.3


          0100 ; ********************************
          0110 ; Configurar condiciones iniciales
          0120 ; ********************************
0000      0130       *=   $600
00CC      0140 FROM  =    $CC
00CE      0150 TO    =    $CE
          0160 ;************************************************
          0170 ; Inicializar y configurar direcciones indirectas
          0180 ; ***********************************************
0600 68   0190       PLA          ; Sacar # de parametros de la Pila
0601 68   0200       PLA          ; Obtener byte alto del origen
0602 85CD 0210       STA FROM+1   ; Guardarlo en el byte alto de FROM
0604 68   0220       PLA          ; Obtener byte bajo del origen
0605 85CC 0230       STA FROM     ; Guardarlo en el byte bajo de FROM
0607 68   0240       PLA          ; Obtener byte alto del destino
0608 85CF 0250       STA TO+1     ; Guardarlo en el byte alto de TO
060A 68   0260       PLA          ; Obtener byte bajo del destino
060B 85CE 0270       STA TO       ; Guardarlo en el byte bajo de TO
060D 68   0280       PLA          ; No existe byte alto (= 0)
060E 68   0290       PLA          ; Obtener byte bajo - numero de paginas
060F AA   0300       TAX          ; Poner # de paginas en el registro X
          0310 ; ***********************************
          0320 ; Ahora transfiramos todo el conjunto
          0330 ; ***********************************
0610 A000 0340       LDY #0       ; Inicializar contador
0612 BlCC 0350 LOOP  LDA (FROM),Y ; Obtener un byte
0614 91CE 0360       STA (TO),Y   ; Y reubicarlo
0616 88   0370       DEY          ; ?Esta terminada la pagina?
0617 D0F9 0380       BNE LOOP     ; No, siga reubicando
0619 E6CD 0390       INC FROM+1   ; Si, byte alto
061B E6CF 0400       INC TO+1     ; Byte alto, para la pagina siguiente
061D CA   0410       DEX          ; ?Hemos hecho las 4 paginas?
061E D0F2 0420       BNE LOOP     ; No, continue
0620 60   0430       RTS          ; Si, entonces regrese al BASIC.	

Ahora hemos configurado la rutina para obtener primero la dirección FROM en dos bytes desde la Pila, y luego la dirección TO de la misma manera. Finalmente, sacamos de la Pila el número de páginas a transferir. Tenga en cuenta que sólo hay 256 páginas de memoria en un ATARI, por lo que nunca puede haber un byte alto en el parámetro de número de páginas. El byte bajo se extrae de la Pila y se transfiere al registro X para configurar el contador del número de páginas que se transferirán.


Con la excepción de estos pocos cambios, el programa es idéntico a nuestro programa para transferir el juego de caracteres de ROM a RAM. De hecho, esta nueva rutina logrará el mismo objetivo si así lo deseamos. A continuación, se proporciona un programa BASIC que utiliza esta nueva rutina para transferir el juego de caracteres:


10 GOSUB 20000:REM Configurar rutina de lenguaje de maquina
20 ORIG=PEEK(106):REM Parte superior de la RAM
30 CHSET=(ORIG-4)*256:REM Lugar para el juego de caracteres reubicados
40 POKE 106,ORIG-8:REM Hacer espacio para ello
50 GRAPHICS 0:REM Configurar nueva Lista de Despliegue
60 X=USR(ADR(TRANSFER$),57344,CHSET,4):REM Transferir todo el conjunto
70 END
20000 DIM TRANSFER$(33):REM Configurarlo como una cadena
20010 FOR I=1 TO 33:REM Configurar la cadena
20020 READ A:REM Obtener un byte
20030 TRANSFER$(I,I)=CHR$(A):REM Introducirlo en la cadena
20040 NEXT I:REM Repetir hasta que la cadena este lista
20050 RETURN :REM Todo listo, regresar
20060 DATA 104,104,133,205,104,133,204,104,133,207
20070 DATA 104,133,206,104,104,170,160,0,177,204
20080 DATA 145,206,136,208,249,230,205,230,207,202
20090 DATA 208,242,96	

UN EJERCICIO PARA EL LECTOR


Utilizando estas técnicas, debería ser bastante sencillo escribir su propia rutina para llenar un número determinado de páginas de memoria con un carácter distinto de cero. Para obtener el máximo beneficio de este ejercicio, no mire atrás los ejemplos de este Capítulo, sino que comience desde cero y vea cómo le va.


LECTURA DEL JOYSTICK


Todos estamos familiarizados con el complejo código necesario en BASIC para leer los bastones o joysticks. Aunque existen algunas formas sofisticadas de acelerar este proceso en BASIC, el enfoque más común utilizado para determinar la posición del joystick y por ejemplo cambiar las coordenadas X e Y de un jugador es el siguiente:


10000 IF STICK(0)=15 THEN 10050:REM hacia arriba
10010 IF STICK(0)=10 OR STICK(0)=14 OR STICK(0)=6 THEN
Y=Y-1:REM Posicion 11,12 o 1 en punto: mover el jugador hacia arriba
10020 IF STICK(0)=9 OR STICK(0)=13 O STICK(0)=5 THEN Y=Y+1:REM
Posicion 7,6 o 5 en punto: mover el jugador hacia abajo
10030 IF STICK(0)=10 OR STICK(0)=11 OR STICK(0)=9 THEN
X=X-1:REM Posicion 10,9 u 8 en punto: mover el jugador a la izquierda
10040 IF STICK(0)= 6 O STICK(0)=7 O STICK(0)=5 THEN X=X+1:REM
Posicion 2,3 o 4 en punto: mover el jugador a la derecha
10050 RETURN:REM no hay otras posibilidades	

Como ya sabe, hay varias maneras de mejorar la velocidad de dicha rutina mediante una programación mejorada. Esta rutina se incluye aquí por simplicidad; es fácil seguir su lógica. En cualquier caso, ni siquiera una programación excelente hará que este tipo de rutina sea la ganadora en un concurso de velocidad. Veamos si podemos acelerarlo significativamente usando lenguaje Ensamblador.


A efectos de este ejemplo, asumiremos que la rutina del joystick que escribiremos será la única forma en que el jugador podrá moverse y, además, que moveremos solo a un jugador. Dado que el jugador sólo puede moverse en dos dimensiones, sólo necesitamos recordar dos coordenadas: las posiciones X e Y. Debido a la forma en que moveremos al jugador, necesitaremos solo 1 byte de almacenamiento para la posición X, pero necesitaremos 2 bytes para la ubicación Y, usando una dirección indirecta. La rutina necesaria para leer el joystick es muy sencilla y se detalla a continuación:


0100 ; ***********************
0110 ; Inicializar ubicaciones
0120 ; ***********************
0130  *= $600 ; Lugar seguro para la rutina
0140 YLOC = $CC ; Direccion indirecta para Y
0150 XLOC = $CE ; Para recordar la posicion X
0160 STICK = $D300 ; Ubicacion hardware STICK(0)
0180 ; *******************************
0190 ; Ahora lea el joystick #1
0200 ; *******************************
0210  PLA ; Mantenga la Pila ordenada
0220  LDA STICK ; Obtener el valor del joystick
0230  AND #1 ; ?Es bit 0 = 1?
0240  BEQ UP ; No - 11, 12 o 1 en punto
0250  LDA STICK ; Obtenerlo otra vez
0260  AND #2 ; ?Es bit 1 = 1?
0270  BEQ DOWN ; No - 5, 6 o 7 en punto
0280 SIDE LDA STICK ; Obtenerlo otra vez
0290  AND #4 ; ?Es bit 3 = 1?
0300  BEQ LEFT ; No - 8, 9 o 10 en punto
0310  LDA STICK ; Obtenerlo otra vez
0320  AND #8 ; ?Es bit 4 = 1?
0330  BEQ RIGHT ; No - 2, 3 o 4 en punto
0340  RTS ; Joystick hacia arriba	

Como puede ver, después del PLA obligatorio para mantener contento al BASIC, leer el joystick es solo una cuestión de cargar el Acumulador a partir de la ubicación del hardware STICK ($D000) y luego hacerle AND con 1, 2, 4 u 8. Los joysticks ATARI restablecen en cero uno o más de los cuatro bits inferiores en la ubicación $D000 si se presiona la palanca en esa dirección: bit cero para arriba, bit 1 para abajo, bit 2 para izquierda y bit 3 para derecha. Si ninguno de los 4 bits se establece en cero, el joystick está en posición vertical. Tenga en cuenta que el joystick no se puede presionar simultáneamente hacia la derecha y hacia la izquierda o hacia arriba y hacia abajo, pero puede estar hacia la derecha y hacia abajo simultáneamente, o hacia la izquierda y hacia arriba.


Este programa no funcionará, como probablemente ya habrá notado, ya que hay 4 etiquetas no definidas: UP, DOWN, LEFT y RIGHT. Agregaremos estas rutinas en breve para producir una subrutina en Lenguaje de Máquina que no solo leerá el joystick, sino que también mueve a un jugador por la pantalla en respuesta a la dirección del joystick.


Primero, tenga en cuenta que cada una de las referencias a una etiqueta para la dirección del joystick utiliza la instrucción BEQ. Esto dice, en efecto que, si el resultado de hacer AND con un bit forzado a 1 con el valor encontrado en STICK es cero, el joystick se presiona en esa dirección. Piénselo. Sabemos que para que el resultado sea cero, en uno o ambos números cada bit debe ser igual a cero. En los números 1, 2, 4 y 8, cada bit menos uno es igual a cero; entonces, en el número almacenado en STICK, ese bit en particular debe ser igual a cero si el resultado de la operación AND es igual a cero. Para un ejemplo gráfico, veamos la operación AND con 4, con el joystick presionado en diferentes direcciones:


JoystickSTICKAND con76543210
derecha
248
-
11110111
-
4
00000100
Resultado =00000100

que no es igual a cero. Dado que la palanca se presionó hacia la derecha y se hizo AND con 4 para comprobar si el joystick se movió hacia la izquierda, este es un resultado correcto. Ahora, otro ejemplo:


JoystickSTICKAND con76543210
izquierda
244
-
11111011
-
4
00000100
Resultado =00000000

el cual es igual a cero, demostrando que la prueba funciona correctamente. Se debe enfatizar que cualquiera de las tres posiciones del joystick a la izquierda habría funcionado, porque todas tienen un cero como bit 2, por lo que todas harán AND con 4 para producir un resultado de cero. De hecho, las tres posiciones del joystick a la izquierda tienen los siguientes patrones de bits:


8 en punto 11111001
9 en punto 11111011
10 en punto 11111010

Vale la pena mencionar aquí que los 4 bits superiores de esta ubicación reflejan la posición de un joystick conectado al segundo puerto de su ATARI, exactamente de la misma manera que los 4 bits inferiores reflejan la posición del joystick 0.


Por supuesto, hay varias formas de escribir el código anterior. Quizás se le ha ocurrido usar una subrutina para cada dirección, como en este extracto:


0210  PLA
0220  LDA STICK
0230  AND #1
0240  BNE D1
0250  JSR UP
0260 D1 LDA STICK
0270  AND #2
0280  BNE D2
0290  JSR DOWN	

Este tipo de construcción funciona bien excepto por un problema: el código es fijo. Las ubicaciones UP, DOWN, LEFT y RIGHT deben estar dentro del programa, y si usamos JSR para acceder a estas rutinas, terminaremos con un código no reubicable. Si eso no le crea ningún problema, escriba la rutina utilizando JSR. Sin embargo, dado que uno de nuestros objetivos en este libro es hacer que tantas rutinas como podamos sean reubicables, usaremos la construcción demostrada.


¿Cómo movemos a los jugadores por la pantalla usando gráficos Player/Missile? El movimiento horizontal es fácil. Todo lo que tenemos que hacer es POKE a la posición horizontal deseada en el registro de posición horizontal para ese Player en particular, que aparecerá allí al instante. En el caso del primer jugador, el Player Cero, el registro de posición horizontal se sitúa en $D000. Lo llamaremos HPOSP0 y necesitaremos agregar la línea 170 al código anterior:


0170 HPOSP0 = $D000	

lo que nos permitirá referirnos a esta ubicación utilizando el nombre de la etiqueta.


¿Qué pasa con el movimiento vertical? Para mover un Player verticalmente, en realidad tenemos que mover cada byte del Player a una nueva ubicación, por lo que necesitamos configurar la dirección indirecta para la posición Y. Usaremos la técnica que ya usamos para mover el conjunto de caracteres. Pero en este caso podemos arreglárnoslas con solo una dirección indirecta, ya que estamos moviendo el Player a solo 1 byte de su dirección actual. Supondremos que el Player tiene 8 bytes de alto y aparece como un cuadrado hueco. Si queremos mover el Player solo 1 byte hacia arriba en la pantalla, tendremos que comenzar moviendo primero el byte superior y así sucesivamente hacia abajo en todo el Player. Si intentamos mover el byte inferior primero, se sobrescribirá el siguiente byte superior y lo perderemos. Visualmente, se verá así:


antes de mover     después de mover
...........        ...........
...........        .xxxxxxxx..
xxxxxxxx...        .x......X..
x......x...        .x......X..
x......x...        .x......X..
x......x...        .x......X..
x......x...        .x......X..
x......x...        .x......X..
x......x...        .xxxxxxxx..
xxxxxxxx...        ...........
...........        ...........	

Por el contrario, para mover el Player solo un byte hacia abajo en la pantalla, tendremos que comenzar moviendo primero el byte inferior y avanzaremos hacia arriba por todo el Player.


Hay un último problema. En la imagen superior, la parte inferior del Player realmente tiene 2 bytes de altura después de ser movido ya que, aunque hemos colocado una copia del byte inferior en la posición correcta 1 byte más arriba, no hemos movido nada al espacio que originalmente ocupaba este byte inferior. Si no corregimos este problema, la nueva figura se verá así:


........
........
XXXXXXXX
X......X
X......X
X......X
X......X
X......X
X......X
XXXXXXXX
XXXXXXXX
........	

De hecho, si no corregimos esto, a medida que movemos la figura hacia arriba en la pantalla, dejaremos una cola colgando detrás de la figura, un efecto inteligente, ¡pero no es lo que pretendemos en absoluto!


Afortunadamente, existe una manera sencilla de solucionar este problema; simplemente mueva 1 byte más de lo que está en el Player. Tenga en cuenta que, dado que el Player tiene 8 bytes de altura, si movemos 9 bytes, moveremos un byte cero al espacio que antes ocupaba la parte inferior del Player; por lo que el nuevo Player seguirá teniendo una sola línea en la parte inferior, en lugar de la línea doble que se muestra arriba. Obviamente, cuando estamos moviendo el Player hacia abajo en la pantalla, también podemos mover 9 bytes en lugar de 8, solucionando el problema allí también. Ahora que sabemos cómo mover a los jugadores tanto horizontal como verticalmente, veamos la rutina completa y luego la describiremos en detalle.


Listado 7.4


            0100 ; *******************************
            0110 ; Inicializar ubicaciones
            0120 ; *******************************
0000        0130        *= $600      ; Lugar seguro para la rutina
00CC        0140 YLOC   =  $CC       ; Direccion indirecta para Y
00CE        0150 XLOC   =  $CE       ; Para recordar la posicion X
D300        0160 STICK  =  $D300     ; Ubicación del hardware STICK(0)
D000        0170 HPOSP0 =  $D000     ; Posición horizontal del Player 0
            0180 ; *******************************
            0190 ; Ahora lea el joystick #1
            0200 ; *******************************
0600 68     0210       PLA           ; Mantenga la Pila ordenada
0601 AD00D3 0220       LDA STICK     ; Obtener el valor del joystick
0604 2901   0230       AND #1        ; ?Es bit 0 = 1?
0606 F016   0240       BEQ UP        ; No - 11, 12 o 1 en punto
0608 AD00D3 0250       LDA STICK     ; Obtenerlo de nuevo
060B 2902   0260       AND #2        ; ?Es bit 1 = 1?
060D F020   0270       BEQ DOWN      ; No - 5, 6 o 7 en punto
060F AD00D3 0280 SIDE  LDA STICK     ; Obtenerlo de nuevo
0612 2904   0290       AND #4        ; ?Es bit 3 = 1?
0614 F02E   0300       BEQ LEFT      ; No - 8, 9 o 10 en punto
0616 AD00D3 0310       LDA STICK     ; Obtenerlo de nuevo
0619 2908   0320       AND #8        ; ?Es bit 4 = 1?
061B F02F   0330       BEQ RIGHT     ; No - 2, 3 o 4 en punto
061D 60     0340       RTS           ; Joystick recto hacia arriba - salir al BASIC
            0350 ; *******************************
            0360 ; Ahora mueve al Player apropiadamente
            0370 ; Comenzando con movimiento ascendente
            0380 ; *******************************
061E A001   0390 UP    LDY #1        ; Configuracion para mover el byte 1
0620 C6CC   0400       DEC YLOC      ; Ahora 1 menos que YLOC
0622 BlCC   0410 UP1   LDA (YLOC),Y  ; Obtener el primer byte
0624 88     0420       DEY           ; Para subirlo una posición
0625 91CC   0430       STA (YLOC),Y  ; Muevalo
0627 C8     0440       INY           ; Ahora el valor original
0628 C8     0450       INY           ; Ahora configurado para el siguiente byte
0629 C00A   0460       CPY #10       ; ?Terminamos?
062B 90F5   0470       BCC UP1       ; No
062D B0E0   0480       BCS SIDE      ; !!!Bifurcacion forzada!!!
            0490 ; *******************************
            0500 ; Ahora mueve al jugador hacia abajo
            0510 ; *******************************
062F A007   0520 DOWN  LDY #7        ; Mueva el byte superior primero
0631 B1CC   0530 DOWN1 LDA (YLOC),Y  ; Obtener el byte superior
0633 C8     0540       INY           ; Para moverlo hacia abajo en la pantalla
0634 91CC   0550       STA (YLOC),Y  ; Muevalo
0636 88     0560       DEY           ; Ahora volvamos al valor inicial
0637 88     0570       DEY           ; Establecido para el siguiente byte inferior
0638 10F7   0580       BPL DOWN1     ; Si Y >= 0 continue
063A C8     0590       INY           ; Establecer en cero
063B A900   0600       LDA #0        ; Para borrar el byte superior
063D 91CC   0610       STA (YLOC),Y  ; Borrelo
063F E6CC   0620       INC YLOC      ; Ahora es 1 mayor
0641 18     0630       CLC           ; Configuracion para bifurcación forzada
0642 90CB   0640       BCC SIDE      ; Bifurcacion forzada nuevamente
            0650 ; *******************************
            0660 ; Ahora de lado a lado: primero a la izquierda
            0670 ; *******************************
0644 C6CE   0680 LEFT  DEC XLOC      ; Para moverlo a la izquierda
0646 A5CE   0690       LDA XLOC      ; Obtenerlo
0648 8D00D0 0700       STA HPOSP0    ; Moverlo
064B 60     0710       RTS           ; De vuelta al BASIC: hemos terminado
            0720 ; *******************************
            0730 ; Ahora movimiento a la derecha
            0740 ; *******************************
064C E6CE   0750 RIGHT INC XLOC      ; Para moverlo a la derecha
064E A5CE   0760       LDA XLOC      ; Obtenerlo
0650 8D00D0 0770       STA HPOSP0    ; Moverlo
0653 60     0780       RTS           ; De vuelta al BASIC: hemos terminado	

Veamos la construcción del programa en su conjunto: el flujo del programa. Primero comprobamos si el joystick está siendo presionado hacia arriba. Si está hacia arriba, bifurcamos a UP. Si no, comprobamos hacia abajo, y si está siendo presionado hacia abajo, bifurcamos a DOWN. En cualquiera de estos casos, después de mover el Player, debemos volver a comprobar el movimiento de lado a lado, ya que es posible moverlo simultáneamente tanto de manera horizontal como vertical. Esta bifurcación de regreso para comprobar el movimiento horizontal se logra mediante bifurcaciones forzadas en las líneas 480 y 640. En la línea 480, se debe configurar el bit de Acarreo ya que, si no se hace, la línea 470 se habría bifurcado desde la línea 480. En la línea 640, la bifurcación forzada es aún más obvia, ya que en la línea 470, limpiamos el bit de Acarreo y luego realizamos la bifurcación si el bit de Acarreo está limpio, ¡Como sabemos que debe estarlo! ¿Por qué no simplemente volver a SIDE? Nuevamente, porque queremos que la rutina sea reubicable y si usamos algún comando JMP, no lo será. Esta técnica de bifurcación forzada es común en código reubicable y es bastante fácil de realizar, ahora que sabe cómo hacerlo.


Una vez que hayamos comprobado el movimiento horizontal y vertical, terminaremos y podremos regresar al BASIC. Tenga en cuenta que esta rutina contiene tres instrucciones RTS. No existe ninguna regla que diga que una rutina debe tener solo un RTS; ¡Lo que sea que funcione, hágalo! En este caso podemos volver si el joystick está vertical (línea 340) o si hemos movido al Player hacia la izquierda (línea 710) o hacia la derecha (línea 780), ya que en cualquiera de estos tres casos hemos agotado todas las posibilidades, comprobando cada combinación de movimientos.


El código específico para moverse hacia la derecha o hacia la izquierda lee la coordenada X actual desde su ubicación de almacenamiento, incrementándola o disminuyéndola según corresponda, y la almacena nuevamente tanto en su ubicación de almacenamiento como en el registro de posición horizontal para el Player Cero, HPOSP0.


Ahora discutiremos el movimiento vertical. Dado que mover el Player hacia arriba en la pantalla da como resultado una posición Y una unidad menor que su valor inicial (cuanto menor sea la coordenada Y, más alto aparecerá el Player en la pantalla), eventualmente necesitaremos disminuir el valor YLOC. Podemos aprovechar este decremento si lo hacemos cerca del inicio de la rutina. Cuando se decrementa YLOC, este apunta al destino del byte superior del Player. Establecer Y en 1 permite que el comando denominado UP1 apunte inicialmente al byte superior del Player, en su ubicación original. Luego disminuimos Y, y la siguiente instrucción STA coloca ese byte en su nueva ubicación más alta en la pantalla. Luego debemos incrementar Y dos veces más, una para la disminución que realizamos y otra para obtener el siguiente byte. Vamos a mover 9 bytes y comenzamos con Y = 1, así que cuando Y = 10, terminamos. Si no hemos terminado, volveremos a subir para obtener el siguiente byte, y si hemos terminado, volveremos a tomar la bifurcación forzada para comprobar el movimiento horizontal. La técnica aquí es utilizar direccionamiento indirecto tanto para LDA como para STA, pero cambiando el desplazamiento (registro Y) en 1 entre el LDA y el STA. Eso nos permite cargar desde un lugar y almacenar en otro, sin muchos problemas.


Usaremos un algoritmo ligeramente diferente para mover un Player hacia abajo en la pantalla. Como se mencionó anteriormente, comenzamos con el byte inferior, por lo que establecemos Y en 7 (los bytes son del 0 al 7 en este caso). Usamos LDA indirecto, luego incrementamos el registro Y y luego STA indirecto, tal como lo hicimos anteriormente; pero en este caso, almacenamos en una ubicación superior a la que cargamos. Luego disminuimos dos veces, una para el incremento y otra para obtener el siguiente byte, y si Y aún es mayor o igual a cero, continuamos. De lo contrario, almacenaremos un cero en el byte más bajo original, incrementando Y para restablecerlo a cero (Anteriormente había llegado a -1, o $FF en hexadecimal) y almacenando un cero en YLOC, indirecto. Luego incrementamos YLOC, ya que hemos movido al Player una posición hacia abajo en la pantalla, y forzamos una bifurcación hacia atrás para verificar el movimiento horizontal. Tenga en cuenta que cuando subimos en la pantalla, en realidad movemos 9 bytes, pero cuando bajamos en la pantalla, movemos 8 bytes y luego rellenamos con un cero para eliminar la cola del jugador. Utilizamos dos métodos para demostrar que cualquiera de ellos funciona.


Por cierto, una preocupación que puede tener acerca de esta rutina es que lee el joystick cuatro veces por separado. "¿Qué sucede", se preguntará, "si la posición del joystick cambia entre lecturas?" Si calculamos el tiempo durante el cual se producen las cuatro lecturas del joystick, podemos ver que todas las lecturas se realizan en menos de 25 microsegundos. ¡Pocas posibilidades de un cambio en ese lapso de tiempo!


Ahora que tenemos nuestra rutina en Lenguaje de Máquina, todo lo que necesitamos hacer es incorporarla a un programa BASIC para que pueda usarla apropiadamente. A continuación, se detalla un programa de este tipo:


10 TOP=PEEK(106)-8:REM Guardar 8 paginas
20 POKE 106,TOP:REM Hacer espacio para PMG
30 GRAPHICS 0:REM Restablecer Display List
40 PMBASE=TOP*256:REM Configurar area PM
50 POKE 54279,TOP :REM Digale al ATARI donde esta PMBASE
60 INITX=120:REM Posicion X inicial
70 INITY=50:REM Posicion Y inicial
80 POKE 559,46:REM Resolucion de línea doble
90 POKE 53277,3:REM Habilitar PM
100 GOSUB 20000:REM Configurar nuestra rutina
110 FOR I=PMBASE+512 TO PMBASE+640:REM Memoria PM
120 POKE I,0:REM Borrar
130 NEXT I:REM ¡Podria usar ERASE$ aqui!
140 RESTORE 25000:REM Los datos del Player se almacenan aqui
150 Q=PMBASE+512+INITY:REM Donde estara el Player en la memoria
160 FOR I=Q TO Q+7:REM El Player tiene 8 bytes de alto
170 READ A:REM Obtener datos del Player
180 POKE I,A:REM Ponerlo en el lugar adecuado
190 NEXT I:REM Y así sucesivamente
200 POKE 53248,INITX:REM Configuracion de la posicion X
210 YHI=INT(Q/256):REM Byte alto de la posicion Y inicial
220 YLO= (PMBASE+512+INITY)-YHI*256:REM Byte bajo
230 POKE 204,YLO:REM Indica a la rutina en LM donde esta Y
240 POKE 205,YHI:REM Indica a la rutina en LM donde esta Y
250 POKE 206,INITX:REM Indica a la rutina ML donde esta X
260 POKE 704,68:REM Haz que el Player sea rojo
270 Q=USR(ADR(JOYSTICK$)):REM ¡Vamos a intentarlo!
280 GOTO 270:REM Simplemente repetir
20000 DIM JOYSTICK$(87):REM Donde poner la rutina
20010 FOR I=1 TO 87:REM Longitud de la rutina
20020 READ A:REM Obtener un byte
20030 JOYSTICK$(I,I)=CHR $(A):REM Ponlo en cadena
20040 NEXT I:RETURN :REM Todo hecho
20050 DATA 104,173,0,211,41,1,240,22,173,0
20060 DATA 211,41,2,240,32,173,0,211,41,4
20070 DATA 240 ,46,173,0,211,41,8,240,47,96
20080 DATA 160,1,198,204,177,204,136,145,204,200
20090 DATA 200,192,10,144,245,176,224,160,7,177
20100 DATA 204,200,145,204,136,136,16,247,200,169
20110 DATA 0,145,204,230,204,24,144,203,198,206
20120 DATA 165,206,141,0,208,96,230,206,165,206
20130 DATA 141,0,208,96,0,208 ,96
25000 DATA 255,129,129,129,129,129,129,255	

La línea 100, que configura la subrutina que acabamos de escribir, nos prepara para llamar a la subrutina en la línea 270. Tenga en cuenta que la línea 280 simplemente regresa a esta llamada de subrutina, por lo que todo lo que este programa hará será mover por la pantalla el Player con forma de cuadrado hueco rojo. El programa podría ampliarse considerablemente añadiendo código desde la línea 280 en adelante, siempre y cuando la línea 270 permanezca en el ciclo principal del juego. Cada vez que se accede a la línea 270, se lee el joystick y el Player se mueve apropiadamente. ¡Inténtelo! Observe cuán suave y uniforme es el movimiento del Player. Luego pruebe un programa similar, todo en BASIC, y observe cómo el movimiento vertical convierte al Player en un gusano que se arrastra lentamente hacia arriba o hacia abajo por la pantalla.


La mayor parte de este programa configura gráficos de Player/Missile en BASIC. Dado que prácticamente todos los parámetros, desde el color del Player hasta su forma y tamaño, se controlan desde este programa BASIC y no desde la subrutina del Lenguaje de Máquina, esta rutina debería combinarse muy bien con casi cualquier programa que requiera el movimiento del joystick del Player Cero. Con modificaciones simples que ahora puede probar, podrá manejar otros Players y otros joysticks. ¡Incluso puede intentar agregar Missiles, tal vez cuando se presiona el botón del joystick (Monitoreado por la ubicación $D010)! La única forma de aprender realmente a programar en lenguaje Ensamblador es a través de la programación, y ¿qué mejor momento para empezar que ahora?



Índice de Contenido | Capítulo anterior | Siguiente capítulo