Trabajar con memoria EEPROM Arduino

Así que llegamos al tercer tipo de memoria disponible en Arduino: EEPROM (Memoria de solo lectura programable y borrable eléctricamente – (EEPROM)), también es memoria no volátil. Recordemos los otros tipos de memoria, Flash y SRAM, y sus capacidades de almacenamiento de datos:


La Eeprom de Arduino.

TipoLeer desde programaGrabación por programaBorrada al reiniciar
FLASHSí, PROGMEMEs posible, pero difícilNo
SRAMSiSiSi
EEPROMSiSiNo
Características de la memoria Eeprom de Arduino.

En palabras simples: EEPROM – memoria a la que tenemos acceso completo desde un programa en ejecución, es decir podemos leer y escribir datos allí en tiempo de ejecución, y estos datos no se borran cuando se reinicia Arduino. 

Usabilidad

  • Almacenamiento de configuraciones que cambian «desde el menú» del dispositivo;
  • Calibración, almacenamiento de datos de calibración de Arduino;
  • Utilizar como memoria SRAM adicional en caso de escasez;
  • «Caja negra»: registro permanente de las lecturas de los sensores para una posterior decodificación de fallas;
  • Registrar el estado de un flujo de trabajo para recuperarse de un reinicio repentino.

Recurso

El único punto importante: EEPROM tiene un recurso en el número de reescritura de células. El fabricante garantiza 100.000 ciclos de escritura para cada celda, de hecho, este número depende de las condiciones específicas del chip y la temperatura, las pruebas independientes mostraron 3-6 millones de ciclos de reescritura a temperatura ambiente antes de que ocurriera el primer error, es decir, los 100.000 declarados se toman con un margen muy amplio. Pero hay una pequeña aclaración: con los 100.000 ciclos de reescritura declarados, la seguridad de los datos grabados está garantizada durante 100 años a una temperatura de 24 ° C; si sobrescribe un millón, los datos se deteriorarán más rápido. Al mismo tiempo, el número de lecturas de cada celda es ilimitado. 

Volumen

EEPROM es un área de memoria formada por celdas unitarias de un byte (como SRAM). El tamaño de EEPROM es diferente para diferentes modelos de Arduino:

  • ATmega328 (Arduino UNO, Nano, Pro Mini): 1 KB
  • ATmega2560 (Arduino Mega): 4 KB
  • ATtiny85 (Digispark): 512 B

Direccionamiento

La tarea principal cuando se trabaja con EEPROM es no confundirla con direcciones, porque cada byte tiene su propia dirección. Si escribe datos de doble byte, se necesitarán dos bytes, y los siguientes datos deberán escribirse en la dirección de al menos +2 a la anterior, de lo contrario serán «reescritos». Considere un ejemplo de almacenamiento de un conjunto de datos de diferentes tipos, ubicados en la memoria secuencialmente uno tras otro (entre paréntesis escribo el tamaño del tipo de datos actual, cuyo tamaño aumentará la dirección para el siguiente «bloque»):

  • byte – dirección 0 (+1)
  • byte – dirección 1 (+1)
  • int – dirección 2 (+2)
  • byte – dirección 4 (+1)
  • float – dirección 5 (+4)
  • int – dirección 9 (+2)
  • etc

Un punto importante: todas las celdas tienen un valor predeterminado (para un nuevo chip) 255 .

Velocidad

Velocidad EEPROM (el tiempo de respuesta no depende de la frecuencia del reloj del sistema Arduino):

  • Escribir un byte toma ~ 3.3ms (milisegundos)
  • La lectura de un byte tarda ~ 0,4 μs (microsegundos)

Voltaje

Puede haber distorsiones al escribir datos en EEPROM con un VCC (voltaje de suministro) demasiado bajo; se recomienda encarecidamente utilizar BOD  o controlar manualmente el voltaje antes de escribir.

Frecuencia

Cuando se utiliza Arduino con un generador de reloj interno de 8 MHz, su desviación no debe exceder el 10% (7.2-8.8 MHz), de lo contrario, en la escritura en EEPROM o FLASH probablemente se cometerá con errores. En consecuencia, todo overclocking del reloj interno es inaceptable cuando se escribe EEPROM o FLASH.

Bibliotecas

