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 5DA1 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 2tama 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
dirIni RMB 2tama 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:
- el vector, en alguna parte de la memoria (a gusto del consumidor)
- la dirección de inicio del vector en dirIni
- el tamaño del vector en tama.
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: