Les Carnets de Byfeel domotique , Objets connectés , DIY , Programmation, Nouvelles Technologies ….

EEPROM ou SPIFFS (module ESP8266) ?

Dans cet article, je vais revenir sur l’utilisation de la mémoire sur les modules esp8266. En effet suite à mon article, sur l’utilisation de la mémoire SPIFFS, plusieurs lecteurs me demandent la différence entre EEPROM et SPIFFS, et l’intérêt qu’il y a à utiliser l’une plutôt que l’autre.

Afin de bien comprendre, je vais partir sur un exemple concret de sauvegarde d’une configuration, incluant plusieurs type de champs (booléen, entier, chaine de caractère …).

Avantages et inconvénients de chaque mémoire

Les modules ESP8266 , disposent de plusieurs type de mémoire . aujourd’hui je vais me concentrer sur les deux types , qui permettent de sauvegardées des données. La mémoire Eeprom et la mémoir flash ( ou SPIFFS ) .
Ces deux mémoires ont des caractéristiques différentes :

EEPROM : ou mémoire morte est une partie de la mémoire qui n’est pas effacé , tant que l’on ne réécrit pas par-dessus. Cette mémoire est assez lente par rapport à la mémoire vive ou à la mémoire flash. Elle est de plus limitée en écriture (environ 100 000 , après elle devient inutilisable ). Sa taille est assez faible, varie en fonction des différents constructeurs.

SPIFFS : Cette mémoire, permet aussi de stocker des données, même après un redémarrage. Plus rapide que l’EEPROM , elle permet une réécriture sans limite. Selon les modules , sa taille varie entre 512ko à 4Mo.Il faut voir cette mémoire comme une SDcard. Plus d’infos sur l’utilisation de SPIFFS , voir mon précédent article .

A la première lecture , on aurait tendance à privilégier la mémoire SPIFFS . Elle est plus performante , plus rapide , il y a plus d’espace , alors pourquoi s’embêter avec la mémoire EEPROM. Dans certains cas , la mémoire EEprom , peut être préféré a l’utilisation de la mémoire SPIFFS.

La gestion de la memoire EEPROM , est une écriture adresse par adresse , alors que la mémoire flash , utilise un système de fichier. Le transfert des fichiers dans cette mémoire , se fait sur l’intégralité de la taille définis.

Par exemple : A chaque fois que je met à jour mes fichiers dans la mémoire SPIFFS , via la méthode data upload ( dossier data ) , la memoire SPIFFS est entièrement reconstruite, tous les fichiers sont remplacé par le contenu du dossier data. Je perd , donc mon fichier de config. Alors que ma sauvegarde en EEPROM , ne peut etre effacé que si je réécris par dessus.

La question qu’il faut e poser , quels sont les informations qui doivent être sauvegardés en EEPROM , et quels autres en SPIFFS.

  • EEPROM : je sauvegarde les données qui seront quasiment jamais modifié , et qui ne doit pas etre effacés lors d’une eventuelle mis à jour de ma zone SPIFFS ( dossier data ).
  • SPIFFS : Je sauvegarde les données , qui seront amenés à êtres modifiés régulièrement et qui n’aurons pas d’incidence sur le fonctionnement , en cas de mise à jour de la zone SPIFFS ( ces données seront effacés ). J’ai besoin d’un espace de stockage important , par exemple fichier historique , etc ….

Sur les modules ESP8266 et selon les fabricants , les différentes mémoires , sont en réalité des blocs mémoires qui se suivent . Par exemple pour un module de 4Mo ( wemos D1 ou mini ) :

/* Flash Split for 4M chips */
/* sketch @0x40200000 (~1019KB) (1044464B) */
/* empty  @0x402FEFF0 (~4KB) (4112B) */
/* spiffs @0x40300000 (~3052KB) (3125248B) */  - environ 3Mo au max
/* eeprom @0x405FB000 (4KB) */
/* rfcal  @0x405FC000 (4KB) */
/* wifi   @0x405FD000 (12KB) */

EXEMPLE : Le fichier de Configuration