Para trabajar con EEPROM en el entorno Arduino, tenemos dos bibliotecas completas, la segunda es un «shell» más conveniente para la primera. Consideremos ambas, porque cualquier cosa se puede encontrar en el boceto de otra persona, y la combinación de estas dos bibliotecas hace que trabajar con EEPROM sea increíblemente adecuado.


Biblioteca avr / eeprom.h

La biblioteca estándar eeprom.h viene con el compilador avr-gcc, que compila nuestros bocetos del IDE de Arduino. Puedes leer la documentación completa aquí. Para conectar la biblioteca al boceto, escriba #include <avr / eeprom.h>

La biblioteca tiene un conjunto de funciones para trabajar con tipos de datos enteros (byte – 1 byte, palabra – 2 bytes, dword – 4 bytes), float y blocks “Bloques”: conjuntos de datos de cualquier formato (estructuras, matrices, etc.). Trabajar significa escribir, leer y actualizar. La actualización es una herramienta extremadamente importante para evitar la sobrescritura innecesaria de las celdas de memoria en Arduino. La actualización escribe si el valor que se escribe difiere del actual en esta celda.

Leer:

  • eeprom_read_byte ( dirección ) – devolverá el valor
  • eeprom_read_word ( dirección ) – devolverá el valor
  • eeprom_read_dword ( dirección ) – devolverá el valor
  • eeprom_read_float ( dirección ) – devolverá el valor
  • eeprom_read_block ( dirección SRAM, dirección EEPROM, tamaño ) – lee contenido por Dirección EEPROM y lo coloca en dirección en SRAM

Gravar:

  • eeprom_write_byte ( dirección, valor )
  • eeprom_write_word ( dirección, valor )
  • eeprom_write_dword ( dirección, valor )
  • eeprom_write_float ( dirección, valor )
  • eeprom_write_block ( dirección SRAM, dirección EEPROM, tamaño ) – escribirá el contenido por dirección en SRAM en Dirección EEPROM

Actualizar:

  • eeprom_update_byte ( dirección, valor )
  • eeprom_update_word ( dirección, valor )
  • eeprom_update_dword ( dirección, valor )
  • eeprom_update_float ( dirección, valor )
  • eeprom_update_block ( dirección SRAM, dirección EEPROM, tamaño ) – actualizará el contenido por dirección en SRAM en Dirección EEPROM

Macros:

  • _EEPUT (addr, val)– escribe (escribe) un byte val en la dirección addr. No se requiere encasillamiento (es una macro)
  • _EEGET ( val, addr)– lee un byte en una dirección addr y lo escribe en una variable val. No se requiere encasillamiento (hecho en una macro)

Consideremos un ejemplo simple en el que hay una escritura y lectura de tipos de datos individuales en diferentes celdas:

#include <avr / eeprom.h>
void setup () {  
  Serial.begin(9600);
  
  // declarar datos de diferentes tipos
  byte dataB = 120;
  float dataF = 3.14;
  int16_t dataI = -634;  
  // escribe uno tras otro
  eeprom_write_byte ( 0, dataB ) ; // 1 byte
  eeprom_write_float ( 1, dataF ) ; // 4 bytes
  // "actualizar" para variar
  eeprom_update_word ( 5, dataI ) ;
  // declaramos variables donde leeremos
  byte dataB_read = 0;
  float dataF_read = 0;
  int16_t dataI_read = 0;
  // leer
  dataB_read = eeprom_read_byte ( 0 ) ;
  dataF_read = eeprom_read_float ( 1 ) ;
  dataI_read = eeprom_read_word ( 5 ) ;
  // imprimirá 120 3,14 -634
  Serial.println ( dataB_read ) ;
  Serial.println ( dataF_read ) ;
  Serial.println ( dataI_read ) ;
}
void loop() {}  

No es muy conveniente almacenar datos de esta manera, porque la administración de direcciones debe hacerse manualmente, contar el número de bytes en cada tipo y «cambiar» la dirección en la cantidad requerida. Es mucho más conveniente almacenar diversos datos en estructuras, hablamos de ellos con más detalle en la lección sobre tipos de datos de Arduino.

Debemos pasar a la función la dirección de los datos en memoria (operador &) es esencialmente un puntero, y también lo convierte a tipo void* porque la función de lectura / escritura del bloque toma exactamente ese tipo. Hablamos sobre los punteros en Arduino con más detalle en una lección separada. Además, a la función de lectura / escritura del bloque se le debe pasar el tamaño del bloque de datos en bytes. Esto se puede hacer manualmente (por número), pero es mejor usar sizeof (), que calculará este tamaño y lo pasará a la función.

