Diferencias entre programar en assembler y CCS C


Pretendo en esta página mostrar un punto de vista siempre mencionado pero nunca cuantificado, la eficiencia de compilación y el aprovechamiento de espacio en la memoria de programa y memoria RAM de un microcontrolador comparando programas de idéntica función, misma procedencia, mismo microcontrolador y distinta herramienta de programación. Antes de comenzar siquiera quiero expresar que no es mi intención hacer lobby por tal o cual lenguaje de programación, a la vista está que hoy por hoy el lenguaje C acapara el 90% de los lenguajes de programación para componentes electrónicos. No es poca casualidad que tanto Microchip como los fabricantes de otros microcontroladores actualmente en mercado opten por esta herramienta y menos aún que Arduino también lo haya adoptado en su IDE. Cuando hice mi primer capacitación allá a fines del siglo pasado el primer programa que nos propuso hacer el docente fue el LED que titila, sería el famoso HOLA MUNDO de los programadores de computadoras! Hoy, habiendo pasado 30 años desde mis primeras armas y también luego de servir una década dando clases creo que es mas conveniente que el primer programa no involucre esperas de tiempos porque para quien empieza de cero, que nada conoce, que ignora cuestiones como registros o espacios de memoria donde contar y tiempos de máquina o instrucción conviene empezar por algo mas simple basado sólo en estados. Es por ello que cuando enseñaba microcontroladores a principios de este siglo los alumnos asistentes debían comprender el programa para este circuito:

Este circuito proveniente de una captura de pantalla de Proteus VSM (pueden encontrar el programa para bajarlo en la sección correspondiente del sitio) representa en resumidas cuentas una compuerta inversora de estado puesto que cuando el pulsador de la izquierda no está presionado la resistencia de valor medio (10K) hace circular una pequeña pero constante cantidad de corriente hacia el pin 3 del microcontrolador, denominado GP4. Este pin entonces se encuentra en estado lógico alto o en 1 cuando el pulsador no está presionado o está suelto. Al presionar el pulsador el pin 3 del microcontrolador pasa a 0 voltios porque el pulsador es un cable o puente mecánico firme, no es una resistencia como sucede con la puesta a 5V. Es entonces que si se presiona el botón se obtiene un valor bajo o un 0 en el terminal GP4 del uC. El LED está conectado al terminal GP0 o pin 7 del microcontrolador; configurando este terminal como salida y poniendo un 1 en el bit apropiado dentro registro que corresponda el pin 7 tendrá 5V y el LED encenderá. Caso contrario con un 0 o nivel bajo en GP0 el LED se apaga. Para comandar el encendido del LED dependiendo del estado del pulsador entonces debemos ver si GP4 está en alto GP0 debe estar en bajo y, por el contrario, si GP4 está en bajo GP0 debe ir a 1. Empecemos por el ejemplo en assembler.

Vemos un programa corto, compacto, simple de seguir porque claramente es algo que no ocuparía en verdad un microcontrolador, esto es trabajo para un transistor cualquiera! La primer línea __CONFIG no es una instrucción como tal y no ocupará espacio en la memoria de programa del chip, su función es indicarle al compilador (programa encargado de convertir el código escrito en assembler a código de máquina o 0's y 1's) que usaremos el oscilador interno del cual dispone este componente evitándonos tener que usar un elemento oscilante externo y que no utilizaremos como entrada de reinicio del programa (o RESET) el pin destinado para tal fin por lo cual no será necesario cablearlo. Otras indicaciones que está estableciendo es que no utilizaremos el sistema de vigilancia ante cuelgues o watchdog timer y no queremos proteger contra lecturas la memoria de programa. Estas opciones pueden verse en la página 42 de la hoja de datos del microcontrolador provista por el fabricante. Explicado el uso de __CONFIG seguimos con las dos primeras instrucciones de verdad del programa que se encargan de establecer como entrada GP4 (donde está conectado el pulsador) y deja como salidas los demás pines en uno de ellos es donde esta conectado el LED (GP0). Estas dos simples lineas y la directiva __CONFIG es todo lo que tenemos que hacer en cuanto a puesta en marcha del microcontrolador si se quiere, por demás simple! El programa itera o repite constantemente un bucle entre la etiqueta CICLO y la instrucción goto CICLO, las cuatro instrucciones que siguen son: mirar el pulsador y saltearse la siguiente instrucción escrita si está suelto, encender el LED, mirar el pulsador y saltear la siguiente línea si está presionado y apagar el LED. Entonces si tengo un 1 en GP4 el microcontrolador pondrá un 0 en GP0 y si tengo un 0 el GP4 el microcontrolador pondrá un 1 en GP0. No hay otro misterio, es todo el programa!

