Mostrando las entradas con la etiqueta relativo. Mostrar todas las entradas
Mostrando las entradas con la etiqueta relativo. Mostrar todas las entradas

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

lunes, 16 de mayo de 2016

Direccionamiento (continuacion). Todo es relativo... bah, no siempre.

La entrada anterior consideró los modos de direccionamiento directo, extendido, inmediato (vagamente, en este post lo desarrollo mejor) e indexado. Llegó el momento de abordar el más sencillo (inherente) y el más difícil (relativo). Aquí viene lo bueno jóvenes...
El modo relativo es el empleado en los saltos. De hecho en el HC11 las instrucciones de la familia BRANCH (no confundir con brunch, eso es una mezcla de almuerzo y desayuno) se manejan solo por direccionamiento relativo, y este modo solo está disponible para los branch. Así que es imposible considerar uno sin el otro. Van juntos, como el cafe con leche y las medialunas.
Las instrucciones de la familia BRANCH (se podría traducir bifurcación) nos permiten alterar el orden de ejecución del programa. CPU continuamente repite el ciclo fetch-decode-execute, por lo tanto al terminar con una instrucción ejecuta la que sigue, y así ad infinitum (o hasta que la apaguen).
Necesitamos comprender bien este hecho para pasar al branch... así que repasémoslo.
Al mismo momento de leer una instrucción, el Program Counter se incrementa, conteniendo en él la dirección del operando que acompaña esa instrucción, o de la siguiente instrucción. Veamoslo en un ejemplo, supongamos esta porción de programa:

ORG  $C000
LDAA  #$03
LDAB   $04

Al ensamblarlo el resultado sería (sugiero tener el set de instrucciones a mano para seguir la explicación):


$C000 86 03
$C002 D6 04

Observe que la instrucción LDAB está en modo directo, por tanto en el acumulador B cargará el contenido de la dirección de memoria 0004. Pero la instrucción LDAA utiliza modo inmediato, por tanto lo que se cargará en el acumulador A es el número 03 (en hexadecimal... y en decimal es igual), y no el contenido de la dirección de memoria 03. Pero ¿De dónde sacó CPU la dirección del operando 03? ¿cómo determina que lo que debe copiar a B es el número 03 en lugar del contenido de la posición de memoria 03?
Al programar le indicamos esto mediante el uso del numeral # para señalar modo inmediato. Este modo implica que lo que acompaña al código de operación es "el" dato. No se trata de una dirección ni una referencia, andamos sin vueltas, ¡es el dato! ¿Cómo lo carga CPU al acumulador?

Veamos nuevamente la instrucción en hexadecimal:

$C000 86 03

¿Cómo se produce el ciclo de instrucción? En primer lugar el PC tiene la dirección $C000. Realiza el fetch y por tanto carga el código de operación 86 en el Instruction Register (IR). Al mismo momento PC se incremente y adopta el valor $C0001. La unidad de control (UC) decodifica el 86 y determina que lo que debe hacer es copiarse el contenido actual del PC al Memory Address Register (MAR). Efectúa un READ de la memoria y lo que toma del Memory Buffer Register (MBR) lo copia al acumulador B. ¿Logró identificar de dónde se toma la dirección del operando? ¡Del PC! Por eso al modo inmediato también se lo llama relativo al PC. La dirección real del operando (DRO) está en el PC.
Ahora bien, una vez que leyó el contenido del PC con el valor $C0001.. ¿qué ocurrió con este registro? Se incrementó nuevamente. De ahí que al concluir el ciclo de esta instrucción el PC queda con el valor $C0002, que es el de la instrucción siguiente. Es importante recordar que el PC se incrementó antes de producirse la búsqueda del operando en memoria. El PC no se incrementa después de haber copiado del MBR al AccA, sino después de haber sido leído.
Habiendo completado la instrucción LDAA #$03, el PC tendrá el valor $C002. Por tanto la UC leerá el valor de la memoria en $C002 y lo copiará en el IR, en este caso el código de operación D6. Ahora al decodificarlo determina que se está empleando direccionamiento directo, por tanto lo que está apuntado por el PC ($C003) es el offset de la página cero. Se copia el PC al MAR, se lee de memoria (el valor 04) y ahora se completa con el número de página para formar la DRO: $0004. Esa dirección se coloca en el MAR y se lee de memoria el operando que se cargará en el acumulador B. Al momento el PC ya está incrementado y tiene el valor $C004, correspondiente a la siguiente instrucción (que no incluimos en el ejemplo).
¿Cómo haríamos para alterar la secuencia del programa? Hemos visto que luego de cada uso del PC (típicamente para copiarlo al MAR y de ahí efectuar la lectura de memoria) el valor del mismo se incrementa. Si quisiéramos que no se ejecute la siguiente instrucción tenemos que modificar el valor del PC antes de que se inicie el siguiente ciclo fetch-decode-execute. Aquí es donde entran a jugar las BRANCH.
Las instrucciones de bifurcación operan sobre el valor del PC. A excepción del salto incondicional (BRA = branch always) y el salto "nunca" (BRN = branch never, o sea "nunca salte") evalúan una condición y si se cumple aplican un offset al valor del PC. Ese offset se representa con un número de 8 bits contemplando negativos en complemento a la base. Por lo tanto cuando la condición del salto se cumple se suma el offset al PC y de esa forma se salta hacia adelante (si el offset es positivo) o hacia atrás (si es negativo).
Cuando empleamos etiquetas en assembler y luego las utilizamos con los branch, dejamos en manos del ensamblador el cálculo del offset. Pero es importante entender cómo funcionan, por ejemplo para comprender por qué los saltos tienen límites.
Si utilizamos 8 bits para representar números enteros y admitimos negativos en complemento a la base (Cb), el rango es de -128 a 127. En otras palabras, desde una instrucción de branch podemos saltar 127 bytes hacia adelante (sumando al PC) o 128 hacia atrás (restando al PC). Peeeeero, aquí el truco: dado que el valor del PC al que se aplica el offset es el que corresponde a la siguiente instrucción -luego de haber leído el branch y el offset- entonces tenemos que descontar esos dos bytes al hacer la cuenta. De tal forma cuando queremos que salte "en el lugar" para formar un bucle infinito, lo que por ejemplo hacemos al cerrar los programas así:

FIN   BRA   FIN

En realidad tenemos que decirle que salte 2 hacia atrás. Porque el PC ya se habrá incrementado pasando por las direcciones de la instrucción BRA y del offset (que es siempre de un byte). Por eso al ensamblarla queda así:
20 FE
Siendo FE el número -2 en Cb. Veamos algunos saltos en el ejemplo del triángulo de un post anterior:
Marqué los saltos repitiendo el color cuando apuntan a la misma etiqueta. Notar que aunque tres veces salta a la etiqueta isos, los tres saltos tienen distinto offset. ¿Por qué? Porque desde esos tres puntos del programa la cantidad de bytes que hay que sumar al PC para que llegue a la dirección $C01B es distinta. En el primer caso es la instrucción:
$C009 27 10
El código de operación del salto BEQ es 27. El offset es 10. Dado que es un número hexadecimal, corresponde al decimal 16. Si sumamos los valores hexadecimales C00B + 10 el resultado es el la dirección de memoria de la etiqueta isos=C01B.
¿Por qué sumé C00B al offset (10) en vez de C009, que es la dirección de la instrucción de salto? Porque el valor del PC luego de realizar el ciclo fetch-decode para BEQ isos (27 10) será C00B, y no C009. Por tanto el offset se aplica al PC incrementado, el PC que apunta a la siguiente instrucción.
Veamos otro ejemplo más fácil de seguir: el salto BRA LISTO, copio la porción de programa desde esa línea hasta la de la etiqueta listo:

$C019 20 02       bra listo
$C01B C6 49 isos  ldab #'I
$C01D D7 03 listo stab tipoT

Cuando la UC ha leído el código de operación 20 y el offset 02 que la acompaña, el valor del PC es C01B. Aun no ejecutó el salto pero ya el PC está incrementado. En este caso el salto se ejecuta siempre (es incondicional), pero si fuera el caso CPU evaluaría la condición correspondiente. Entonces en caso de ejecutar el salto, aplica el offset (02) al valor del PC, el valor ya incrementado (C01B). Por tanto esa cuenta C01B+02 da por resultado la dirección C01D, que corresponde a la etiqueta listo.

El límite a los branch que impone el rango antes mencionado [-128;126] se expresa en bytes, o sea en posiciones de memoria. Si tomamos un promedio de dos bytes por instrucción, significa que tenemos la posibilidad de realizar saltos aproximadamente 60 instrucciones hacia adelante o hacia atrás. ¿Qué pasa si no es suficiente? Podemos recurrir a realizar otra clase de saltos, JUMP, que serán motivo de alguna otra publicación del blog. (Al leer JUMP pueden pensar en el tema de Van Halen, un nerd normalmente lo hace).















¿Qué hay del modo inherente? Contamos con muchas instrucciones que emplean operandos en registros. Tal es el caso por ejemplo de:

¿es correcto decir que no usan direccionamiento? Si y no. Estas instrucciones emplean lo que Motorola llama direccionamiento inherente, en los libros lo encontramos como "modo implícito". Si vamos a hilar finito no se trata de un modo de direccionamiento con todas las letras (podría ser un mdo dirccionamto) porque la misma definición de los modos de direccionamiento refiere a traer operandos de memoria... y si ya está en un registro no se lo trae de memoria.
Estamos en un caso en el dependiendo quién lo diga se trata de la verdad o de "la verdad". 


(El libro que tiene Hutz podría ser perfectamente el manual de Motorola).
Respecto a esto, hay que tener en cuenta -como bien aclaró el Ingeniero F. S.- que lo que alguna bibliografía (Murdocca) llama "modo registro" se asemeja más a lo que en el plano teórico planteamos como modo indexado (porque no refiere a operandos en registros sino a que los la dirección base y/o el offset se hallan en registros). 

En resumen, hemos visto:
* cómo funciona el direccionamiento relativo, cómo se calculan los saltos y qué limitaciones tiene
* a qué llama Motorola modo inherente (nosotros modo implícito)

Trivia: ¿para qué les parece que existe la instrucción BRN (BRANCH NEVER)?