martes, 20 de septiembre de 2016

Instalación del simulador THRSim

Haré una breve reseña de cómo obtener e instalar el simulador THRSim, excelente herramienta para la programación Assembler. Funciona bajo Windows, versiones de 32 y 64 bits, testeado incluso en Windows 10.

Se puede descargar desde la Web oficial del THRSim. Solo tener en cuenta un punto: en la web encontrarán:
  • Download THRSim11 with C support for free 
  • Download THRSim11 without C support


Aunque el primer instinto es descargar desde el primer link, se han reportado muchos casos de fallos, particularmente al ingresar a las opciones del menú "Memory" y en equipos de 64 bits. Por tanto mi humilde consejo: descargue e instale "without C support".
La instalación no tiene mayores secretos, puede darle al "siguiente, siguiente" y en pocos instantes tendrá la aplicación funcionando.

Para empezar a trabajar no hace falta hacer nada más. Tal vez algún puritano quiera ver la configuración de la memoria del HC11 simulado, esto se puede hacer desde el menú View -> Memory -> Memory Configuration... a tener en cuenta que se cerrará el programa (por tanto habría que guardar cualquier archivo que querramos preservar).

Luego de un aviso y de cerrar la aplicación podemos ver (y opcionalmente modificar) la configuración de la memoria:
En rojo marqué los rangos de memoria RW y RO que usaremos en los ejemplos del blog (en los valores por default). Notar sin embargo que se pueden configurar más secciones de memoria, tanto RW (aunque diga RAM, ya todos sabemos que toda la memoria del HC11 es random access) y RO. 
No veo necesidad inmediata de aclarar más aspectos de la instalación, pero si tienen dudas coméntenlas y agregaré el contenido que sea necesario.

Enjoy!

viernes, 16 de septiembre de 2016

Assembler: ¿y esto con qué se come?

En los lejanos '90 Calamaro decía "¿de qué hablamos cuando hablamos de amor?", hoy podría quasi-citarlo: ¿de qué hablamos cuando hablamos de assembler? 
El Assembly o Assembler es el lenguaje de programación más cercano al CPU. Cada instrucción tiene una equivalencia -en general 1:1- con un código binario que CPU comprende. Esto significa que es el lenguaje del procesador, un idioma bastante ajeno a los seres humamos normales.
A diferencia de los lenguajes de programación de alto nivel, que se han construido con términos del lenguaje cotidiano (tal vez en inglés, pero bueno, cotidiano para quienes hablan ese idioma), el assembly se compone de las instrucciones que entiende el procesador. ¿Cuáles serán? Generalmente tareas muy sencillas y puntuales que involucren operaciones aritméticas, lógicas, de carga o almacenamiento en memoria, manejo binario, control de flujo, etc.
Resulta más sencillo comprender la programación de bajo nivel si entendemos cómo funciona el procesador. He aquí que a diferencia del alto nivel en que el hardware en que se ejecutará el programa tiene poca o ninguna importancia para el programador (en términos generales obviamente), cuando desarrollamos en bajo nivel estamos trabajando para un procesador o familia de procesadores específica. Esto significa que debemos conocer el procesador, al menos la parte que el fabricante quiso que conozcamos, lo que normalmente denominamos la arquitectura de programación: set de instrucciones, modos de direccionamiento, registros accesibles (dedicados o no), espacio de direccionamiento, etc.
Un programa en bajo nivel puede apoyarse en rutinas preexistentes del BIOS (Basic Input Output System) de la computadora, o incluso en algunas provistas por el sistema operativo. Sin embargo, dada la característica del acceso directo al hardware, Assembler es el único lenguaje de programación en el que podemos prescindir por completo de cualquier plataforma y operar en forma directa con el hardware. Hacerlo así convierte cada tarea rutinaria en alto nivel en un desafío artesanal considerable, por ello verán que los algoritmos de este blog en general se enfocan en un problema sencillo y no invertimos esfuerzos en hacer una entrada o salida elegante de la información. Cuando queremos cargar variables o ver el resultado de un programa, siempre podemos usar el simulador y "mirar" la memoria.
El micro en el que centraremos los esfuerzos es el Motorola 68HC11, así que conozcámoslo un poco:
  • Cuenta con dos acumuladores o registros de propósito general de 8 bits denominados A y B
  • Brinda la posibilidad de usar los dos registros A y B como un solo registro de 16 bits denominado D (tener en cuenta que D no es un registro independiente, sino solo la agrupación de A y B, donde A es la parte alta de D y B es la parte baja de D)
  • Un bus de direcciones de 16 bits, por lo tanto 2^16 direcciones de memoria de 8 bits, un total de 65536 (64K) direcciones. Sin embargo no estarán todas implementadas.
  • Entrada/Salida mapeada en memoria (estructura de 3 buses), lo que significa que un intervalo de direcciones de memoria realmente no son tales, sino interfaces de E/S.
  • Cuenta con una palabra de estado de 8 bits, que podremos utilizar para tomar decisiones condicionales, cuyos flags indicarán si la última operación arrojó acarreo (Carry), acarreo entre el cuarto y quinto bit (Half carry), desbordamiento (oVerflow), fue negativa (Negative) o cero (Zero), además de permitirnos algunas cosillas respecto a las interrupciones que veremos más adelante.
  • Modos de direccionamiento: inmediato (relativo al PC, el operando se indica a continuación de la instrucción), indexado (cuenta con dos índices de 16 bits: IX e IY, este es el modo más práctico para recorrer vectores), directo (paginado pero limitado a la página cero, de hecho no hay registro de página para escoger otra), relativo (utilizado para los saltos), inherente (los operandos están en registros), extendido (se indica la DRO: dirección real del operando en 16 bits).
  • Manejo de pila mediante un registro puntero de pila (SP: Stack Pointer). La pila puede ubicarse en cualquier lugar de la memoria, pero obviamente debe ubicarse en un área RW. Crecerá hacia las direcciones de memoria inferiores a la indicada en el SP.
