6502 fácil

Índice

1 Prefacio

Lo que sigue es una traducción del ebook Easy 6502 de Nick Morgan. No he preservado el formato original ni su aspecto, pero la traducción (salvo errores) debe ser relativamente fiel.

Los enlaces a wikipedia han sido cambiados para enlazar con su equivalente en es.wikipedia.org. Desafortunadamente no he podido encontrar una referencia de opcodes decente en castellano, con lo que se está enlazando con las versiones en inglés. Si alguien sabe de alguna agradecería que me lo dijera.

La mejor referencia en castellano es posiblemente el manual de referencia del programador de Commodore 64, pero puede ser bastante difícil de encontrar. Si te haces con una copia guárdala como oro en paño, algún día la podrás subastar a buen precio. Dicen las malas lenguas que se puede descargar de sitios de dudosa reputación.

2 Introducción

En este pequeño ebook voy a mostrarte cómo empezar a escribir ensamblador de 6502. El procesador 6502 fue muy popular en los años setenta y ochenta, estando presente en ordenadores muy conocidos como el BBC Micro, Atari 2600, Commodore 64, Apple II, y la Nintendo Entertainment System. Bender de Futurama tiene un procesador 6502 processor como cerebro. Incluso el Terminator fue programado en 6502.

¿Por qué podrías querer aprender 6502? Es un lenguaje muerto, ¿verdad? Bien, también lo es el Latín. Y todavía lo enseñan. QED.

De hecho, he sido informado de buena tinta de que los procesadores 6502 todavía son fabricados por Western Design Center, así que ¡claramente el 6502 no es un lenguaje muerto! ¿Quien iba a decirlo?

Hablando en serio, creo que es valioso tener nociones de lenguaje ensamblador. El lenguaje ensamblador es el nivel más bajo de abstracción en los ordenadores - el punto en el cual el código es todavía legible. El lenguaje ensamblador se traduce directamente a los bytes que se ejecutan por el procesador de tu ordenador. Si entiendes cómo funciona, basicamente te conviertes en un mago de los ordenadores.

Entonces, ¿por qué 6502? ¿Por qué no un lenguaje útil, como el x86? Bien, no creo que aprender x86 sea útil. No creo que jamás tengas que escribir lenguaje ensamblador en tu trabajo diario - esto es puramente un ejercicio académico, algo para expandir tu mente y tu pensamiento. El 6502 se escribía originariamente en una epoca distinta, un tiempo en el que la mayoría de desarrolladores escribían en ensamblador directamente, en lugar de en estos novedosos lenguajes de programación de alto nivel. Por lo tanto, fue diseñado para ser escrito por humanos. Los lenguajes ensambladores más modernos han sido orientados a ser escritos por compiladores, así que dejémosles la tarea. Además, el 6502 es divertido. Nadie ha llamado al x86 divertido.

3 Nuestro primer programa

¡Empecemos! Eso que ves abajo es un pequeño ensamblador y simulador de 6502 en Javascript que he adaptado para este libro. Pulsa Assemble y luego Run para ensamblar y ejecutar el fragmento de lenguaje ensamblador.

Es de esperar que el área de la derecha tenga ahora tres "pixels" de colores arriba a la izquierda. Si no funciona, probablemente necesites actualizar tu navegador a algo más moderno, como Chrome o Firefox.

Bien, ¿qué está haciendo realmente este programa? Vayamos paso a paso a través del mismo con el depurador. Pulsa Reset y luego activa la casilla Debugger para iniciar el depurador. Pulsa Step una vez. Si te fijas bien, notarás que A= ha cambiado de $00 a $01, y PC= ha cambiado de $0600 a $0602.

Cualquier número con el prefijo $ en el lenguaje ensamblador de 6502 (y por extensión, en este libro) está en formato hexadecimal. Si no estás familiarizado con los números hexadecimales, te recomiendo que leas el artículo de Wikipedia. Un número con el prefijo # es un valor numérico literal. Cualquier otro número se refiere a una posición de memoria.

Sabiendo eso, debes ser capaz de ver que la instrucción LDA #$01 carga el valor hexadecimal $01 en el registro A. Entraré en más detalle en el tema de los registros en la siguiente sección.

