Acelera y optimiza tu sketch Arduino

Publicado por Loli Diéguez en

En el artículo de hoy os traigo una recopilación de algunos trucos o consejos para acelerar vuestros skecth de Arduino, algunos son para ahorrar RAM, otros para simplificar y optimizar el código de tu sketch para que sean más fáciles de leer y otros son consejos puros de programación.

Son trucos que he ido encontrando a lo largo de los años y que a mí me han funcionado, creo que son útiles, así que me he decidido a ponerlos todos juntos.

Acelerar el acceso de E/S

En cada placa Arduino basada en AVR, la velocidad del reloj es de 16MHz. Cada instrucción en el controlador se completa en 4 ciclos, por lo que teóricamente, puedes hacer 4 millones de instrucciones en un segundo.

Sin embargo, la velocidad de E / S es mucho más lenta y esto se debe a las funciones digitalWrite y digitalRead.

Cada vez que ejecutas esas funciones, también se ejecuta una gran cantidad de código adicional. El código adicional es responsable de detectar y asignar los pines a un puerto de salida.

En cada microcontrolador, los dispositivos de E / S se asignan en grupos de 8 pines a los puertos, que son registros especiales en el controlador.

Por lo que te estarás preguntando, ¿Qué rápido son estas operaciones de E/S? la velocidad máxima de salida del siguiente programa es alrededor de 116-117KHz:

void setup()

{

   pinMode(13, OUTPUT);

}

void loop()

{

   digitalWrite(13, LOW);

   digitalWrite(13, HIGH);

}

Este sketch se puede mejorar envolviendo el código dentro del loop en un bucle infinito. Acelerará la ejecución un poco, porque la CPU tiene que hacer menos llamadas a funciones. El bucle mejorado sería este:

void setup()

{

    pinMode(13, OUTPUT);

}

 

void loop()

{

    while (1)

    {

        digitalWrite(13, LOW);

        digitalWrite(13, HIGH);

        // requerido si haces comunicación serial

        if (serialEventRun) serialEventRun();

    }

}

En las mediciones, la velocidad de salida mejoró subiendo a 126-127 kHz, un 8.6% más rápido, aunque se puede optimizar más accediendo a los puertos a bajo nivel, esto es algo que aun no he probado pero lo haré y comentaré los resultados.

Usa constantes, PROGMEM y variables globales

Si tienes que almacenar grandes cantidades de datos en tu código (como un mapa de bits), usa la variable global const e incluye la declaración PROGMEM.

Esta declaración le indica al compilador que ponga esta información en la memoria flash, en lugar de en la SRAM. Recuerda que PROGMEM es parte de la biblioteca pgmspace.h que está disponible solo en la arquitectura AVR, debes usarlo solo con los tipos de datos definidos en pgmspace.h

La sintaxis de esta declaración sería así:

const dataType variableName[] PROGMEM = {data0, data1, data3...}

Donde:

  • dataType – es cualquier tipo de variable
  • variableName – es el nombre que le damos al dato o matriz

Y un ejemplo sería este:

#include <avr/pgmspace.h>

//un array de 5120 byte, normalmente no podría ser almacenado en la RAM,

//pero con almacenamiento estático se grabará en la memoria del programa

   const byte big_array[5120] PROGMEM = {0};

Olvídate del IDE predeterminado

El IDE de Arduino es excelente, pero no está destinado al desarrollo de software profesional, cuando el número de líneas que tienes que desarrollar es grande, puede ser un poco incomodo este IDE.

Alternativamente, puedes usar Atmel Studio o Visual studio con el complemento Visual Micro. El complemento Visual Micro presenta un depurador, pero no es gratuito, lo podrás descargar desde este enlace.

También puedes usar un editor de texto para escribir tus sketch de Arduino. Desde la version 1.5, el IDE de Arduino admite la creación y carga del código del sketch a través de la línea de comandos.

La sintaxis de la línea de comando es la siguiente:

arduino --board (board type description) --port (serial port) --upload (sketch path)

arduino --board (board type description) --port (serial port) --verify (sketch path)

La placa se especifica mediante la etiqueta --board. El argumento pasado a –board debe ser de la forma package:architecture:board[:parameters], donde:

  • Para placas Arduino, pon package=arduino.
  • Para placas basadas ​​en AVR, incluidos Arduino Uno, Mega o Leonardo, pon architecture=avr.
  • Para placas basadas en SAM de 32 bits, incluido el Arduino Due, pon architecture=sam.
  • Para el Arduino Uno, pon board=uno, y para el Arduino Mega, board=mega.

El valor para cualquier placa dada se puede encontrar en el archivo boards.txt en la carpeta de arquitectura.

Los parámetros que se pasan por la etiqueta parameters son a menudo opcionales, pero se pueden usar para especificar una variante de una placa dada, como la variante mega168 de la placa Arduino Nano (en este caso, pon los parámetros de este modo cpu=atmega168).