#include <avr/eeprom.h>
void setup() {
  Serial.begin(9600);
  //declara la estructura
  struct MyStruct {
    byte a;
    int b;
    float c;
  };
  // crea y llena la estructura
  MyStruct myStruct;
  myStruct.a = 10;
  myStruct.b = 1000;
  myStruct.c = 3.14;
  // escribir en la dirección 10, especificando el tamaño de la estructura y convertirlo en void *
  eeprom_write_block((void*) &myStruct, 10, sizeof(myStruct));
  //  crea una nueva estructura vacía
  MyStruct newStruct;
  //leer de la dirección 10
  eeprom_read_block((void*) &newStruct, 10, sizeof(newStruct));
  // controlar
  // imprime 10 1000 3,14
  Serial.println(newStruct.a);
  Serial.println(newStruct.b);
  Serial.println(newStruct.c);
}
void loop() {}

Las matrices se pueden almacenar de la misma manera:

#include <avr / eeprom.h>
void setup() {
 Serial.begin(9600);
  // crea una matriz
  float dataF [] = { 3,14 , 60,25 , 9132,5 , -654,3 } ;
  // escribe en la dirección 20, especificando el tamaño
  eeprom_write_block (( void * ) & dataF, 20, sizeof ( dataF )) ;
  // ¡crea una nueva matriz vacía del mismo tipo y tamaño!
  float dataF_read [ 4 ] ;
  // leer de la dirección 20
  eeprom_read_block (( void * ) & dataF_read, 20, sizeof ( dataF_read )) ;
  // controlar
  // dará como resultado 3.14 60.25 9132.5 -654.3
  for ( byte i = 0; i < 4; i ++ ) 
   Serial.println(dataF_read[i]);
}
void loop() {}

Existe otra herramienta muy útil en la biblioteca Arduino avr / eeprom.h – EEMEM, que te permite realizar direccionamiento automático de datos creando punteros a los que el compilador asignará un valor.

Considere un ejemplo en el que escribimos varias variables, una estructura y una matriz en la EEPROM, dándoles direcciones automáticamente.

 Â¡Un punto importante! Las direcciones se establecen de abajo hacia arriba en el orden de la declaración EEMEM:

#include <avr / eeprom.h>
struct MyStruct {
  byte val1;
  int val2;
  float int3;
} ;
uint8_t EEMEM byteAddr;    // 27
uint16_t EEMEM intAddr;    // 25
uint32_t EEMEM longAddr;   // 21
MyStruct EEMEM myStructAddr ; // 14
int EEMEM intArrayAddr [ 5 ] ; // cuatro
float EEMEM floatAddr;     // 0

El propio EEMEM distribuye las direcciones según el tamaño de los datos. Un punto importante: este enfoque no ocupa espacio de memoria adicional, es decir Numerar las direcciones manualmente con números, sin crear «variables» EEMEM, ¡no ahorramos memoria!

Volvamos a nuestro primer ejemplo y lo reescribamos con EEMEM. Al especificar una dirección a través de EEMEM, debe utilizar el operador de toma de dirección &.

#include <avr/eeprom.h>
byte EEMEM dataB_addr;
float EEMEM dataF_addr;
int16_t EEMEM dataI_addr;
void setup() {
  Serial.begin(9600);
  // declarar datos de diferentes tipos
  byte dataB = 120;
  float dataF = 3.14;
  int16_t dataI = -634;
  // escribe uno tras otro
  eeprom_write_byte(&dataB_addr, dataB);
  eeprom_write_float(&dataF_addr, dataF);
  //"actualizar" para variar
  eeprom_update_word(&dataI_addr, dataI);
  // declaramos variables donde leeremos
  byte dataB_read = 0;
  float dataF_read = 0;
  int16_t dataI_read = 0;
  // leer
  dataB_read = eeprom_read_byte(&dataB_addr);
  dataF_read = eeprom_read_float(&dataF_addr);
  dataI_read = eeprom_read_word(&dataI_addr);
  //imprimirá 120 3,14 -634
  Serial.println(dataB_read);
  Serial.println(dataF_read);
  Serial.println(dataI_read);
}
void loop() {}

