Comme l’a très bien expliqué l’article Bibliothèque EEPROM, la mémoire EEPROM est un réservoir très limité en taille et en durée de vie, mais qui reste le seul moyen simple de conserver des informations entre deux allumages d’un Arduino. Certaines fonctions sont proposées par l’IDE Arduino pour écrire dans cette mémoire, mais il y a d’autres possibilités...
Attention toutefois, tous les Arduino ne disposent pas d’une EEPROM, par exemple le DUE, l’Arduino Zero ou l’Arduino 101 n’en ont pas. Dans ce cas précis, des circuits extérieurs comme les 24LC256 ou 24LC512 peuvent combler ce manque.
Bibliothèque EEPROMextent
.
Par :
DIFFICULTÉ :★★★
Ça, c’était avant...
Avant la version 1.6.2 de l’IDE Arduino, la seule solution directe à disposition pour stocker quelque chose dans cette mémoire EEPROM tenait dans les deux fonctions de la bibliothèque EEPROM livrée avec l’IDE : read et write. Ces fonctions permettaient le strict minimum, c’est à dire lire et écrire un seul octet à l’emplacement demandé entre le début et la fin de la mémoire. Elles s’appuyaient sur des fonctions d’encore plus bas niveau read, write et update de byte, word, dword, float et block pour lire, écrire et mettre à jour des octets (8 bits), des mots (16 bits), des mots longs (32 bits), des nombres flottants (quatre octets) et des blocs de mémoire non typés.
Le changement, c’est maintenant !
Depuis la 1.6.2, la bibliothèque a été améliorée pour gérer la durée de vie et stocker n’importe quoi dans cette mémoire (voir l’article de Guillaume). Les anciennes fonctions restent bien sûr disponibles.
Et EEPROMextent dans tout ça ?
Ma petite bibliothèque répond à plusieurs besoins.
Sauver / restaurer une chaîne de caractères :
Parmi les fonctions récemment ajoutées dans la version de base de la bibliothèque EEPROM, il y a get et put pour lire et écrire des données quel que soit leur type. Mais un put sur l’adresse d’un tableau aurait pour effet de sauver l’adresse du tableau, et pas son contenu ! Il fallait donc une fonction supplémentaire pour simplement sauver une chaîne de caractères si l’on ne veut pas bêtement sauver tout le tableau de chaîne, y compris au delà de sa fin.
char textRead[30];
EEPROMextent.writeString(50, "Bonjour Locoduino");
EEPROMextent.readString(50, textRead, 29);
EEPROMextent.writeString va écrire la chaîne "Bonjour Locoduino" à partir de l’adresse EEPROM 50, et jusqu’au dernier caractère. EEPROMextent.readString va relire une chaîne à partir de l’adresse EEPROM 50 pour la stocker dans textRead avec une limite à 29 caractères maximum. On laisse la place pour le zéro de fin.
Sauver / restaurer n’importe quoi :
Pour permettre aux utilisateurs d’anciennes versions de l’IDE de profiter eux aussi des nouveautés, j’ai ajouté également les fonctions EEPROMextent.writeAnything, EEPROMextent.readAnything et EEPROMextent.updateAnything qui sont strictement identiques aux put, get et update apparus dans la 1.6.2 ...
Pour le ménage !
Avant de commencer à sauver et relire des informations, il peut être important de vider la mémoire EEPROM de tout précédent contenu, c’est le rôle de EEPROMextent.clear()
qui peut prendre deux formes :
EEPROMextent.clear(debut, taille);
EEPROMextent.clear(debut, taille, 10);
Le premier appel va remplir la zone de mémoire EEPROM depuis l’adresse ’debut’ sur la longueur ’taille’ avec des zéros.
Le second va faire la même chose, mais en remplissant avec le caractère au code ASCII 10 ! (Si ça ne vous parle pas, voyez Les chaînes de caractères).
La solution Atmel
Dans ses documents techniques, Atmel propose une solution logicielle pour prolonger la durée de vie de cette mémoire EEPROM : le tampon (buffer dans la langue de Mark Twain) circulaire. L’idée est simple : à chaque sauvegarde, on écrit un peu plus loin, à un emplacement différent. Lorsque l’on arrive au bout de la mémoire, on recommence au début ! Ainsi la mémoire est uniformément usée. Pour savoir ce qui est déjà utilisé, on inscrit un octet dans une liste circulaire d’index (le ’Status Buffer’ à droite sur l’image) parallèle au tampon principal (le ’parameter buffer’ à gauche). La liste d’index est initialisée avec des zéros. Chaque nouvel octet écrit est égal au plus grand déjà présent plus un. Et lorsque l’octet dépasse sa capacité, il revient à 0. enfin pour savoir où l’on doit écrire la prochaine donnée, on trouve l’index à 0 ou la valeur la moins élevée.
C’est le rôle de la classe CircularBuffer que j’ai codé d’après leur source. Voici l’exemple livré avec la bibliothèque :
#include "EEPROMextent.h"
struct Point
{
bool exists;
int number;
unsigned long delay;
};
CircularBuffer cb;
void setup()
{
// Writing data...
Point p1, p2;
p1.delay = 12345;
p1.exists = true;
p1.number = 789;
// ----------------------------
// Writing buffer
int totalsize = cb.begin(10, sizeof(Point), 4);
Serial.print("Total size of the buffer : ");
Serial.println(totalsize);
//cb.Clear();
cb.update(&p1);
#ifdef EEPROMEXTENT_DEBUG_MODE
cb.printStatus();
#endif
cb.read(&p2);
if (p2.number != 789 || p2.delay != 12345 || p2.exists != true)
Serial.println("CB ERROR !!");
p2.delay++;
p2.exists = false;
p2.number++;
cb.update(&p2);
#ifdef EEPROMEXTENT_DEBUG_MODE
cb.printStatus();
#endif
cb.read(&p1);
if (p1.number != 790 || p1.delay != 12346 || p1.exists != false)
Serial.println("CB ERROR !!");
// --------------------------
// Position only buffer
p1.delay = 12345;
p1.exists = true;
p1.number = 789;
int pos = cb.startWrite();
EEPROMextent.writeAnything(pos, p1);
pos = cb.getStartRead();
cb.read(&p2);
if (p2.number != 789 || p2.delay != 12345 || p2.exists != true)
Serial.println("CB ERROR !!");
p2.delay++;
p2.exists = false;
p2.number++;
for (int i = 0; i < 10; i++)
pos = cb.startWrite();
EEPROMextent.writeAnything(pos, p2);
pos = cb.getStartRead();
cb.read(&p1);
if (p1.number != 790 || p1.delay != 12346 || p1.exists != false)
Serial.println("CB ERROR !!");
}
void loop()
{
}
cb est le buffer déclaré. cb.begin a trois arguments : l’adresse de départ du buffer, la taille d’une sauvegarde, et le nombre maxi de sauvegardes. La valeur de retour de begin donne la taille totale de la mémoire EEPROM qui sera utilisée. Cela peut permettre de savoir de combien d’octets se décaler pour écrire au delà du buffer si besoin.
L’écriture passe par la fonction Update tandis que la lecture utilise Read . La fonction de debug PrintStatus est juste là pour afficher l’état courant du contenu en mode debug sur la console série. Pour activer ce mode, enlever le ’//’ devant la ligne #define EEPROMEXTENT_DEBUG_MODE de EEPROMextent.h .
Sauver une liste hétérogène hiérarchique... ou pas !
Dans ma dernière application Arduino dont je parlerai bientôt ici, j’avais besoin de sauver une liste de locomotives, avec pour chacune des paramètres divers comme le nom, le code Dcc et d’autres paramètres, et surtout une liste de fonctions associées. Chaque fonction comprend aussi un nom, un code Dcc et d’autre choses. Plus important encore, le nombre de fonctions d’une locomotive donnée peut évoluer ! Et il doit être possible de créer, modifier ou détruire n’importe quelle locomotive et son contenu.
Pour cela, j’ai créé la classe EEPROM_ItemList.
Le principe consiste en une liste finie d’éléments de taille fixe. Cette taille doit être la plus grande taille entre tous les types d’éléments à y stocker. Le nombre maximum d’éléments (des items dans la langue d’Obama) est fixe et est compris entre 1 et 255 au maximum. Un emplacement dans cette liste est appelé ’slot’ et est identifié par un byte lui aussi, forcément entre 0 et le nombre maxi d’éléments - 1, comme dans un tableau C. Chaque élément est typé par un octet, c’est le T dans l’image. Cela peut permettre d’en faire des parents et des enfants, mais on pourrait envisager autre chose... Chaque élément stocké a aussi un propriétaire, (un owner dans la langue de Donald Trump). Bien vu, c’est le ’P’ de l’image ! En jouant avec les types et le propriétaire, il est possible de stocker une liste arborescente seulement limitée par le nombre de slots...
Les fonctions sont assez simples :
- begin() va fixer les caractéristiques de votre liste : la position de départ de stockage, la taille d’un élément et la mémoire totale occupée maximum.
- clear() est à appeler uniquement si l’on veut vider la liste de son contenu. C’est à appeler au moins une fois au moment de sa création.
- GetFirstFreeSlot() va trouver le premier emplacement (slot) disponible.
- GetItemPos() va calculer l’adresse du premier octet à sauver ou à lire à partir d’un numéro de slot.
- GetSlotFromPos() fait l’inverse et calcule un numéro de slot à partir d’une adresse située n’importe où à l’intérieur d’un élément stocké.
- GetItemType() et GetItemOwner() récupèrent les données type et propriétaire d’un slot.
- SaveItemPrefix() sauve le type et le numéro de slot du propriétaire pour un slot donné.
- FreeItem() va libérer les emplacements pris par un élément dans un slot donné, ainsi que tout ceux qui le considèrent comme leur propriétaire.
- FreeOwnedItem() libère tous les emplacement utilisés par les enfants d’un slot.
- FindItem() va retrouver un item d’après son type, un slot de départ pour la recherche, et éventuellement un parent précis.
- Enfin les Count() vont compter les items d’un type particulier, ou les enfants d’un parent particulier...
Prenons l’exemple livré pour mieux comprendre. Le but de cet exemple est de stocker des parents et leurs enfants.
#include "EEPROMextent.h"
// Liste qui va contenir notre sauvegarde
EEPROM_ItemListClass ItemList;
struct parent
{
byte id;
int nombre;
char texte[10];
};
struct enfant
{
byte id;
char descr[10];
int donnee;
};
On va d’abord déclarer les structures correspondantes des parents et des enfants. Pour chacun, un id qui est le slot de stockage commence les données. S’y ajoute un nom, plus une donnée quelconque...
byte sauve_parent(parent &aParent);
byte sauve_enfant(enfant &aEnfant, byte aParent);
#define ITEM_SIZE 20
byte sauve_parent(parent &aParent)
{
// On commence par récupérer un numéro de place libre que l'on met dans la structure.
byte p1_place = ItemList.GetFirstFreeSlot();
// Sauver le préfixe standard pour tous les items:
// 'P' pour un parent
// 255 est le numéro du slot du parent. Il doit être compris entre 0 et la fin de la liste ou 254. Ici 255 signifie qu'il n'y a pas de parent !
int pos = ItemList.SaveItemPrefix(p1_place, 'P', 255);
// La valeur de retour pos donne la position de la prochaine écriture.
// On écrit alors le contenu de la structure parent : id, nombre et texte.
EEPROMextent.writeByte(pos++, aParent.id);
pos += EEPROMextent.writeAnything(pos, aParent.number);
EEPROMextent.writeString(pos, aParent.text);
// Il faut ajouter à pos la taille de ce que l'on vient de sauver pour aller écrire la suite.
pos += 10; // là on sait que ça doit faire dix octets de long...
// Petite vérification de la taille, inutile en production.
if (pos - ItemList.GetItemPosRaw(p1_place) > ITEM_SIZE)
{
Serial.print("Parent trop long : augmentez la taille d'un item à ");
Serial.print(pos - ItemList.GetItemPosRaw(p1_place));
Serial.println(" minimum !");
}
return p1_place;
}
byte sauve_enfant(enfant &aEnfant, byte aParent)
{
// On commence par récupérer un numéro de place libre que l'on met dans la structure.
byte e1_place = ItemList.GetFirstFreeSlot();
// Sauver le préfixe standard pour tous les items:
// 'E' pour un enfant
// aParent est le numéro du slot du propriétaire, ou du parent.
int pos = ItemList.SaveItemPrefix(e1_place, 'E', aParent);
// La valeur de retour pos donne la position de la prochaine écriture.
// On écrit alors le contenu de la structure enfant : id, descr et donnee.
EEPROMextent.writeByte(pos++, aEnfant.id);
EEPROMextent.writeString(pos, aEnfant.descr);
pos += 10; // là on sait que ça doit faire dix octets de long...
EEPROM.put(pos, aEnfant.donnee);
pos += sizeof(int);
// Nouveau test de taille
if (pos - ItemList.GetItemPosRaw(c1_place) > ITEM_SIZE)
{
Serial.print("Enfant trop long : augmentez la taille d'un item à ");
Serial.print(pos - ItemList.GetItemPosRaw(e1_place));
Serial.println(" minimum !");
}
return e1_place;
}
Les fonctions d’écriture d’un parent et d’un enfant viennent ensuite.
void setup()
{
// La taille de chaque item est évaluée à la louche à 15 octets.
// En réalité, il faudrait calculer la bonne taille exacte à partir des sizeof() des items:
// byte tailleItem = sizeof(parent);
// if (sizeof(enfant) > tailleItem)
// tailleItem = sizeof(enfant);
// On fixe un début arbitraire de la zone de sauvegarde à 10.
// Et une taille de liste à 1000 octets.
ItemList.begin(10, ITEM_SIZE, 1000);
// On déclare ensuite les éléments à sauver.
// Pour l'exemple ils sont écrits en dur...
parent A;
A.id = 0;
A.nombre = 123;
strcpy(A.text, "Parent A");
enfant A1;
A1.id = 100;
A1.donnee = 123;
strcpy(A1.descr, "Enfant A1");
enfant A2;
A2.id = 101;
A2.donnee = 456;
strcpy(A2.descr, "Enfant A2");
parent B;
B.id = 1;
B.nombre = 78;
strcpy(B.text, "Parent B");
enfant B1;
B1.id = 102;
B1.donnee = 89;
strcpy(B1.descr, "Enfant B1");
enfant B2;
B2.id = 103;
B2.donnee = 1000;
strcpy(B2.descr, "Enfant B2");
enfant B3;
B3.id = 104;
B3.donnee = 1001;
strcpy(B3.descr, "Enfant B3");
// La zone qui va de 10 à 1010 dans l'EEPROM est écrasée avec des 0.
// Cette appel ne doit être fait que lors de la toute première utilisation de l'EEPROM.
ItemList.clear();
// Ecriture des elements :
// sauve le parent A
byte p1_place = sauve_parent(A);
// sauve le premier enfant de A : A1
sauve_enfant(A1, p1_place);
// sauve le second enfant de A : A2
sauve_enfant(A2, p1_place);
// sauve le second parent B
byte p2_place = sauve_parent(B);
// sauve le premier enfant de B : B1
sauve_enfant(B1, p2_place);
// sauve le deuxième enfant de B : B2
sauve_enfant(B2, p2_place);
// sauve le troisième enfant de B : B3
save_enfant(B3, p2_place);
// Carte symbolique des éléments dans la mémoire EEPROM:
//
// slots 0 1 2 3 4 5 6 7(premier slot disponible)
// items A A1 A2 B B1 B2 B3
Serial.print("Le premier slot dispo devrait être le 7 : ");
Serial.println(ItemList.GetFirstFreeSlot());
Serial.print("Nombre de parents stockés 2 : ");
Serial.println(ItemList.CountItems('P'));
Serial.print("Nombre total d'enfants 5 : ");
Serial.println(ItemList.CountItems('C'));
Serial.print("Nombre d'enfants de A : 2 : ");
Serial.println(ItemList.CountOwnedItems(p1_place));
Serial.print("Nombre d'enfants de B: 3 : ");
Serial.println(ItemList.CountOwnedItems(p2_place));
}
void loop()
{
}
Enfin le Setup teste l’écriture puis la lecture.
Et encore ?
Non, c’est tout. Il y a déjà de quoi faire avec ces types de sauvegarde, mais il est évident que d’autres sont possibles. N’hésitez pas à exploiter cette mémoire EEPROM pour de la sauvegarde ponctuelle genre configuration... C’est possible mais beaucoup plus dangereux d’y sauver des données en continu comme l’état de chaque aiguillages, servo ou lumière à chaque changement...
Où ça se passe...
Bibliothèque disponible ici : Forge Locoduino
Notez que dans le répertoire ’extras/Doc’ se trouve une documentation html visible sur n’importe quel système avec n’importe quel navigateur un peu moderne. Cette documentation reprend en Anglais l’ensemble des informations données ici et détaille plus précisément les rôles des classes, des fonctions et de leurs arguments. Sous Windows pour la lancer, il suffit de double cliquer sur le fichier StartDoc.bat présent dans le répertoire de la bibliothèque, sinon il faut manuellement double-cliquer sur le fichier ’extras/Doc/index.html’ .
Il faut aussi savoir que d’autres moyens de sauvegardes existent comme les cartes SD, des circuits extérieurs d’EEPROM ou la liaison vers un ordinateur qui assurerait cette sauvegarde... Bref c’est un vaste sujet !