La memoria de un programa es bastante crítica a la hora de extender nuestros proyectos.
Cuando uno se dedica a programar algunos aspectos en esencia muy básicos como encender y apagar componentes o controlarlos, en principio no hay problema alguno.
Pero a medida que se avanza, y se llega a un nivel riguroso de detalle nos podemos encontrar con problemas asociados a la memoria. Y es que en la mayoría de aplicaciones Arduino dispone de muy poca memoria (32 KB).
Así que vamos a establecer una revisión de aspectos a tener en cuenta para evitar situaciones indeseables.
- Casos en los que la memoria es insuficiente
- Alternativas a las placas Arduino UNO estándar
- Librerías más útiles con un uso amplio de memoria
- Aspectos básicos de almacenamiento de memoría
- Consejos prácticos en la programación para ahorro de memoria
Casos en los que la memoria es insuficiente
Los casos en los que nos podemos encontrar con un problema de memoria en nuestra placa pueden ser:
Comunicaciones IOT
En un principio las comunicaciones IOT no suponen un problema para Arduino cuando la cantidad de datos son mensajes cortos en intervalos largos. Por ejemplo, la monitarización de un cultivo con distintos sensores; pero IOT nos ofrece posibilidades muy amplias que hacen sobrecargar Arduino con muchos recursos. Por ejemplo obtener datos de una página web con muchas lineas de código o de otra manera, crear una página web por si misma en una red.
Este caso suele estar asociado a la gestión del buffer interno desde puertos serie(Serial) de datos que entran y salen de la misma. Este puerto serie a veces es algo crítico si no se gestiona bien en memoria y muchas veces puede dar errores creando un caos de caracteres cuya única solución es disponer de un reset remoto.
Las soluciones a estos casos es un conjunto de medidas que llegan desde establecer un TimeOut definido a través de la función setTimeOut() ; limpiar el buffer cada vez que sepamos qué hemos obtenido la información deseada mediante la función flush() ; conocer la esencia de la clase Stream y gestionar bien los controles de lectura y escritura mediante captura de eventos con SerialEvent() o if (Serial.available()). Aún con todo, muchas veces puede dejar de funcionar si se satura y es que hay que recordar que el tamaño del buffer es de 64 bytes. Si un dato obtenido supera ese tamaño producirá un error.
Pantallas TFT
Las pantallas TFT son capaces de proporcionar gráficos de alta calidad. Las principales librerías ya ocupan la mitad de la memoría Flash, por lo que queda muy poca maniobra en la extensión de estas pantallas. Es el caso de una librería que estoy diseñando para crear modelos animados pixelados TFTCanvas.
La mayoría de imágenes se acceden a través de una tarjeta SD y tarda un rato en aparecer, por lo que no se pueden crear GIFs animados con tan poca memoria de almacenamiento. Para ello, hay que simplificar los recursos de imagen. Pero la dependencia de tantos datos en memoria interna de Arduino no es lo más recomendable.
Módulos SD
Los módulos SD son componentes capaces de acceder a la información de una tarjeta de almacenamiento gestionado en directorios y ficheros. Por una parte esta es la mejor solución para poder almacenar memoria, pero la librería que utiliza para establecer la comunicación por ICSP ya consume un 35% de la memoria ya que requiere de dos librerías. La comunicación por ICSP (SPI Library) y la librería SD para lectura y escritura de ficheros y gestión en carpetas.
Alternativas a las placas Arduino UNO estándar
Una solución rápida a esta problemática es la de no usar una placa Arduino estándar; ya que la placa Arduino UNO, utiliza el chip Atmega328 de Atmel con una memoria de 32KB de memoria Flash y 2KB de memoria RAM y la mayoría de placas de la familia Arduino utilizan este chip.
Para estos casos es mejor utilizar otras placas como:
- Arduino MEGA, MegaADK –> 256KB Flash, 8KB RAM
- Arduino ZERO –> 256 KB Flash, 32 KB RAM
- Arduino M0 PRO –> 256 KB Flash, 32 KB RAM
- Arduino 101 –> 196KB Flash, 24KB RAM
Otras placas especiales
- Arduino YUN –> 64MB RAM
- Arduino TIAN –> 64MB RAM, 16MB +4GB eMMC Flash
- Arduino Industrial 101 –> 16MD Flash, 64MB RAM
Librerías más útiles con un uso amplio de memoria
Las librerías que se utilizan más para según quáé aplicaciones pueden ser las siguientes con un consumo de memoria del programa:
- Adafruit_GFX –> 56%
- MPU6050 –> 46%
- ESP8266 –> 33%
- Ethernet –> 30%
- GSM_GPRS –> 25%
- LiquidCrystal –> 10%
- LiquidMenu –> 23%
- WiFi –> 20%
- XBee –> 16%
- SD Library –> 15%
- SPI Library –> 15%
- SoftwareSerial –> 10%
Aspectos básicos de almacenamiento de memoría
Existen 3 tipos de memoria en Arduino.
- La Memoria Flash
- La Memodria SRAM
- Static Data –> Variables estáticas
- Heap –> Variables dinámicas
- Stack –> Funciones y clases.
- La Memoria EEPROM
La memoria Flash (espacio del programa) es donde Arduino almacena el sketch o código del programa que se sube y ejecuta en la placa Arduino. Esta memoria es no volátil, si Arduino deja de ser alimentado eléctricamente los datos que haya en esta memoria permanecerán. Dentro de la memoria Flash se almacenan las estructuras de datos que se manejan fuera del setup y el loop. En este memoria se encuentran las librerías y es un indicador de la complejidad del programa a ejecutar.
La memoria SRAM (Static Random Access Memory ) es de tipo volátil. Es el espacio donde el programa almacena y manipula las variables al ejecutarse. Esta memoria es la que se crea y se destruye durante el transcurso del programa para evaluar los cambios que se producen. La información guardada en esta memoria será eliminada cuando Arduino pierda la alimentación.
Para esta memoria, el compilador no puede determinar si se saturará la memoria en el peor de los casos. Es por ello, que puede dejar de funcionar de forma imprevista, aunque se compile el programa y se suba a la placa. Si superamos el 80% de uso de esta memoría, el compilador nos avisará de que pueden ocurrir fallos durante su ejecución.
La Memoria EEPROM es un espacio de memoria que puede ser utilizado por los programadores para almacenar información a largo plazo. Este tipo de memoria es no volátil, por lo que los datos guardados en ella permanecerán aunque Arduino pierda la alimentación.
Consejos prácticos en la programación para ahorro de memoria
-
Tipos de dato
Entre los distintos tipos de dato que se pueden declarar, unos reservan mucho más espacio que otro. Es por ello, que debemos conocer qué límite máximo se va a requerir para unas u otras variables.
El modo más crítico en la declaración y manipulación de datos está en la definición como caracteres o texto.
Los vectores de caracteres son un buen ejemplo de este hecho, sobre todo relacionado con la comunicaciones. El texto se puede controlar mediante un vector de caracteres o mediante un String. El String ocupa más espacio, pero dispone de un mejor control de comunicación y manipulación que los vectores de caracteres. Ya que un vector debe de estar limitado con un tamaño definido del vector, cuando nuestro problema es que podemos asumir un rango variable de datos.
El vector de caracteres ocupa menos, pero si no se manipula bien es posible que encontremos problemas en los bucles y errores imprevistos que con un String se solucionan de forma rápida.
Existen modos de interpretación entre uno y otro como el método de String.toCharArray().
Dentro de una función es una buena combinación el uso de punteros para compatibilizar unos con otros.
void setup(){ Serial.begin(9600); readStr("Hola Mundo"); } void loop(){ } void readStr(char *data[]){ };
Las declaraciones #define son datos de variables constantes que se crean antes de que el programa se haya compilado. Estas constantes no ocupan espacio RAM, sino que son sustituidas automáticamente por el compilador en el código pasando a ocupar espacio de programa en memoria Flash que es más amplia.
Las declaraciones #include es una manera de insertar código de otras librerías al igual que #define desde el precompilador.
-
Uso del precompilador de manera condicional
Así como existen sentencias de precompilador para definir variables, también se pueden ejecutar instrucciones lógicas de manera que el precompilador añada o no funcionalidades al programa en función de distintas características de nuestro interés.
Por ejemplo, si queremos definir un PIN para transmitir información, distinto en función del tipo de la placa a programar.
#if defined(ARDUINO_AVR_ESPLORA) //PinOut for Arduino Esplora #define TFT_CS 7 //Chip Select #define TFT_DC 0 //Data/Command #define TFT_RST 1 #define TFT_SD_CS 5 #else //PinOut for Arduino UNO #define TFT_CS 7 //Chip Select #define TFT_DC 6 //Data/Command #define TFT_RST 5 #define TFT_SD_CS 8 #define SCLK 15 #define MOSI 16 #endif
-
Debug
A la hora de escribir una librería es muy útil dejar espacios desde los que poder visualizar qué es lo que está ocurriendo dentro de nuestra placa durante su ejecución. A través del monitor serie podemos visualizar los valores de distintas variables. Pero también hay que tener en cuenta que esta función Serial.print() consume recursos tanto en variables como en tiempo de ejecución.
Una de las soluciones es crear una variable booleana de debug que se gestione con un condicional, y de esta manera solo escribir por puerto serie los mensajes de interes durante su depuración y una vez solucionados desactivarlo.
bool debug = 1; if (debug) { Serial.print(F("Error opening CSV File --> ")); }
*Memories of an Arduino by Adafruit