Una vez compilado podemos usar el programador que tengamos en nuestro taller, yo tengo varios pero prefiero el PICkit 2. Al abrir el archivo HEX entregado por el compilador nos encontramos con una ocupación de la memoria de programa de tan solo siete palabras apenas por sobre el 1% del espacio total disponible de 512 palabras! Este programa se ha probado tanto en simulación como en un protoboard y funciona como debe ser.

Esta es la contrapartida en el C de CCS, el mejor C disponible hoy día para microcontroladores PIC a mi criterio. Sin entrar en demasiado detalle porque esto no es un curso de programación vemos que hay básicamente dos etapas del programa las líneas previas al conjunto formado por void main() y lo que sigue entre llaves donde nos encontramos todo lo necesario para inicializar el compilador y configurar microcontrolador y pines de E/S y luego dentro de ese bloque main() tenemos un bucle infinito while(true) haciendo que el programa quede funcionando entre sus llaves y no saliendo mas de ahí dentro. Que se hace ahí dentro ? Bueno, quien entienda ingles lee y ya cae en la ficha: SI la entrada PIN_B4 presenta un estado bajo o cero poner en la salida PIN_B0 un estado alto o 1. DE LO CONTRARIO poner en la salida un estado bajo o cero. Es todo, parece mas chico, se lo ve mucho mas entendible y eso que comprendo assembler. Quien no tenga práctica con archivos ASM no me quiero imaginar lo que debe estar pensando... Lo cierto es que luego de compilar ya nos va tirando algún que otro dato preocupante y podríamos decir que es una guarangada que le demande el 3% de memoria de programa, mas del doble que lo demandado en ASM pero lo que me llama poderosamente la atención es que necesite espacio en RAM!!!! Para hacer que ? Que se necesita poner en RAM para leer un pin y actuar sobre otro pin ? Si no estoy contando, midiendo, leyendo, haciendo NADA que requiera datos a almacenar en forma temporal... En fin, el compilador trabaja en forma oculta al programador uno no sabe a ciencia cierta que hace y el resultado es este:

Quince palabras ocupadas en vez de las siete ocupadas por ASM. Y todavía no me puse a ver que posiciones de memoria RAM ocupó y para meterles que información si sólo es chequear bits y actuar sobre bits... Y si, pueden decirlo! En el programa en CCS C me olvidé de poner el fusible para el oscilador interno pero por alguna razón que no estoy seguro pero intuyo tenga que ver con los parámetros por omisión del compilador al archivo HEX  resultante de la compilación le quedan en los apropiados bits seteados y limpiados para oscilador interno sin salida de clock. De todas formas es un FUSE o directiva de compilador que no va a la memoria de programa del micro y no ocupa espacio de código sino que establece parámetros a la hora de grabar el microcontrolador.

Chau pulsador, quedó sólo el LED conectado a través de una resistencia como estaba antes y en la misma terminal de antes, GP0 ahora toca el turno del intermitente. Los que tengan conocimientos de assembler van a ver el código que sigue un poco familiar, es la misma rutina de demoras clavando el microcontrolador en eso (técnica casi nunca recomendable) usada en la gran mayoría de los sitios de hace diez o mas años. Una rutina muy eficiente en la relación de espacio ocupado en la memoria de programa, cantidad de registros de RAM ocupados, precisión y muy utilizable porque nos va a permitir hacer demoras de hasta 8 bits/milisegundo o sea de 1 a 255mS.

La parte inicial del programa no amerita comentarios extra, sólo cambia que no necesitamos GP4 como entrada así que todo el puerto va como salidas. Usamos dos posiciones de memoria para almacenar datos (números de ocho bits que pueden ir de 0 a FF o de 0 a 255 o de 0 a 11111111 se lo mire con el sistema de notación que se lo mire) y le asignamos dos nombres de referencia que sólo son para que nos sea mas fácil recordar donde quedan en la memoria RAM. Esto tampoco ocupa espacio de memoria de programa ya que el compilador reemplaza cada vez que encuentre en el código TIEMPO1 pone en el código de máquina 0x07 y donde encuentra TIEMPO2 escribe 0x08. El programa en sí es poca cosa, un bucle continuo que sucede entre la instrucción que enciende el LED y que tiene la etiqueta CICLO delante y la instrucción GOTO CICLO que devuelve inexorablemente la ejecución a la parte superior donde se vuelve a encender el LED. Osea que enciende el LED, espera sin hacer nada por un cuarto de segundo, apaga el LED vuelve a esperar el mismo tiempo y repite encendiendo el LED. Entra a ejecutar lo que hay en DEMORA cada vez que se lo llama por medio de CALL DEMORA, lo que hay dentro de dicho segmento del programa es un ciclo anidado dentro de otro ciclo ambos calculados de tal forma que tras devolver la ejecución al programa principal al punto inferior inmediato a donde fue llamado pasaron 250 milisegundos. Y eso es todo, un LED que destella a intervalos regulares de tiempo.

Este programa demanda 19 posiciones o palabras en la memoria de programa, que como dije tiene 512 palabras de capacidad por ende ocupamos un 3,7% de su capacidad total y como bien vimos en el ASM ocupamos sólo dos posiciones de memoria RAM las destinadas a las mal llamadas variables TIEMPO1 y TIEMPO2. Demás está escribir que este código se probó tanto en Proteus VSM como en un protoboard físico y en ambos casos cumplió perfectamente su función.

La versión en CCS C sigue teniendo el mismo error que el programa del pulsador al no tener establecido el tipo de oscilador a usar y al igual que en dicho programa aquí también funciona igual! Se indica al compilador que vamos a hacer demoras basadas en ciclos de instrucción o básicamente lo mismo que en el ASM perder tiempo clavando (mal) el micro en ese evento, configuramos un pin de salida, PIN_B0 que vendría a ser la salida al LED hasta ahí el panorama esperable. Luego sigue el bloque main donde tenemos otro bucle eterno formado por el While(true) y el destello en si, encender el LED, esperar 250ms, apagar el LED, volver a esperar 250ms y sigue en eso. Ya vemos en los reportes de compilación lo distinto, RAM al 20% o sea que esta utilizando 5 de las 25 posiciones de memoria RAM disponibles contra 2 que usaba el programa en ASM y ROM o memoria de programa 6% que serían 36 palabras ocupadas contra las 19 ocupadas en la versión en assembler.

La vista del PICkit no deja lugar a duda, ocupa unas 36 posiciones de memoria de programa. Este ejemplo iba a quedar aquí y mirando el C mientras edito este documento me doy cuenta que no va a faltar el clásico comentario de los puristas del C diciendo "si vas a hacer una rutina en el assembler a la cual llamas desde dos lados porqué no haces una rutina en el C y la llamas desde dos lados en vez de meter DOS rutinas de demora con el mismo tiempo?" y saben que ? CLARO! Porqué no! Estamos en cuarentena, tenemos tiempo, mucho tiempo, demsiado tiempo y si, tengo muchas ganas de hacerlo y ver que pasa! Así que me pongo y lo hago YA MISMO.

Por cuestiones propias de las dimensiones de pantalla no salen en el IDE del CCS C las primeras tres líneas que son las mismas de los dos ejemplos anteriores en este mismo lenguaje. Tenemos ahora una función que se encarga de ejecutar una demora de 250 milisegundos y hacemos uso de la llamada a dicha función que es la misma tanto para el encendido como el apagado del LED.

Y aquí está, en directo para nuestros amigos mas puristas! Si, ocupa menos, sólo dos words menos pero a veces esos dos words marcan la diferencia entre un programa que cabe dentro de un micro y un programa que no cabe dentro de un micro! Y me ha pasado mas de una vez, por supuesto que principalmente por haber errado en la elección del micro claro esta pero pasar me ha pasado y he tenido que ponerme a quemarme las pestañas para hacer que entre a como de la forma! Mismo programa, mismo uso, mismo programador y misma filosofía de programación! Y tenemos una diferencia de 19 palabras en ASM y 34 palabras en CCS C lo mas optimizado que se me ocurrió. Si me pondría a hacer esta clase de programación a esta altura de mi vida, metiendo a delay_ms las pocas veces que la uso en una rutina llamable desde afuera ? No lo creo, menos por un ahorro tan poco significativo no siendo imperativo hacerlo pero la opción sin dudas esta.

