¿Sabías que las tarjetas Arduino tienen una cantidad de memoria limitada, y que en algún momento puede ser llenada en su totalidad?

El signo más obvio para detectar que la memoria se encuentra totalmente utilizada es cuando el compilador te dice que tu sketch es demasiado grande.Pero muchos problemas de memoria tienen síntomas mucho más sutiles. Tu programa puede que cargue, pero que no funcione, que se caiga o que simplemente actúe raro.
Si tu código de programación compila y carga satisfactoriamente, pero cualquiera de las siguientes afirmaciones es cierta, entonces hay una alta probabilidad de que te encuentres ante un problema de memoria.
“Mi programa funcionaba bien hasta que:” (elije una opción)
- “agregué otra librería”
- “agregué un par más de pixeles LED”
- “abrí un archivo en la tarjeta SD”
- “encendí un display grafico”
- “pegué otro sketch que ya funcionaba”
- “agregué una nueva función”
Si cree tener un problema de memoria entonces puedes saltar directamente a la sección “resolviendo problemas de memoria”, pero de igual manera te aconsejamos revisar los siguientes pasos de este tutorial, para entender mejor cómo funciona la memoria en Arduino y como administrarla de mejor manera.
Arquitecturas de memoria Harvard v/s Princeton:
En los primeros días de la computación electrónica, dos arquitecturas de procesador/memoria diferentes surgieron:
La arquitectura de Von Neumann (también conocida como Princeton) desarrollada para la computadora ENIAC, utiliza las mismas líneas de memoria tanto para data del programa como para almacenamiento.
La arquitectura de Harvard que caracterizada por la Harvard Mark 1, usaba memoria para datos y memoria para aplicaciones por separado.
¿Cuál es mejor?
Cada arquitectura tiene sus ventajas: el modelo de Harvard lleva la delantera en tanto se trata de performance, en cambio el modelo Von Neumann es mucho más flexible.
Híbridos modernos
Hoy en día la mayoría de los computadores multipropósito (PC, Mac, etc.) son diseñados con administración de memoria hibrida, lo que les permite tener lo mejor de ambas arquitecturas. En lo profundo de la CPU operan usando el modelo Harvard, usando memoria Cache separada para instrucciones y datos para maximizar performance. Sin embargo tanto el cache para instrucciones como para datos, se carga automáticamente desde un espacio de memoria común. Desde una perspectiva de programación estos computadores aparentan ser maquinas Von Neumann puras con muchos GB de almacenamiento virtual.
Microcontroladores
Los microcontroladores, como los que se incluyen en las tarjetas Arduino, están diseñados para aplicaciones embebidas. A diferencia de los computadores de propósito general, un procesador embebido típicamente se enfoca en realizar una tarea en específico de manera confiable, eficiente y a un costo mínimo. Los diseños de microcontroladores tienden a ser sencillos y a dejar de lado lujos como son el cache multi capa y memoria virtual, conceptos basados en disco y solo cuentan con lo esencial para realizar su tarea.
El modelo Harvard resulta una acertada opción para aplicaciones embebidas, como el Atmega328 presente en el Arduino UNO, el cual presenta una arquitectura Harvard casi pura. Los programas se guardan en la memoria flash y la data en la memoria SRAM.
En su mayor parte, el compilador y otros sistemas se encargan de organizar esto por ti, pero cuando se hace estrecho el espacio ayuda mucho conocer cómo funcionan las cosas en su interior, ya que con este tipo de máquinas tan pequeñas la memoria comienza a escasear mucho antes que en otros equipos.
En una escala totalmente diferente
La mayor diferencia entre estos microcontroladores y tu computadora de propósito general es la cantidad de memoria disponible. El Arduino UNO solo tiene 32K bytes de memoria Flash y 2K bytes de memoria SRAM, eso es más de 100.000 veces menos memoria que la incluida en PC de gama baja aun sin contar el disco duro. Es por estas razones que trabajar en este ambiente minimalista requiere aprender a usar estos recursos de manera sabia.
Memorias en Arduino
Existen 3 tipos de memoria en Arduino:
- Memoria Flash o memoria de programas.
- SRAM.
- EEPROM.
Memoria Flash
La memoria flash se usa para guardar la imagen de tu programa y la data inicializada. Puede ejecutar código de programa desde la memoria Flash, pero no se pueden modificar los datos contenidos en ella. Para modificar la data primero se deben copiar aquellos datos en la memoria SRAM.
La memoria flash usa la misma tecnología encontrada en los pen drives y memorias SD. Y además es una memoria del tipo no volátil, lo que significa que el programa no se borrara en caso de perder la energía.
La memoria Flash tiene un tiempo de vida de alrededor de 100.000 ciclos de escritura.
SRAM
La memoria SRAM o “Static Random Access Memory”, puede ser leída y escrita por el programa en ejecución y es usada para múltiples propósitos por el programa en ejecución.
- Memoria estática: este es un bloque de memoria reservado de SRAM para todas las variables globales y estáticas de tu programa. Para variables con valores iniciales el sistema copia los valores iniciales desde la memoria Flash cuando el programa inicia.
- Heap: se usa para datos dinámicos y crece desde la parte superior de los datos estáticos a medida que más ítem de datos se asignan a la memoria.
- Stack: se usa para variables locales y para mantener un registro de las interrupciones y las llamadas de función. El Stack crece desde el tope de la memoria hasta el Heap. Cada interrupción, llamada de función o direccionamiento de variable, hace que crezca el Stack. Volver de una interrupción o llamada de función reclamara todo el espacio que la misma usaba en el Stack.
La mayoría de los problemas de memoria ocurren cuando el Stack y el Heap colisionan. Cuando esto ocurre, una o ambas áreas de memoria se corrompen con resultados impredecibles lo que en algunos casos ocasiona una caída inmediata. En otros casos los efectos de la corrupción pueden no notarse hasta mucho después.
EEPROM
La EEPROM es otra forma de memoria no volátil que puede ser leída y escrita desde tu programa en ejecución. Solo puede ser leída byte a byte así que puede ser algo incomoda de usar. También es mucho más lenta que la SRAM y tiene un tiempo de vida finito de alrededor de 100.000 ciclos de lectura y escritura.
Comparación de memorias en Arduino
La tabla que se presenta a continuación muestra la cantidad de memoria disponible en algunas de las tarjetas Arduino:
Midiendo la cantidad de memoria usada
Una forma de diagnosticar problemas de memoria es medir cuanta memoria existe en uso.
Memoria Flash:
Medir la cantidad de memoria flash en uso es trivial ya que el compilador muestra ese dato al final de la compilación cada vez que se compila.
Memoria EEPROM:
De la memoria EEPROM se puede tener un control de un 100% ya que se debe escribir y leer cada byte en una dirección en específico, así que no hay excusa para no saber exactamente cuántos están en uso.
// ************************************************ // Write floating point values to EEPROM // ************************************************ void EEPROM_writeDouble(int address, double value) { byte* p = (byte*)(void*)&value; for (int i = 0; i < sizeof(value); i++) { EEPROM.write(address++, *p++); } } // ************************************************ // Read floating point values from EEPROM // ************************************************ double EEPROM_readDouble(int address) { double value = 0.0; byte* p = (byte*)(void*)&value; for (int i = 0; i < sizeof(value); i++) { *p++ = EEPROM.read(address++); } return value; }
Memoria SRAM:
El uso de memoria SRAM es más dinamico y por lo tanto mas difícil de medir. La función free_ram(), que aparece en el código de programación de más abajo, es una forma de hacer esto. Se puede agregar esta función en partes de tu código para que reporte la cantidad de SRAM libre.
int freeRam () { extern int __heap_start, *__brkval; int v; return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); }
Lo que reporta realmente esta función es el espacio entre el Heap y el Stack. No reporta la memoria no localizada que está entre medio del Heap. Este espacio no es utilizable por el stack y puede estar tan fragmentado que no será posible usarlo por muchas aplicaciones del Heap. El espacio entre el Heap y el Stack es lo que es necesario monitorear si realmente necesitas evadir choques del Stack.
Grandes consumidores de memoria
Existen dispositivos que requieren grandes cantidades de SRAM para operar. Algunos de los más grandes consumidores de memoria son:
Tarjetas SD:
Cualquier cosa con una interfaz SD o MicroSD requiere al menos 512 bytes de buffer de SRAM para comunicarse con la tarjeta.
Pixeles:
Cada pixel requiere solo 3 bytes de SRAM para guardar el color. Pero esos bytes se comienzan a sumar rápidamente cuando se usan varios metros de cinta o un arreglo grande de pixeles.
En un arduino UNO se pueden mover aproximadamente 500 pixeles asumiendo que no se utilizan gran cantidad de memoria en otros propósitos.
Display de matriz RGB:
Al igual que los pixeles, las matrices RGB requieren de varios bytes de SRAM.
El modelo de 32 x 32 pixeles requiere de aproximadamente 1600 bytes de SRAM y uno de 16×32 requiere de unos 800 bytes.
Display OLED monocromo:
Estos display solo requieren de 1 byte cada 8 pixeles, pero debido a su alta resolución aun necesitan de una alta cantidad de memoria.
Una pantalla de 128 x 64 requiere de alrededor de 1K de SRAM.
Pantallas LCD ST7565:
Al igual que los OLED monocromo, las pantallas LCD ST7565 necesitan de solo 1 byte por cada 8 pixeles pero tienen muchos pixeles por lo que requieren de un buffer de 1K.
Display e-ink:
Estas pantallas de alta resolución soportan modos básicos de dibujado que no requieren de buffer en el procesador, pero para ocupar al máximo sus capacidades graficas es necesario un buffer de SRAM.
La versión de 2” de este display requiere de 3K de SRAM así que solo es utilizable con un Arduino Mega.
Resolviendo problemas de memoria
La memoria es un recurso finito de estos pequeños procesadores y algunas aplicaciones son de plano muy grandes para un Arduino, pero la mayoría de los códigos tienen espacio para algo de optimización, así que si tu programa está llegando a niéveles elevados de memoria probablemente con algo de optimización pueda funcionar de forma correcta nuevamente.
Optimizando memoria de programa
Cuando compilas tu código, el programa IDE te mostrara que tan grande es la imagen de tu codigo. Si es que has llegado o excedido el espacio disponible, algunas de las siguientes optimizaciones te ayudaran a disminuir tu memoria utilizada.
Remover código muerto
Si tu proyecto es una combinación de código de varias fuentes, entonces existe la posibilidad que partes de él no se estén usando y puedan ser eliminadas para ahorrar espacio.
- Librerías en desuso: ¿Se están usando todas las variables #include?
- Funciones en desuso: ¿Se está llamando a todas las funciones?
- Variables en desuso: ¿Se están usando todas las variables declaradas?
- Código inalcanzable: ¿Existen expresiones en el código que nunca serán ciertas?
Nota: Si no estás seguro de algún #include, una función o una variable, entonces coméntala. El programa aun así compilara pero no se usara la programación comentada.
Consolidar código repetido
Si tienes la misma secuencia de código en dos o más lugares, considera hacerlos una sola función.
Eliminar el Bootloader
Si la memoria está realmente en su límite, entonces podrías considerar eliminar el Bootloader, lo que puede liberar entre 2 a 4K de Flash dependiendo del bootloader usado.
La desventaja de esto es que necesitaras cargar el código usando un programador ISP en vez de un cable USB estándar.
Optimizando SRAM
La SRAM es el recurso más preciado de Arduino y la causa más común de los problemas de memoria en el mismo, además de ser los más difíciles de diagnosticar. Si tu programa está fallando de una manera inexplicable, entonces son grandes las posibilidades de que tu programa se caiga por falta de SRAM.
Existe una cantidad de cosas que se pueden hacer para reducir la cantidad de SRAM usada y estas son algunas de las que puedes aplicar fácilmente:
Remover variables en desuso
Si no estás seguro si una variable se está o no utilizando, puedes comentarla y compilar nuevamente tu código. Si tu código funciona correctamente, puedes eliminarla ya que no se estaba utilizando dicha variable.
Transforma en F() esos Strings!
Los string literales son los más comunes causantes de problemas de memoria ya que usan espacio en la imagen del programa en Flash y luego son copiados a la SRAM como variables estáticas. Esto es un horrible desperdicio de SRAM ya que nunca los vamos a modificar.
Paul Stroffregen del PJRC desarrollo la macro F() como una simple solución a este problema, lo que hace esta función es decirle al compilador que guarde el string en PROGMEM. Todo lo que debes hacer es encerrar el string en la macro F().
Por ejemplo, intenta reemplazar esto:
Serial.println("Sram sram sram sram. Lovely sram! Wonderful sram! Sram sra-a-a-a-a-am sram sra-a-a-a-a-am sram. Lovely sram! Lovely sram! Lovely sram! Lovely sram! Lovely sram! Sram sram sram sram!");
Con esto:
Serial.println(F("Sram sram sram sram. Lovely sram! Wonderful sram! Sram sra-a-a-a-a-am sram sra-a-a-a-a-am sram. Lovely sram! Lovely sram! Lovely sram! Lovely sram! Lovely sram! Sram sram sram sram!"));
Te ahorrara 180 bytes de SRAM!
Reserva tus strings
La librería string de Arduino te permite reservar espacio en buffer para un string usando la función reserve(). La idea de esto es prevenir que un string fragmente el Heap usando el reserve, para pre direccionar la memoria para un string que crece.
Con la memoria ya direccionada, el String ya no necesitara llamar a la función realloc() si el string crece en tamaño. Para la mayoría de los usos se usan un montón de objetos string temporales a medida que se ejecutan las operaciones del programa lo que fuerza a que el nuevo string se conforme dejando un vacío donde se encontraba anteriormente usualmente lo único necesario es usar la función reserve() en cualquier string de larga vida que sepas que va a aumentar de largo a medida que se procese el programa.
Mover data constante a PROGMEM
Los ítems de datos declarados como PROGMEM no se copian a la SRAM en partida. Son un poco menos convenientes para trabajar, pero pueden liberar una gran cantidad de SRAM.
Reducir tamaños de buffer
- Direccionamiento de buffer y arreglo: Si direccionas un buffer asegúrate de que no sea más grande de lo que necesita ser.
- Buffers en librerías: Notar que algunas librerías direccionan buffers que pueden ser candidatos para un recorte.
- Buffers de sistema: Otro buffer escondido en lo profundo del sistema es el bus serial de 64 byte para recepción de datos. Si tu sketch no está recibiendo datos seriales de alta velocidad entonces, probablemente puedas cortar este buffer a la mitad o incluso menos.
- El buffer serial está definido en HardwareSerial.cpp, archivo que puede ser encontrado en el archivo de instalación de Arduino: ….\Arduino-1.x.x\hardware\arduino\cores\arduino\HardwareSerial.cpp
- En ese archivo busca la línea #define SERIAL_BUFFER_SIZE 64, y cámbiala por 32 o menos.
- Reduce variables sobredimensionadas
- No uses un “float” cuando necesites un “int” y no uses un “int” cuando con un “byte” sería suficiente. Trata de usar el tamaño de datos más pequeño posible para mantener la información.
Piensa globalmente y direcciona localmente
Veamos nuevamente como es usada (y abusada) la memoria SRAM.
Variables globales y estáticas
Las variables globales y estáticas son las primeras cosas en cargarse en la SRAM. Estas variables acercan el inicio del Heap en dirección al Stack y ocuparan este espacio por siempre.
Direcciones dinámicas
Los objetos direccionados dinámicamente causan que el Heap se acerque al Stack. Pero a diferencia de las variables globales y estáticas, estas si pueden ser re-direccionadas para salvar espacio. Pero esto no necesariamente causa que el Heap disminuya de tamaño, ya que si existen otros datos dinámicos sobre este último entonces el final del Heap no se moverá. Cuando el Heap está lleno de vacíos, entonces se le llama un Heap fragmentado.
Variables locales
Cada llamada de función crea una trama en el Stack que hace que este crezca en dirección hacia el Heap, cada trama del Stack contendrá:
- Todos los paramentos que ocupa la función.
- Todas las variables locales declaradas en la función.
Estos datos son utilizables dentro de la función, pero el espacio se reclama en su totalidad al existir la función.
Notas:
- Evadir el direccionamiento dinámico de datos para el Heap, lo que puede fragmentar rápidamente el limitado espacio del Heap.
- Preferir direccionamiento local a global ya que las variables solo existen cuando son usadas. Si tienes variables que solo se ocupan en una pequeña sección de código, entonces considera transformar este en una función y declara las variables locales a la función.
Uso de EEPROM
La EEPROM es una memoria de almacenamiento que funciona bien para guardar datos de calibración o constantes que no son prácticos de guardar en memoria Flash.
Es inusual quedarse sin memoria EEPROM, ni tampoco es practico usarla para descargar datos de la memoria SRAM pero se mencionara igualmente a objeto de completar el tutorial.
Para usar la memoria EEPROM es necesario importar la librería EEPROM:
#include <EEPROM.h>
Esta librería entrega dos funciones:
Uint8_t read(int) //Lee un byte de la dirección especifica de la memoria EEPROM. Void write(int, unint8_t) //Escribe un byte en la dirección especifica de la memoria EEPROM.
Notar que mientras las lecturas son ilimitadas, existe un número finito de ciclos de escritura (típicamente unos 100,000).