CURSO de ASM 6502 [NES]

Saltos y branchs

Por DaRKWiZaRDX

La última lección, y una de las más importantes... :)~~~ ¡Adelante! xD

En ASM no existen sentencias de if...then...else (como en Visual Basic) sino saltos, y no existen while, for, etc. sino que se usan branchs. Y eso es precisamente lo que sigue :)

INTRODUCCIÓN A BUCLES Y SUBRUTINAS

Cuando en un programa (programado en el lenguaje que sea) hay cierto trozo de código que deberá ser llamado varias veces (y posiblemente con distintos parámetros) lo llamamos procedimiento, función, subrutina (como se les llama en ASM), etc. Es una forma de ahorrar espacio no escribiendo tanto código, por ejemplo (ejemplo de C, si no lo entiendes mira la explicación más abajo):

int numero = 0;

void main() {
    numero = ( (numero *2) +4) *18 ;  // en realidad no entiendo por qué querríamos hacer una
    numero = ( (numero *2) +4) *18 ;  // operación así pero bueno, los ejemplos pueden ser a
    numero = ( (numero *2) +4) *18 ;  // veces un poco descabellados 
}

Lo que haría este trozo de código sería ejecutar esa operación (multiplicar por 2, sumar 4, y multiplicar por 18) tres veces sobre el número, pero estamos repitiendo código que podríamos ahorrar usando un bucle o una llamada a función. Por ejemplo:

Usando subrutina / función

int numero = 0;

void main() {
    operacion();
    operacion();
    operacion();
}

void operacion() {
    numero = ( (numero *2) +4) *18 ;  // la operación
}

Este código haría exactamente lo mismo que el anterior, pero ocupando menos espacio.

Usando un bucle

int numero = 0;
int contador_bucle = 3;

void main() {
    while (contador_bucle--) {
        numero = ( (numero *2) +4) *18 ;
        }
}

En este bucle (repetición), ponemos un contador a 3, y por cada vez que se ejecute el cuerpo de código decrementa el contador en 1, hasta que llega a 0 y no se realiza más la repetición.

Usando un bucle y una subrutina / función

int numero = 0;
int contador_bucle = 3

void main() {
    while (contador_bucle--) {
        operacion();
    }
}

void operacion() {
    numero = ( (numero *2) +4) *18 ;  // la operación
}

Aquí una combinación de las dos, no creo que haga falta una explicación, a menos que no hayas entendido las dos anteriores :)

Usando subrutinas

Las subrutinas son trozos comunes de código (como los que venimos viendo) a los que se llega mediante la instrucción JSR (Jump to SubRoutine), y que acaba con un RTS (Return from SubRoutine).

Lo que realmente hace JSR es saltar a una posición (de 16bits, 2 bytes) y guardar en pila el offset del JSR + 3. Esto es así porque si no guardara en pila el offset del salto (del JSR) luego el juego no sabría dónde volver cuando encuentra un RTS, ya que lo único que hace esta instrucción (RTS) es tomar los últimos 16bits (2 bytes) introducidos en la pila y saltar a esa posición (recuerda que todo esto se hace con los valores Big-Endian, es decir, invertidos). Veámoslo con un ejemplo a ver si se entiende:

1) $C2F0: JSR $D422
2) $C2F3: INX
3) $C2F4: JSR $D422
4) $C2F7: LDA #$80
... (más código)
$D422: LDA $60,X
$D424: STA $4801
$D426: RTS

Explicación: Este simple trozo de código lo que hace es:

1- Salta a la rutina en $D422, guardando en pila lo siguiente: OFFSET DEL JSR ($C2F0) + 3 = $C2F3
2- Sólo incrementa X, lo importante de esta parte es que aquí viene el RTS de la subrutina.
3- Vuelve a llamar a la rutina de $D422, esta vez guarda en pila $C2F4 + 3 = $C2F7
4- Y luego un LDA #$80, que es a donde vendrá el RTS de la subrutina.

Realmente no es IMPRESCINDIBLE que sepas esto de cómo se guarda la dirección de regreso del salto, simplemente JSR salta a una rutina y RTS salta a la instrucción que viene después del JSR. Todo esto, siempre y cuando no hayas dejado algún valor que no debías en la pila (cosa que no debería pasar si escribes bien tu código :P)

Saltos comunes

Estos son más simples, es la instrucción JMP $XXXX, que sigue ejecutando código en la dirección que le especificas, por ejemplo:

$C268: JMP $D000

Dejará de ejecutar el código después de $C268 y pasará a ejecutar el que se encuentra en $D000.

Branches (saltos condicionales) y bucles

