Aquila Reloaded – Chapitre 9 : Utilisation de l’EEPROM pour sauvegarder la configuration

Bonjour tout le monde,

Ci-dessous un nouveau sujet sur mon projet « Aquila Reloaded » ! C’est un sujet quelque peu « off topic » et qui peut servir pour d’autres besoins. En effet, il est parfois nécessaire de mémoriser des données en mémoire, on va voir comment utiliser la mémoire EEPROM de l’Arduino :

  • Comment lire et écrire des données
  • Comment éviter des écritures intempestives pour préserver les cycles d’écriture
  • Comment « initialiser » les données d’un Arduino vierge
  • Comment gérer l’ajout de nouvelles données sans effacer les anciennes

Je précise que je m’inspire en très grande partie des techniques présentées sur cette page écrite en français. Je vous invite grandement à la visiter, ce que je vais raconter sur cette page n’est qu’un résumé 🙂 .

Qu’est ce qu’une EEPROM ?

Une mémoire EEPROM est une mémoire non-volatile, c’est à dire qu’elle conserve les données après interruption de l’alimentation. Cela ressemble à la mémoire FLASH que l’on retrouve dans les SSD ou les clés USB, si ce n’est que sa technologie, plus ancienne, est plus lente et avec plus de latence à l’écriture, et une capacité très réduite.

Les EEPROM servent entres autres choses à stocker le BIOS de nos cartes mères. Notre Arduino, comme beaucoup de Microcontrôleurs dispose aussi d’une mémoire EEPROM :

  • Arduino UNO et Nano : 1ko
  • Arduino MEGA : 4ko

Les EEPROM ont une durée de vie limitée à 100000 écritures environ, il faut donc les préserver, en ne stockant que des données qui changent peu souvent (données de calibration, configuration de carte, …).

Lire et écrire des données dans l’EEPROM de l’Arduino

Le B-A-BA : lire et écrire des données unitaires

Tout d’abord, il faut insérer la librairie EEPROM.h

#include <EEPROM.h>

Ensuite pour lire une donnée, il faut utiliser la fonction EEPROM.read()

byte EEPROM.read (int adresse)

Dans cet exemple, nous allons récupérer l’octet n°42

byte data = EEPROM.read(41);

(Non non il n’y a pas d’erreur, c’est bien 41, les adresses mémoires débutent à 0 😉 )

Pour l’écriture, on peut utiliser la fonction EEPROM.write()

EEPROM.write(int adresse, byte valeur)

Le gros défaut de EEPROM.write() est qu’il écrit systématiquement. Or notre EEPROM a un nombre de cycles d’écriture limités avant de s’altérer. Heureusement, il existe une fonction qui vérifie le contenu de la donnée pour ne l’écrire que si c’est nécessaire. Elle se nomme EEPROM.update().

Par exemple le code ci-dessous écrira 242 à l’adresse 41 :

EEPROM.update(41, 242);

Astuce, vous pouvez utiliser aussi la librairie EEPROM comme un tableau 😉

byte data = EEPROM[41]; 
EEPROM[41] = 242;

Mieux : lire et écrire des variables dans l’EEPROM de l’Arduino

Lire et écrire des octets (bytes), c’est bien mais c’est quelque peu fastidieux si on doit écrire des valeurs autres que celles comprises entre 0 et 255. (Float, Int, …)

Donc comment faire pour stocker des variables ? Il existe deux fonctions : EEPROM.get() et EEPROM.put(). Elles s’utilisent de la manière suivante :

EEPROM.get(int adresse, variable)
EEPROM.put(int adresse, variable)

Attention aux tailles des variables ! Une variable « int » prend 2 octets, une variable « long » prend 4 octets (pour ne citer qu’elles) ! Donc dans la définition de vos adresses mémoire, faites très attention au chevauchement. Au pire, prévoyez large, il y a 1024 octets a minima de disponible. Pour connaitre la taille d’un objet, utilisez la fonction sizeof(variable) combiné à Serial.println().