Y finalmente, escribir y leer un bloque a través de EEMEM. La dirección deberá convertirse a (const void*) a mano:

#include <avr / eeprom.h>
// obtener la dirección (habrá 0)
int EEMEM intArrayAddr [ 5 ] ;
void setup() {
  Serial.begin(9600);
  // crea una matriz
  int intArrayWrite [ 5 ] = { 10, 20, 30, 40, 50 } ;
  // escribir en intArrayAddr
  eeprom_write_block (( void * ) & intArrayWrite, ( const void * ) & intArrayAddr, sizeof ( intArrayWrite )) ; 
  // crea una nueva matriz para leer
  int intArrayRead [ 5 ] ;
  // leer en intArrayAddr
  eeprom_read_block (( void * ) & intArrayRead, ( const void * ) & intArrayAddr, sizeof ( intArrayRead )) ; 
  // controlar
  for (byte i = 0; i < 5; i++)
    Serial.println(intArrayRead[i]);
}
void loop() {}

Por lo tanto, puede agregar «datos» para su almacenamiento en la EEPROM durante el desarrollo del programa, sin pensar en las direcciones. Recomiendo agregar nuevos datos secuencialmente sobre los últimos para que el direccionamiento no se pierda (recuerde, el direccionamiento va de abajo hacia arriba, comenzando desde cero).


Biblioteca EEPROM.h de Arduino.

La biblioteca EEPROM.h viene con el núcleo Arduino y es una biblioteca estándar. De hecho, EEPROM.h es un shell preparado para avr / eeprom.h, que amplía ligeramente sus capacidades y simplifica su uso. Un punto importante: al conectar EEPROM.h al sketch, automáticamente conectamos avr / eeprom.h y podemos usar sus comandos, como EEMEM. Considere las herramientas que nos ofrece la biblioteca:

  • EEPROM.write ( dirección, datos )– escribe datos (¡ solo byte! ) en la dirección
  • EEPROM.update ( dirección, datos )– actualiza (el mismo registro, pero mejor) el byte de datos ubicado en la dirección
  • EEPROM.read ( dirección )– lee y devuelve el byte de datos ubicado en la dirección
  • EEPROM.put ( dirección, datos )– escribe (de hecho – update, actualiza) datos de cualquier tipo (el tipo de la variable pasada) en la dirección
  • EEPROM.get ( dirección, datos )– lee datos en la dirección y los escribe en la variable especificada
  • EEPROM [] – la biblioteca le permite trabajar con la memoria EEPROM como con una matriz de bytes ordinaria (uint8_t)

A diferencia de avr/eeprom.h, no tenemos herramientas separadas para trabajar con tipos de datos específicos que no sean bytes, y no podemos escribir / actualizar / leer un float / long / int. Pero luego tenemos un put / get omnívoro, ¡que es muy conveniente de usar! También podemos usar lo que nos da avr / eeprom.h, que se conecta automáticamente desde EEPROM.h. Consideremos un ejemplo con bytes de lectura / escritura:

#include <EEPROM.h>
void setup() {
  Serial.begin(9600);
  
  // escribe 200 en la dirección 10
  EEPROM.update(10, 200);  
  Serial.println(EEPROM.read(10));  //muestra 200
  Serial.println(EEPROM[10]);       // muestra 200
}
void loop() {}

¡La lógica de trabajar con direcciones es la misma que en el párrafo anterior de la lección! Preste atención a trabajar con la EEPROM como una matriz, puede leer, escribir, comparar e incluso usar operadores compuestos, por ejemplo EEPROM [ 0 ] + = 10 pero esto solo funciona para celdas atómicas, bytes.

Ahora veamos cómo funciona put / get:

#include <EEPROM.h>
void setup() {
  Serial.begin(9600);
  // declaramos las variables que escribiremos
  float dataF = 3.14;
  int16_t dataI = -634;
  byte dataArray[] = {10, 20, 30, 40};
  EEPROM.put(0, dataF);
  EEPROM.put(4, dataI);
  EEPROM.put(6, dataArray);
  // declaramos variables donde leeremos
  float dataF_read = 0;
  int16_t dataI_read = 0;
  byte dataArray_read[4];
  // leer exactamente como lo escribimos
  EEPROM.get(0, dataF_read);
  EEPROM.get(4, dataI_read);
  EEPROM.get(6, dataArray_read);
  // controlar
  Serial.println(dataF_read);
  Serial.println(dataI_read);
  Serial.println(dataArray_read[0]);
  Serial.println(dataArray_read[1]);
  Serial.println(dataArray_read[2]);
  Serial.println(dataArray_read[3]);
}
void loop() {}