Pour illustrer cette article , j’ai choisi de monter comment stocker un fichier de configuration , qui à la particularité de montrer un peu tous les cas de figure .

Je souhaites enregistrer une donnée de type texte , une donnée de type entier et une donnée de type Booléen.

Pour faciliter les enregistrements et les lectures , je vais utiliser les structures. Les structures permettent d’assembler des types de données différentes dans une sorte de tableau. La définition d’une structure en C++ est très simple , il suffit d’utiliser le mot clé struct , suivi des définitions de chaque variable composant cette structure.

struct sConfig {
char[10] nom;
int NumeroPIN;
bool defaut;
};

Je viens de définir une structure sConfig , qui contient les champs nom ( une chaine de 10 caractères ) , un champ entier NumeroPIN et un champ booléen defaut. Pour créer une variable config de type structure sConfig il suffit de l’initialiser :

// initialise une variable config de type Structure sCOnfig
sConfig config;

// pour affecter une valeur , je peux utiliser la syntaxe
config.defaut=true;

pour affecter une valeur à notre structure , il suffit de l’appeler de la façon suivante : config.defaut=true;

Enregistrer la configuration sur EEPROM

Pour utiliser la bibliotheque eeprom , inclus dans les bibliothèques systémes de l’esp8266 , il faut l’inclure avec la commande : #include eeprom.h

La bibliothèque eeprom.h pour l’esp8266 est différente de la bibliothèque dédié à l’Arduino. Dans le cas d’un ESP8266 , la mémoire eeprom , est un espace réservé situé à la fin de la mémoire SPIFFS. Pour utiliser ce type de mémoire il va falloir , dans un premier temps , renseigner la taille de la mémoire que l’on va utiliser , afin de la charger en RAM.

EEPROM.begin(512); Cette instruction , permet de reserver 512 Octets en RAM , pour manipuler les données de l’EEprom.

Ensuite , on retrouve , les mêmes fonctions que dans la bibliothèque Arduino .

EEPROM.read() & EEPROM.write() , qui se charge de lire ou d’écrire dans un octet . C’est bien, mais il est souvent bien plus intéressant de lire ou écrire des données typées, comme des nombres entiers, , du texte, des infos binaires , etc … . Pour cela il y a EEPROM.get() et EEPROM.put() , qui se charge de lire ou écrire un bloc de donnée ( comme une structure ).

Il ne faudra pas oublier à la fin de renseigner la commande : EEPROM.commit(); qui se charge d’ecrire les données en RAM à l’adresse mémoire sélectionné. Puis EEPROM.end() , qui permet de libérer la mémoire utilisé en RAM.

Ecrire EEPROM

#include <EEPROM.h>
// ma structure config
struct sConfig {
  char nom[10];
  int NumeroPIN;
  bool defaut;
};
sConfig config;
int eeAddress = 0;   //Adresse de départ pour enregistrement sur EEPROM

void setup() {
// affichage dans moniteur serie
Serial.begin(57600);
// charge 128 octet memoire eeprom en RAM
EEPROM.begin(128);
// verification taille eeprom affecté ( devrait affiché 128 )
Serial.println("taille eeprom "+String(EEPROM.length()));

// affectations  variables
strcpy(config.nom, "byfeel");
config.NumeroPIN=10;
config.defaut=true;

//mise en mémoire pour ecriture par la methode put
  EEPROM.put(eeAddress, config);
//pour copier le cache mémoire en eeprom ( l'écriture se fait à ce nomment la )
EEPROM.commit();
EEPROM.end(); // pour liberer la memoire
Serial.println("Enregistrement memoire ok");
}

void loop() 
{   
}

Lecture EEPROM