Attention également aux pointeurs ! Toutes les variables exploitant des tableaux, des chaines de caractères, des objets,… utilisent des pointeurs. Or, si on sauvegarde un pointeur, on ne sauvegarde pas son contenu, mais uniquement l’adresse des données dans la RAM. Une RAM s’effaçant a chaque reboot, le pointeur sera d’aucune utilité… Il vaut mieux, dans ce cas, sauvegarder octet par octet.

Encore mieux : lire et écrire des données structurées dans l’EEPROM de l’Arduino

Comme vu ci-dessus, c’est quelque peu fastidieux de gérer les « positions mémoire » quand plusieurs variables cohabitent. On va utiliser une astuce permettant de ne sauvegarder qu’une seule variable, contenant toutes les autres.

La technique consiste à utiliser un objet struct. Ce type d’objet permet de définir un ensemble de variables « groupées ». Et c’est la technique que nous allons utiliser (merci le carnet du maker ^^)

Pour définir un objet struct :

struct DATAEEPROM {
 int valeur1; int valeur2;}

Pour utiliser un objet struct :

DATAEEPROM de;
de.valeur1 = 42;de.valeur2 = 1337;

Vous voyez, c’est facile à manipuler 🙂 . Maintenant pour le sauvegarder, on va écrire l’objet structure sur l’EEPROM à partir de l’octet 0 :

EEPROM.put(0, de);

Et enfin, pour lire le contenu de l’EEPROM et le stocker dans la structure, on va lire depuis l’octet 0 :

DATAEEPROM de;EEPROM.get(0, de);

Je recommande personnellement cette technique, c’est la plus simple à utiliser !

Cas concret : lire et écrire les données de paramétrage de l’Aquila

Il serait intéressant de stocker dans notre EEPROM toutes les données de paramétrage de la carte :

  • Paramétrage des moteurs : nombre de pas maxi avant butée, vitesse maximale, accélération, …
  • Paramétrage des capteurs CTN : résistance nominale, température nominale, coefficient B, …
  • Paramétrage des LED : mode RGB ? mode dimmer ? …
  • Timeout DMX, …

Il serait également intéressant de détecter quand les données sont erronées (EEPROM vierge ou contenant des valeurs incohérentes).

Il serait également intéressant de permettre l’ajout de nouvelles données au fil des mises à jour logicielles, sans écraser les anciennes données.

Définition de la structure

Dans cet exemple, je vais me concentrer sur le stockage de données pour la CTN. (Si vous ne vous rappelez pas comment ca fonctionne, voir le chapitre 7 ;-))

Voici les données que nous avons besoin de connaitre :

  • Température nominale, en degrés celcius : on choisira une variable « int »
  • Résistance nominale, en ohms : on choisira une variable « unsigned long » (un int peut être trop petit)
  • Coefficient B : on choisira une variable de type « unsigned int »
  • Résistance du pont diviseur, en ohms : on choisira une variable « unsigned long »

On va également utiliser une variable statique arbitrairement choisie. Elle nous servira à savoir si les données en EEPROM sont initialisées. En l’absence de ce nombre, cela signifiera que les valeurs en mémoire ne sont pas bonnes. On choisira un « unsigned long » pour avoir une valeur codée sur 4 octets et limiter ainsi le risque de « faux positif ».

Enfin, on va ajouter une variable indiquant au programme de quelle version de structure il s’agit. On verra un peu plus bas comment exploiter cette information.

En partant de ce principe, on va créer une structure comme ceci :

static const unsigned long DATAEEPROM_MAGIC = 123456789;
static const byte DATAEEPROM_VERSION = 1;

struct DATAEEPROM {
 // Nombre magique
 unsigned long magic;
 
 // Version de la structure
 byte struct_version;
 
 unsigned long ntcNominalRes; //In ohm
 int ntcNominalTemp; //In celcius
 unsigned int ntcBCoeff;
 unsigned long ntcBridgeRes; //In ohm
}

Lecture de l’EEPROM

Comme vu précédemment, la lecture de l’EEPROM avec stockage dans une structure se fait comme ceci :

DATAEEPROM de;
EEPROM.get(0, de);

Détection des données non-initialisées

