En este post intentaré mostraros algunos tips sobre como ahorrar espacio en la memoria SRAM de tu Arduino. Como todos sabemos, nuestro Arduino está limitado de recursos, y es por eso que tenemos que utilizar al máximo nuestro ingenio y las herramientas que nos proporcionan para intentar no desperdiciarla.

Evitar uso de strings

El string es un tipo de variable muy cómodo, fácil de usar y con diversas funcionalidades, pero a cambio tiene el defecto de desperdiciar recursos. Aparte de desperdiciar ciclos de CPU, consume también más memoria y por lo tanto nos limitará mucho su uso en Arduino. Por ello, en lugar de utilizar Strings, deberemos utilizar char siempre que nos sea posible.

En el siguiente ejemplo:

String hello = "Hola Mundo";
char hello[] = "Hola Mundo";

Ambas declaraciones parecen igual y nos van a dar el mismo resultado, pero sin embargo, la opción del String nos consumirá más recursos que usando char.

Utilizar el tipo de variable adecuado

Utilizar el tipo de variable más adecuado para lo que queramos hacer es importante, ya que sino podríamos estar desperdiciando memoria SRAM. Por ejemplo, si prevemos que vamos a utilizar un número entre cero y cien, si usamos una variable de tipo float estaríamos desperdiciando memoria.

En el siguiente ejemplo:

int cien = 100;
float cien = 100;
double cien = 100;

En las tres opciones de arribar tendremos el mismo número, pero float y double consumirá el doble que int. Además de eso, si en lugar de usar int usamos char o byte, estaremos consumiendo la mitad.

Si deseas más información acerca de los tipos de variables, puedes pasarte por mi post Arduino y sus tipos de variables.

Utilizar Progmem

Progmem nos permitirá guardar variables en la memoria de programación en lugar de hacerlo en la SRAM, por lo que si tenemos variables que no van a cambiar en todo el programa y tenemos espacio de programa libre, podremos pasarlas a este liberando SRAM.

const PROGMEM uint16_t num = 512;
const PROGMEM uint16_t numsArray[] = { 500, 20, 12 };
const char holaMundo[] PROGMEM = {"Hola Mundo"};
const PROGMEM char holaMundo[] = {"Hola Mundo"};
const PROGMEM char character = "A"

Es importante utilizar la función correcta para recuperar los datos, ya que dependiendo de cuál usemos, nos devolverá un tipo de variable u otro.

Utilizando el ejemplo de arriba como base, extraeremos los datos de la siguiente manera:

for (byte k = 0; k < 5; k++) {
    displayInt = pgm_read_word_near(numsArray + k);
    Serial.println(displayInt);
}
for (byte k = 0; k < strlen_P(char ); k++) {
    myChar = pgm_read_byte_near(char + k);
    Serial.print(myChar);
}

En los ejemplos de arriba se puede observar la forma de recuperar variables del tipo word y byte de la memoria de programación, pero además de estas existen otras más, las cuales podemos ver en las referencias de abajo.

Para más información se recomienda la lectura de los siguientes enlaces, donde podremos encontrar la documentación oficial de cómo usar PROGMEM y las opciones de las que disponemos para poder extraer los datos.

Utilizar la función F con los strings

Si no nos queda más remedio que usar strings o tenemos mensajes Serial.print, podemos utilizar la función F (en mayúsculas) con cada texto en formato String para indicar que lo guarde en la memoria de programación en lugar de utilizar la SRAM:

Serial.println(F("Hola Mundo"));
Imagen en donde se muestra la cantidad de memoria que utiliza una simple frase sin usar la función F
Uso de memoria sin usar la función F
En esta imagen se puede observar cómo la función F ha reducido considerablemente el uso de RAM.
Uso de memoria usando la función F

Como se puede observar en las imágenes superiores, la función F tiene un gran impacto en cuanto al ahorro de memoria, siendo esta bastante notable. El consumo de SRAM sin su utilización es de 294 bytes, mientras que usándola es de tan sólo 188 bytes (una reducción de 106 bytes). Mientras tanto, observamos que el consumo de memoria de programación ha crecido tan sólo 12 bytes, por lo que el ahorro es considerable en ambos aspectos.