Mucho más conveniente que write_block y read_block, ¿no? Poner y leer tipos y calcular el tamaño del bloque de datos por sí mismos, es muy conveniente. Trabajan tanto con matrices como con estructuras.


EEPROM.h + avr / eeprom.h

Y, por supuesto, puede utilizar todas las ventajas de ambas bibliotecas de arduino al mismo tiempo, por ejemplo, el direccionamiento automático de EEMEM y put / get. Considere el ejemplo anterior, en lugar de configurar direcciones manualmente, usamos EEMEM, pero el valor tendrá que convertirse a un tipo entero, primero tomando la dirección de él, es decir ( int ) y eem_address.

#include <EEPROM.h>
float EEMEM dataF_addr;
int16_t EEMEM dataI_addr;
byte EEMEM dataArray_addr[5];
void setup() {
  Serial.begin(9600);
  // declaramos las variables que escribiremos
  float dataF = 3.14;
  int16_t dataI = -634;
  byte dataArray[] = {10, 20, 30, 40};
  EEPROM.put((int)&dataF_addr, dataF);
  EEPROM.put((int)&dataI_addr, dataI);
  EEPROM.put((int)&dataArray_addr, dataArray);
  //declaramos variables donde leeremos
  float dataF_read = 0;
  int16_t dataI_read = 0;
  byte dataArray_read[4];
  // leer exactamente como lo escribimos
  EEPROM.get((int)&dataF_addr, dataF_read);
  EEPROM.get((int)&dataI_addr, dataI_read);
  EEPROM.get((int)&dataArray_addr, dataArray_read);
  EEPROM[0] += 10;
  // control
  Serial.println(dataF_read);
  Serial.println(dataI_read);
  Serial.println(dataArray_read[0]);
  Serial.println(dataArray_read[1]);
  Serial.println(dataArray_read[2]);
  Serial.println(dataArray_read[3]);
}
void loop() {}

Habiendo descubierto las capacidades de las bibliotecas, pasemos a la práctica.


Ejemplo real en Arduino

Considere un ejemplo en el que sucede lo siguiente: dos botones controlan el brillo del LED conectado al pin PWM. El brillo ajustado se guarda en la EEPROM, es decir cuando el dispositivo se reinicia, se encenderá el último brillo configurado. La biblioteca GyverButton se usa para sondear botones.

Primero, mire el programa original, donde no se guarda el brillo establecido. El programa se puede optimizar ligeramente, pero este no es el propósito de esta lección.

Cambia el brillo con botones

#define BTN_UP_PIN 3    //  pin del botón arriba
#define BTN_DOWN_PIN 4  // pin del botón abajo
#define LED_PIN 5       //Pin LED
#include <GyverButton.h>
GButton btnUP(BTN_UP_PIN); // botón "aumentar el brillo"
GButton btnDOWN(BTN_DOWN_PIN); // botón "bajar brillo"
int LEDbright = 0;
void setup() {
  pinMode(LED_PIN, OUTPUT); // Pin LED como salida
}
void loop() {
  //  botones de sondeo
  btnUP.tick();
  btnDOWN.tick();
  if (btnUP.isClick()) {
    //aumentar al hacer clic
    LEDbright += 5;
    setBright();
  }
  if (btnDOWN.isClick()) {
    //  bajar al hacer clic
    LEDbright -= 5;
    setBright();
  }
}
void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); //limitado
  analogWrite(LED_PIN, LEDbright);    // cambió el brillo
}

Preservación del brillo

#define BTN_UP_PIN 3 // pin del botón arriba
#define BTN_DOWN_PIN 4 // pin del botón abajo
#define LED_PIN 5 // Pin LED
#include <EEPROM.h>
#include <GyverButton.h>
GButton btnUP(BTN_UP_PIN); //botón "aumentar el brillo"
GButton btnDOWN(BTN_DOWN_PIN); / botón bajar brillo
int LEDbright = 0;
void setup() {
  pinMode(LED_PIN, OUTPUT); //  Pin LED como salida
  EEPROM.get(0, LEDbright); // lee el brillo de la dirección 0
  analogWrite(LED_PIN, LEDbright);  // incluido
}
void loop() {
  //botones de sondeo
  btnUP.tick();
  btnDOWN.tick();
  if (btnUP.isClick()) {
    // aumentar al hacer clic
    LEDbright += 5;
    setBright();
  }
  if (btnDOWN.isClick()) {
    // bajar al hacer clic
    LEDbright -= 5;
    setBright();
  }
}
void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // limitado
  EEPROM.put(0, LEDbright);           //escrito en la dirección 0
  analogWrite(LED_PIN, LEDbright);    // cambió el brillo
}

Entonces, ahora al inicio, se restaura el último brillo configurado y cuando se cambia, se registra. Permítame recordarle que la EEPROM se desgasta por sobrescribir. Por supuesto, «hacer clic» en el brillo varios millones de veces y matar una celda, le llevará mucho tiempo, pero el proceso de escribir un nuevo valor puede y debe optimizarse, especialmente en proyectos más serios, lo haremos, hablo de esto con más detalle a continuación.

También en nuestro código hay un momento más desagradable: en el primer inicio después de reiniciar, la EEPROM no se inicializa, cada celda almacena el número 255, y este es el valor que tomará la variable LEDbright después del primer inicio de arduino, durante el tiempo -llamada «primera lectura». No importa aquí, pero en un dispositivo más serio, deberá establecer los valores predeterminados deseados en la EEPROM para el primer inicio, también hablaremos de esto a continuación. De lo contrario, ¡imagínese qué en las “configuraciones predeterminadas Arduino” recibiría su dispositivo, para brillo / velocidad / volumen / número de modo / etc… un valor no deseado o incluso peligroso!


Trucos útiles.

Inicialización

Por inicialización, me refiero a configurar los valores de las celdas en la EEPROM «por defecto» durante el primer inicio del dispositivo. En el ejemplo anterior, actuamos en este orden:

  1. Lectura de EEPROM a variable
  2. Usar una variable para su propósito previsto

En la primera ejecución del código (y para todas las posteriores, en las que no se escribe nada nuevo en la celda), la variable recibirá el valor que estaba en la EEPROM por defecto. En la mayoría de los casos, este valor no es adecuado para el dispositivo, por ejemplo, la celda almacena el número de modo, de acuerdo con la idea del desarrollador, de 0 a 5, y de la EEPROM leemos 255. ¡Fuera de servicio! En el primer inicio, debe inicializar la EEPROM para que el Arduino funcione correctamente, para ello debe definir este primer inicio.

Puede hacer esto manualmente al flashear un programa que llenará la EEPROM con los datos necesarios. A continuación, flashee el programa que ya está funcionando con datos actualizados.

Al desarrollar un programa, esto no es muy conveniente, porque la cantidad de datos guardados puede cambiar durante el desarrollo, por lo que puede utilizar el siguiente algoritmo:

  1. Reservamos alguna celda (por ejemplo, la última) para almacenar la «clave» del primer lanzamiento
  2. Leemos la celda y si su contenido no coincide con la clave: ¡este es el primer lanzamiento!
  3. En el controlador del primer lanzamiento, escriba la clave requerida en la celda
  4. Escribimos los valores predeterminados requeridos en las celdas restantes
  5. Y después de eso, ya leemos los datos en todas las variables necesarias.

Echemos un vistazo al mismo ejemplo con un LED y botones:

Preservación del brillo.

#define INIT_ADDR 1023  // número de celda de respaldo
#define INIT_KEY 50     //clave del primer lanzamiento. 0-254, a elección
#define BTN_UP_PIN 3    // pin del botón arriba
#define BTN_DOWN_PIN 4  // pin del botón abajo
#define LED_PIN 5       //Pin LED
#include <EEPROM.h>
#include <GyverButton.h>
GButton btnUP(BTN_UP_PIN); //botón "aumentar el brillo"
GButton btnDOWN(BTN_DOWN_PIN); // botón "bajar brillo"
int LEDbright = 0;
void setup() {
  pinMode(LED_PIN, OUTPUT); //  Pin LED como salida
  if (EEPROM.read(INIT_ADDR) != INIT_KEY) { //  primer inicio
    EEPROM.write(INIT_ADDR, INIT_KEY);    // anota la clave
  // escribió el valor de brillo predeterminado
  // en este caso, este es el valor de la variable declarada arriba
    EEPROM.put(0, LEDbright);
  }
  EEPROM.get(0, LEDbright); //lee el brillo
  analogWrite(LED_PIN, LEDbright);  // incluido
}
void loop() {
  //  botones de sondeo
  btnUP.tick();
  btnDOWN.tick();
  if (btnUP.isClick()) {
    //aumentar al hacer clic
    LEDbright += 5;
    setBright();
  }
  if (btnDOWN.isClick()) {
    // bajar al hacer clic
    LEDbright -= 5;
    setBright();
  }
}
void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // limitado
  EEPROM.put(0, LEDbright);           //escribió
  analogWrite(LED_PIN, LEDbright);    // cambió el brillo
}