Pulsa Step de nuevo para ejecutar la segunda instrucción. El pixel de arriba a la izquierda de la pantalla del simulador debe ser ahora blanco. Este simulador usa las posiciones de memoria $0200 a $05ff para dibujar pixels en su pantalla. Los valores $00 a $0f representan 16 colores diferentes ($00 es negro y $01 es blanco), de modo que almacenar el valor $01 en la posición de memoria $0200 dibuja un pixel blanco en la esquina superior izquierda. Esto es una simplificación de cómo un ordenador real mostraría video, pero por ahora es suficiente.

La instrucción STA $0200 almacena el valor del registro A en la posición de memoria $0200. Pulsa Step cuatro veces más para ejecutar el resto de las instrucciones, mirando como cambia el registro A.

3.1 Ejercicios

  1. Intenta cambiar el color de los tres pixels.
  2. Cambia uno de los pixels de modo que se dibuje en la esquina inferior izquierda (posición de memoria $05ff).
  3. Añade más instrucciones para dibujar más pixels.

4 Registros e indicadores (flags)

Ya le hemos echado un pequeño vistazo a la sección de estado del procesador (la que muestra A, PC etc.), pero ¿qué significa?

La primera linea muestra los registros A, X e Y (a menudo se llama al registro A el "acumulador"). Cada registro alberga un único byte. La mayoría de las operaciones trabajan en el contenido de estos registros.

SP es el puntero de pila. No voy a meterme en la pila todavía, pero básicamente este registro se decrementa cada vez que se apila un byte y se incrementa cada vez que se desapila un byte.

PC es el contador de programa. Es la manera que tiene le procesador de saber en qué punto del programa se encuentra. Es como el número de linea actual de un script en ejecución. En el simulador en Javascript, el código se ensambla comenzando en la posición de memoria $0600, de manera que PC siempre comienza ahí.

La última sección muestra los flags del procesador. Cada flag es un bit, de manera que los siete flags residen en un único byte. Los flags son fijados por el procesador para dar información sobre la instrucción previa. Entraremos en detalle más tarde. Puedes leer más sobre registros y flags aquí.

5 Instrucciones

Las instrucciones en lenguaje ensamblador son como un pequeño conjunto de funciones predefinidas. Todas las instrucciones de 6502 toman un argumento o ninguno. Aquí hay algo de código fuente anotado para presentar unas cuantas instrucciones:

Ensambla el código, luego activa el depurador y ejecuta paso a paso el código mirando los registros A y X. Algo un poco raro sucede en la linea ADC #$c4. Puede que esperes que sumar $c4 a $c0 dé el resultado $184, pero este procesador devuelve el resultado como $84. ¿Por qué sucede eso?

El problema es que $184 es demasiado grande para caber en un único byte (el máximo es $FF), y los registros sólo pueden albergar un byte. No pasa nada, el procesador no es tonto. Si te fijas bien, habrás notado que el flag de acarreo (carry) se ha puesto a 1 después de esta operación. Así es como te das cuenta de que el resultado no cabe.

En el simulador de debajo, escribe (no pegues) el siguiente código:

LDA #$80
STA $01
ADC $01

Una cosa importante es darse cuenta de la distinción entre ADC #$01 y ADC $01. La primera suma el valor $01 al registro A, pero la segunda suma el valor almacenado en la posición de memoria $01 al registro A.

Ensambla, activa la casilla Monitor y ejecuta paso a paso estas tres instrucciones. El monitor muestra una sección de memoria, y puede ser útil para visualizar la ejecución de los programas. STA $01 almacena el valor del registro A en la posición de memoria $01, y ADC $01 suma el valor almacenado en la posición de memoria $01 al registro A. $80 + $80 debe dar como resultado $100, pero como ese valor no es representable en un byte, el registro A se pone a $00 y el flag de acarreo se activa. Además de esto, el flag de cero (zero) se activa. El flag de cero se activa con todas las instrucciones cuyo resultado es cero.

Una lista completa del juego de instrucciones de 6502 está disponible aquí y aquí. Normalmente menciono ambas páginas porque cada una tiene sus fortalezas y debilidades. Estas páginas detallan los argumentos de cada instrucción, que registros usan, y que flags activas. Son tu biblia.

5.1 Ejercicios

  1. Has visto TAX. Probablemente adivinarás qué hacen TAX, TXA y TYA, pero escribe algo de código para comprobar que estás en lo cierto.
  2. Reescribe el primer ejemplo de esta sección para usar el registro Y en lugar del X.
  3. Lo contrario de ADC es SBC (restar con acarreo, substract with carry). Escribe un programa que use esta instrucción.

6 Saltos condicionales

Hasta ahora sólo hemos podido escribir programas básicos sin ninguna lógica de saltos condicionales. Pongamos remedio a eso.

El ensamblador de 6502 tiene un puñado de instrucciones de salto condicional y todas ellas saltan dependiendo de si ciertos flags están o no activos. En este ejemplo vamos a ver BNE: "Saltar si es distinto" (branch on not equal).

Primero cargamos el valor $08 en el registro X. La siguiente linea es una etiqueta (label). Las etiquetas simplemente marcan ciertos puntos en un programa, de manera que nos podamos referir a ellos más tarde. Después de la etiqueta decrementamos X, lo almacenamos en $0200 (el pixel superior izquierdo), y después comparamos X con el valor $03. CPX compara el valor almacenado en el registro X con otro valor. Si los dos valores son iguales el flag Z se pone a 1, en cualquier otro caso se pone a 0.

La siguiente linea BNE decrementar, desplaza la ejecución a la etiqueta decrementar si el flag Z vale 0 (significando que los dos valores en la comparación CPX no eran iguales), y no hace nada en casa contrario, con lo que almacenaremos X en $0201 y finalizaremos el programa.

En lenguaje ensamblador, normalmente usaras etiquetas con las instrucciones de salto. Sin embargo, cuando se ensambla, esta etiqueta se convierte en un desplazamiento relativo de un único byte (el número de bytes que hay que ir hacia atrás o hacia adelante desde la siguiente instrucción) de manera que una instrucción de salto condicional únicamente puede ir hacia adelante o hacia atrás en un rango de 256 bytes. Esto significa que únicamente pueden usarse para moverse en código cercano. Para moverse más lejos tienes que usar instrucciones de salto incondicional.

6.1 Ejercicios

  1. Lo contrario de BNE es BEQ. Intenta escribir un programa que use BEQ.
  2. BCC y BCS ("saltar si no hay acarreo, branch on carry clear" y "saltar si hay acarreo, branch on carry set") se usan para saltar condicionalmente dependiendo del flag de acarreo. Escribe un programa que use una de las dos.

7 Modos de direccionamiento

El 6502 usa un bus de direcciones de 16 bits, lo que significa que hay 65536 bytes de memoria direccionables por el procesador. Recuerda que un byte se representa con dos caracteres hexadecimales, así que las direcciones de memoria se representan generalmente como $0000-$ffff. Hay varias formas de referirse a estas direcciones de memoria, como se detalla abajo.

Con todos estos ejemplos puede que encuentres útil usar el monitor de memoria para ver como cambia la memoria. El monitor toma una posición de memoria de inicio y el número de bytes a mostrar desde esa posición. Ambos son valores hexadecimales. Por ejemplo, para mostrar 16 bytes de memoria desde $c000, escribe $c000 y 10 en Start y Length respectivamente.

7.1 Absoluto: $c000

Con direccionamiento absoluto, la dirección de memoria completa se usa como argumento a la instrucción. Por ejemplo:

STA $c000 ;Almacenar el valor del acumulador en la posición de memoria $c000

7.2 Página cero: $c0

Todas las instrucciones que soportan direccionamiento absoluto (con la excepción de las instrucciones de salto) también tienen la opción de tomar una dirección de un único byte. Este tipo de direccionamiento se llama "página cero" - únicamente la primera página (los primeros 256 bytes) de memoria es accesible. Esto es más rápido, ya que sólo hay que leer un byte, y consume menos espacio en el código ensamblado.

7.3 Página cero,X: $c0,X

Aquí es donde los modos de direccionamiento se ponen interesantes. En este modo, se da una dirección de página cero y se le suma el valor del registro X. Aquí hay un ejemplo:

