miércoles, 28 de octubre de 2015

EEPROM



El arduino a primera vista podemos ver la cantidad de pines, el boton de reset, la entrada de alimentación, pero además tenemos que saber que tiene tres memorias que le podemos dar uso.

  • La memoria flash (espacio de programa), es donde se almacena el programa, algunos le llaman el boceto del arduino ya que grabamos, grabamos y volvemos a grabar en este espacio.
  • SRAM (memoria estática de acceso aleatorio) es donde el boceto crea y manipula las variables cuando se ejecuta.
  • EEPROM es el espacio de memoria que los programadores pueden utilizar para almacenar información a largo plazo.
La memoria flash y la memoria EEPROM no son volátiles (la información persiste después de que se quita la fuente de alimentación (se apaga)). SRAM es volátil, o sea, que cuando lo apague se perderá toda la información.


EEPROM son las siglas de Electrically Erasable Programmable Read-Only Memory (ROM programable y borrable eléctricamente). Es un tipo de memoria ROM que puede ser programada, borrada y reprogramada eléctricamente, a diferencia de la EPROM que ha de borrarse mediante un aparato que emite rayos ultravioleta. Son memorias no volátiles.
Fuente: wikipedia, arduinoRference, educachip

La EEPROM generalmente aprovecha ya sea por el miedo a usarla o como me ha pasado a mi por el desconocimiento que se podía usar.

Esta tiene una capacidad muy pequeña, dependiendo el arduino que utilicemos es la cantidad de K que tendremos para almacenar




Yo en mi caso utilizaré un arduino mega para hacer las pruebas.

Para programar la EEPROM
Utilizaremos la librería llamada EEPROM.h. Cual no se falta instalar o descargar nada, viene por defecto en la IDE
En la memoria EEPROM debemos trabajar por direcciones, estas direcciones están compuestas por byte

Ejemplo. Arduino uno: 
Tiene 1KB sea que tendremos desde la dirección 0 hasta la 999 y podremos utilizar valores de 0 a 255. Si tuvieramos la necesidad de usar un valor más grande por ejemplo 344 deberíamos utilizar dos bytes
En mi caso que utilizaré el arduino mega, paso de tener de 1K que tiene el arduino uno a 4K que tiene el arduino mega, o sea, cuadriplico la cantidad de información que puede guardar. Todo va a depender de lo que vaya a guardar, sino guardo mucha información probablemente vuelva al arduino uno.

En la EEPROM encontramos funciones como
  • read()
  • write()
  • update()
  • get()
  • put()
  • EEPROM[]
También podemos encontrar los ejemplos
  • clear: borra los bytes de la EEPROM
  • crc: Calcula el CRC de contenidos de la EEPROM como si fuera una arreglo/matriz
  • get: Obtiene los valores de la EEPROM y los imprimo como números flotante
  • iteration: Sirve para entender como leer las posiciones de la memoria EEPROM
  • put: Ponemos/almacenamos valores en la EEPROM utilizando la semática de variables
  • read: Lee la EEPROM y envia los valores a la computadora
  • update: Actualizar. Almacena valores leído de la EEPROM, pero se almacena solo si es diferente al ya leído, para aumentar la vida de la EEPROM
  • write: Almacena los valores de una entrada analógica en la EEPROM
Todos estos ejemplos utilizan las funciones antes nombradas, read(), write(), update(), etc...


Los micro-controladores compatibles en los diferentes placas Arduino tienen diferentes cantidades de EEPROM:

  • 1024 bytes en el ATmega328
  • 512 bytes en el ATmega168 y ATmega8
  • 4 KB (4096 bytes) en el ATmega1280 y Atmega2560.

Vamos a empezar a grabar y escribir en la EEPROM



Escribir en la EEPROM
#include <EEPROM.h>

int addr = 0; 
int val = 15;

void setup() {
  
}

void loop() {
  EEPROM.write(addr, val);
  EEPROM.write(3, 17);

}

Vamos a necesitar la librería  #include <EEPROM.h>

