jueves, 23 de marzo de 2017

Usar asm (esamblador) en un sketch de programación para Arduino

En la necesidad de optimizar un programa que fuera más rápido en la ejecución de un algoritmo, ya que usa los recursos para cifrar archivos, busque en la red como reducir el número de instrucciones que el procesador deberá realizar para ciertas tareas de la encriptación  busque como utilizar el lenguaje ensamblador insertado en el código de lenguaje C, y utilizando el entorno de programación del Arduino, buscando en las redes me enteré que la programación del Arduino hace uso de las librerías de lenguaje C de proyecto de código libre AVR y dentro de sus páginas web, encontré el manual de uso para insertar ensamblador(asm), dentro del código de lenguaje C, Inline Assembler Cookbook, aqui esta en inglés es un manual que dicho por su propio autor no esta completo, pero es una buena base para entender la forma de insertar ensamblador dentro del código C, y por lo tanto dentro de la programación del Arduino, después de tratar varios ejemplos y no logar mas que al compilar marcara errores en la programación, y navegando un poco mas empece a lograr poner ciertas lineas sin que aparecieran errores, es por eso que me di a la tarea (dentro de mis limitaciones tanto del inglés como de programación) de primeramente hacer una traducción  de el manual AVR sobre el uso de lenguaje ensamblador dentro del lenguaje C
Ya con la traducción y con un poco mas de entendimiento pude dentro del entorno de Arduino usar ensamblador en el programa sencillo .

En la imagen parte del sketch de ejemplo.
Este  genera el código similar al que se generado con el básico, Blink, que hace destellar (enciende y apaga) el led conectado a el pin 13 de la placa de Arduino, pero usando el lugar de la función delay( ), la función llamada asm_delay,que esta escrita, con instrucciones de ensamblador. La declaración del nombre de la función, es parte del lenguaje c y no hay cambios, el uso de uint8_t en el tipo de variable usada para el pasar el argumento, así uint8_t ms, es una variable de 8 bits sin signo es similar a escribir unsigned char ms, algo similar al declarar el tipo uint16_t solo que este es de 16 bits sin signo.
La declaración en lenguaje c que le indica al compilador que se hará uso de ensamblador, es la declaración asm volatile, esta le indica al compilador, que lo que seguirá entre paréntesis son instrucciones escritas en ensamblador y datos asociados a estas instrucciones, como son registros, o valores numéricos, su escritura general como si se usara una función pero con ciertas características especiales :

     asm volatile( : : : );

Esta sentencia esta seccionada en 4 partes divididas por la escritura de dos puntos ":" entre ellas, la primera, parte son las instrucciones que son leídas como cadenas de caracteres, escritas entre comillas, por ejemplo:

     "mov %A0 %A2"

la segunda parte los operandos de salida,  la tercera parte son los operandos de entrada , la ultima parte es opcional y en este ejemplo no es necesaria.
Así todo lo que esta entre la declaración de uso de ensamblador  asm volatile( );, le indica al compilador que son instrucciones en ensamblador.
Las instrucciones en ensamblador;
"L_dl1%="  "mov %A0, %A2" "mov %B0, %B2" L_dl2%" "dec %1" " brne L_dl!%="
provienen del conjunto de instrucciones AVR de Atmel ya que los procesadores de Arduino usan chips con este conjunto de instrucciones, ya que son fabricados por Atmel así por ejemplo:
"mov %A0, %A2"
lo encontramos como
Donde mov es la instrucción,  %A0 es la referencia a un registro en este caso Rd (registro destino) donde sera copiado %A1 que hace referencia el referencia al registro Rr que es el registro de donde sera copiado el dato, para incrustar código ensamblador dentro de c no se escribe literalmente como se hace al escribir un programa en lenguaje ensamblador exclusivamente. recomiendo que lea la traduccion del manual para entender como se hace.
Pero, vamos por partes, en la lista de instrucciones (primera parte):

"L_dl1%=:" "\n\t"
"mov %A0, %A2" "\n\t"
"mov %B0, %B2" "\n"
"L_dl2%=:" "\n\t"
"sbiw %A0, 1" "\n\t"
"brne L_dl2%=" "\n\t"
"dec %1" "\n\t"
"brne L_dl1%=" "\n\t"