Aquí lo vemos sonriendo para la foto:
Esta belleza es el modelo A1FN del HC11, incluye 256 bytes de memoria R/W, 512 de EEPROM y 8 Kbytes de ROM. Existen diversos modelos, cada uno con distintas configuraciones de memoria y características particulares.

Un concepto básico de la programación es la ejecución en secuencia de las instrucciones que componen el programa. En otras palabras, luego de cada línea se ejecuta la siguiente, a menos que... lo que se haya ejecutado altere esa secuencia. Conociendo el funcionamiento de una CPU vemos la relación de esto con el PC (Program Counter) y su incansable incremento. En Assembly la siguiente instrucción a ejecutar será siempre la apuntada por el PC. Cuando querramos generar bucles o sentencias condicionales tendremos que generar estos comportamientos en base a comparaciones y saltos (go to) condicionales.
Los programas en assembler se almacenan como texto simple (plain text in english, puedo discutir varias horas respecto a que plain significa simple en este contexto y no plano), lo que significa que podemos abrirlo con cualquier editor de texto (desde el insípido Notepad hasta algo más profesional como el Vim o Notepad++).
Al programar en alto nivel no nos preocupamos por muchas cosas que en bajo nivel necesitan más atención. Tal es el caso de la ubicación en memoria del programa. En alto nivel no nos preocupa porque es tarea del sistema operativo cargar el programa en memoria (lo que incluye encontrar dónde cargarlo o avisar al usuario que compre más memoria o cierre programas si no lo puede lograr). Sin embargo aquí no hay sistema operativo... será nuestra tarea determinar dónde se debe cargar. Dado que trabajaremos en un entorno simulado, siempre ubicaremos el programa en un área de memoria de solo lectura (ROM), manteniendo por separado las variables en un área de lectura-escritura (RWM). Siendo que no todo el espacio direccionable está implementado -ni siquiera en el entorno simulado- deberemos prestar atención a los rangos de memoria disponibles.
Contaremos con un puñado de directivas al ensamblador: sentencias que no serán ejecutadas por el procesador sino que le indicarán al ensamblador -en nuestro caso el incluido en THRSim- por ejemplo dónde ubicar cada porción de programa para asegurarnos de que vaya al tipo de memoria que queremos. Otras directivas se pueden emplear para generar valores constantes, sean escalares o vectoriales. Incluso podremos reservar memoria, en la aproximación más asintótica que hallaremos al uso de variables en la aventura con el HC11.
Los programas tienen una estructura de cuatro columnas, que podemos generar con espacios o tabulaciones de forma indistinta. Las columnas contienen:
Sin embargo estas columnas son meramente lógicas: no espere verlas delimitadas como si de una planilla o tabla se tratara. Note que todo lo que escribamos a continuación de los operandos se considerará comentario, por tanto será ignorado por el ensamblador. No hay necesidad de utilizar un carácter especial para indicar el comienzo del comentario o su final, aunque a algunos se les escapa el // y no hay nada malo en eso. Tampoco se requiere el uso de punto y coma para delimitar sentencias. No se usan llaves para los bloques, de hecho no hay bloques en el sentido pleno de la palabra, aunque podremos generarlos mediante el control de flujo.
Asimismo es posible generar comentarios de línea completa iniciando con el caracter * (asterisco), lo que le dirá al ensamblador que ignore esa línea.
En una siguiente entrega veremos un programa sencillo en el que intentaremos distinguir algunos de los conceptos aquí vertidos.
Una aclaración final: lo expuesto aquí aplica al HC11, un microcontrolador que no está pensado en emplearse con un sistema operativo dada su simpleza y el tipo de implementación en que se utiliza. Cuando se trabaja en assembler de otros procesadores es posible utilizar rutinas del BIOS y/o del sistema operativo para simplificar algunas de las tareas tales como la entrada/salida.

