[ESP32] : Sauvegarder dans une EEPROM

27 avril 2022 rdorigny 0 commentaires

Dans cet article, nous allons voir comment stocker des données dans une mémoire non-volatile, c'est à dire persistante après un arrêt du microcontrôleur ESP32.

Tout d'abord, nous évoquerons la gestion de la mémoire de l'ESP32, puis l'utilisation de la mémoire flash de l'ESP32 comme de l'EEPROM et enfin nous verrons comment utiliser un composant EEPROM externe.




1) Organisation de la mémoire dans l'ESP32


Le microcontrôleur ESP32 dispose d’une mémoire Flash de 4MB qui est organisée selon un schéma de partition. Ce schéma est accessible par l'intermédiaire de l'IDE Arduino par Outils-->Partition Scheme.


On voit que sur cette exemple la mémoire de 4MB est répartie à hauteur de 1.2MB pour le code d'application, 1,2 Mo est pour OTA (Over The Air) et 1,5 Mo est réservé pour SPIFFS (stockage de fichiers). Pour plus de détails la répartition est définie sous
C:/Users//AppData/Local/Arduino15/packages/esp32/hardware/esp32/1.0.6/tools/partitions
En réalité, la mémoire EEPROM est émulée sur ESP32 dans un espace en mémoire FLASH appelé NVS (Non Volatile Storage). On peut l'observer en ouvrant les fichiers de partitions. Pour rappels, les mémoires Flash et EEPROM sont des méthodes de stockage numériques non volatile ROM sur lesquelles vous pouvez écrire plusieurs fois. La différence entre Flash et EEPROM est la manière d'effacement des données. En effet, l'EEPROM efface les différents octets de mémoire utilisée pour stocker des données, alors que la technologie flash permet d'effacer des blocs plus larges . Cela rend la mémoire flash plus rapide à réécrire puisqu'elles peut effacer de grandes portions de mémoire en une seule opération.

Par exemple, le fichier default.csv :
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
spiffs,   data, spiffs,  0x290000,0x170000,

Pour bien comprendre la typologie des partitions :
  • nvs correspond au stockage non volatile ;
  • otadata contient des données de gestion OTA ;
  • app0 contient le code de l'application ;
  • app1 est réservé au stockage des fichiers OTA téléchargés ;
  • spiffs correspond au stockage de fichiers.

  • Au bilan, il y a deux solutions internes à l'ESP32 pour stocker des données non-volatiles pour faire comme une mémoire EEPROM (Electrically-Erasable Programmable Read-Only Memory ou mémoire morte effaçable électriquement et programmable). La première est le NVS et la seconde est le SPIFFS en stockant les données par des fichiers.

    Dans le chapitre suivant, nous allons voir comment utiliser la mémoire NVS pour stocker des données. Je ne me suis pas attardé sur le stockage SPIFFS, peut-être une prochaine fois.

    2) Utilisation de la mémoire NVS

    Les données enregistrées à l'aide des préférences sont structurées comme suit :
     namespace{
      key1: value1
      key2: value2
    }
    Les préférences sont disponibles par programmation orientée objets, et la bibliothèque Preferences.h fournit les méthodes de gestion de ces objets (getter, seter, ...).

    Pour illustrer la mise en œuvre des préférences et vous montrer comment on exploite la mémoire NVS, je vais prendre deux exemples. Ce sera plus simple qu'un long discours, et franchement, ce n'est pas difficile. Pour cela, je vais commencer avec un exemple basique d'utilisation de la bibliothèque Preferences.h. Puis un autre, avec la mise en pratique d'un cas réel, en utilisant la mémoire nvs afin de conserver le statut d'un bouton poussoir. Let's go!

    2.1) Exemple basique, démonstration avec un compteur

    Le programme ci-dessous incrémente un compteur, affiche la valeur stockée en mémoire, sauvegarde la variable compteur et redémarre le microcontrôleur.
    /*********
    Robert DORIGNY le 29 avril 2022 
    rdorigny@free.fr - www.doritique.fr
    *********/
    #include "Preferences.h"
    
    Preferences preferences;
    
    //Phase d'initialisation++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    void setup() {
      //initialisation du moniteur série
      Serial.begin(115200);
      delay(1000);   
      }
    
    //Boucle d'attente de réception de la distance+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    void loop() {
      
      delay(5000);
      
      // Si besoin de faire le vide
      //preferences.clear();
      //Ouverture des namespace
      preferences.begin("my-app", false);
    
      //Récupération de la valeur du compteur
      unsigned int cpt=preferences.getUInt("cpt", 0);
      Serial.printf("Cpt : %un", cpt);
      
      cpt++;
      
      //Sauvegarde de la valeur du compteur
      preferences.putUInt("cpt",cpt);
    
      //Fermeture des namespace
      preferences.end();
      
      Serial.println("Reboot");
      ESP.restart();
      }

    Ce qui donne bien un compteur qui s'incrémente grâce à la mémoire NVS.

    2.2) Sauvegarde en mémoire de la position d'un bouton poussoir

    L'idée est de conserver en mémoire la dernière position d'un bouton poussoir, l'objectif étant d'être résilient en cas de coupure électrique ou d'un redémarrage du microcontrôleur. On utilise une diode LED pour afficher le statu ON/OFF de notre petit système.

    Le schéma de notre montage :

    Le circuit monté sur une breadboard vite fait sur le gaz.


    Et pour le code:
    #include "Preferences.h"
    
    Preferences preferences;
    
    const int buttonPin = 4;    
    const int ledPin = 5;      
    
    bool ledState;         
    bool buttonState;             
    int lastButtonState = LOW;
    
    unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
    unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers
    
    void setup() { 
      Serial.begin(115200);
    
      //Create a namespace called "gpio"
      preferences.begin("gpio", false);
    
      pinMode(buttonPin, INPUT);
      pinMode(ledPin, OUTPUT);
    
      // lecture du dernier état en mémoire flash
      ledState = preferences.getBool("state", false); 
      Serial.printf("LED state before reset: %d n", ledState);
      // set the LED to the last stored state
      digitalWrite(ledPin, ledState);
    }
    
    void loop() {
      //lecture du bouton
      int reading = digitalRead(buttonPin);  
      //sequence anti-rebond
      if (reading != lastButtonState) { 
        lastDebounceTime = millis();
      }
      if ((millis() - lastDebounceTime) > debounceDelay) {
        if (reading != buttonState) {
          buttonState = reading;
          if (buttonState == LOW) {
            ledState = !ledState;
          }
        }
      }
      lastButtonState = reading;
      if (digitalRead(ledPin)!= ledState) {  
        Serial.println("State changed");
        // change the LED state
        digitalWrite(ledPin, ledState);
        
        // save the LED state in flash memory
        preferences.putBool("state", ledState);
        
        Serial.printf("State saved: %d n", ledState);
      }
    }
    

    A noter le code anti-rebond qui attend une stabilisation du signal de bouton. On peut limiter les rebonds avec un condensateur de 10 à 100nF en parallèle du bouton pour court-circuiter les oscillations.

    3) Utilisation d'une mémoire externe

    La difficulté avec la mémoire EEPROM, c'est qu'elle est limitée en nombre d'écritures et généralement de l'ordre de 10K à 100K écritures selon les constructeurs. Au de là, il est possible qu'elle devienne défectueuse ce qui est problématique pour un microcontrôleur. L'idée pour des programmes qui l'utilisent beaucoup est d'utiliser une mémoire externe à l'ESP32. Si jamais elle devient HS, il suffira de la changer.

    Il existe plusieurs technologies de mémoire EEPROM, j'ai opté pour de la RAM ferroélectrique (FRAM). C'est une technologie récente, qui consomme moins d'énergie, plus stable contre le champ électromagnétique externe, plus rapide et surtout qui possède une capacité de plusieurs milliards d'écritures. Son seul inconvénient, et qu'elle présente une densité de stockage plus faible ce qui la disqualifie pour certains usages. Mais si vous n'avez pas besoin de beaucoup de mémoire pour votre application, la fiabilité de la FRAM la rend intéressante.

    Pour le choix de la mémoire, on m'a conseillé la mémoire FRAM MB85RC256V 64 kbits sur mini-plaquette et pour une autre en CMS RC64V SOP8 MB85RC64V 64 kbits sur Alliexpress pour un peu plus de 5€ environ. Attention, pour la version CMS, il est nécessaire de la souder sur un adaptateur SOP8 (avec de la pâte à braser pour moi).


    S'agissant de puces I2C, il est nécessaire de connaitre son adresse. Pour cela j'utilise un code scanner I2C qui affiche les adresses trouvées.
    #include "Wire.h"
     
    #define I2C_Freq 100000
    #define SDA_0 21 //A modifier selon votre ESP32
    #define SCL_0 22 //A modifier selon votre ESP32
     
    TwoWire I2C_0 = TwoWire(0);
     
    void setup()
    {
      Serial.begin(115200);
      I2C_0.begin(SDA_0 , SCL_0 , I2C_Freq);
    }
     
    void loop()
    {
      byte error, address;
      int nDevices;
      Serial.println("Scanning...");
      nDevices = 0;
      for(address = 1; address < 127; address++ )
      {
        // The i2c_scanner uses the return value of
        // the Write.endTransmisstion to see if
        // a device did acknowledge to the address.
        I2C_0.beginTransmission(address);
        error = I2C_0.endTransmission();
        if (error == 0)
        {
          Serial.print("I2C device found at address 0x");
          if (address<16)
            Serial.print("0");
          Serial.print(address,HEX);
          Serial.println("  !");
          nDevices++;
        }
        else if (error==4)
        {
          Serial.print("Unknown error at address 0x");
          if (address<16)
            Serial.print("0");
          Serial.println(address,HEX);
        }    
      }
      if (nDevices == 0)
        Serial.println("No I2C devices foundn");
      else
        Serial.println("donen");
      delay(5000);           // wait 5 seconds for next scan
    }
    


    Visiblement, l'adresse de ma puce est 0x50. :-) Le code ci-dessous réalise une écriture de la valeur 72 à l'adresse 5.
    #include "Wire.h"  // bibliothèque i2c
    
    #define adresse_EEPROM 0x50    //Addresse i2c de l'EEPROM
    
    void setup(void)
    {
    
      // on veut écrire la valeur 100 à l'adresse 5
      int adresse = 5; // max: 8191
      int valeur = 72; // max: 255
    
      Serial.begin(115200);
      Wire.begin();
      delay (100);
    
      writebyte(adresse_EEPROM, adresse, valeur);
      
      Serial.print("La valeur ");
      Serial.print(valeur);
      Serial.print(" a été enregistrée en mémoire à l'adresse ");
      Serial.println(adresse);
    }
    
    void loop() {
    
    }
    
    void writebyte(int adressei2c, unsigned int adresseMem, byte data )
    {
      Wire.beginTransmission((int)(adressei2c));  // adresse i2c du 24C64A
      Wire.write((int)(adresseMem >> 8));   // bits de poids fort de l'adresse mémoire
      Wire.write((int)(adresseMem & 0xFF)); // bits de poids faible de l'adresse mémoire
      Wire.write(data);  // valeur qu'on désire enregistrer à cette adresse
      Wire.endTransmission();
      delay(5);
    }
    

    Le code ci-dessous affiche les 30 premières valeurs de la mémoire FRAM.
    #include "Wire.h"  // bibliothèque i2c
    
    #define adresse_EEPROM 0x50    //Addresse i2c de l'EEPROM
    #define adresseMax 8192 // taille de l'EEPROM
    
    int adresse = 0;
    
    void setup(void)
    {
      Serial.begin(115200);
      Wire.begin();
      delay(100);
    
      Serial.println("Lecture des valeurs enregistrees sur l'EEPROM:");
    
      //for (int i = 0; i < adresseMax; i++) {
      for (int i = 0; i < 30; i++) {
        Serial.print("Adresse: ");
        Serial.print(i);
        Serial.print("t");
        Serial.print("Valeur: ");
        Serial.println(readbyte(adresse_EEPROM, i), DEC);
      }
    }
    
    void loop() {
    
    }
    
    byte readbyte(int adressei2c, unsigned int adresseMem )
    {
      byte lecture = 0;
    
      Wire.beginTransmission((int)(adressei2c));
      Wire.write((int)(adresseMem >> 8));   // bits de poids fort de l'adresse mémoire
      Wire.write((int)(adresseMem & 0xFF)); // bits de poids faible de l'adresse mémoire
      Wire.endTransmission();
      Wire.requestFrom((int)(adressei2c), 1);
      delay(5);
    
      if (Wire.available()) {
        lecture = Wire.read();
      }
    
      return lecture;
    }
    

    Ce qui donne :

    Avec le composant CMS MB85RC64V de chez FUJITSU, j'obtiens le même résultat. Ce qui est beaucoup plus économique (trois puces avec 8 fois plus de mémoire pour le même prix!).

    Conclusion

    Voilà pour ma première expérience autour de l'utilisation de la mémoire EEPROM avec un ESP32.

    Franchement, je n'ai rencontrée aucune difficulté dans son utilisation. Le plus difficile, si j'ose dire, est de bien comprendre comment on adresse l'espace mémoire.







    Pseudonyme (obligatoire) :
    Adresse mail (obligatoire) :
    Site web :




    © 2022 www.doritique.fr par Robert DORIGNY