Ahora, en la primera ejecución, obtendremos la inicialización de las celdas requeridas. Si necesita reinicializar la EEPROM, por ejemplo, si se agregan nuevos datos, es suficiente cambiar nuestra clave a cualquier otro valor dentro de el byte (0-254). Me refiero exactamente hasta 254 porque 255 es el valor de celda predeterminado de fábrica y nuestro truco no funcionará.

Velocidad

Como escribí anteriormente, la velocidad de trabajo con EEPROM es:

  • Escribir / actualizar un byte tarda ~ 3.3ms (milisegundos)
  • La lectura de un byte tarda ~ 0,4 μs (microsegundos)

Si realmente lo desea, puede usar una celda en lugar de una variable, es decir, arriba consideramos un ejemplo en el que la EEPROM se leyó en una variable en el programa, y ​​ya se estaba trabajando con ella. Con una grave falta de RAM, puede leer el valor directamente desde la EEPROM, porque lleva un tiempo insignificante. Pero con la grabación, todo es mucho peor, tarda hasta 3,3 ms. Por ejemplo así: analogWrite(LED_PIN, EEPROM.read(0));

Para cambiar el valor, debe leer la celda, realizar las operaciones necesarias y escribir en ella nuevamente.

Otro truco: puede ingresar macros para leer y escribir valores específicos, por ejemplo:

#define GET_MODE EEPROM.read (0) // obtén el número de modo
#define GET_BRIGHT EEPROM.read (1) // obtén brillo
#define SET_MODE (x) EEPROM.write (0, (x)) // recuerda el modo
#define SET_BRIGHT (x) EEPROM.put (1, (x)) // recuerda el brillo

Obtendremos macros convenientes con las que será un poco más rápido y factible escribir código, es decir la línea SET_MODE (3) escribirá 3 en la celda 0.

Desgaste reducido