LDX #$01   ;X es $01
LDA #$aa   ;A es $aa
STA $a0,X  ;Almacenar el valor de A en la posición de memoria $a1
INX        ;Incrementar X
STA $a0,X  ;Almacenar el valor de A en la posición de memoria $a2

Si el resultado de la suma se sale del rango de un byte, únicamente la parte que cabe en el byte se usará para direccionar. Por ejemplo:

LDX #$05
STA $ff,X  ;Almacenar el valor de A en la posición de memoria $04

7.4 Página cero,Y: $c0,Y

Este es equivalente a página cero,X, pero sólo se puede usar con LDX y STX.

7.5 Absoluto,X y absoluto,Y: $c000,X y $c000,Y

Estos son los equivalentes con direccionamiento absoluto de página cero,X y página cero,Y. Por ejemplo:

LDX #$01
STA $0200,X ;Almacenar el valor de A en la posición de memoria $0201

7.6 Inmediato: #$c0

El direccionamiento inmediato no trata estrictamente con direcciones de memoria. Este es el modo donde realmente se usan valores. Por ejemplo, LDX #$01 carga el valor $01 en el registro X. Esto es muy diferente a la instrucción LDX $01 que carga el valor almacenado en la posición de memoria $01 en el registro X.

7.7 Relativo: $c0 (o etiqueta)

El direccionamiento relativo se usa para instrucciones de salto condicional. Estas instrucciones toman un único byte, que se usa como un desplazamiento relativo a la siguiente instrucción.

Ensambla el siguiente código y pulsa el boton Hexdump para ver el código ensamblado.

El hexadecimal debe tener esta pinta:

a9 01 c9 02 d0 02 85 22 00

a9 y c9 son los códigos de operación (opcodes) respectivos para LDA y CMP con direccionamiento inmediato. 01 y 02 son los argumentos a estas instrucciones. d0 es el opcode de BNE y su argumento es 02. Esto significa "sáltate los siguientes dos bytes" (85 22, la versión ensamblada de STA $22). Intenta editar el código de manera que STA tome una dirección absoluta de dos bytes en lugar de una dirección de página cero de un único byte (por ejemplo, cambia STA $22 por STA $2222). Vuelve a ensamblar el código y mira el volcado hexadecimal de nuevo- el argumento a BNE debe ahora ser 03, porque la instrucción que el procesador se va a saltar ocupa ahora tres bytes.

7.8 Implícito

Algunas instrucciones no tratan con direcciones de memoria (por ejemplo, INX - incrementar el registro X). De estas instrucciones se dice que tienen direccionamiento implícito (el argumento está implícito en la instrucción).

7.9 Indirecto: ($c000)

El direccionamiento indirecto usa una dirección absoluta para buscar otra dirección. La primera dirección contiene el byte menos significativo de la dirección y el byte que le sigue contiene el byte más significativo. Esto puede ser un poco difícil de entender, así que aquí hay un ejemplo:

En este ejemplo, $f0 contiene el valor $01 y $f1 contiene el valor $cc. La instrucción JMP ($f0) causa que el procesador busque los dos bytes contenidos en $f0 y $f1 ($01 y $cc) y los junte para formar la dirección $cc01, que se convierte en el nuevo contador de programa. Ensambla y ejecuta paso a paso el programa de arriba para ver qué sucede. Hablaré más sobre JMP en la sección [Saltos](#jumping).

7.10 Indexado indirecto: ($c0,X)

Este es algo raro. Es como una mezcla entre página cero,X e indirecto. Básicamente, tomas la dirección de página cero, le sumas el valor del registro X, y entonces usas el resultado para extraer una dirección de dos bytes. Por ejemplo:

Las direcciones de memoria $01 y $02 contienen los valores $05 y $06 respectivamente. Piensa en ($00,X) como ($00 + X). En este caso, X es $01, de manera que esto se simplifica a ($01). Desde aquí las cosas continúan como en un direccionamiento indirecto normal (los dos bytes en $01 y $02 ($05 y $06) se buscan para formar la dirección $0605). Esta es la dirección donde el registro Y se almacenó en la anterior instrucción, de manera que el registro A acaba teniendo el mismo valor que Y, aunque a través de una ruta mucho más retorcida. No vas a ver muchos de estos.

7.11 Indirecto indexado: ($c0),Y

El indirecto indexado es como el indexado directo pero menos retorcido. En lugar de sumar el registro X a la dirección antes de extraer los bytes que componen la dirección final, se accede a la página cero y se le suma el registro Y a la dirección resultante.

En este caso ($01) accede a los dos bytes en $01 y $02: $03 y $07. Estos forman la dirección $0703. El valor del registro Y se suma a esta dirección para dar la dirección final $0704.

7.12 Ejercicio

  1. Intenta escribir fragmentos de código que usen cada uno de los modos de direccionamiento. Recuerda, puedes usar el monitor para ver una sección de memoria.

8 La pila

La pila de un procesador 6502 es como cualquier otra pila; los valores se apilan (push) y se desapilan (pop, pull en terminología 6502) en ella. La profundidad actual de la pila es medida por el puntero de pila, un registro especial. La pila reside en la memoria entre $0100 y $01ff. El puntero de pila es inicialmente $ff, que apunta a la posición de memoria $01ff. Cuando un byte se apila, el puntero de pila se convierte en $fe, o la dirección de memoria $01fe.

Dos de las instrucciones de pila son PHA y PLA, "apilar acumulador, push accumulator" y "desapilar acumulador, pull accumulator". Abajo hay un ejemplo de estas dos en acción.

X contiene el color del pixel e Y la posición del pixel actual. El primer bucle dibuja el color actual como un pixel (con el registro A), apila el color y luego incrementa el color y la posición. El segundo bucle desapila, dibuja el color desapilado como un pixel e incrementa la posición. Como es de esperar, esto crea un patrón simétrico.

9 Saltos incondicionales

Los saltos incondicionales son como los condicionales con dos diferencias principales. Primero, los saltos incondicionales no dependen de una condición, se ejecutan siempre. Segundo, toman una dirección absoluta de dos bytes. Para programas pequeños, este segundo detalle no es muy importante, pero en programas más largos los saltos incondicionales son la manera de moverse de una sección de código a otra.

9.1 JMP

JMP es un salto incondicional. Aquí tienes un ejemplo realmente simple que lo muestra en acción:

9.2 JSR/RTS

JSR y RTS ("saltar a subrutina, jump to subroutine" y "volver de subrutina, return from subroutine") son un duo dinámico que normalmente verás usados juntos. JSR se usa para saltar de la dirección actual a otra parte del código. RTS vuelve a la posición previa. Esto es básicamente como llamar a una función y volver.

El procesador sabe dónde volver porque JSR apila la dirección menos uno de la siguiente instrucción antes de saltar a la dirección dada. RTS extrae esta dirección, le suma uno, y salta a esa dirección. Un ejemplo:

La primera instrucción causa que la ejecución salte a la etiqueta inicia. Esta subrutina fija X y luego vuelve a la siguiente instrucción, JSR bucle. Esta salta a la etiqueta bucle, que incrementa X hasta que sea igual a $05. Después de eso, volvemos a la siguiente instrucción, JSR termina, que salta al final del fichero. Esto muestra cómo JSR y RTS se pueden usar juntos para crear código modular

10 Creando un juego

¡Ahora démosle un buen uso a todo este conocimiento y hagamos un juego! Vamos a hacer una versión realmente simple del juego clásico 'Snake'.

El simulador de debajo contiene el código fuente completo del juego. Voy a explicar cómo funciona en las siguientes secciones.

10.1 Estructura general

Tras el bloque inicial de comentarios (lineas que empiezan con punto y coma), las dos primeras lineas son:

jsr inicializa
jsr bucle

inicializa y bucle son subrutinas. inicializa inicializa el estado del juego y bucle es el bucle principal del juego.

La subrutina bucle simplemente llama a una serie de subrutinas secuencialmente antes de saltar de nuevo a sí misma.

bucle:
    jsr leeTeclas
    jsr compruebaColision
    jsr actualizaSerpiente
    jsr dibujaManzana
    jsr dibujaSerpiente
    jsr espera
    jmp bucle

Primero, leeTeclas comprueba para ver si una de las teclas de dirección (W, A, S, D) ha sido pulsada y, en ese caso, fija la dirección de la serpiente. Después, compruebaColision comprueba si la serpiente ha colisionado consigo misma o con la manzana. actualizaSerpiente actualiza la representación interna de la serpiente basándose en su dirección. A continuación se dibujan la serpiente y la manzana. Finalmente, espera hace que el procesador haga una espera para que el juego no corra demasiado rápido. El juego continua hasta que la serpiente colisiona con un muro o consigo misma.

10.2 Uso de la página cero

La página cero de memoria se usa para almacenar un número de variables de estado del juego, como se hace notar en el bloque de comentarios al inicio. Todo lo que hay en $00, $01 y $10 en adelante son pares de bytes representando posiciones de memoria que se extraerán usando direccionamiento indirecto. Estas posiciones de memoria estarán todas entre $0200 y $05ff, la sección de memoria que corresponde a la pantalla del simulador. Por ejemplo, si $00 y $01 contienen los valores $01 y $02, se referirían al segundo pixel de la pantalla ($0201, recuerda, el byte menos significativo viene primero en el direccionamiento indirecto).

Los dos primeros bytes albergan la posición de memoria de la manzana. Se actualiza cada vez que la serpiente se come la manzana. El byte $02 contiene la dirección actual. 1 significa arriba, 2 derecha, 4 abajo y 8 izquierda. La razón de usar estos números quedará más clara después.

Finalmente, el byte $03 contiene la longitud actual de la serpiente, en términos de bytes de memoria (de manera que una longitud de 4 significa 2 pixels).

10.3 Inicialización

La subrutina inicializa llama a otras dos subrutinas, inicializaSerpiente y generaPosicionManzana. inicializaSerpiente fija la dirección de la serpiente, su longitud y carga las posiciones de memoria iniciales de la cabeza de la serpiente y el cuerpo. El par de bytes alojado en $10 contiene la posición de pantalla de la cabeza, el par en $12 contiene la posición del único segmento de cuerpo y $14 contiene la posición de la cola (la cola es el último segmento del cuerpo y se dibuja en negro para hacer que la serpiente se mueva). Esto sucede en el siguiente código:

lda #$11
sta $10
lda #$10
sta $12
lda #$0f
sta $14
lda #$04
sta $11
sta $13
sta $15

Esto carga el valor $11 en la posición de memoria $10, el valor $10 en $12, y $0f en $14. Luego carga el valor $04 en $11, $13 y $15. Esto deja la memoria como sigue:

0010: 11 04 10 04 0f 04

que representa las posiciones de memoria direccionadas de manera indirecta $0411, $0410 y $040f (tres pixels en mitad de la pantalla). Me estoy extendiendo en este punto, pero es importante pillar como funciona el direccionamiento indirecto.

La siguiente subrutina, generaPosicionManzana, fija la ubicación de la manzana a una posición aleatoria en la pantalla. Primero, carga un byte aleatorio en el acumulador ($fe es un generador de números aleatorios en este simulador). Lo almacena en $00. Luego se carga un número aleatorio distinto en el acumulador, al cual se le hace un AND con el valor $03. Vamos a hacer un pequeño inciso en esta parte.

El valor hexadecimal $03 se representa en binario como 00000011. El código de operación AND realiza un AND bit a bit del argumento con el acumulador. Por ejemplo, si el acumulador contiene el valor binario 01010101, el resultado del AND con 00000011 será 00000001.

El efecto de esto es enmascarar los dos bits menos significativos del acumulador, fijando los otros a cero. Esto convierte un número en el rango 0..255 a un numero en el rango 0..3.

Tras esto, el valor 2 se suma al acumulador para crear un número aleatorio en el rango 2..5.

El resultado de esta subrutina es cargar un byte aleatorio en $00 y un número aleatorio entre 2 y 5 en $01. Como el byte menos significativo viene primero en el direccionamiento indirecto, esto se traduce en una dirección de memoria entre $0200 y $05ff: el rango exacto usado para dibujar la pantalla.

10.4 El bucle del juego

Casi todos los juegos tienen su núcleo en el bucle del juego. Todos los bucles de juego tienen la misma forma básica: aceptar entrada del usuario, actualizar el estado del juego y representar el estado del juego. Este bucle no es diferente.

10.4.1 Leyendo la entrada

La primera subrutina, leeTeclas, hace el trabajo de aceptar la entrada del usuario. En este simulador, La posición de memoria $ff alberga el código ASCII de la tecla pulsada más recientemente. El valor se carga en el acumulador, se compara con $77 (el código hexadecimal de W), $64 (D), $73 (S) y $61 (A). Si alguna de estas comparaciones tiene éxito, el programa salta a la sección adecuada. Cada sección (teclaArriba, teclaDerecha, etc…) primero comprueba si la dirección actual es la contraria de la nueva. Vamos a hacer otro pequeño inciso.

Como se mencionó antes, las cuatro direcciones se representan internamente con los números 1, 2, 4 y 8. Cada uno de estos números es una potencia de 2, luego su representación binaria sólo tiene un único 1:

1 => 0001 (arriba) 2 => 0010 (derecha) 4 => 0100 (abajo) 8 => 1000 (izquierda)

El código de operación BIT es similar a AND, pero el cálculo sólo se usa para fijar el flag cero; el resultado final se descarta. El flag cero se activa sólo si el resultado de hacer el AND del acumulador con el argumento es cero. Cuando estamos operando con potencias de dos, el flag de cero únicamente se activara si ambos números son distintos. Por ejemplo, 0001 AND 0001 no es cero, pero 0001 AND 0010 sí lo es.

Entonces, mirando teclaArriba, si la dirección actual es abajo (4), la comprobación de bits será cero. BNE significa "salta si el flag de cero está limpio", y en ese caso saltaremos a movimientoIlegal, que simplemente retorna de la subrutina. En caso contrario, la nueva dirección (1 en este caso) se almacena en la dirección de memoria apropiada.

10.4.2 Actualizando el estado del juego

La siguiente subrutina, compruebaColision, llama a compruebaColisionManzana y compruebaColisionSerpiente. compruebaColisionManzana simplemente comprueba si los dos bytes que albergan la posición de la manzana coinciden con los dos bytes que contienen la posición de la cabeza. Si coinciden, la longitud se incrementa y se genera una posición nueva para la manzana.

compruebaColisionSerpiente itera por los segmentos del cuerpo de la serpiente, comparando cada par de bytes con el par de la cabeza. Si coinciden, el juego termina.

Tras la detección de colisiones, actualizamos la posición de la serpiente. Esto se hace en alto nivel como sigue: primero, movemos cada par de bytes del cuerpo una posición hacia arriba en memoria. Segundo, actualizamos la cabeza de acuerdo con la dirección actual. Finalmente, si la cabeza está fuera de rango, lo gestionamos como una colisión. Voy a ilustrar esto con un diagrama de texto. Cada par de corchetes contiene una posición x,y en lugar de un par de bytes por simplicidad.

   0    1    2    3    4
Cabeza               Cola

 [1,5][1,4][1,3][1,2][2,2]    Posición inicial

 [1,5][1,4][1,3][1,2][1,2]    El valor de (3) se copia a (4)

 [1,5][1,4][1,3][1,3][1,2]    El valor de (2) se copia a (3)

 [1,5][1,4][1,4][1,3][1,2]    El valor de (1) se copia a (2)

 [1,5][1,5][1,4][1,3][1,2]    El valor de (0) se copia a (1)

 [0,5][1,5][1,4][1,3][1,2]    El valor de (0) se actualiza basándose en la dirección

A un nivel bajo, esta subrutina es algo más complicada. Primero, la longitud se carga en el registro X, que es entonces decrementado. El fragmento de debajo muestra la dirección de inicio de la serpiente.

Posición de memoria: $10 $11 $12 $13 $14 $15

Valor:               $11 $04 $10 $04 $0f $04

La longitud está inicializada a 4, de manera que X empieza como 3. LDA $10,x carga el valor de $13 en A y STA $12,x almacena este valor en $15. X se decrementa y repetimos el bucle. Ahora, X es 2, de manera que cargamos $12 y lo almacenamos en $14. El bucle se repite mientras X sea positivo (BPL significa "salta si positivo").

Una vez que los valores se han desplazado hacia abajo en la serpiente, tenemos que ver qué hacer con la cabeza. Primero cargamos la dirección en A. LSR significa "desplazamiento lógico a la derecha, logical shift right", o "desplaza todos los bits una posición a la derecha". El bit menos significativo se desplaza al bit de acarreo, de manera que si el acumulador es 1, tras un LSR será 0 con el flag de acarreo activo.

Para comprobar si la dirección es 1, 2, 4 o 8, el código desplaza a la derecha hasta que el flag de acarreo está activo. Un LSR significa "arriba", dos significa "derecha" y así sucesivamente.

El siguiente trozo actualiza la cabeza de la serpiente dependiendo de la dirección. Esta es probablemente la parte más complicada del código, y es dependiente de cómo las posiciones de memoria se mapean a la pantalla, así que mirémosla con más detenimiento.

Puedes pensar en la pantalla como cuatro tiras horizontales de 32x8 pixels. Estas tiras se mapean a $0200-$02ff, $0300-$03ff, $0400-$04ff y $0500-$05ff. Las primeras filas de pixels son $0200-$021f, $0220-$023f, $0240-$025f, etc.

Siempre y cuando te muevas dentro de una de estas tiras horizontales, las cosas son simples. Por ejemplo, para mover a la derecha, simplemente incrementa el byte menos significativo (por ejemplo, $0200 se convierte en $0201). Para ir abajo, suma $20 (por ejemplo, $0200 se convierte en $0220). Izquierda y derecha son al contrario.

Ir entre secciones es más complicado, ya que tenemos que tener en cuenta el byte más significativo también. Por ejemplo, ir abajo desde $02e1 debe llevar a $0301. Afortunadamente, esto es bastante fácil de conseguir. Sumar $20 a $e1 da como resultado $01 y fija el bit de acarreo. Si el bit de accarreo está activo, sabemos que también necesitamos incrementar el byte más significativo.

Tras un movimiento en cada dirección, también tenemos que comprobar si la cabeza se sale de rango. Esto se gestiona de manera diferente para cada dirección. Para izquierda y derecha, podemos comprobar si la cabeza ha "dado la vuelta". Ir a la derecha desde $021f incrementando el byte menos significativo llevaría a $0220, pero esto es en efecto saltar desde el último pixel de la primera fila al primer pixel de la segunda fila. Así, cada vez que nos movemos a la derecha tenemos que comprobar si el nuevo byte menos significativo es un múltiplo de $20. Esto se hace usando una comprobación de bits con la máscara $1f. Con suerte la ilustración de debajo te mostrará cómo enmascarar los 5 bits más bajos nos dice si un número es un múltiplo de $20 o no.

$20: 0010 0000
$40: 0100 0000
$60: 0110 0000

$1f: 0001 1111

No voy a explicar en profundidad cómo funciona cada una de las direcciones, pero la explicación de arriba debe darte suficiente para poder figurártelo con un poco de estudio.

10.4.3 Representando el juego

Como el estado del juego es almacenado en términos de posiciones de pixels, representar el juego es muy directo. La primera subrutina, dibujaManzana, es extremadamente simple. Fija Y a cero, carga un color aleatorio en el acumulador, y luego almacena este valor en ($00),y. $00 es donde se almacena la posición de la manzana, de manera que $(00),y se refiere a esta posición de memoria. Lee la sección "Indirecto indexado" para más detalles.

A continuación viene dibujaSerpiente. Esta también es bastante simple. X se fija a cero y A a uno. Almacenamos entonces A en ($10,x). $10 alberga la posición de dos bytes de la cabeza, de manera que esto dibuja un pixel blanco en la posición actual de la cabeza. A continuación cargamos $03 en X. $03 alberga la longitud de la serpiente, de manera que ($10,x) en este caso será la posición de la cola. Como A es cero ahora, esto dibuja un pixel negro en la cola. Como sólo la cabeza y la cola de la serpiente se mueven, esto es suficiente para hacer que la serpiente se mueva.

La última subrutina, espera, está únicamente porque el juego iría demasiado rápido de otra manera. Todo lo que hace espera es contar mediante X desde cero hasta que llega de nuevo a cero. El primer dex da la vuelta, haciendo que X sea #$ff.

Autor: Jorge Acereda

Email: jacereda@gmail.com

Creado: 2013-10-14 Mon 23:16

Emacs 24.3.1 (Org mode 8.2.1)

Validate