Idiomas disponibles:

Programando como si fuera 1982. Parte V – Control de Flujo

En el post anterior vimos cómo el procesador mueve datos y hace aritmética. Pero un programa que solo ejecuta instrucciones en línea recta no hace gran cosa útil. En algún momento necesita tomar una decisión. Ir por un camino u otro, dependiendo de un resultado. Necesita bucles, condicionales, la capacidad de comportarse de manera diferente según el estado de las cosas.

En el assembly del 6510, todo eso se construye sobre un único mecanismo. Flags.

El Status Register y sus flags

Cada operación que ejecuta el procesador deja rastros. Cuando sumas dos números y el resultado es cero, el procesador lo anota. Cuando una resta produce un número negativo, el procesador lo anota. Cuando una suma desborda el límite de un byte, el procesador lo anota. Esos rastros viven en un registro especial llamado Status Register, o SR.

El SR no guarda un número. Guarda ocho bits independientes, cada uno con su propio significado. Esos bits se llaman flags, pequeñas marcas que el procesador levanta o baja automáticamente según ocurren las operaciones.

Status Register del 6510

De todas las flags, tres aparecen en prácticamente todo programa:

La flag Z (Zero) se activa cuando el resultado de una operación es exactamente cero. Una resta que da cero, una suma que da cero, un registro que se puso a cero. Todo eso levanta Z.

La flag N (Negative) se activa cuando el bit más alto del resultado es 1, que en aritmética con signo indica un número negativo. Si restas un número mayor de uno menor, N va a 1.

La flag C (Carry) se activa cuando una suma supera 255, el máximo que cabe en un byte. Es el “me llevo una” de la aritmética binaria. Si sumas $FF + $01, el resultado sería 256, que no cabe en 8 bits. A queda con $00 y el bit que no cupo va al Carry.

Estas tres flags son la base de cada decisión que toma un programa en assembly.

Comparando valores

Antes de desviar, necesitas comparar. Para eso existen CMP (compara con A) y CPX (compara con X).

CPX #$05    ; compara X con el valor 5

Lo que esta instrucción hace por dentro es una resta. Calcula X - 5 y descarta el resultado, manteniendo solo el efecto en las flags. Si X vale 5, el resultado es cero y Z se activa. Si X es menor que 5, el resultado es negativo y N se activa. Si X es mayor que 5, ninguna de las dos se activa.

El resultado en sí va a la basura. Lo que importan son las flags que quedaron.

Saltos condicionales, el if sin if

Con las flags en su lugar, entran las instrucciones de salto. Leen una flag específica y deciden si siguen adelante o saltan a otra dirección.

InstrucciónCondición para saltar
BEQZ = 1 (resultado fue cero, valores son iguales)
BNEZ = 0 (resultado no fue cero, valores son diferentes)
BMIN = 1 (resultado fue negativo)
BPLN = 0 (resultado fue positivo o cero)
BCSC = 1 (carry activo, resultado sin signo superó 255)
BCCC = 0 (carry inactivo)

Hay un detalle que confunde al principio. La lógica del salto es inversa a lo que harías en Python o Go. En lenguajes modernos piensas “si la condición es verdadera, ejecuta este bloque”. En assembly piensas “si la condición es falsa, salta este bloque”.

Antes de ver el código, aquí aparece por primera vez un elemento de sintaxis nuevo. Los labels. Un label es un nombre que le das a una ubicación específica de tu programa. Cuando escribes done: en el código, el ensamblador reemplaza cada referencia a done con la dirección real de memoria de esa instrucción. El procesador nunca ve el nombre. Para cuando el código se ejecuta, done ya se ha convertido en un número. Los labels existen para que no tengas que recalcular y codificar a mano las direcciones de memoria cada vez que añades o eliminas una línea.

Para implementar if x == 5, escribirías:

        CPX #$05         ; compara X con 5
        BNE fin          ; si X != 5, salta
        JSR hace_algo    ; X es 5, llama la función
fin:
        RTS

El BNE rechaza a quien no tiene entrada, no a quien la tiene. El resultado final es el mismo, pero el razonamiento se invierte. Acostumbrarse lleva un tiempo, pero tras unos pocos bucles se vuelve instinto.

Construyendo un bucle

Un while en Python:

x = 0
while x != 10:
    hace_algo()
    x += 1

El equivalente en assembly:

        LDX #$00         ; x = 0

loop:
        JSR hace_algo    ; hace_algo()
        INX              ; x++
        CPX #$0A         ; compara X con 10
        BNE loop         ; si X != 10, vuelve al loop

        RTS

El BNE al final es el guardián. Mientras la comparación no resulte en cero, mientras X no haya llegado a 10, el salto ocurre y el bucle reinicia. Cuando X finalmente vale 10, Z se activa, BNE no salta y la ejecución sigue adelante.

Punteros y direccionamiento indirecto

Todos los ejemplos hasta ahora accedían a direcciones fijas. LDA $0200, STA $D418. Pero a veces no sabes en tiempo de escritura de código a qué dirección quieres acceder. Lo sabes en tiempo de ejecución, porque la dirección depende de algún cálculo. Eso es lo que resuelven los punteros.

En el 6510, puedes guardar una dirección de 16 bits en dos bytes consecutivos de la Zero Page y luego decirle al procesador que vaya a la dirección guardada allí, le sume Y y lea lo que haya en esa posición.

; Guarda la dirección $0400 en la Zero Page en $42 y $43
LDA #$00
STA $42     ; byte bajo: $00
LDA #$04
STA $43     ; byte alto: $04

; Ahora lee RAM[$0400 + Y] usando el puntero
LDY #$03
LDA ($42),Y ; A = RAM[$0400 + 3] = RAM[$0403]

La notación ($42),Y significa leer la dirección de 16 bits guardada en la Zero Page en $42 y $43, sumarle Y y usar el resultado como dirección final. En C esto sería exactamente ptr[3] donde ptr apunta a $0400.

Un puntero no es un concepto abstracto. Es una dirección guardada en memoria que el procesador sabe cómo seguir. Una vez que lo ves así, todo desde arrays dinámicos hasta tablas de funciones empieza a tener sentido a nivel de hardware.


En el próximo post llegamos al capítulo más sorprendente de la serie. Cómo escribir un byte en una dirección de memoria podía tocar una nota musical, mover un sprite en la pantalla o leer el estado de un botón del joystick. Sin driver, sin syscall, sin ninguna abstracción.