La primer forma es utilizando variables addr (para la dirección) y val (para el valor)
y la segunda forma es un poco más manual.
En fin grabamos/escribimos en la EEPROM el número 15 y el 17 en las direcciones 0 y 3 respectivamente

Una vez que ya esta escrita podemos pasar a leer lo que escribimos

Leer en la EEPROM
#include <EEPROM.h>
int addr = 0; 
int val = 15;
void setup() {
    Serial.begin(9600);
}

void loop() {
  EEPROM.write(addr, val);
  EEPROM.write(3, 17);

  val = EEPROM.read(addr);

  Serial.print(addr);
  Serial.print("\t");
  Serial.print(val, DEC);
  Serial.println();

  Serial.print("");
  Serial.print(3);
  Serial.print("\t");
  Serial.print(EEPROM.read(3), DEC);
  Serial.println();

  delay(50000);
}

Donde vamos a tener la siguiente salida


Dirección 0: valor 15
Dirección 3: valor 17
El  Serial.print("\t"); Es para gener una sangría entre la dirección y el valor
El  delay(50000); Le puse un número grande, para que no muestre muchas veces lo mismo, recordemos que hay un número limitado de escrituras y lecturas en la memoría EEPROM
Y si quiere saber más sobre Serial.print vean -> Tutorial Serial.print() detallo sobre todas las formas posibles, y sobre DEC (formato decimal)

Si tenemos un Arduino donde nunca se ha escrito, vamos y utilizamos el siguiente código para leer lo que tiene

#include <EEPROM.h>

int address = 0;
byte value;

void setup(){
  Serial.begin(9600);
}

void loop(){
  value = EEPROM.read(address);

  Serial.print(address);
  Serial.print("\t");
  Serial.print(value, DEC);
  Serial.println();

  address = address + 1;
  if(address == EEPROM.length())
    address = 0;
    
  delay(500);
}

Vamos a tener la siguiente salida

Para poder movernos entre las direcciones, vamos incrementando "address" hasta que llegamos al tamaño de la EEPROM, que no las devuelve EEPROM.length())
address = address + 1;
  if(address == EEPROM.length())
    address = 0;

Cuando address = EEPROM.length() se reinicia y vuelve desde el principio

EEPROM.length()) En Arduino UNO

Esto representa la cantidad de direcciones que tiene el Arduino UNO, que son 1024 direcciones.


Esto representa la cantidad de direcciones que tiene el arduino MEGA, que son X4 del arduino UNO
Esto es un poco lo que hablamos al principio del tutorial



Leer y Escribir Flotantes 
#include <EEPROM.h>

float f = 0.00f; 
int eeAddress = 0;

void setup() {
  Serial.begin(9600);

  Serial.print("Valor anterior: ");
  EEPROM.get( eeAddress, f );
  Serial.println( f, 3 ); 
  
  f = 78.25; 
  EEPROM.put( eeAddress, f );

  Serial.print("Valor nuevo: ");
  EEPROM.get( eeAddress, f );
  Serial.println( f, 3 );  
}

void loop() {
}

En EEPROM.get(eeAddress, f);

  • eeAddress: esla dirección de la EEPROM
  • f: el valor de la dirección "eeAddress" se extrae y se mete en f
  • Serial.print() se muestar el valor de "f" con 3 dígitos después de la coma



Leer y Escribir Enteros
#include <EEPROM.h>

int val = 0;
int eeAddress = 1; 

void setup() {
  Serial.begin(9600);
  Serial.print("Valor anterior: ");
  Serial.println(EEPROM.read(eeAddress));//leemos

  val = 20;
  EEPROM.write(eeAddress, val); //grabamos

  Serial.print("Valor nuevo: ");
  Serial.println(EEPROM.read(eeAddress)); //leemos
}

void loop() {
}

Si empezamos a comprar con el código anterior.
Cuando hablamos de 
  • flotantes utilizamos la función put() y get() 
  • enteros utilizamos la función write() y read()

La pregunta es ¿Por qué? Acaso no se pueden usar las mismas funciones para números enteros y flotantes?.
Veamos...

Lo voy a hacer a continuación es

  • leer la dirección=0 donde tengo el número flotante y 
  • leer la dirección=1 donde tengo el número entero