Agrupar booleanos en un byte

Por definición un booleano utiliza un byte de memoria, por lo que para indicar uno o cero estaremos consumiendo 8 bits. Si por ejemplo tenemos 8 diodos leds y queremos guardar su estado, utilizando booleanos estaremos consumiendo 8 bytes, mientras que si utilizamos bits podremos almacenar esa información en un sólo byte.

// Tenemos 8 leds y queremos guardar su estado
// 
// Con este método utilizaremos 8 booleans (8 bytes)
bool led1 = true;
bool led2 = true;
bool led3 = false;
bool led4 = true;
bool led5 = false;
bool led6 = false;
bool led7 = true;
bool led8 = true;

// Esto es equivalente, pero sólo usaremos un byte
// el 0b del principio es para indicar que se trata de bits
byte leds = 0b11010011;

Como se puede observar, hemos conseguido guardar la misma cantidad de datos en 8 veces menos espacio. Aún así el ejemplo de bits de arriba no sería correcto del todo, y ahora explicaremos el por qué.

Recuperar bits

Una vez que hayamos creado nuestra variable byte con 8 booleans, necesitaremos extraer cada uno de ellos para poder usarlos. Para ello usaremos la función bitRead, pero tenemos que tener en cuenta que esta función recupera los bytes de derecha a izquierda, y esa es la razón por la que el ejemplo de arriba no es correcto.

byte leds = 0b01010011;
if (bitRead(leds, 0)){
    Serial.println(F"El led 1 está encendido");
} else {
    Serial.println(F"El led 1 está apagado");
}

En el ejemplo de arriba recibiremos un mensaje diciendo que el led está encendido, ya que leerá el bit que está más a la derecha.

Cambiar bits

Al igual que podemos recuperar los bits, también podemos cambiar los que hay para cambiar el estado de un led por ejemplo. Para esto usaremos la función bitWrite, y al igual que bitRead, esta función también escribirá de derecha a izquierda:

byte leds = 0b01010011;
bitWrite(leds, 0, 0);
if (bitRead(leds, 0)){
    Serial.println(F"El led 1 está encendido");
} else {
    Serial.println(F"El led 1 está apagado");
}

En este ejemplo hemos cambiado previamente el estado del led uno a cero (apagado), por lo que el mensaje que recibiremos será que está apagado.

Otro método de cambiar el estado de los bits es utilizar bitwise, el cual es más cómodo ya que nos permitirá cambiar varios bits al mismo tiempo.

Usando operaciones bitwise

El bitwise and permite cambiar un bit a cero por ejemplo, manteniendo el estado del resto.

byte leds = 0b01010011;
// Bitwise AND: nos permitirá cambiar bits a 0
leds = leds & B11111110; // El resultado es 0b01010010

Para que entendamos mejor cómo funciona, utilizaremos una tabla para comparar:

ABResultado
010
111
010
111
010
010
111
100

Como podemos observar, el bitwise and devuelve uno si ambos bits son uno, y cero si alguno de ellos es cero.

Si por el contrario queremos convertir algún bit a uno, tendremos que utilizar el bitwise or el cual dará como resultado cero si ambos son cero, o uno si alguno de los dos es uno:

byte leds = 0b01010011;
// Bitwise AND: nos permitirá cambiar bits a 1
leds = leds & B10000000; // El resultado es 0b11010011

Al igual que antes, vamos a poner una tabla para poder ver mejor cómo funciona:

ABResultado
011
101
000
101
000
000
101
101

Como podemos observar, en este caso todos los ceros y unos se han mantenido, y ha cambiado el primer bit.

Espero que os haya servido para al menos ahorrar espacio SRAM en tu Arduino.

¡Un saludo!

Daniel Carrasco

DevOps con varios años de experiencia, y arquitecto cloud con experiencia en Google Cloud Platform y Amazon Web Services. En sus ratos libres experimenta con Arduino y electrónica.

Esta entrada tiene 2 comentarios

  1. Luciano

    Muy bueno, solo acordate que es SRAM, es estática no dinamica (RAM). Excelente comienzo 👏👏👏

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.