guardar-variables-entre-reinicios-con-arduino-y-la-memoria-no-volatil-eeprom

Guardar variables en Arduino y la memoria no volatil EEPROM

A estas alturas ya debemos saber que al reiniciar Arduino toda la memoria donde almacenamos las variables se borra, por lo que no es posible conservar los valores.

Sin embargo, en ocasiones querremos que ciertos valores se conserven entre reinicios. Por ejemplo, valores de calibración, mediciones y fechas u horas para hacer dataloggers, guardar un contador, o saber cuál era el estado del procesador cuando perdió la alimentación, entre otros muchos.

Para tales efectos, Arduino incorpora una memoria de almacenamiento no volátil denominada EEPROM (Electrically Erasable Programable Read-Only Memory)

Por tanto, en Arduino tenemos tres tipos de memoria

  • FLASH, no volátil, donde grabamos el sketch (incluido el bootloader).
  • SRAM (static random access memory), volátil, donde se almacenan las variables durante el funcionamiento.
  • EEPROM, no volátil, que podemos usar para almacenar información entre reinicios.

Características de la EEPROM

La memoria EEPROM tiene sus propias características y peculiaridades que la distinguen del resto de memorias. En primer lugar, y más evidente, es no volátil, es decir, mantiene los valores almacenados cuando se pierde la alimentación.

Por otro lado, la memoria EEPROM es un recurso más escaso que el resto de memorias. La mayoría de modelos de Arduino disponen de 1KB, mientras que el Mega tiene 4KB.

Una desventaja de la memoria EEPROM es que es mucho más lenta que la memoria SRAM. El proceso de escritura de una celda (byte) cuesta en torno a 3.3 ms. El proceso de lectura es mucho más rápido (aunque sigue siendo más lento que la SRAM), leer 1024 bytes cuesta en torno a 0.3ms, es decir, 10.000 veces más rápida que la escritura.

Otra particularidad de la memoria EEPROM es que tiene una vida limitada, que se reduce con cada operación de escritura. No existen límites para las operaciones de lectura.

Las especificaciones garantizan que cada celda tiene una vida útil de al menos 100.000. Aunque en la práctica puede llegar a ser muy superior, hasta un millón de operaciones, por encima de 100.000 el funcionamiento no está garantizado.

100.000 operaciones de lectura pueden parecer muchas, pero hay que tener en cuenta que son

  • Apenas 5 minutos si, por error, grabamos constantemente.
  • Aproximadamente un día, si grabamos cada segundo.
  • Unos 27 años si grabamos 10 veces al día.

Es decir, la memoria EEPROM está pensada para realizar escrituras con tiempos largos entre ellas, no un uso constante de la misma.

Funciones para usar la EEPROM

El IDE Standard de Arduino incorpora una librería EEPROM.h que incorpora las funciones necesarias para manipular la memoria no volátil de Arduino.

Las funciones más simples son la función Read y Write que, respectivamente, leen y escriben un byte en una dirección de la memoria. La dirección de memoria podrá tener valores de 0 a N-1, siendo N el número de bytes disponibles (ejemplo, 0 a 1023 en Arduino Uno y Nano, 0 a 4095 en Arduino Mega).

//Lee un único byte de la dirección address
EEPROM.Read(address) 

//Lee un único byte de la dirección address
EEPROM.Write(address)

Las funciones anteriores graban un único byte pero, frecuentemente, necesitaremos guardar variables que tienen un tamaño superior a un byte. Para ello disponemos de las funciones Put, Get y Update, que son las que usaremos con mayor frecuencia.

// Funciones para variables completas (tiene en cuenta el tamaño de la variable)
//Lee una variable en la dirección address
EEPROM.Put(address, variable) 

//Lee una variable en la dirección address
EEPROM.Get(address, variable) 

EEPROM.Update(address, variable) //Actualiza el valor de una variable, es decir, primero la lee, y sólo la graba si su valor es distinto del que vamos a guardar. Esto favorece a reducir el número de escrituras, y alargar la vida útil de la memoria.

Las funciones Put, Get y Update tienen en cuenta el tamaño de la variable, y funcionan incluso con variables y estructuras definidas por nosotros. Sin embargo, tendremos que tener en cuenta el tamaño de la variable para saber cuál es la siguiente dirección a escribir, y evitar que se “solapen” las variables.

Ejemplos de código

Iterar en toda la EEPROM

En el primer ejemplo, empleamos la librería EEPROM para grabar 0 en toda la memoria. En general no emplearemos este tipo de código porque supone una operación de escritura en toda la memoria, que recordemos tiene un ciclo de escritura limitado.

Pero el objetivo es presentar el uso de la función EEPROM.length() para obtener la cantidad de memoria disponible (independientemente del modelo) y el uso de EEPROM[index] para acceder a las posiciones de memoria.

#include <EEPROM.h>

void setup(
{
  for( int index = 0 ; index < EEPROM.length() ; index++ )
  {
    EEPROM[ index ] = 0;
  } 
} 

void loop()
{
}

Grabar una variable en la EEPROM

En este ejemplo, realizamos la escritura de una variable. En el ejemplo grabamos una variable float, pero podría ser cualquier otro tipo de variable. Empleamos una función ReadSensor() que emula una función que podría, por ejemplo, leer un sensor de temperatura, un sensor de potencia, o cualquier otro tipo de sensor.

Observar el empleo de la función sizeof() y length() para obtener la posición de la siguiente casilla a escribir.

#include <EEPROM.h>

float sensorValue;
int eeAddress = 0;

//Funcion que simula la lectura de un sensor
float ReadSensor()
{
  return 10.0f;
}

void setup()
{
}

void loop()
{
  sensorValue = ReadSensor(); //Lectura simulada del sensor
  EEPROM.put( eeAddress, sensorValue );  //Grabamos el valor
  eeAddress += sizeof(float);  //Obtener la siguiente posicion para escribir
  if(eeAddress >= EEPROM.length()) eeAddress = 0;  //Comprobar que no hay desbordamiento

  delay(30000); //espera 30 segunos
}

Grabar una estructura en la EEPROM

En C++ y, por tanto, en Arduino, podemos definir nuestros propios tipos de variables y estructuras. Las funciones de la EEPROM funcionan igualmente con estas variables propias.

En este ejemplo grabamos una estructura, de una forma similar a la de cualquier otra variable.

#include <EEPROM.h>

struct MyStruct{
  float field1;
  byte field2;
  char name[10];
};

void setup()
{
  int eeAddress = 0;
  MyStruct customVar = {
    3.14f,
    65,
    "Working!"
  };

  eeAddress += sizeof(MyStruct);
  
  EEPROM.put( eeAddress, customVar ); 
}

void loop()
{
}

Usar Update para actualizar valores

Como hemos comentado, la función Update() comprueba antes de escribir el valor existente en la memoria, y escribe únicamente si el valor es diferente del almacenado.

Esto supone una pequeña pérdida de rendimiento (pequeña, porque la operación de lectura es mucho más rápida que la de escritura) pero contribuye a alargar la vida útil de la memoria.

En el siguiente ejemplo, se escribe en la EEPROM la lectura analógica A0.

#include <EEPROM.h>

int eeAddress = 0;

void setup()
{
}

void loop()
{
   int val = analogRead(0) / 4;
   EEPROM.update(eeAddress, val);
  
  eeAddress += sizeof(int);
  if(address == EEPROM.length()) eeAddress = 0;

  delay(10000);  //Espera de 10 segundos
}

Leer variables con Get

Finalmente, tenemos que recuperar las variables guardadas, para lo cual usaremos la función Get(). Por supuesto necesitaremos saber la dirección en la que está guardada la variable, así como su tipo.

El siguiente ejemplo realiza la lectura de una variable float, y una estructura de ejemplo, que previamente deberemos haber guardado en la memoria EEPROM.

#include <EEPROM.h>

struct MyStruct{
  float field1;
  byte field2;
  char name[10];
};

void setup(){
  
  float f;
  int eeAddress = 0; //EEPROM address to start reading from    
  EEPROM.get( eeAddress, f );
  Serial.print( "Float leido: " );
  Serial.println( f, 3 );  
 
  eeAddress += sizeof(float);

  MyStruct customVar;
  EEPROM.get( eeAddress, customVar );  
  Serial.println( "Estructura leida: " );
  Serial.println( customVar.field1 );
  Serial.println( customVar.field2 );
  Serial.println( customVar.name );
}

void loop()
{
}

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github. github-full