Seguramente otros puristas con justo derecho me van a decir "pero no contemplas demoras dispares incluso mas allá de los ocho bits" y es cierto, tienen razón! Comparando por comparar podemos quedarnos toda la cuarentena comparando! Pero esa comparación la tenía pensada también! Mientras manejamos números que "entran" en ocho bits anduvimos fenómeno pero que pasa si queremos, por ejemplo hacer una baliza de supervivencia ? Esos simpáticos flashers que tiran tres disparos de LED bien cortitos bien TA! TA! TA! y esperan cinco segundos maximizando el tiempo de vida de la carga de la batería... A ver de que te disfrazas ahí con tu cochino TIEMPO1 y TIEMPO2 intentando hacer movlw d'5000'... En fin, manos a la obra!

Arranco en C esta vez sólo porque al momento tenía el CCS en ejecución y el simulador tiene también cargado dicho HEX así que evito algunos pasos, todo ahorro sirve! El programa si se quiere es muy básico también hace un ciclo que se repite tres veces dentro del for, uso una variable char y no int debido a que int es de 16 bits y demandaría dos posiciones de memoria mientras que char es de 8 bits por lo que sólo demanda una. Dentro de ese for dispara tres veces al LED en tiempos que se leen solos mas que bien, agotadas las tres repeticiones del for se queda en una demora de cinco segundos y repite. El resultado tras la compilación: 28% de RAM y 10% de memoria de programa es decir 7 posiciones de RAM y algo mas de 51 palabras de memoria de programa.

Así es 52 posiciones en la memoria de programa, tampoco es que creció desmesuradamente el código teniendo presente el tamaño de las magnitudes manejadas en la demora de cinco segundos, de seguro el compilador ha sido bastante eficiente al respecto. Veamos ahora cuan inventivo resulto haciéndolo en assembler.

Contando línea por línea me da 30 palabras ocupadas y eso que intenté aprovecharme de algunos trucos como condicionar cuanto tiempo dura la demora haciendo que en vez de ser un literal lo que cargo en W para meter en TIEMPO2 a W le meto el valor que quiero en cada etapa de donde es llamada la demora!. Del mismo modo para los cinco mil milisegundos busco un múltiplo menor que entre dentro de los ocho bits por eso el 250 y llamándolo 20 veces tengo el tiempo requerido. Lo notorio es que sólo necesité agregar una posición de memoria mas con respecto al LED que titila osea que estoy necesitando sólo tres posiciones de RAM contra las 7 que necesitó el CCS C para lograr el mismo resultado.

Y queda constatado que el programa ocupa sólo 30 posiciones de la memoria de programa en contraposición con las 52 ocupadas por su homónimo desarrollado en CCS C.

Quiero volver a remarcar que esto no es un "Los hombres de verdad programan en ASM!" nada mas lejos de la realidad, al contrario hoy día a menos que sea estrictamente necesario no se justifica de ninguna manera programar en ASM. Sería muy justo hacer otra comparativa o continuar una segunda versión de esta comparativa con funciones de cálculo. Hacer un medidor de tensión y corriente con display de LCD mostrando potencia y consumo... Te quiero ver haciendo eso en ASM, por supuesto que lo podemos hacer no es que el ASM no sirva sólo que hay que dedicarle muchísimo mas tiempo a la programación algo que en C con dos líneas está terminado! Cuando justifico el ASM ? Un cliente me pidió en algún momento un controlador que recibía impulsos de un captor y variaba la velocidad de un motor DC en función a la frecuencia de los pulsos. Hacerlo con un micro que tuviera captura, compara y PWM en C me hubiera demandado un chip de al menos 28 pines. Use un micro de estos de sólo ocho pines "midiendo" a mano el pulso en alto recibido y actuando también a mano en consecuencia y quedó resuelto. Por supuesto que dediqué mucho tiempo a dejarlo finito y que funcione como a mi me gusta pero al fin de cuentas quedó hecho con un micro bien pequeño que era prioritario en la aplicación. También ese mismo cliente me pidió un semáforo decorativo para regalarle a uno de sus clientes y por supuesto lo hice con un micro de estos de ocho pines que son mas baratos que andar comprando un 4017 y un 555 y juntar todo y encima lo hice en C con lo cual la programación me tomó minutos nada mas. Como no es un desarrollo comercial sino un regalo en breve creo que voy a subir el circuito por si a alguien le sirve.

Espero que no se hayan embolado leyendo estas líneas y que saquen algo útil del tiempo que me tomé, hace mucho rato quería hacerlo y esta cuarentena obligatoria me dio la excusa perfecta.

Pablo Canello, 26/MAR/2020