Programar en assembler es lo más parecido a hacerlo en binario... del assembler al lenguaje que realmente habla CPU hay muy poca distancia. Cuando usamos un entorno de desarrollo (como el del simulador THRSim) puede que no nos demos cuenta de qué tan cerca del hardware estamos. Aprendamos a realizar el proceso de ensamblado y llevar nuestros programas del elegante assembler al rústico binario ¿Estás listo para entrar en la matrix?
Para esto usaremos como ejemplo la clásica búsqueda del menor valor dentro de un vector de números enteros sin signo de 8 bits (easy cake).:
VEC EQU $0040
ORG 0000
MENOR RMB 1 Aqui quedara el menor
DIRINI FDB VEC Direccion inicial del vector
CANT FCB 07 Cantidad de elementos del vector
* El vector lo estoy ubicando en otro lugar de memoria R/W
ORG VEC
VECTOR FCB $14,$33,$FF,$E0,$09,$11,$10
* Notar que la dir inicial del programa es la que cargo en el vector de reset al final, para que inicialice el PC.
ORG $C000
main LDX DIRINI * Cargo IX con la direccion inicial
LDAA 0,X * Cargo el primer elemento del vector en A
DEC CANT * Decremento la cant directamente en memoria
SIGO INX
LDAB 0,X * Cargo el segundo elemento del vector
CBA * y los comparo
BLS Amenor * Si el primero ya era menor, no lo cambio
TBA * Copio B en A
Amenor DEC CANT * Decremento la cant directamente en memoria
BNE SIGO * Si aun quedan elementos, sigue
STAA MENOR * Sino guarda el menor en donde se pidió
FIN BRA FIN
ORG $FFFE
RESET FDB main
La resolución del ejercicio introduce elementos que hasta ahora no hemos considerado en el blog, como ser el uso de EQU, el vector de reset y alguna mas. Las dudas urgentes sobre esto pueden resolverse consultando en los comentarios. Más adelante habrá artículos que abarquen esos temas.
Ensamblar el programa consiste en transformarlo en el código binario que interpretará CPU. Para ahorrarnos la incomodidad de escribir en binario emplearemos el sistema de numeración hexadecimal (seguro está pensando: sí claro, porque es MUCHO MÁS FÁCIL entender hexadecimal que binario). Sugiero que si desea aprender este tema transcriba el programa en una hoja dejando espacio en el margen izquierdo para poder escribir los códigos hexadecimales. No vale ensamblarl con el THRSim y copiarse!
Como primer paso al ensamblar el programa construiremos la "tabla de
símbolos". Esto es lo que hace el ensamblador también. Los símbolos son las palabras que componen el programa y que no son directivas al ensamblador ni instrucciones. Usamos símbolos para:
- nombrar posiciones de memoria (al reservarlas con RMB o definirlas con DB por ejemplo),
- identificar la parte alta -o primera dirección, correspondiente con la porción más significativa- de un valor multibyte en memoria (por ejemplo al crear un word con DW),
- determinar la primera dirección de un vector -sea de bytes, words, etc- con FCB, FDB,
- marcar un punto determinado del programa, al que puede que utilicemos en saltos,
- definir constantes (con EQU).
Todo símbolo deberá existir una vez como etiqueta de una línea. Luego lo emplearemos como operando en una o más ocasiones. No necesariamente tiene que darse en ese orden.
Ejemplo: en el programa anterior definimos
VEC EQU $0040
y luego lo usamos:
ORG VEC
La tabla de símbolos tiene dos columnas: identificador y contenido. La directiva EQU crea una entrada en la tabla, con la etiqueta como identificador (VEC) y el valor como contenido ($0040). Otras directivas tendrán como contenido la direccion asociada a ellas. Volviendo al ejemplo:
ORG 0000
MENOR RMB 1
DIRINI FDB VEC
Agregamos dos entradas a la tabla con los identificadores MENOR y DIRINI. El valor para MENOR es la dirección que le corresponde a la reserva. La directiva de origen que antecede a esa etiqueta nos marca la ubicación en memoria que se le asigna: 0000. (Podemos escribir $0000 o 0000, sin importar en qué sistema de numeración trabajemos, el cero es el cero). La directiva DIRINI recibe la dirección contigua de MENOR. ¿Por qué? Porque MENOR se usó con RMB -reserve memory bytes- y solo se reservó un byte para ella. Por ende el siguiente byte -la siguiente dirección de memoria- 0001 le corresponde a DIRINI. Note que la dirección de cada símbolo se determina en función de la dirección del símbolo anterior y de la cantidad de bytes que este requiere.
La siguiente línea es:CANT FCB 07
como DIRINI define un double-byte (16 bits = 2 bytes) y se le asignó la dirección 0001, razonamos que la dirección de CANT será 0001 + 2=0003.
Nuestra tabla de símbolos va quedando así:
identificador valor (hexadecimal)
VEC 0040
MENOR 0000
DIRINI 0001
CANT 0003
VECTOR es un símbolo fácil para agregar, ya que tiene su directiva de origen justo antes, dice:
ORG VEC
VECTOR FCB $14,$33,$FF,$E0,$09,$11,$10
¡Aquí ya estamos usando uno de los símbolos para determinar otro! Sabemos que VEC = $ 0040, por lo tanto agregamos VECTOR:
identificador valor (hexadecimal)
VEC 0040
MENOR 0000
DIRINI 0001
CANT 0003
VECTOR 0040
¿Por qué tienen el mismo valor? En el ejemplo VEC se usa para ubicar VECTOR en un lugar de la memoria sin necesidad de escribir la dirección junto a su directiva de origen ORG. En otras palabras, es decisión de diseño, no hay una regla para ello. Entiéndase que la tabla de símbolos no es una tabla de variables, y esto se vuelve más evidente cuando le agregamos las etiquetas: main, Amenor, SIGO, FIN y RESET. Los valores de RESET y main son sencillos de determinar porque nuevamente están junto a directivas ORG:
identificador valor (hexadecimal)
VEC 0040
MENOR 0000
DIRINI 0001
CANT 0003
VECTOR 0040
main C000
Amenor
SIGO
FIN
RESET FFFE
En rigor tampoco podemos decir que todos los valores de la tabla de símbolos sean direcciones, ya que con la directiva EQU podemos generar entradas con valores de cualquier tipo.
Las otras etiquetas las completaremos durante el ensamblado. Vayamos al segundo paso: ensamblemos.
Al realizar este proceso para grabar el programa en la memoria read-only (ROM) de un microcontrolador HC11 necesitamos determinar cuál es el contenido de cada dirección de memoria que comprende el programa. Aunque el HC11 direcciona 64K de memoria, cierto es que suele implementar bastante menos que eso. De cualquier manera solo nos interesa el contenido de la memoria en la porción que requiere el programa. Por ello sobre el margen izquierdo indicaremos la dirección de memoria y a la derecha el contenido de esa dirección. Para facilitar el trabajo cuando la instrucción (sea el código de operación o el/los operandos) tenga una longitud de más de un byte, los representaremos uno a la derecha del otro. Pero claro está, en la siguiente línea indicaremos la dirección de memoria tomando en consideración la longitud de la instrucción anterior. Por ejemplo, si la instrucción que fuésemos a ensamblar ocupa un byte, como ser el caso de INX, la dirección de la siguiente instrucción será la siguiente dirección. Pero si la instrucción que estamos ensamblando es INY -que ocupa dos bytes- entonces la dirección de la siguiente instrucción la obtendríamos sumando 2 a la dirección de INY.
El HC11 maneja instrucciones de longitud variable, es una de sus características, otros procesadores tienen todas sus instrucciones del mismo largo (o sea misma cantidad de bytes).
Necesitamos un punto de partida, un origen... ¿le suena? La directiva del ensamblador ORG es la respuesta. Ensamblaremos la parte ejecutable del programa, que empieza así:
ORG $C000
main LDX DIRINI
Entonces anotamos la dirección $C000 a la izquierda, así:
ORG $C000
$C000 main LDX DIRINI
Necesitamos determinar cuál es código de operación de la instrucción LDX que aplica en este caso. ¿Cómo lo podemos averiguar? Veamos cuáles son los modos de direccionamiento que soporta LDX, sabiendo que cada modo tiene su propio código de operación:
En rojo vemos los modos de direccionamiento, en azul los códigos de operación de cada uno y en verde se presenta el formato de los operandos para cada modo. ¿Cuál corresponde a la instrucción que estamos ensamblando?
Podemos determinarlo por descarte: no se trata de modo inmediato (IMM) porque no tiene el numeral junto al operando. Tampoco es modo indexado (IND,X o IND,Y) porque no hay una referencia a ningún índice en el operando. Eso nos deja los modos extendido y directo. Hemos visto en un post anterior que siempre que se puede se usa el modo directo. ¿Cuándo se puede usar? Cuando la instrucción lo soporta y el operando se encuentra en la página cero. En este caso comprobamos que LDX soporta modo DIRecto, y en la tabla de símbolos acabamos de anotar que DIRINI equivale a $0001, por tanto tenemos un ganador! El código de operación será DE. Note que el operando tiene el formado dd. Cada par de letras representa un byte. Por tanto el operando será de un byte. Pero DIRINI es de 16 bits ($0001), ¿cómo nos queda en un byte? Justamente por el modo directo, el cual lleva implícito que el operando está en la página cero. Por tanto el operando será 01. Quedaría entonces:
ORG $C000
$C000 DE 01 main LDX DIRINI
Dado que la instrucción ocupó dos bytes, la siguiente comenzará en $C000 + 2:
ORG $C000
$C000 DE 01 main LDX DIRINI
$C002 LDAA 0,X
Para esta instrucción el modo de direccionamiento es evidente: indexado en X. Por tanto el código de operación es A6. El operando (que en el set de instrucciones aparece como ff) corresponde al offset del indexado. En este caso es cero, por tanto ensamblando queda:
ORG $C000
$C000 DE 01 main LDX DIRINI
$C002 A6 00 LDAA 0,X
$C004
¿Se anima a continuar el ensamblado? En el siguiente post lo completamos. Mientras tanto les dejo una pregunta y un desafío: ¿qué cambio habría que hacerle al programa para que realice la búsqueda en un vector de números signados (negativos en complemento a la base)?
Adapte el programa para que funcione correctamente aun si el vector tiene un solo elemento (estrictamente hablando ya no sería un vector).
Adapte el programa para que funcione correctamente aun si el vector tiene un solo elemento (estrictamente hablando ya no sería un vector).
hola profe diculpe que lo moleste cuando cargo el segundo elemento en ldab no tendria que ser 1,x?
ResponderBorrarNo es molestia! Fijate que antes de cargar el segundo elemento hay un INX. El algoritmo es así: carga el primero y lo toma como el "menor". Luego incrementa IX y empieza a ciclar para comparar a partir del siguiente todos contra el menor. Cuando encuentra uno más chico, actualiza el menor (que lo mantiene en el AccA). Se entiende?
Borrarhola profe, para responder el desafió de los valores signados alcanzaría con cambiar el BLS por un BLE para buscar el menor o un BGE para buscar el mayor?
ResponderBorrarSuena convincente... simulalo y contame!
BorrarBuenas Jair,
ResponderBorrarProbé resolver el ejemplo "...la clásica búsqueda del menor valor dentro de un vector..." y quisiera saber si de esta manera también es válida:
ORG $0000
DIRINI RMB 2
DIRFIN RMB 2
MENOR RMB 1
ORG $C000
CLR MENOR
LDX DIRINI
LDAA 0,X
STAA MENOR
LOOP INX
LDAA 0,X
CMPA MENOR
BCC ES_MAYOR
STAA MENOR
CPX DIRFIN
BLO LOOP
ES_MAYOR CPX DIRFIN
BLO LOOP
FIN BRA FIN
(Queda todo apretado en la vista previa, espero que se logre entender, lo probé en el simulador pero no me queda claro)
Lorenzo
Hola! Si, es correcto. Las dos lineas que siguen a STAA MENOR las podrias eliminar. Son redundantes, ya que estas haciendo la misma comparacion luego (asi que si no salta, se hace dos veces). Bien ahi!
Borrar