Existen conceptos que captamos mejor cuando los ejemplificamos. Hay un ejercicio que considero es lo suficientemente sencillo para que un principiante lo razone pero no tan simple como para que sienta su intelecto mancillado: determinar el tipo de triángulo. Partimos de la suposición de que contamos con la información de la longitud de los tres lados del mismo y queremos determinar qué tipo de triángulo es: isósceles, escaleno o equilátero. Dejaremos de lado la comprobación de que realmente se trata de un triángulo y utilizaremos datos enteros. Después de todo es un ejercicio para principiantes.
Supongamos que en tres direcciones de memoria contamos con las longitudes de los lados y que se trata de números enteros positivos de 8 bits. Las longitudes negativas en complemento a la base las dejamos para alguna tarde de lluvia y alcohol. Ubiquemos las longitudes desde la dirección de memoria 0000 y reservemos también un byte para indicar el tipo de triángulo.
ORG 0000 // el cero es cero en hexa y en decimal
lado1 RMB 1
lado2 RMB 1
lado3 RMB 1
tipoT RMB 1
El tipo de triángulo lo indicaremos con un ASCII. Si es equilátero guardaremos una E en tipoT, si es isósceles una I y si es escaleno una K.
¡IMPORTANTE! Como en tantos órdenes de la vida hay más de una manera válida de resolver el problema. Podríamos hacerlo bien, mal o a la "Max Power". Sería ideal que antes de seguir leyendo intente hacerlo por sí mismo. No importa si le toma un buen rato, haga su mejor intento. Luego puede ver la solución que aparece a continuación, que puede ser distinta a la suya sin que por eso alguna sea incorrecta. Si no logra completar la solución -recordando que hay muchos escenarios posibles- trate de lidiar con algunas de las combinaciones.
El HC11 de alguna manera nos fuerza a utilizar los acumuladores, ya que las operaciones que podemos hacer directamente contra memoria son muy pocas. Por tanto para evaluar los lados vamos a necesitar que siquiera uno de ellos esté en un registro de la CPU. Dado que estamos limitados a realizar comparaciones de dos valores (no podemos hacer lado1==lado2==lado3 o lado1==lado2 AND lado1==lado3) tendremos que realizar varias comparaciones. Tenemos dos caminos para las comparaciones:
- podemos cargar los dos registros acumuladores A y B y comparar entre ellos con CBA;
- o podemos comparar un registro contra memoria con CMPA / CMPB.
Avancemos gradualmente a la solución. Empecemos por comparar dos lados y saltar si no son iguales, quedaría así:
ORG $C000
LDAA lado1
CMPA lado2
BNE salto1 // salta si no son iguales
La instrucción que siga será la que se ejecute si el salto NO se ejecuta. Si la condición se cumple y el salto se ejecuta, el flujo de programa continuará donde esté la etiqueta salto1. Esta forma de razonar los saltos trae algunas dificultades al programador de alto nivel porque básicamente es al revés de lo que solemos pensar. Aquí el "else" es lo que sigue al "if" y el lado verdadero (true o "then") es lo que irá donde ubiquemos la etiqueta. Es por ello que el salto suele ser el opuesto al que razonaríamos en alto nivel.
Luego de cada
comparación tenemos un salto. Esto ocurre en el 95% de las situaciones
en que usamos comparaciones, básicamente porque después de preguntar
algo, tenemos que hacer algo con la respuesta.
¿Qué hacemos si los lados 1 y 2 son iguales? Sabemos que no es escaleno. Vamos a compararlo con el tercero para determinar si es isósceles o equilátero.
LDAA lado1
CMPA lado2
BNE salto1
LDAB lado3
CBA
BNE esIso
Hemos comparado el lado 1 (en el acumulador A) con el lado 3 (en el acumulador B). Si son iguales se ejecutará la instrucción que siga... ya sabemos que es equilátero.
Aunque
podríamos cargar directamente el valor numérico del ASCII de la "E",
vamos a dejar que el ensamblador trabaje por nosotros. Con una comilla
simple como prefijo le estamos indicando que lo que sigue debe
interpretarse como ASCII:
LDAB #'E
Además
dado que queremos guardar el ASCII de la "E" en la posición "tipoT",
primero necesitamos cargar el ascii en un acumulador y luego guardarlo
con STAA. ¿Por qué no lo guardamos directamente en memoria?
Respuesta corta: porque con el HC11 no se puede.
Respuesta larga: el HC11 no admite instrucciones con dos referencias a memoria.
Necesitamos que uno de los operandos esté en registro. Así que cargamos
el ASCII de la E en el acumulador B (podríamos tranquilamente usar A,
pero bueno, para que no se ponga celoso....) y luego lo guardamos en
memoria.
Pasemos esto a código:
LDAA lado1
CMPA lado2
BNE salto1
LDAB lado3
CBA
BNE esIso
LDAA #'E
STAA tipoT
STAA tipoT
Si son distintos se ejecutará el salto "esIso" porque podemos afirmar que es isósceles. Tenemos entonces dos saltos para desarrollar: salto1 y esIso. El segundo es el más fácil, así que lo podemos agregar:
ORG $C000
LDAA lado1
CMPA lado2
BNE salto1
LDAB lado3
CBA
BNE esIso
LDAA #'E
STAA tipoT <- punto 1
esIso LDAA #'I
STAA tipoT <- punto 2
¿Qué ocurre si dejamos eso como está? Además de que falta resolver un salto, obviamente.
Recordemos que la ejecución se da en forma secuencial. Luego de que la CPU ejecute la instrucción "STAA tipoT" del punto 1, continuará con la instrucción "LDAA #'I" y el siguiente "STAA tipoT" del punto 2. ¿Por qué? Respuesta corta: porque la CPU continuamente repite el ciclo de instrucción. Respuesta larga: porque si queremos que se altere el flujo de ejecución, debemos hacerlo con instrucciones que impacten en el contador de programa. Una etiqueta no tiene ese efecto. ¡Es solo una etiqueta! Si queremos que luego del punto 1 el programa continúe su ejecución en otra instrucción, debemos agregar un salto. Dado que deseamos que ese salto se ejecute siempre, será un salto incondicional. Veamos cómo queda:
ORG $C000
LDAA lado1
CMPA lado2
BNE salto1
LDAB lado3
CBA
BNE esIso
LDAA #'E
STAA tipoT <- punto 1
BRA fin
esIso LDAA #'I
STAA tipoT <- punto 2
Note que luego de ejecutar la instucción del punto 1 el flujo del programa se ve alterado por el branch always.
Aun tenemos que resolver el "salto1". Dicho salto se ejecuta cuando el lado 1 (en A) y el lado 2 son distintos. En ese punto solo podemos decir que el triángulo no es equilátero. Vamos a cargar el lado 3 en el acumulador B y comparar entonces lado 1 con lado 3 y lado 2 con lado 3. Si hallamos que hay alguna igualdad, entonces es isósceles (observar que la carga de lado3 en el acumulador B se produce justo después del salto).
ORG $C000
LDAA lado1
CMPA lado2
BNE salto1
LDAB lado3
CBA
BNE esIso
LDAA #'E
STAA tipoT
BRA fin
esIso LDAA #'I
STAA tipoT
BRA fin <- agrego un salto por lo expuesto antes
salto1 LDAB lado3
CMPB lado2
BEQ esIso // salta si lado 3 y lado 2 son iguales
CBA
BEQ esIso // salta si lado 1 y lado 3 son iguales
LDAA #'K // si no salto hasta aca...escaleno
STAA tipoT
fin BRA fin
Con esto agregamos las comparaciones que faltaban. El BRA del final lo usamos para
darle un cierre elegante al programa, pero no cumple ninguna función más
que gastar ciclos. Se puede hacer más prolijo: notar que se repite el STAA tipoT en más de un lugar. Eso es justo lo último que hace el programa, así que cambiaremos los saltos a fin por saltos a una nueva etiqueta para que guarde con una misma instrucción:
ORG $C000
LDAA lado1
CMPA lado2
BNE salto1 // salta si lado 1 y lado 2 son distintos
LDAB lado3 // si son iguales compara con lado 3
CBA
BNE esIso // salta si lado 2 y lado 3 son distintos
LDAA #'E // si no saltó es porque es equilátero
BRA guarda
esIso LDAA #'I
BRA guarda
salto1 LDAB lado3
CMPB lado2
BEQ esIso // salta si lado 3 y lado 2 son iguales
CBA
BEQ esIso // salta si lado 1 y lado 3 son iguales
LDAA #'K // si no salto hasta aca...escaleno
guarda STAA tipoT
fin BRA fin
La programación en bajo nivel no tiene por qué ser "estructurada", pero si nos esforzamos un poco podemos dejar un solo punto de finalización para que se vea más claro.
¿Se puede optimizar más? Seguramente. Estamos ejecutando dos veces la instrucción "LDAB lado3". Podríamos tratar de ubicarla de forma que aparezca solo una vez. ¿Dónde le parece que quedaría bien? Tal vez justo antes del salto "BNE salto1"... ¿qué habría que verificar? Recordemos que la instrucción CMPA actualiza los flags de la palabra de estao. Luego BNE utiliza el flag Z (zero) para determinar si hay que saltar o no. Por lo tanto debiéramos constatar que LDAB no modifique el flag Z para ubicarla entre CMPA lado2 y BNE salto1. Si aun le quedan dudas, revise el set de instrucciones:
Dado que LDAB actualiza el flag Z no podemos ubicarla antes del BNE. Pero sí podríamos acomodarla antes del CMPA. Así nos queda la versión definitiva del programa:
lado1 RMB 1
lado2 RMB 1
lado3 RMB 1
tipoT RMB 1
ORG $C000
LDAA lado1 LDAB lado3
CMPA lado2
BNE salto1 // salta si lado 1 y lado 2 son distintos
CBA // si son iguales compara con lado 3
BNE esIso // salta si lado 2 y lado 3 son distintos
LDAA #'E // si no saltó es porque es equilátero
BRA guarda
esIso LDAA #'I
BRA guarda
salto1 CMPB lado2
BEQ esIso // salta si lado 3 y lado 2 son iguales
CBA
BEQ esIso // salta si lado 1 y lado 3 son iguales
LDAA #'K // si no salto hasta aca...escaleno
guarda STAA tipoT
fin BRA fin
Llegó el momento de probar el programa en el simulador, pero es tema de otro post. ¿Se puede optimizar más? Tal vez, pero ya queda a criterio del lector. Acepto ideas en los comentarios!
Sugerencias: partiendo del fuente provisto realice los cambios siguientes:
- manejar valores de lado de triángulo de 16 bits
- acceder a los lados del triángulo como elementos de un vector