#include <EEPROM.h>
// ma structure config
struct sConfig {
  char nom[10];
  int NumeroPIN;
  bool defaut;
};
sConfig config;
int eeAddress = 0;   //Adresse de départ pour lecture

  
void setup() {
// affichage dans moniteur serie
Serial.begin(57600);
// charge 128 octet memoire eeprom en RAM
EEPROM.begin(128);
Serial.println("taille eeprom "+String(EEPROM.length()));

// Lit la mémoire EEPROM
EEPROM.get(eeAddress, config);
// affiche les valeurs lu
Serial.print("valeur Nom : ");
Serial.println(config.nom);
Serial.println("Valeur PIN : "+String(config.NumeroPIN));
Serial.println("valeur defaut : "+String(config.defaut));

EEPROM.end(); // pour liberer la memoire
Serial.println("Lecture memoire ok");
}

void loop() 
{   
}

Dans la vrai vie…

Dans cet ordre , pas de soucis , je récupère bien ma structure , car j’ai pris la peine de l’enregistrer avant. Mais dans la plupart des programmes , le fichier de config , ne sera pas forcément présent des le départ. Il faudra peut être le créer si celui ci n’existe pas.
Comment détecter si le fichier existe ? Si je me contente de faire un test sur une variable bool , je risque de me retrouver avec un faux positif , car je ne suis pas certains de ce qui a était écrit dans cette mémoire avant que je ne l’utilise ( surtout si mon module , à servis à de nombreux test ).

Pour constater , par vous même , essayer de lire , sans enregistrer avant ( changer le début de l’adresse , par exemple int eeAddress = 64; ) et observer ce que vous avez récupéré ….

Pour éviter de remonter de fausses valeurs , on va ajouter un champ de type long int , afin de vérifier si la structure lu , correspond bien à une réalité , et non à des résidus de données.

Par exemple long magic=12345678; ( clé de vérification ).
Ce qui donne le code source complet ci dessous , qui a pour but de lire le fichier de config , si ce dernier n’existe pas , initialise les valeurs par défaut et sauvegarde le fichier.

/**
 * TUTO  enregistrement fichier config avec structure dans EEPROM
 */

#include <EEPROM.h>

/** Le nombre magique  */
const unsigned long MAGIC=12345678;

// ma structure config
struct sConfig {
  unsigned long magic;
  char nom[10];
  int NumeroPIN;
  bool defaut;
};
sConfig config;

int eeAddress = 64;   //Adresse de départ pour enregistrement

/** Sauvegarde en mémoire EEPROM le contenu actuel de la structure */
void sauvegardeEEPROM() {
  // Met à jour le nombre magic et le numéro de version avant l'écriture
  config.magic = MAGIC;
  EEPROM.put(eeAddress, config);
  EEPROM.commit();
}

/** Charge le contenu de la mémoire EEPROM dans la structure */
void chargeEEPROM() {
  // charge 128 octet memoire eeprom en RAM
  EEPROM.begin(128);
  Serial.println("taille eeprom "+String(EEPROM.length()));
  // Lit la mémoire EEPROM
  EEPROM.get(eeAddress,config);  
  // Détection d'une mémoire non initialisée
  byte erreur = config.magic != MAGIC;
  Serial.println("valeur lu de magic : "+String(config.magic));
  if (erreur) {
    Serial.println("Erreur fichier config non initialisé");
    // Valeurs par défaut pour les variables de config
    // affectation variable
strcpy(config.nom, "byfeel");
config.NumeroPIN=10;
config.defaut=true;
  // Sauvegarde les nouvelles données
  sauvegardeEEPROM();
  }
  else Serial.println("config ok");
EEPROM.end();
}

void setup() {
// affichage dans moniteur serie
Serial.begin(57600);

// Charge le contenu de la mémoire
  chargeEEPROM();
  
// affiche les variables config
Serial.print("valeur Nom : ");
Serial.println(config.nom);
Serial.println("Valeur PIN : "+String(config.NumeroPIN));
Serial.println("valeur defaut : "+String(config.defaut));
}

void loop() 
{   
}

Enregistrer la configuration sur SPIFFS

Pour un enregistrement de la structure config dans un espace SPIFFS , les données devront êtres écrites dans un fichier.

Pour activer la prise en charge de SPIFFS , sous interface arduino IDE , pensez à bien sélectionner l’info FlashSize dans le menu outil . Si vous laissez sur no SPIFFS , il n’y aura aucun message d’erreur , tout semble fonctionner mais pas possible de lire ou écrire un fichier. Il est facile d’oublier de contrôler ce paramètre.