Utilizando el read() y el get() para leer ambas direcciones

#include <EEPROM.h>

int eeAddressFlotante = 0; 
int eeAddressEntero = 1; 
float f = 0.00f;

void setup() {
  Serial.begin(9600);
  Serial.println("Valor Flotante: ");
  Serial.println(EEPROM.read(eeAddressFlotante));
  EEPROM.get(eeAddressFlotante, f); //guardamos el valor en f
  Serial.println(f, 3);  

  Serial.println("");
  Serial.println("Valor Entero: ");
  Serial.println(EEPROM.read(eeAddressEntero));
  EEPROM.get(eeAddressEntero, f); //guardamos el valor en f
  Serial.println(f, 3); 

}

void loop() {
}


Los resultados que obtuvimos son
Valor flotante
0
78.039
Valor Entero
20
-194.610

Los valores verdaderos son
  • 20
  • 78.039
Con esto podemos demostrar que 
  • Cuando tenemos valores enteros tenemos que usar las funciones read() y write() 
  • Cuando tenemos valores flotantes tenemos que usar las funciones get() y put() 
En caso contrario vamos a tener cualquier resultado


Ahora nos surge otra pregunta. ¿Como saber si tenemos un valor entero o flotante para saber si usar una función u otra?.
Bueno, aunque me gustaría poder responder esa pregunta, la verdad que no puedo.
Pero si les puedo decir que como es una memoria limitada, les puedo dar por seguro que nosotros vamos a saber que estamos guardando una cosa u otra, y de ahí vamos saber si usar un read() y write() o un get() y put()

Voy a pasar a hacer una aclaración más, antes de pasar a las struct
En un dirección podemos almacenar un byte (1byte), cual son 8 bits, y 2*8 = 256
En este ejemplo, voy a almacenar desde la dirección=10 en adelante, los enteros
  • -50
  • 50
  • 200
  • 255
  • 256
  • 257
Pero cuando muestro los imprimo recibo
  • -50 -> 206  
  • 50 -> 50     (sigue siendo 50, porque esta entre 0 y 255)
  • 200 -> 200 (sigue siendo 200, porque esta entre 0 y 255)
  • 255 -> 255 (sigue siendo 255, porque esta entre 0 y 255)
  • 256 -> 0  (como se pasa, vuelve desde el principio, 256 % 255 = 0)
  • 257 -> 1  (como se pasa, vuelve desde el principio, 257 & 255 = 1)

#include <EEPROM.h>

int eeAddress = 3; 
int val = 257;

void setup() {
  Serial.begin(9600);
  EEPROM.write(10, -50);
  Serial.println(EEPROM.read(10));
  EEPROM.write(11, 50);
  Serial.println(EEPROM.read(11));
  EEPROM.write(12, 200);
  Serial.println(EEPROM.read(12));
  EEPROM.write(13, 255);
  Serial.println(EEPROM.read(13));
  EEPROM.write(14, 256);
  Serial.println(EEPROM.read(14));
  EEPROM.write(15, 257);
  Serial.println(EEPROM.read(15));
  
}

void loop() {
}



Hasta el momento hablamos de entero y de flotantes, que los valores más comunes en la programación para poder empezar a hacer pruebas o nuestros primeros programitas.

Continuamos en este mega tutorial de EEPROM a utilizar las llamadas sturc{}
#include struct MyObject{
  float field1;
  byte field2;
  char name[10];
};

Este struct es una esctructura que en este caso llamada MyObject que con tiene tres campos, field1 de tipo float, field2 de tipo byte, name[] de tipo char.
Este concepto de objetos es difícil de verlos para aquellos que no saben programar en Programación Orientada a Objetos, cual es muy diferente a la programación procedural

Pero antes de trabajar con struct debemos saber cuanto ocupa cada campo
Los tipos podemos encontrar