Maintenant que notre objet DATAEEPROM contient toutes les données, on peut vérifier que notre « magic number » est bon. S’il est faux, c’est que les données n’ont pas été initialisées.

byte err = de.magic != DATAEEPROM_MAGIC;

Ecriture de valeurs par défaut

Si notre « magic number » n’est pas bon, alors il ne faut pas tenir compte des valeurs lues en EEPROM. Il faut tout d’abord les mettre à jour avec des valeurs par défaut, puis les écrire en EEPROM.

Le changement se fait comme ceci :

if (err) {
 de.ntcNominalRes = 10000; //In ohm
 de.ntcNominalTemp = 25; //In celcius
 de.ntcBCoeff = 3950;
 de.ntcBridgeRes = 10000; //In ohm
}

Puis on déclenche une écriture

de.magic = DATAEEPROM_MAGIC;
de.struct_version = DATAEEPROM_VERSION;
EEPROM.put(0, de);

Gérer le versioning

Si vous améliorez votre programme au fil du temps, vous risquez de modifier votre structure, alors cela modifiera également la structure des données stockées dans l’EEPROM.

  • Si vous supprimez une variable, les données seront décalées
  • Si vous ajoutez une variable, il faudra détecter si les données nouvelles sont également initialisées dans l’EEPROM

Il est peu probable que des variables soient supprimées, de ce fait, on va gérer l’ajout de variables. Pour cela, on va mettre en place un numéro de version de la structure. Si le numéro de version récupéré en mémoire EEPROM est inférieur à la valeur stockée dans le programme, alors les données ne sont pas à jour.

Dans cet exemple, je vais créer une structure « version 2 » intégrant une nouvelle variable gérant le timeout DMX.

if (de.struct_version < 2 || err) {
 de.dmxIdleTimeout = 3000;
}

Ainsi, seule les données « version 2 » seront mises à jour si l’EEPROM contient déjà des données de la version 1

On peut répéter le schéma indéfiniment :

if (de.struct_version < 3 || err) {
 de.dimmerLedMode = true;
}

Il faut penser à mettre à jour la valeur statique de la version.

static const byte DATAEEPROM_VERSION = 3;

Résultat final

Ce qui nous donne le résultat suivant :

static const unsigned long DATAEEPROM_MAGIC = 123456789;
static const byte DATAEEPROM_VERSION = 3;

struct DATAEEPROM {
 // Magic Number
 unsigned long magic;
 // Struct version
 byte struct_version;
 
 // Data from V1 struct
 unsigned long ntcNominalRes; //In ohm
 int ntcNominalTemp; //In celcius
 unsigned int ntcBCoeff;
 unsigned long ntcBridgeRes; //In ohm

 // Data from V2 struct
 unsigned int dmxIdleTimeout;

 // Data V3
 bool dimmerLedMode;
}

void writeEEPROM() {
 de.magic = DATAEEPROM_MAGIC;
 de.struct_version = DATAEEPROM_VERSION;
 EEPROM.put(0, de);
}

void readEEPROM() {
 bool writeNeeded = false;
 
 // Read data content from EEPROM to struct
 EEPROM.get(0, de);
 
 // Detect non-initialized flash memory (bad magic number)
 byte err = de.magic != DATAEEPROM_MAGIC;

 // Default values
 if (err) {
  writeNeeded = true;
 
  // LED Temperature
  de.ntcNominalRes = 10000; //In ohm
  de.ntcNominalTemp = 25; //In celcius
  de.ntcBCoeff = 3950;
  de.ntcBridgeRes = 10000; //In ohm
 }
 
 if (de.struct_version < 2 || err) {
  de.dmxIdleTimeout = 2000;
 }
 
 if (de.struct_version < 3 || err) {
  de.dimmerLedMode = true;
 }

 // Update data to flash if necessary
 if(writeNeeded) {
  writeEEPROM();
 }
}

Encore merci le carnet du maker pour toutes les astuces que j’ai pu y lire ! Comme vous voyez, je n’ai rien inventé. Je n’ai fait que réexpliquer tout ce qui est déjà dit sur leur site et l’adapter à mes besoins. Pas la peine de réinventer la roue 😉

La suite au prochain épisode !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *