viernes, 14 de octubre de 2016

Direccionamiento inmediato. Quiero mi operando y lo quiero YA!

Aunque en una publicación previa ya abordamos el funcionamiento del modo inmediato, me veo en la necesidad de abordar el tema nuevamente ya que de forma empírica comprobé que representa ciertas dificultades para quien está adentrándose en el bajo nivel. En otras palabras: en un examen hicieron desastres con "el modo del hashtag" como le dicen ahora los millenials.
Intentaré esclarecer el funcionamiento de modo inmediato mediante algunos ejemplos adicionales.
El modo inmediato del HC11 (IMM en el set de instrucciones) corresponde al modo relativo teórico. Es relativo directo porque se provee la dirección del dato. Distingamos de relativo indirecto donde se obtendría la dirección de la dirección.
¿Qué significa que sea un modo relativo directo? Que CPU recibe la información de cómo llegar a la dirección real del operando (DRO). Esa información es una dirección de referencia y un desplazamiento, o en términos habituales: base y desplazamiento. 
Lo que tal vez confunda del modo inmediato es que la dirección de referencia (base) es el PC y el desplazamiento es cero. Esto genera algún conflicto neuronal en quien lo estudia. Al código de operación le sigue el operando mismo. Por tanto luego de que CPU lee el CodOp, el program counter se incrementa y contiene la dirección del operando. Así que sin agregarle nada (desplazamiento cero) CPU tiene ante sí la dirección del operando.

¿Cuándo se usa y cuándo no?
El hecho de tener los operandos mismos acompañando las instrucciones nos da una idea de las restricciones del modo inmediato. Para empezar, dado que el programa se almacenará en memoria de solo lectura (ROM), los operandos indefectiblemente quedarán en la misma memoria. Por tanto no podemos usar "variables" en modo inmediato. Queda claro que serán constantes, por más que empleemos etiquetas para referirnos a ellas. También se desprende que no será posible guardar un dato en modo inmediato. Primero porque es una aberración lógica: no puedo guardar (p/e) el acumulador A en un número. Segundo porque es memoria RO, por tanto no se puede guardar nada en ella.
Entendemos entonces que este modo de direccionamiento sólo nos servirá cuando se conozca el dato al momento de codificar/ensamblar el programa. No puede proveerse una vez ensamblado. 
Ahora bien, cuando se trata de constantes o de valores preconocidos (que no cambiarán una vez que ensamblamos el programa) resulta práctico. 
Por ejemplo, si hacemos el algoritmo básico de determinar el tipo de triángulo a partir de la longitud de tres lados, se hace evidente que no podemos acceder a la longitud de los lados en modo inmediato. De otra forma habremos hecho un programa para un juego de datos, pero si queremos cambiar el juego de datos hay que cambiar el programa. Una cosa de locos diría un amigo mío. Pero si estamos indicando el tipo de triángulo mediante una letra (Escaleno, eQuilátero, Isósceles), dado que las letras que usamos no van a cambiar, tranquilamente podemos usar modo inmediato para ello. Por ejemplo, si queremos guardar la I de isósceles en una posición de memoria a la que etiquetamos como "tipoT", podríamos hacer:

       LDAA #'I
       STAA tipoT

En el acumulador A guardamos primero el Ascii de la letra I mayúscula (por eso la comilla simple como prefijo). El uso del modo inmediato aquí hace que en A se guarde el Ascii de la I (49 hexa, 73 decimal). ¿Hay alguna forma de resolver esto sin usar modo inmediato? Podríamos hacer así:

 ISO   DB  'I
       .
       .
       LDAA ISO
       STAA tipoT

De esa forma la etiqueta ISO queda asociada a la dirección de memoria donde está el Ascii de la I. Luego al usar LDAA ISO estarmos guardando en A lo que está en la dirección ISO (a lo que llegará en modo directo o extendido, segun corresponda).

Ejemplos concretos
Veamos ahora algunas situaciones concretas para distinguir dónde corresponde el uso de inmediato y dónde no. En general la ensalada se hace cuando no entendemos bien cómo funcionan las directivas al ensamblador. Comprendiendo cómo se genera la tabla de símbolos en base a ellas todo se hace más claro.
Las directivas RMB, FCB, FDB, DB y DW asocian el símbolo que les precede (etiqueta) a la dirección de memoria que corresponda segun la directiva de origen (ORG) que le corresponda. La directiva EQU asocia la etiqueta al valor que le sigue. Podríamos decir que EQU es la única directiva que almacena en la tabla de símbolos "tal cual" lo que le indicamos. 
Te propongo practicar esto. A continuación va un fragmento de código, tratá de construir por vos mismo la tabla de símbolos. No voy a darte la respuesta, sino una forma de confirmar que las tuyas sean correctas.

   ORG $0000
DA0 EQU 5
DA1 RMB 5
DA2 RMB DA0
DA3 DB  %10000000
DA4 DW  $FAFA,$FAFA
DA5 EQU $0020
DA6 RMB $F
DA7 DW  DA5
DA8 FCB $52,$49,$56,$45,$52,$20,$53,$4f,$53,$20,$44,$45,$20,$4c,$41,$20,$42,$21
DA9 FDB 1986,1978 


Los símbolos son DA0, DA1,.... DA8. Hacé una lista y al lado indicá cuál es el valor que le asignará el ensamblador. Para verificar la respuesta, ensamblá ese código en el THRSim, luego andá al menú "View -> Label list". Aparecerá una ventana con etiquetas y un valor asociado a ellas. Esa es la tabla de símbolos. Aunque aparecen algunos "extra" que no nos importan ahora, a partir del décimo valor estarán las entradas DA0 en adelante.
Adicionalmente podrías verificar mediante un "Memory dump" cómo quedó la memoria (sugiero que lo hagas, después podés contarme en los comentarios qué viste).

Es importante entender que EQU genera una entrada en la tabla de símbolos "tal cual". No importa si el valor a la derecha es de 16 bits y se parece a una dirección. Lo que se asocia es ese valor. Dado que EQU no ocupa lugar en la memoria cuando compares el Memory Dump con el Label list verás que no aparecen en memoria ni el 15 ni el $0020.
La directiva RMB asociará la etiqueta a la primer dirección de memoria de los bytes reservados. Entonces si hacemos un RMB de 20 bytes, la etiqueta se asociará a la dirección del primero de tales bytes, o la "parte alta" de dicha palabra multi-byte.
Lo mismo pasa con DB, DW, FCB y FDB. Sin importar si generan un valor o un vector de valores, sean de 8 o 16 bits, la etiqueta se asocia a la dirección del primero.
Entendiendo que EQU funciona muy parecido a un #define de C y familia, comprendemos la lógica de utilizar los valores cargados en la tabla de símbolos en otras directivas (por ejemplo: DA2 RMB DA0).

¿Dónde va cada uno?
Supongamos que en el requerimiento de un algoritmo se nos dice que ordenemos de menor a mayor un vector de elementos de 8 bits del que conocemos la dirección inicial y el tamaño del vector. Hay dos formas de interpretar ese "conocimiento". Podemos asumir que al momento de codificar sabemos las dos cosas, lo cual hace al programa algo rígido (habría que reensamblar si cambia el vector), o entender que se nos provee la dirección donde hallaremos tanto un dato como el otro. Abordemos ambas alternativas. Preste cuidadosa atención al uso (o no) de modo inmediato en ambos casos.

Comentarios sobre el código: 
  • notarás que las etiquetas aparecen en un renglón en blanco. Se puede usar así para darle algo más de claridad. Quedarán asociadas a la siguiente instrucción de todos modos.
  • al final del programa incluí una directiva define word para guardar la dirección de inicio del programa. Al guardar esa dirección en $FFFE y $FFFF (dos posiciones consecutivas porque es una dirección por tanto tiene 16 bits) el HC11 sabrá qué cargar en el PC al simular el programa. Ese es el "vector de reset" tal como aparece en la documentación.
  • el algoritmo comprende dos bucles anidados (ordenamiento por burbujeo). El bucle interior utiliza un contador que va decrementando. El exterior reinicia el bucle interior salvo que en la última pasada no se haya realizado ningún intercambio en el mismo. De esa forma cuando el vector ya está ordenado no sigue iterando. 
  • la instrucción TST hasta ahora no la había empleado en el blog, compara un valor en memoria con cero sin pasar por los acumuladores.

Caso 1: tenemos los datos concretos. 
No es mi opción preferida, pero vamos a ella. En este caso debiéramos incluir tanto el vector como su tamaño. Puede simularlo tal cual.

                 ORG    $0000
vec    DB     $52,$4f,$4a,$4f,$53,$4f,$53,$41,$4d,$41,$52,$47,$4f
tama   EQU    13   En hexadecimal seria 0D
todoOK RMB    1    Bandera para saber cuando ya esta ordenado
aux    RMB    1

       ORG    $C000
bucleE   
       CLR    todoOK   
       LDX    #vec
       LDAA   #(tama-1) // porque comparo vec[x] con vec[x+1]
       STAA   aux
bucleI   
       LDAA   0,x
       CMPA   1,x
       BLE    yaEsta   // o sea ya estan ordenados
       LDAB   1,x
       STAA   1,x
       STAB   0,x
       INC    todoOK
yaEsta   
       INX
       DEC    aux
       BNE    bucleI
       TST    todoOK  // compara todoOK con cero
       BNE    bucleE    // si no es cero, que vuelva a pasar
FIN    BRA    FIN

       ORG    $FFFE    // esto es para no cargar el PC manualmente
reset  DW     bucleE