void setup() {
  Serial.begin(9600);

  Serial.print("char: ");
  Serial.println(sizeof(char));

  Serial.print("byte: ");
  Serial.println(sizeof(byte));

  Serial.print("int: ");
  Serial.println(sizeof(int));

  Serial.print("unsigned int: ");
  Serial.println(sizeof(unsigned int));

  Serial.print("long: ");
  Serial.println(sizeof(long));

  Serial.print("unsigned long: ");
  Serial.println(sizeof(unsigned long));

  Serial.print("double: ");
  Serial.println(sizeof(double));
}

void loop() {
  // put your main code here, to run repeatedly:

}
  • char: 1
  • byte: 1
  • int: 2
  • unsigned int: 2
  • long: 4
  • unsigned long: 4
  • float: 4
  • double: 4
Para saber el tamaño utilizamos la función sizof(tipoVariable)

Veamos el siguiente código

#include <EEPROM.h>
int dir = 0;
float f = 0.00f;
void setup() {
  Serial.begin(9600);

  EEPROM.put(dir, f); //grabo 0.00

  dir += sizeof(float);
  f = 1.11f;
  EEPROM.put(dir, f); //grabo 1.11

  dir += sizeof(float);
  f = 2.22f;
  EEPROM.put(dir, f); //grabo 2.22

  dir = 0; //comienoz en la dirección 0
  Serial.println(EEPROM.get(dir, f));//muestro
  dir += sizeof(float); //subo 4 direcciones
  Serial.println(EEPROM.get(dir, f));
  dir += sizeof(float);//subo 4 direciones
  Serial.println(EEPROM.get(dir, f));
  
}

void loop() {
  // put your main code here, to run repeatedly:

}

Entonces

  1. en la dirección=0 grabamos el 0.00 de tipo float que tiene un tamaño de 4 bytes
  2. saltamos 4 direcciones
  3. en la dirección=4 grabamos el 1.11 de tipo float que tiene un tamaño de 4 bytes
  4. saltamos otras 4 direcciones
  5. en la dirección=8 grabamos el 2.22 de tipo float que tiene un tamaño de 4 bytes
La pregunta que nos podríamos llegar hacer que guardo en la dirección 1,2,3,5,6,7

#include <EEPROM.h>
float f = 0.00f;
void setup() {
  Serial.begin(9600);
  Serial.println(EEPROM.get(0, f));
  Serial.println(EEPROM.get(1, f));
  Serial.println(EEPROM.get(2, f));
  Serial.println(EEPROM.get(3, f));
  Serial.println(EEPROM.get(4, f));
  Serial.println(EEPROM.get(5, f));
  Serial.println(EEPROM.get(6, f));
  Serial.println(EEPROM.get(7, f));
  Serial.println(EEPROM.get(8, f));
}
void loop() {
}

Salida
0.00
ovf
0.00
-0.00
1.11
ovf
0.00
0.00
2.22


Por esto motivo es muy importante saber cuanto ocupa cada tipo de variable, así saber correctamente cuanto desplazarnos así para la lectura como para la escritura.

Ahora ya estamos listo para pasar a los tipos struct

Struct: Es un tipo compuesto definido por el usuario. Se compone de campos o miembros que pueden tener diferentes tipos. En C++, una estructura es la misma que una clase excepto que sus miembros son públicos por defecto

Grabar
Vamos un ejemplo.
Si yo quisiera guardar mi edad. Yo lo podría hacer en dos pasos
  1. La declaración del a variable de tipo entera
  2. La asignación de la edad en esa variable
int edad;
edad = 28;

Si llevamos esto mismo a el struct, quedaría de la siguiente manera

Declaración de la structura llamada MyObject con campos field1, field2, name[10]. (Paso 1)

#include <EEPROM.h> 
struct MyObject{
  float field1;
  byte field2;
  char name[10];
};

La asignación de los valores a cada uno de los campos correspondientes, utilizando el mismo orden de como se crearon. (Paso 2)

MyObject customVar = {
    3.14f,
    65,
    "Working!"
  };

Por último para poder almacenarlo

int eeAddress = 0;        
EEPROM.put( eeAddress, customVar );

Código completo

#include <EEPROM.h>

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

void setup() {
  Serial.begin(9600);

  MyObject customVar = {
    3,14f,
    65,
    "Working!"
  };

  EEPROM.put(0, customVar);  
}
void loop() {  
  
}