Todas las instrucciones están puestas como cadenas de caracteres, por lo que están entre comillas, así que tenemos 16 cadenas de caracteres. La primera cadena de caracteres "L_d11%=:", es la referencia de una etiqueta, que debe ser única, así la literal L_ %=: indica una etiqueta, para ensamblador d11 es lo que diferencia la etiqueta de otra como d12, por lo que, "L_d11%=:" y "L_d12%=:" son etiquetas en ensamblador. La segunda cadena "\n\t" solo le indica al compilador que ponga una nueva linea \n y luego que haga un tabulador \t esto solo es para organizar nuestro codigo y que sea mas facil de entender, podemos quitarlas pero debemos poner todas las instrucciones en una sola linea de otra forma nos marcara errores. La tercera cadena "mov %A0, %A2"  es la instrucción  MOV Rd,Rr que copia el valor que hay en el registro Rr al registro Rd. , los valores o registros que serán usados en las instrucciones no se colocan directamente sino solo una referencia a ellos, esto es el caso de %A0,%A2, solo son referencias de las variables o valores usados, estos registros o variables están en la segunda y tercera parte de la sección de asm, recuerde que los separadores de dos puntos dividen la sección de asm, hasta en 4 partes, la segunda están los registros que serán escritos:
: "=&w" (cnt) 
y en la tercera los que serán leídos 
: "r" (ms), "r" (delay_count)
Ya que las referencias se van numerando desde el cero en adelante, aumentado de uno, a la variable cnt que es la primera en aparecer en la lista, se le asigna  %0, pero ya que es una variable de dos bytes (uint16_t) a cada byte se le asigna una letra iniciando con la letra A mayúscula, así el primer byte de la variable cnt se referencia como %A0, y el segundo byte como %B0, si fuera el caso y la variable sea de 4 bytes las referencias serían %A0, %B0, %C0, y %D0. La segunda variable que aparece en la lista es ms por lo que le corresponde %1, la tercera variable es delay_count como también es de 2 bytes de longitud, le corresponden las referencias, %A2 y %B2, asi que:
cnt                 %A0 %B0
ms                 %1
delay_count   %2A %2B
La quinta cadena es similar a la tercera, y la  sexta a la primera, la novena cadena "sbiw %A0, 1"  resta en 1 a la variable cnt, que solo esta referenciado el primer byte pero la instruccion sbiw es una instruccion que resta en los 2 bytes de la variable. La onceava cadena "brne L_dl2%="  salta si el resultado de la anterior operacion no fue cero hacia donde esta la etiqueta L_d12 y lo mismo hace la 15ª cadena pero hacia la etiqueta L_d11 , la cadena 13ª decrementa en 1 la variable ms uqe es la que tiene referenciada como %1.

El código completo de ejemplo, esta en seguida, se cargo en un Arduino Mega2560, pero el funcionamiento debe ser igual para el Arduino uno, o cualquier otra placa de Arduino, como es ensamblador hay que tener cuidado con los registros especiales que tiene cada chip usado por las diferentes placas de Arduino ya que  algunos no tiene los mismos registros especiales o las mismas capacidades de memoria , puertos o pines especiales como la ubicación del puerto serial, y otras características de cada uno de ellos, aunque en este ejemplo no debe haber problemas, siempre es bueno revisar las características del micro controlador.
El programa para el sketch en IDe de Arduino:



/*  retardo_asm
Este programa de arduino genera el codigo
similar al que se generado con el Basico de
Blink, que hace destellar (enciende y apaga) el
led conectado a el pin 13 del arduino
el código asm para este sketch se obtuvo de:
http://www.nongnu.org/avr-libc/user-manual/inline_asm.html
y una traducción al español en:
http://numeroscelestes.blogspot.mx/2017/03/instrucciones-de-ensamblador-dentro-de.html
que pueda servir como ejemplo de
inserción de código ensamblador, dentro del lenguaje C
*/
#define FRECUENCIA_RELOJ_CPU 16000000

uint16_t delay_count;      // la definicion del tipo de dato uint8_t es similar a usar  unsigned char
void asm_delay(uint8_t ms);   // prototipo de la función

void setup() {
   // inicializa como salida digital el pin 13.
  pinMode(13, OUTPUT);
  delay_count= FRECUENCIA_RELOJ_CPU/4000;

}

void loop() {
  digitalWrite(13, HIGH);   // Enciende el LED 
  asm_delay(250);           // función de retardo con ensamblador 
  digitalWrite(13, LOW);   // Apaga el LED 
  asm_delay(250);
  
}
/*------------------------------
retardo usando instrucciones en ensamblador (assambly)
*/
void asm_delay(uint8_t ms)
{
uint16_t cnt;      // variable cnt,tipo de datos de 2 bytes sin signo
asm volatile (
"\n"
"L_dl1%=:" "\n\t"
"mov %A0, %A2" "\n\t"
"mov %B0, %B2" "\n"
"L_dl2%=:" "\n\t"
"sbiw %A0, 1" "\n\t"
"brne L_dl2%=" "\n\t"
"dec %1" "\n\t"
"brne L_dl1%=" "\n\t"
: "=&w" (cnt)
: "r" (ms), "r" (delay_count)
);
}
// fin del programa ///
//////////////////////////////// /* una rutina equivalente para lenguaje ensamblador
    como ejemplo solamente
////////////////////////////////
#define delay_count 0C904h asm_delay: bucle_externo:   mov  r24, low(delay_count)  ; carga el valor en el par    mov  r25, high(delay_count) ; de registros r24_r25 bucle_interno:   sbiw r24,1                  ; resta 1 al par de registros r24_25   brne bucle_interno          ; No es cero regresa hacia bucle_interno   dec  ms                     ; decrementa la variable ms   brne bucle_externo          ; No es cero regresa hacia bucle externo   ret                         ; regresa de la subrutina */

No hay comentarios:

Publicar un comentario