FlashSize Arduino

Sous platformIO , la détection de la taille memoire est automatique , vous n’aurez pas besoin de préciser la taille à utiliser.

Pour verifier , la bonne prise en charge de la memoire flash , il est possible d’utiliser la commande SPIFFS.info , qui renseigne la taille de la mémoire , l’espace utilisé et d’autres paramètres.

SPIFFS.begin();
FSInfo fsInfo;
// info fs
SPIFFS.info(fsInfo);
//fsInfo.totalBytes
float total=(fsInfo.totalBytes/1024);
Serial.println("info totalbytes : "+String(total));
Serial.println("info usedbytes : "+String(fsInfo.usedBytes));
Serial.println("info blocksize : "+String(fsInfo.blockSize));
Serial.println("info pagesize: "+String(fsInfo.pageSize));
Serial.println("info maxOpenFiles : "+String(fsInfo.maxOpenFiles));
Serial.println("info maxPathLength : "+String(fsInfo.maxPathLength));

Pour faciliter la lecture et l’ecriture du fichier de configuration , je vais utiliser la bibliotheque ArduinoJSon , afin de structurer le tout au format json.

Les différentes étapes , seront donc les suivantes

  • Fabrication du json : Transfert de la structure dans une variable « chaine »
  • Ecriture du json , dans un fichier config.json
  • Lecture de ce fichier

Fabrication du json

Pour la fabrication du Json ,j’utilise la bibliothèque arduinojson de Benoit BLANCHON . Dans un premier temps je définis un espace memoire , qui contiendra une copie de la config. Pour plus de detail , sur la définition de la taille du buffer , je vous invite à consulter le site Arduinjson de benoit BLANCHON , ou tout est très bien expliqué. Puis on affecte les variables et on stock le tout dans une chaine par la commande serializeJson .

Ce qui donne :

const int capacity = JSON_OBJECT_SIZE(3); 
StaticJsonDocument<capacity> doc;
doc["nom"] = config.nom; 
doc["pin"] = config.NumeroPIN; 
doc["defaut"] = config.defaut;

// Enregistrement du json dans une variable string
String JsonString;

serializeJson(doc, JsonString);
// a ce stade la variable JsonString contient le descriptif json de ma structure config

Ecriture du json dans le fichier

Pour cela , on va utiliser la bibliothèque Spiffs ( fs.h ) . La commande SPIFFS.open permet d’ouvrir un fichier , soit en mode ecriture avec ‘w’ ou en lecture avec ‘r’ . D’autres modes existe , à retrouver sur la doc en ligne de la bibliotheque .

// chemin et nom du fichier de config
const char *fileconfig = "/config/config.json";  // fichier config

// ouverture du fichier en Ecriture (w ) , si fichier existe pas , il est créé
File  f = SPIFFS.open(fileconfig, "w");
  if (!f) {
    // Fichier config Jeedom absent
    Serial.println("Fichier Config absent - création fichier");
   }
   f.print(JsonString);  // sauvegarde de la chaine qui contient le JSON dans le fichier ouvert
   f.close();

Lecture du json

Je commence par ouvrir le fichier en lecture : SPIFFS.open(fileconfig, « r »);
Puis on extrait les éléments du json par la commande : deserializeJson(docConfig, file);
on obtient un tableau docConfig[key]=value;

// ouverture du fichier config
File file = SPIFFS.open(fileconfig, "r");
   if (!file) {
     //fichier config absent
     Serial.println("Fichier Config absent");
    }
  DynamicJsonDocument docConfig(capacityConfig);
  DeserializationError err = deserializeJson(docConfig, file);
  if (err) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(err.c_str());
  }
// affectation a faire

affectation des variables

l’affectation des variables , permet de renseigner la structure config , par les elements json , le symbole | permet d’affecter une valeur par défaut si celle ci est Null .