Leer
Comienza de la misma forma que del gabar
#include <EEPROM.h> 
struct MyObject{
  float field1;
  byte field2;
  char name[10];
};

Con el "." punto, podemos accesor a los campos del struct
MyObject customVar; //Variable de tipo struct que utilizaremos para guardar
EEPROM.get( eeAddress, customVar );
  
Serial.println( "Ahora leemos lo que guardamos en el Struct " );
Serial.println( customVar.field1 );
Serial.println( customVar.field2 );
Serial.println( customVar.name );

Código completo
#include <EEPROM.h>

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

void setup() {
  Serial.begin(9600);

  MyObject customVar;
  EEPROM.get(0, customVar);

  Serial.println("ahora leemos el struct");
  Serial.println(customVar.field1);
  Serial.println(customVar.field2);
  Serial.println(customVar.name);
 
}
void loop() {  
  
}

Ya conociendo como se almacena y se lee un solo scruct, pasemos a seguir trabajando con esto, por ejemplo como guardar varios struct y almacenar otro tipos de datos en el mismo

Si le agrego la linea

Serial.println(sizeof(customVar));

Vamos a ver que tiene salida = 15
Que es el tamaño que ocupa el struct que hemos armado

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

Investiguemos donde proviene ese 15

float = 4
byte = 1
char[10] = 10
Total = 15

Entonces luego de almacenar nuestro struct de tamaño 15, aumentamos el direccionamiento para poder guardar el siguiente struct, float, int, o lo que queramos almacenar

eeAddress += sizeof(customVar);

Almacenemos 3 struct

#include <EEPROM.h>

int eeAddress = 0;

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

void setup() {
  Serial.begin(9600);

  MyObject customVar = {
    1.11f,
    61,
    "aaaa"
  };
  EEPROM.put(eeAddress, customVar);  
  eeAddress += sizeof(customVar);
  
  MyObject customVar2 = {
    2.22f,
    62,
    "bbbb"
  };
  EEPROM.put(eeAddress, customVar2);
  eeAddress += sizeof(customVar2);

  MyObject customVar3 = {
    3.33f,
    63,
    "cccc"
  };
  EEPROM.put(eeAddress, customVar3);
}
void loop() {  
  
}

Básicamente es repetir lo siguiente 3 veces
 MyObject customVar = {
    1.11f,
    61,
    "aaaa"
  };
  EEPROM.put(eeAddress, customVar);  
  eeAddress += sizeof(customVar);

Para Leer los 3 struct
#include <EEPROM.h>

int eeAddress = 0;

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

void setup() {
  Serial.begin(9600);

  MyObject customVar;
  EEPROM.get(eeAddress, customVar);

  Serial.println("ahora leemos el struct");
  Serial.println(customVar.field1);
  Serial.println(customVar.field2);
  Serial.println(customVar.name);

  eeAddress += sizeof(customVar);
  EEPROM.get(eeAddress, customVar);
  Serial.println("ahora leemos el struct2");
  Serial.println(customVar.field1);
  Serial.println(customVar.field2);
  Serial.println(customVar.name);

  eeAddress += sizeof(customVar);
  EEPROM.get(eeAddress, customVar);
  Serial.println("ahora leemos el struct3");
  Serial.println(customVar.field1);
  Serial.println(customVar.field2);
  Serial.println(customVar.name);
  
}
void loop() {  
  
}

Básicamente es repetir los siguiente 3 veces
Serial.println("ahora leemos el struct");
  Serial.println(customVar.field1);
  Serial.println(customVar.field2);
  Serial.println(customVar.name);

  eeAddress += sizeof(customVar);

Saquemos conclusiones de lo que hicimos, pudimos triplicar lo que en la primera instancia hicimos en la lectura y almacenamiento del struct, pero si seguimos aumentando la cantidad de veces este proceso se vuelve muy poco óptimo.
Lo anterior sirve para almacenar muy poco struct, pero si ya hablamos de 30, 40, 100 de estos (teniendo en cuenta la capacidad limitada del arduino que estamos usando), se vuelve muy engorroso y son demasiadas lineas repetidas

Empecemos mejorando la lectura
#include <EEPROM.h>

int eeAddress = 0;

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

void setup() {
  Serial.begin(9600);

  MyObject customVar;

  for (int i=0; i < 3; i++){
    Serial.print("Struct numero: ");Serial.println(i+1);
    EEPROM.get(eeAddress, customVar);
    Serial.println(customVar.field1);
    Serial.println(customVar.field2);
    Serial.println(customVar.name);
    eeAddress += sizeof(customVar);
  }
  
}
void loop() {  
  
}

Salida


Para la escritura cambie un campo de char[10] a String, cual fue un cambio inteligente porque pase de ocupar 10 byte a 6 byte y además tengo mayor cantidad de información que puedo almacenar.
Antes podía almacenar 10 caracteres y ahora pude grabar en 
_s = "ccccccccccccccccccccccccccccccccccccccccccccccccccc"


Escritura
Para esta voy a utilizar funciones para poder optimizar las repetición de tareas

#include <EEPROM.h>

int eeAddress = 0;

struct MyObject{
  float field1;
  byte field2;
  String field3; //tamaño 6
}
;
MyObject customVar = {0.00f,00,""};

void setup() {
  Serial.begin(9600);
  grabarStruct(1.11f, 11, "aaazz");
  grabarStruct(2.22f, 22, "bbbzz");
  grabarStruct(3.33f, 33, "ccczz");

}
void loop() {  
  
}

void grabarStruct(float x, byte y, String z){
  customVar.field1 = x;
  customVar.field2 = y;
  customVar.field3 = z;
  EEPROM.put(eeAddress, customVar); 
  eeAddress += sizeof(customVar);
}

Noten que pequeño ahora quedo el setup(), incluso ahora mas legible, mas entendible y más fácil de mantener. Que es el objetivo de usar funciones, empezar a separar las repetición de tareas y llamarlas con una función.

Y para la lectura del mismo, solo le cambie el char[10] por el String

#include <EEPROM.h>

int eeAddress = 0;

struct MyObject{
  float field1;
  byte field2;
  String field3;
};

void setup() {
  Serial.begin(9600);

  MyObject customVar;

  for (int i=0; i < 3; i++){
    Serial.print("Struct numero: ");Serial.println(i+1);
    EEPROM.get(eeAddress, customVar);
    Serial.println(customVar.field1);
    Serial.println(customVar.field2);
    Serial.println(customVar.field3);
    eeAddress += sizeof(customVar);
  }
  
}
void loop() {  
  
}

Y la salida fue la misma

En este punto, ya llevamos halando mucho de struct.
  1. Empezamos con una simple lectura y escritura
  2. Después triplicamos lo anterior
  3. Lo optimizando
  4. Y a través del sizeof, vimos que mas óptimo trabajar con String que con char[10] para nuestro caso particular, a lo mejor si hablamos de un dni, esta bueno limitar la cantidad de datos, porque un DNI, no puede ser 32.1312.321312.43213.312321
Los invito a ver el siguiente tutorial que voy a trabajar con practicas más específicas utilizando la base de conocimiento de este tutorial.


6 comentarios:

  1. ¡¡Estupendo!! muchas gracias por tu esfuerzo en realizar este tutorial.

    ResponderEliminar
  2. Muy buen trabajo tenia dudas con el tema de porque al guardar un char[20] por ejemplo ocupaba los 20 espacios y al guardar un String con el mismo texto del char[20] me esta ocupando solo 4 bytes en la eeprom cuando la leo uno por uno.

    ResponderEliminar
  3. Llevo varios dias mirando por ahi el por que el string siempre me daba 6 posiciones independientemente del contenido. Aqui veo que, efectivamente, es asi y por primera vez, se me ha quedado claro como hacerlo. Perfecta explicacion. Lo que no me termina de quedar claro es por que siempre ocupa 6 posiciones independientemente del contenido. ¿Lo podrías explicar un poco?
    Gracias anticipadas.

    ResponderEliminar