Para especificar el puerto se hace por la etiqueta –port, este argumento debe ser de la forma /dev/deviceFileName, donde deviceFileName es el nombre del dispositivo al que se está conectando.

Puedes encontrar más información sobre el uso de la línea de comandos en el documento del manual Arduino IDE, que se encuentra aquí .

Poner cadenas de texto en la memoria ahorra RAM

La forma en la que gestionas las cadenas de texto puede suponer un alto consumo de RAM, para ahorrar memoria puedes declarar las cadenas de texto de este modo Serial.println(F("Hello World!")) en vez de este otro Serial.println("Hello World")

Cuando escribes Serial.println("Hello World!"); el texto '¡Hola Mundo!' se almacena en dos lugares:

  • Memoria de programa
  • Memoria de acceso aleatorio (RAM)

El contenido de RAM se pierde cada vez que se reinicia o se apaga Arduino, al contrario que la memoria del programa que se mantiene después de un reinicio o pérdida de energía, por lo que al comienzo de cada programa, las cadenas de texto se copian de la memoria del programa a la RAM para que tu programa las pueda usar.

Sin embargo, muchas placas Arduino no tienen mucha RAM: solo hay 2 kb en el chip 328 utilizado en el Uno, por ejemplo. Por lo tanto, es fácil quedarse sin RAM si tu sketch usa muchas cadenas u otras variables.

Para conseguir el ahorro de memoria RAM, el método para trabajar con cadenas de texto sería: Serial.println(F("Hello World!"));

La función println obtiene el texto de la memoria del programa directamente, utilizando un bufer temporal que no consume mucha RAM.

Usa constantes en tu sketch Arduino

El empleo de constantes en el código hace que los programas sean más fáciles de leer y de modificar, te pongo un ejemplo

Digamos que quieres molestar a tu compañero de piso con una sirena y un Arduino, para lo cual se te ocurre conectar una salida de tu placa Arduino a un zumbador, digamos a la salida 6, y harás que suene varias veces durante un tiempo.

Esto implica que tendrás que encender y apagar la salida 6 varias veces durante el tiempo que quieras que el zumbador haga su trabajo.

Si el zumbador está conectado al puerto 6, deberás introducir el 6 en tres lugares de tu código:

  1. Donde indicas el modo en el que el pin 6 trabajará.
  2. En la línea de código donde enciendes el zumbador actuando sobre la salida 6 poniéndola en HIGH
  3. Y en la línea de código donde apagas el zumbador actuando sobre la salida 6 poniéndola en LOW

Lo anterior está bien, pero si en un futuro quieres modificar la salida y conectar el zumbador a otro pin, tendrás que repasar el código en busca del 6. En vez de esto, define una constante en la parte superior del sketch de este modo.

// Definición de la variable que identifica al puerto donde se conecta el zumbador

const int BuzzerPort = 6;

// Ahora declaramos la frecuencia con la que el zumbador funcionaria.

const int BuzzerPeriod = 5000; // [milliseconds]

const int BuzzerOnTime = 100; // [milliseconds] 

void setup()

{

  pinMode(BuzzerPort, OUTPUT);    

}

void loop()

{

  digitalWrite(BuzzerPort, HIGH);

  delay(BuzzerPeriod);

  digitalWrite(BuzzerPort, LOW);

  delay(BuzzerOnTime);

}

Con esto, si ahora quieres conectar el zumbador en otro pin, por ejemplo, el 7, solo debes cambiar el puerto en un solo lugar.

Como ves, usar constantes hace que tu código sea mucho más claro. Cuando más adelante, necesites cambiar el código, inmediatamente sabrás que digitalWrite(BuzzerPort, HIGH) es la parte del código que encenderá el zumbador; antes, sin el uso de constantes, con digitalWrite(6, HIGH), habrías necesitado recordar lo que está conectado al puerto 6.

Entonces, ¿por qué usar una constante en lugar variables? ¿No funcionaria igual de bien usando solo int BuzzerPort=6?

Si funcionaría, de esta manera también tendrías solo un sitio donde cambiar el valor del puerto, pero cuando la 'variable' se declara como una constante, el compilador, la herramienta que convierte tu programa en algo que Arduino puede usar, no permite cambiarlo accidentalmente. Entonces, si tienes const int BuzzerPort=6 e intentas cambiar el valor, como en el sketch que aquí te pongo: 

void setup()

{

  BuzzerPort = 7;

  pinMode(BuzzerPort, OUTPUT);

}

El compilador dará un error. Entonces, con las constantes, puedes confiar que el valor nunca se cambiará después de que se establezca.

Los retrasos son aburridos 

La función delay es muy usada para dejar tu placa esperando durante un tiempo para hacer una acción, pero realmente estar perdiendo un tiempo muy valioso.