¿Dónde usamos modo inmediato? Para la dirección de inicio del vector y el tamaño. ¿Por qué? La tabla de símbolos indica:
VEC  0000
TAMA 000D
Por tanto lo que queremos cargar en IX es justamente el valor de vec: 0000. Allí comienza el vector. Esa es la dirección de inicio. Si no usamos modo inmediato le diríamos que en 0000 está contenida la dirección de inicio, lo que es incorrecto porque en 0000 está contenido el primer elemento del vector. Nuevamente, tama es una macro de reemplazo (recuerde el concepto del #define). Por tanto lo que queremos cargar en el acumulador es el valor de tama. Si no usamos el modo inmediato (LDAA TAMA) estaríamos cargando en A lo que está en la dirección de memoria 0D. No queremos eso. Queremos que el 0D se guarde en A. Por eso se usa LDAA #tama.
Caso 2: tenemos la dirección donde están los datos.
Creo que es el escenario más real.

        ORG    $0000
dirIni  RMB    2
tama    RMB    1
todoOK  RMB    1    Esta bandera indica si ya esta ordenado
aux     RMB    1

        ORG    $C000
bucleE   
        CLR    todoOK   
        LDX    dirIni
        LDAA   tama
        STAA   aux
bucleI   
        LDAA   0,x
        CMPA   1,x
        BLE    yaEsta  //  o sea ya estan ordenados
        LDAB   1,x
        STAA   1,x
        STAB   0,x
        INC    todoOK
yaEsta   
        INX
        DEC    aux
        BNE    bucleI
        TST    todoOK  // compara todoOK con cero
        BNE    bucleE  // si no es cero, que vuelva a pasar
FIN     BRA    FIN

        ORG    $FFFE   // esto es para no cargar el PC manualmente
reset   DW     bucleE

¿Dónde usamos el modo inmediato? ¡En ningún lado! Como el vector, su dirección de inicio y su tamaño no son conocidos al momento de codificar, no puedo emplear modo inmediato. Siempre nos manejaremos con la dirección del dato. Si se trata del vector, tendremos la dirección de la dirección. Después de todo dirIni justamente es eso: la dirección donde guardamos la dirección de inicio. Para simular este segundo algoritmo debemos cargar tres cosas:
  1. el vector, en alguna parte de la memoria (a gusto del consumidor)
  2. la dirección de inicio del vector en dirIni
  3. el tamaño del vector en tama.
Caso 3: tenemos algunas cosas constantes y otras no.
Un caso intermedio sería el de contar con la dirección inicial del vector y el tamaño, pero no con el vector en sí. Conocemos al momento de codificar dónde inicia el vector y la cantidad de bytes, aunque el vector en sí mismo habría que cargarlo en el simulador (desde 0050H).

         ORG   $0000
dirIni   DW    $0050
tama     DB    13
todoOK   RMB   1    Bandera para saber cuando ya esta ordenado
aux      RMB   1

         ORG   $C000
bucleE   

         CLR   todoOK             
         LDX   dirIni         
         LDAA  tama         
         DECA         
         STAA  aux
bucleI             
         LDAA  0,x         
         CMPA  1,x         
         BLE   yaEsta    o sea ya estan ordenados         
         LDAB  1,x         
         STAA  1,x         
         STAB  0,x         
         INC   todoOK
yaEsta            
         INX         
         DEC   aux         
         BNE   bucleI         
         TST   todoOK  // compara todoOK con cero        
         BNE   bucleE    // si no es cero, que vuelva a pasar
FIN      BRA   FIN         

         ORG   $FFFE    // esto es para no cargar el PC manualmente
reset    DW    bucleE

¿Dónde usamos modo inmediato? Nuevamente en ningun lado. Las directivas DW y DB asocian la etiqueta a la dirección, por tanto al usarlas en el código nos referimos a la dirección donde está el operando. En el caso de dirIni incluso será la dirección de la dirección.

Conclusión: el modo inmediato tiene una aplicación concreta: requiere que preconozcamos los datos a operar. Para no equivocarse lo más sano es entender BIEN cómo funcionan las directivas al ensamblador y cómo se construye la tabla de símbolos.

Pregunta para los comentadores: ¿Tiene sentido usar DB en memoria RW en aplicaciones reales? ¿Tiene sentido cargar un vector en memoria RO y ordenarlo in situ?

Para los que piensan en memoria Flash, no aplica al HC11. Pero les dejo una canción que sí aplica:

5 comentarios:

  1. ¿Tiene sentido cargar un vector en memoria RO y ordenarlo in situ?

    O no entiendo nada, o esto tiene 0 sentido. Para ordenar el vector no habría que escribir memoria (más específicamente la memoria en la que está escrita el vector)? Digo, fue lo que hicimos, y en memoria RO dudo que podamos escribir :P
    Saludos.

    ResponderBorrar
    Respuestas
    1. Hola! Efectivamente no tiene sentido en un microcontrolador real. Pero el simulador te deja trabajar la memoria RW inicializándola, lo que permite hacer el reordenamiento en el lugar.

      Borrar
  2. Respuestas
    1. Intento explicar en clase que no tiene sentido levantar vectores en RWM con directivas que sirven para crear CONSTANTES en memoria y luego modificar esas CONSTANTES. Pero es una de esas cosas en las que ponerse en exigente le complica la vida al alumno.

      Borrar

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