Un tema importante: reducir el desgaste de las celdas mediante sobreescrituras frecuentes. Puede haber muchas situaciones y hay soluciones interesantes para ellas también. Consideremos el ejemplo más simple: el mismo código con un LED y un botón en Arduino. Haremos lo siguiente: escribiremos el nuevo valor solo si ha pasado algún tiempo desde la última pulsación del botón. Es decir, necesitamos un temporizador (usaremos el temporizador milis(), cuando se presione el botón, el temporizador se reiniciará, y cuando se active el temporizador, escribiremos el valor real en la EEPROM. También necesitará una bandera que señalará la grabación y le permitirá grabar exactamente una vez. El algoritmo es como sigue:

  • Pulsando el botón:
    • Si se omite la bandera, fija la bandera
    • Restablecer temporizador
  • Si se activa el temporizador y se levanta la bandera:
    • Baja la bandera
    • Escribir valores en EEPROM

Veamos el mismo ejemplo:

Preservación del brillo.

#define INIT_ADDR 1023 // número de celda de respaldo
#define INIT_KEY 50 // clave del primer inicio. 0-254, a elección
#define BTN_UP_PIN 3 // pin del botón arriba
#define BTN_DOWN_PIN 4 // pin del botón abajo
#define LED_PIN 5 // Pin LED
#include <EEPROM.h>
#include <GyverButton.h>
GButton btnUP(BTN_UP_PIN); // botón "aumentar el brillo"
GButton btnDOWN(BTN_DOWN_PIN); // botón bajar brillo
int LEDbright = 0;
uint32_t eepromTimer = 0;
boolean eepromFlag = false;
void setup() {
  pinMode(LED_PIN, OUTPUT); // Pin LED como salida
  if (EEPROM.read(INIT_ADDR) != INIT_KEY) { // primer inicio  
    EEPROM.write(INIT_ADDR, INIT_KEY);    // anotó la clave
   // escribió el valor de brillo predeterminado
    // en este caso, este es el valor de la variable declarada arriba
    EEPROM.put(0, LEDbright);
  }
  EEPROM.get(0, LEDbright); //  lee el brillo
  analogWrite(LED_PIN, LEDbright);  //incluido
}
void loop() {
  // comprobar EEPROM
  checkEEPROM();
  // botones de sondeo
  btnUP.tick();
  btnDOWN.tick();
  if (btnUP.isClick()) {
    // aumentar al hacer clic
    LEDbright += 5;
    setBright();
  }
  if (btnDOWN.isClick()) {
    //  bajar al hacer clic
    LEDbright -= 5;
    setBright();
  }
}
void setBright() {
  LEDbright = constrain(LEDbright, 0, 255); // limitado
  analogWrite(LED_PIN, LEDbright);          // cambió el brillo
  eepromFlag = true;                        //levanta la bandera
  eepromTimer = millis();                   // reiniciar el temporizador
}
void checkEEPROM() {
   // si la bandera está levantada y han pasado 10 segundos desde el último clic (10,000 ms)
  if (eepromFlag && (millis() - eepromTimer >= 10000) ) {
    eepromFlag = false;           // dejó caer la bandera
    EEPROM.put(0, LEDbright);     // a la EEPROM
  }
}

De una manera tan simple, hemos reducido significativamente el desgaste de la EEPROM, muy a menudo uso este «algoritmo» para trabajar con las configuraciones en mis Arduinos.

Hay otras tareas en las que los datos se escriben en la EEPROM no cuando el usuario cambia algo, sino constantemente, es decir, la memoria opera en modo de caja negra y registra valores continuamente. Este puede ser, por ejemplo, un controlador de horno que mantiene el régimen de temperatura de acuerdo con un algoritmo especial, y después de un reinicio repentino debe regresar al lugar en el proceso donde se interrumpió. Hay dos opciones a nivel general:

  • Un capacitor de gran capacidad para alimentar el microcontrolador, que le permite guardar el funcionamiento del Arduino después de apagar la alimentación durante un tiempo suficiente para escribir en la EEPROM (~ 3.3 ms). Además, el Arduino debe tener en cuenta que la alimentación general se ha apagado: si es un voltaje alto (por encima de 5 voltios), entonces puede ser un divisor de voltaje por un pin analógico. Si es de 5 Voltios, puede medir el voltaje del Mc, y también se puede capturar el momento de apagado (descarga del capacitor) y se pueden registrar los datos necesarios. Se puede definir una interrupción que se activará cuando la tensión de alimentación caiga por debajo de un nivel peligroso. Puede llevar 5 voltios directamente al pin digital y alimentar el Arduino a través de un diodo y poner un condensador; luego, el voltaje en el pin de medición desaparecerá antes de que el Arduino se apague, y funcionará un poco más desde el condensador. Aquí hay un diagrama: 
Alimentación de arduino con condensador y diodo
Alimentación de arduino con condensador y diodo
  • Puede escribir datos (no necesariamente un byte, puede tener una estructura completa) inteligentemente, extendiéndolos por toda la EEPROM. Hay dos opciones a nivel general:
    • Escriba datos cada vez en la siguiente celda y repita la transición a la primera. También necesitará almacenar un contador en algún lugar, apuntando a la dirección de la celda actual, y este contador también deberá almacenarse inteligentemente para que no desgaste la celda. Por ejemplo, un contador es una estructura que consta de un contador de reescritura para esa estructura y un contador de direcciones para una estructura grande.
    • Escriba datos hasta que se alcance el límite en el número de reescrituras; almacene el número de reescrituras actuales, por ejemplo, en la misma estructura. Digamos que la estructura ocupa 30 bytes, es decir, en el futuro podemos encontrar esta estructura en una dirección que sea múltiplo de 30. El programa se ejecuta, el contador cuenta el número de reescrituras, cuando se alcanza una cantidad peligrosa, todo la estructura se “mueve” a las siguientes 30 direcciones.

Puede encontrar muchas opciones para reducir el desgaste de las celdas EEPROM, únicamente para su situación. Incluso hay bibliotecas listas para usar, por ejemplo EEPROMWearLevel


Deja un comentario