// affectation des valeurs , si existe pas on place une valeur par defaut
strlcpy(config.nom,docConfig["nom"] | "byfeel",sizeof(config.nom));
config.NumeroPIN = docConfig["pin"] | 10;
config.defaut = docConfig["defaut"] | true;

Le code complet

Ci dessous , le code complet , permettant de lire un fichier de configuration . si ce fichier existe , il le charge , sinon il affecte des valeurs par défaut .

Dans un deuxième temps , je modifie une variable et enregistre les modifications dans le fichier de configuration.

**
 * TUTO  enregistrement / lecture fichier config dans SPIFFS
 */
// bibliothèque
#include <FS.h>   //Include SPIFFS
#include <ArduinoJson.h>

// ma structure config
struct sConfig {
  char nom[10];
  int NumeroPIN;
  bool defaut;
};
sConfig config;

// chemin fichier config dans memoire flash ( SPIFFS )
const char *fileconfig = "/config/config.json";  // fichier config
//taille buffer pour json
const size_t capacityConfig = 2*JSON_ARRAY_SIZE(3);

void chargeFS() {
  File file = SPIFFS.open(fileconfig, "r");
   if (!file) {
     //fichier config absent
     Serial.println("Fichier Config absent");
    }
  DynamicJsonDocument docConfig(capacityConfig);
  DeserializationError err = deserializeJson(docConfig, file);
  if (err) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(err.c_str());
  }

// affectation des valeurs , si existe pas on place une valeur par defaut
strlcpy(config.nom,docConfig["nom"] | "byfeel",sizeof(config.nom));
config.NumeroPIN = docConfig["pin"] | 10;
  config.defaut = docConfig["defaut"] | true;
 //fermeture fichier
  file.close();
}

void sauveFS() {
  String jsondoc="";
  DynamicJsonDocument docConfig(capacityConfig);
  docConfig["nom"]=config.nom;
  docConfig["pin"]=config.NumeroPIN;
  docConfig["defaut"]=config.defaut;

File  f = SPIFFS.open(fileconfig, "w");
  if (!f) {
    Serial.println("Fichier Config absent - création fichier");
   }
   serializeJson(docConfig,jsondoc);
   f.print(jsondoc);  // sauvegarde de la chaine
   f.close();
   Serial.print(jsondoc);
}

void setup() {
// affichage dans moniteur serie
Serial.begin(57600);
//montage du disque flash
SPIFFS.begin();
// charge dans un premier temps le fichier config
chargeFS();

// affiche les variables config apres lecture FS
Serial.print("valeur Nom : ");
Serial.println(config.nom);
Serial.println("Valeur PIN : "+String(config.NumeroPIN));
Serial.println("valeur defaut : "+String(config.defaut));

//modif donnée PIN
config.NumeroPIN=5;
// sauve fichier
sauveFS();

// lit de nouveau le fichier
chargeFS();

// affiche les variables config apres lecture du nouvel enregistrement
Serial.print("valeur Nom : ");
Serial.println(config.nom);
Serial.println("Nouvelle Valeur PIN : "+String(config.NumeroPIN));
Serial.println("valeur defaut : "+String(config.defaut));

// Bonus : info sur memoire SPIFFS pour DEBUG
FSInfo fsInfo;
// info fs
SPIFFS.info(fsInfo);
//fsInfo.totalBytes
float total=(fsInfo.totalBytes/1024);
Serial.println("info totalbytes : "+String(total)+" ko");
Serial.println("info usedbytes : "+String(fsInfo.usedBytes));
Serial.println("info blocksize : "+String(fsInfo.blockSize));
Serial.println("info pagesize: "+String(fsInfo.pageSize));
Serial.println("info maxOpenFiles : "+String(fsInfo.maxOpenFiles));
Serial.println("info maxPathLength : "+String(fsInfo.maxPathLength));

}
void loop() 
{   
}

conclusion

J’espére avoir été un peu plus clair , que dans mon précédent article .
Il ne vous reste plus , qu’a adapter votre fichier de configuration à votre utilisation , et à choisir quelle , est votre meilleur options , SPIFFS , EEPROM ou les deux ?