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:
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:
Joystick | STICK | AND con | 76543210 |
---|---|---|---|
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:
Joystick | STICK | AND con | 76543210 |
---|---|---|---|
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:
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