jueves, 3 de agosto de 2017

¿Qué tipo de triángulo es?

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.

        ORG     $C000
        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:

        ORG     $C000
        LDAA    lado1
        CMPA    lado2
        BNE     salto1

        LDAB    lado3
        CBA
        BNE     esIso
        LDAA    #'E
        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:

        ORG     0000   
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

No hay comentarios.:

Publicar un comentario

Aunque no es obligatorio, es de buena educación firmar los comentarios.