Los branches son las instrucciones de control de flujo en ASM. Es decir, que controlan qué código ha de ejecutarse y qué código no. Son esencialmente los IF-ELSE del ASM. Se utilizan tanto para los saltos condicionales como para los bucles.
Su uso es bastante fácil, mediante las instrucciones CMP/CPX/CPY se puede comparar A, X o Y respectivamente con un valor (sea una constante o un valor contenido en memoria), haciendo esto se "setean" (ponen a 1) o "resetean" (se ponen a 0) algunos bits (denominados "flags") del registro P, y en ellos se fijan las instrucciones de "branches" (saltos condicionales) para ver si se cumple su condición y si efectivamente saltan. En todos estos casos el valor de A es $80.

CMP #$90           ; A es menor, entonces -> flag c (carry) = 0
                   ;                      -> flag z (zero)  = 0

CMP #$60           ; A es mayor, entonces -> flag c (carry) = 1
                   ;                      -> flag z (zero)  = 0

CMP #$80           ; A es igual, entonces -> flag c (carry) = 0
                   ;                      -> flag z (zero)  = 1

(El Flag Z se pone a 1 si el resultado de la última operación fue cero, es decir, un LDA #$00 pondría el flag a 1, así como una resta que da como resultado 0 o un incremento/decremento, etc.)

(Los flags x (overflow) y n (negative) también pueden ser modificados pero no por una comparación, sino por una suma / resta: Si el resultado de una suma es demasiado grande para caber en el acumulador entonces el flag x se pone a 1, y si el valor es mayor a $80 (es decir, tiene el bit más alto a 1 y por lo tanto es negativo) el flag n se pone a 1)

Funciona esencialmente igual usando CPX / CPY, aunque obviamente, en lugar de comparar el valor de A con el del operando se compara el valor de X / Y.

Una vez que se ha hecho la comparación entonces pueden usarse los branches.

BCC -> Salta si flag c = 0
BCS -> Salta si flag c = 1
BEQ -> Salta si flag z = 1
BNE -> Salta si flag Z = 0
BMI -> Salta si flag n = 1
BPL -> Salta si flag n = 0

Ejemplo de bucle

LDX #$00         ; carga el contador del bucle (X) a 0
bucle:           ; etiqueta que determina dónde debe saltar el branch
LDA $60,X        ; Carga un valor de $60 +X . Como ves, en este ejemplo usamos X como contador y como indexador.
STA $70,X        ; Lo guarda en $70 +X.
INX              ; Incrementa el contador/índice
CPX #$10         ; compara con $10
BNE bucle        ; Si no es igual, es decir, si X es menor a $10 vuelve a ejecutar el bucle

Lo que haría este código sería copiar $10 bytes de $60 +X a $70 +X, una simple función de copiado de datos ^^

Ejemplo de salto condicional (Código real de Castlevania I de NES ^^)

$E77F: A5 71  LDA $71 = #$05      ; Carga el número actual de corazones en el acumulador
$E781: 18     CLC                 ; pone el carry a 0 para hacer una suma
$E782: 69 01  ADC #$01            ; suma 1
$E784: C9 63  CMP #$63            ; compara con $63 (99 en decimal)
$E786: 90 02  BCC $E78A           ; si es MENOR entonces salta a $E78A
$E788: A9 63  LDA #$63            ; sino carga el acumulador con #$63 (otra vez, 99 decimal)
$E78A: 85 71  STA $71 = #$05      ; lo guarda en el contador de corazones (actualiza)

(En este ejemplo $71 es la posición de memoria que indica la cantidad de corazones que se posee, este código se ejecuta cuando se agarra un corazón pequeño -que suma 1-)

Ya con sólo ver el código es fácil darse cuenta de lo que hace, pero por si acaso lo explicaré, primero carga en el acumulador el número actual de corazones (antes de sumar el que agarramos), entonces le suma 1 (porque acabamos de agarrar un corazón), y lo compara con 99 decimal, si es menor entonces salta a $E78A, es decir, guarda el viejo valor + 1 en la actual cantidad de corazones, mientras que si no es menor, carga 99 decimal y lo guarda en la cantidad de corazones. Esto significa que no es posible tener más de 99 corazones, ya que esta rutina se encarga de limitar esa cantidad.

Ejemplo de subrutina

LDA $03
JSR subrutina
STA $03
(más código)
subrutina:
ASL A
CLC
ADC #$13
RTS

A estas alturas no creo que haga falta que explique esta rutina, carga el valor contenido en $03, y salta a la subrutina, que lo multiplica por 2, le suma #$13 y regresa, entonces $03 es actualizado ^^

Espero que haya quedado claro con los ejemplos, cualquier duda, comentario o correxión (xD) o lo que sea que tengas puedes enviármelo a mi mail :)

<< Anterior - Siguiente >>