Mira el siguiente código, el clásico programa de parpadeo, este sketch pasa la mayor parte del tiempo sin hacer absolutamente nada:

 

void loop()

{

  digitalWrite(LedPin, HIGH);

  delay(1000); // oh! Nada pasa aqui.

  digitalWrite(LedPin, LOW);

  delay(1000); // tampoco aqui!

}

En el momento que se ejecuta la función delay, la placa estará esperando 1000 milisegundos antes de seguir ejecutando el programa.

Piensa en todas las grandes cosas que podrías hacer en ese tiempo muerto: leer sensores, actuar sobre una salida, conducir motores. Los retrasos pueden ser útiles, pero a menudo son solo una pérdida de tiempo.

Hay una manera fácil de evitarlo, pero necesitaremos algunas variables, mira el siguiente código:

// Marca de tiempo cuando el LED se encendió o apagó por última vez.

unsigned long LastLEDToggleTime = 0;

// Estado actual del LED; verdadero => encendido; falso => apagado.

bool IsLEDOn = false;

// Tiempo entre encender o apagar el LED

const int LEDTogglePeriod = 1000; // [milliseconds]

 // El pin al que está conectado el LED

const int LEDPin = 13;

 

Y el bucle se convierte en:

void loop()

{

  // Esto activará y desactivará el LED cada segundo, sin demoras

  if ((millis() - LastLEDToggleTime) > LEDTogglePeriod)

  {

    LastLEDToggleTime = millis();

    if (IsLEDOn)

    {

      IsLEDOn = false;

      digitalWrite(LEDPin, LOW);

    }

    else

    {

      IsLEDOn = true;

      digitalWrite(LEDPin, HIGH);

    }

  }

 

// y aquí es donde puedes hacer otras cosas con tu placa

}

 

Trucos con ternas

C ++ es el lenguaje que utiliza la plataforma Arduino, e incluye una fórmula para seleccionar entre dos valores, es lo que se conoce como terna y tiene esta expresión:

(expression) ? (true-value) : (false-value)

Esto se llama operador ternario y tiene 3 argumentos:

  • (expression), una prueba que es verdadera o falsa
  • (true-value), el resultado si (expression) es verdadero
  • (false-value), el resultado si (expression) es falso

Esta terna te permite escribir cosas como esta:

IsLEDOn = !IsLEDOn; // alternar el valor de IsLEDOn

digitalWrite(LEDPin, IsLEDOn ? HIGH : LOW)

Si no usaras lo anterior, el código se convertiría en esto otro:

    if (IsLEDOn)

    {

      IsLEDOn = false;

      digitalWrite(LEDPin, LOW);

    }

    else

    {

      IsLEDOn = true;

      digitalWrite(LEDPin, HIGH);

    }

 

Como ves ahorras mucho código, aunque esta forma de escribir el sketch lo puede hacer más incompresible, así que, si lo usas, documéntalo bien! 

Watch-dog, domar programas desbocados con el temporizador

El microprocesador del Arduino incluye un temporizador de vigilancia o watch-dog. Este temporizador es una especie de interruptor de hombre muerto.

Si tu sketch no le dice al watch-dog que todo está bien, la placa se reiniciará.

Para usar este mecanismo, deberás hacer esto:

  1. incluir una biblioteca
  2. habilitar el watch-dog
  3. decirle al watch-dog que todo está bien

Este ejemplo debería funcionar en cualquier Arduino que use un microcontrolador de Atmel, incluidos Uno, Mini y Mega.

Ten en cuenta que los Mega anteriores tenían un problema en su gestor de arranque. Si configuras el tiempo de espera del watch-dog demasiado bajo, no podrás reprogramar el Arduino utilizando el cargador de arranque en serie. Actualiza al último cargador de arranque Mega para arreglar eso.

Un ejemplo de watch-dog seria este:

#include wdt.h

unsigned CountDown = 10;

void setup()

{

  Serial.begin(9600);

  // habilitar el temporizador del watchdog y configura el contador.

  // Si no llamamos a wdt_reset () antes de que se acabe el tiempo, el programa se reiniciará automáticamente.

  wdt_enable(WDTO_1S); // WDTO_1S => 1 segundo.

}

void loop()

{

  // el programa esta vivo...por ahora.

  wdt_reset();

 

  while (CountDown >= 0)

  {

    Serial.println(CountDown);

    CountDown = CountDown - 1;

    delay(30);

  }

 

  Serial.println(F("Kaboom!"));

}

Este programa contiene un error y es que la cuenta regresiva nunca terminará. El programa se atascará en el buble while.

Solo se llama a wdt_reset() la primera vez, al entrar en loop, lo que provoca que el temporizador del  watchdog se agote y el programa se reinicie, es la única manera de salir del bucle while.

Espero que estos trucos te puedan ayudar y comparte, si te ha gustado.

 


Compartir esta publicación



← Publicación más antigua Publicación más reciente →