viernes, 9 de septiembre de 2016

¿Par o impar? ¿Cero o no cero? ¿Multiplo de algo? ¿Bit o no bit? (parte 4 y final)

En las entregas previas vimos la dinámica del CCR y cómo nos arreglamos para ver si un número es par, cero, etc. Nos resta aprovechar lo aprendido y aplicarlo a la determinación de múltiplos. Veamos en esta última parte de la serie cómo se logra esto.
Recordemos en primer lugar el concepto de múltiplo: un número es múltiplo de otro cuando puede expresarse como un producto de dos enteros siendo el segundo uno de los factores. Por ejemplo, el número 12 es múltiplo de 2 porque es el producto de 2 * 6. No debemos confundir múltiplos con potencias enteras. Las potencias enteras de un número obviamente también serán múltiplos del mismo, pero no son los únicos múltiplos. Pongamos por caso el 8, que es potencia entera de 2 y por tanto también múltiplo de 2. 
Nota: aunque el concepto de múltiplos se puede aplicar perfectamente a números fraccionarios, vamos a restringir las explicaciones al campo de los enteros.
¿Cómo se determina si un número (A) es múltiplo de otro (B)? El algoritmo matemático es sencillo: dividimos el número A por B y observamos el resto de la división... si es cero, significa que A es múltiplo de B. El asunto es que como hemos visto en otra entrada de esta serie, la división con el HC11 no es precisamente lo más sencillo de realizar. ¿Hay forma de verificar si un número es múltiplo de otro sin requerir de la división? A veces...

¿Cómo verificamos si un número es múltiplo de 2?
¡Esto equivale a determinar si el número es par! Hemos visto que basta con hacer un desplazamiento (shift) a derecha y observar si el LSB -copiado a carry- es cero o uno. Si es cero, entonces el número es par.

¿Cómo podríamos verificar si es múltiplo de 4?
Un número es múltiplo de 4 si sus dos dígitos menos significativos (los de menor peso) son cero. Por tanto bastaría con desplazar a derecha una vez, verificar que carry sea cero, volver a desplazar y verificar nuevamente si es cero. Algo así:

      ORG   0000
A     db    8      veremos si este numero es multiplo de 4
esMul RMB   1      y si lo es, lo indicaremos con un 1 en este

      ORG   $C000
      CLR   esMul  al indicador de multiplo lo ponemos en cero
      LDAA  A      cargamos el numero en el acumulador A...
      LSRA         ...y hacemos el primer desplazamiento a derecha
      BCS   fin    entonces LSB->Carry, si Cy=1, no es multiplo de 4
      LSRA         sino, volvemos a desplazar
      BCS   fin    y verificamos lo mismo. Si Cy=1, no es multiplo
      INC   esMul  sino, es multiplo de cuatro
fin   BRA   fin


Si quisiéramos determinar si es múltiplo de 8, es tan sencillo como seguir haciendo desplazamientos bajo la misma premisa del caso anterior (todos los bits menos significativos debieran ser cero). Este método es válido para determinar si un número es múltiplo de cualquier potencia entera de 2. 

¿Cómo podemos verificar si un número es múltiplo de cualquier otro?
Usando el método clásico: dividiendo. La instrucción IDIV realiza una división entera de 16 bits entre los operandos que ubiquemos en D (dividendo) y IX (divisor). El resultado queda en IX y el resto en D. Por tanto debemos verificar si D es cero y de esa forma determinamos si es múltiplo o no. Pongamos un par de números de ejemplo para facilitar el "copy-paste" al THRSim...

       ORG    $0000
num    FDB    23456  * quiero saber si este
mul    FDB    2      * es multiplo de este
esMul  RMB    1      * lo voy a indicar aqui 0=no 1=si

       ORG    $C000
       CLR    esMul
       LDD    num    * cargo el numero en num
       LDX    mul    * y el divisor en mul
       IDIV          * El resultado va a IX y el resto a D
       CPD    #0   
       BNE    fin
       INC    esMul
FIN    BRA    fin


Algunos comentarios adicionales:

Puede que esté pensando... ¿y no conviene directamente usar el algoritmo general en todos los casos? La respuesta es no por al menos dos razones: primero requerimos el uso de D (o sea de los dos acumuladores) y del índice IX, los que puede que tengan información que no podamos perder, por lo que habría que guardar su contenido y recuperarlo, por ejemplo si recorriéramos un vector. La segunda tiene que ver con la performance. Sume la cantidad de ciclos de cada rutina y me cuenta...

Detalle para quien copie el segundo fuente en el simulador: tenga presente que las directivas FDB cargan un valor en memoria solo cuando se ensambla. Por tanto si realizará sucesivas pruebas tendría que modificar directamente los valores de divisor y dividendo en memoria, o bien re-ensamblar con cada modificación del fuente.

Con esto cerramos la serie preguntona. ¿Alguna pregunta adicional? Me anticipo a una que debe girar en su mente en este momento...