vendredi 8 mars 2019

Arduino: sauvegarde de données en EEPROM en cas de coupure d'alimentation


Arduino: sauvegarde de données en EEPROM

en cas de coupure d'alimentation



Dans cet article nous allons décrire une méthode de sauvegarde de données en EEPROM en cas de coupure brutale de l'alimentation d'un ARDUINO.

1. Principe

La méthode repose sur le maintien de l'alimentation par un condensateur ayant suffisamment de capacité pour permettre l'écriture de la quantité de données à sauvegarder. La détection de la coupe d'alimentation est réalisée en appliquant une partie de la tension d'alimentation sur une pin digitale du microcontrôleur :
Le condensateur C1 se charge à travers une diode si le 5V est présent.
L'entrée digitale D2 reçoit une partie de la tension d'alimentation à travers un pont diviseur de tension.

1.1. Interruption

L'entrée D2 va pouvoir générer une interruption qui sera traitée rapidement par le logiciel.
Un ARDUINO UNO possède deux pins pouvant générer une interruption : D2 et D3.
Un ARDUINO MEGA en possède 6 : 2, 3, 18, 19, 20, 21

1.2. Diode

La diode D1 empêche le condensateur de se décharger dans le pont diviseur.

Lors de la mise sous tension la valeur de la tension à ses bornes va croître rapidement et approcher les 5V. Cette tension va dépendre de la diode utilisée.La chute de tension provoquée par la diode, dite VF pour "forward voltage" peut être plus ou moins élevée. De plus elle dépend du courant traversant la diode.
Mesurons celle-ci pour quelques diodes courantes, avec un courant de 30mA, la consommation typique d'un ARDUINO UNO :
  • 1N4001 : 0.7V
  • BAT46 : 0.5V
  • 1N5819 : 0.3V
On voit tout de suite que la 1N5819 provoque une chute de tension nettement inférieure et permettra d'assurer 4.7V d'alimentation à l'ARDUINO.

1.3. Pont diviseur

Ceci nous amène à expliquer l'utilité du pont diviseur de tension.
Nous avons 5V en amont de la diode et 4.7V en aval de celle-ci et donc aussi 4.7V sur la broche VCC de l'ARDUINO.

Il est assez peu recommandé d'appliquer sur une entrée d'un microcontrôleur une tension supérieure à celle de l'alimentation. Même s'il y a peu de chances que cette différence de tension provoque des dégâts, car elle est faible, elle pourrait entraîner une surconsommation.
Avec ce pont diviseur, la tension sur l'entrée est ramenée à :

Vd2 = 5V / (R1 + R2) * R2 = 5V / (100K + 1M) * 1M = 4.54V

Cette tension est suffisante pour être vue comme un niveau haut par l'entrée D2.

1.4. Alimentation par VIN

Il est parfaitement possible de faire la même chose en alimentant par la broche VIN. Dans ce cas la diode pourra être une simple diode de redressement.

Il faudra recalculer le pont diviseur.

Exemple pour 7.5V d'alimentation et une 1N4001 :
La tension sur VIN sera égale à :

VIN = 7.5V - 0.7V  = 6.8V

En fonction de la carte utilisée 6.8V peuvent être suffisants ou non. Tout dépend du régulateur 5V embarqué sur celle-ci. Avec un AMS117 ayant une tension de  drop-out de 1.1V ces 6.8V suffiront.

Le processeur est alimenté en 5V dans ce cas par le régulateur de la carte. Il faut simplement que la tension sur D2 soit inférieure à 5V.

Avec un pont diviseur R1=100K et R2=80K :

Vd2 = 7.5V / (R1 + R2) * R2 = 5V / (100K + 180K) * 180K = 4.82V

On peut appliquer le même calcul pour 12V bien entendu.

2. Fonctionnement

Lors de la mise sous tension de l'alimentation, le condensateur C1 se charge à 4.7V et cette tension alimente l'ARDUINO.
Le microcontrôleur voit 4.5V sur son entrée D2.

Lorsque l'alimentation disparaît, le condensateur C1 se décharge dans l'ARDUINO et décroît lentement (plus ou moins rapidement suivant la taille du condensateur), jusqu'à atteindre une valeur ne permettant plus le fonctionnement du microcontrôleur.
Le microcontrôleur voit rapidement 0V sur son entrée D2. C'est la capture de cet événement qui va déclencher la sauvegarde de nos précieuses données dans l'EEPROM.
Dans le sketch décrit plus bas, on verra que cette information est traitée sous interruption.

3. Calculs

Arrivés à ce point, nous avons quelques inconnues :
  • quelle est la tension minimale de fonctionnement de l'ARDUINO ?
  • quel temps va prendre la sauvegarde de nos données ?
  • quelle valeur donner au condensateur C1 ?

3.1. Tension minimale

Comment connaître la tension minimale de fonctionnement de l'ARDUINO ?
L'idée est de charger un sketch faisant clignoter la LED D13 à 10Hz. Ensuite on alimente par la broche 5V et on débranche le cordon USB.
La LED clignote toujours. Diminuons la tension doucement jusqu'à ce que la LED ne clignote plus.

Le résultat obtenu : la LED clignote encore à 3V. J'obtiens le même résultat avec un autre multimètre.
Cet exemplaire de UNO est exceptionnel !

On ne peut pas considérer que cette valeur sera vérifiée sur un nombre important de cartes.
J'ai déjà eu entre les mains une carte NANO que j'avais alimenté par erreur en 3.7V et qui fonctionnait aléatoirement.

Nous allons plutôt adopter une valeur de 4V.

3.2. Durée de sauvegarde

Nous allons parler des données à sauvegarder mais par seulement.
En effet il serait bien de s'assurer que les données de l'EEPROM soient effectivement nos données, et pas des valeurs au hasard, comme celles qui sont présentes dans l'EEPROM d'un ARDUINO n'ayant jamais servi.
Lors de la relecture, il va falloir vérifier que nos données soient valides.

Une approche simple consiste à écrire un nombre dit "magic" au début de nos données.
A la relecture, ce nombre magique sera comparé à sa valeur supposée.

On peut faire encore mieux : un CRC16 par exemple (voir en fin de document).

Il serait intéressant d'enregistrer aussi la date et l'heure de la sauvegarde, si le système possède un moyen quelconque d'obtenir cette information :
  • circuit RTC
  • liaison avec un ordinateur ou un serveur NTP
  • etc.
Nous avons déjà une bonne idée du contenu. Nous allons l'organiser en structure :

#define MAGIC     0xDEADBEEF
#define EEPROM_ADDR 0

struct __attribute__ ((packed)) eeprom_data 

{
  time_t timeStamp;
  char data[160];
  unsigned long magic;
};

struct eeprom_data eepromData;


La structure est "packed", afin d'éviter l'alignement sur des multiples de 4 octets, et réduire ainsi la taille totale. Cela peut sembler inutile car la taille de tous ses éléments est multiple de 4, mais rien de dit que ce sera votre cas.

Ensuite nous allons écrire une fonction de sauvegarde et l'appeler dans le code pour mesurer son temps d'exécution :

void powerLoss()void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), 0);
  EEPROM.put(EEPROM_ADDR, eepromData);
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), MAGIC);
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}


Le résultat obtenu : 620µs.
La quantité de données sauvegardée est de 160 + 4 + 4 = 168 octets.

La sauvegarde des données d'heure, date et magic (2 fois) dure donc 620µs / 168 * 10 = 37µs
La sauvegarde des 160 octets de données prend le reste, c'est à dire 583µs.

Il sera donc facile de faire un calcul en fonction de la vraie quantité de données à sauvegarder.

3.3. Valeur du condensateur

Lorsque l'alimentation va disparaître le condensateur va se décharger comme ceci :


Cela va très vite semble t-il mais pas d'affolement, tout dépend de la consommation de la carte et de la taille du condensateur.

Tout d'abord intéressons-nous à la valeur que représente notre tension minimale de fonctionnement par rapport à la tension nominale :

rapport = Vmin / Vmax  = 0.85 = 85%

Pour rappel on appelle constante de temps d'un circuit RC le temps que met un condensateur pour se charger à 63% (ou à se décharger de 63%) à travers une résistance.
Le temps de charge ne nous intéresse pas car la charge se fait pratiquement sans résistance.
Seule la décharge nous intéresse. On peut dire que la constante de temps définit le temps au bout duquel le condensateur aura perdu 63% de sa tension. Il nous restera donc 37%.

Dans un circuit RC composé d'une résistance de 1KΩ et d'un condensateur de 1µF le condensateur se déchargera donc de 63% de la tension appliquée à l'ensemble du circuit au bout de :
t = R * C = 0.000001F * 1000Ω = 0.001s

Or 37% ne nous intéresse pas, nous voulons 85%.
Sur la courbe ci-dessus ces 85% sont atteints au bout d'un temps égal à environ 0,15 * T.

Pour vérifier cette valeur il va nous falloir résoudre une équation différentielle.

Uc(t) = U(exp(-t / RC))

A l'aide d'un tableur on peut tracer une courbe plus détaillée du début de la décharge :


On voit qu'avec 1000Ω et 1000µF (RC = 1s) on a bien 0.15s à 85% d'alimentation.

Quelle est notre résistance ?
Dans notre cas, il s'agit d'un ARDUINO qui consomme environ 25mA s'il est alimenté par sa broche 5V :

R = U / I = 5V / 0.025A = 200Ω

Pour un montage consommant plus, il faudra recalculer cette valeur.

En première approximation pour assurer nos 620µs de maintien de l'alimentation il nous faudrait un condensateur de :

C = T / R / 0.15 = 0.000620s / 200 / 0.15 = 0,00002066 = 20µF.

Il nous faut donc tracer une courbe de la tension du condensateur (Uc) en fonction du temps (t) et de la constante RC.

En modifiant les valeurs de R et C on obtient ceci :


On voit qu'avec 200Ω et 20µF on atteint bien notre objectif de 600µs à 85% d'alimentation.

C'était la théorie, mais que dit la pratique ?

4. Les essais

L'heure est donc venue d'essayer de voir si cela fonctionne.

Voici le petit sketch utilisé pour les tests :

#include <time.h>
#include <EEPROM.h>

#define MAGIC       0xDEADBEEF
#define EEPROM_ADDR 0

struct __attribute__ ((packed)) eeprom_data
{
  unsigned long magic;
  time_t timeStamp;
  char data[160];
};

struct eeprom_data eepromData;

bool backup;

void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), 0);
  EEPROM.put(EEPROM_ADDR, eepromData);
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), MAGIC);
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

void setup()
{
  struct eeprom_data data;
  time_t t;

  Serial.begin(115200);
  set_system_time(getTimeFromAnywhere());
  time(&t);
  Serial.println(ctime(&t));
  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);
  EEPROM.get(EEPROM_ADDR, data);
  if (data.magic != MAGIC) {
    Serial.println("No Backup available");
  }
  else {
    memcpy(&eepromData, &data, sizeof(data));
    Serial.println("Valid Backup data has been found in the EEPROM: ");
    Serial.print("MAGIC: "); Serial.println(data.magic, HEX);
    Serial.print("DATE: "); Serial.println(ctime(&eepromData.timeStamp));
    Serial.print("DATA: "); Serial.println(eepromData.data);
  }
}

void loop()
{
  time(&eepromData.timeStamp);
  strcpy(eepromData.data, "MY-PRECIOUS-DATA");
  Serial.println("Zzzzzzzzzzzzz");
  delay(30000);
}

// get time from RTC, NTP, etc.
time_t getTimeFromAnywhere(void)
{
  char s_month[5];
  struct tm t;
  int s, m, h, d, y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";


  memset(&t, 0, sizeof(struct tm));
  sscanf(__DATE__, "%s %d %d", s_month, &d, &y);
  sscanf(__TIME__, "%d:%d:%d", &h, &m, &s);
  t.tm_sec = s;
  t.tm_min = m;
  t.tm_hour = h;
  t.tm_mday = d;
  t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
  t.tm_year = y - 1900;
  t.tm_isdst = -1;
  return mktime(&t);
}


Dans la fonction loop(), les données - une structure en RAM - sont mises à jour régulièrement, mais elles restent en RAM.

Dans la fonction setup() :

  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);

Cet appel permet d'accrocher la fonction powerLoss() au vecteur d'interruption de la pin D2. Cette fonction sera appelée y compris pendant l'exécution de la routine delay(30000).

Dans la fonction powerLoss le nombre magique est mis à zéro au départ. Cela permet de s'assurer que la fonction aura bien le temps d'écrire l'intégralité des données plus le nombre magique.
Si celui-ci est relu et qu'il vaut zéro, cela voudra dire que logiciel n'a pas eu le temps de l'écrire.

Dans la fonction setup(), la validité des données en EEPROM est vérifiée le nombre magic de l'EEPROM est comparé au nombre magic 0xDEADBEEF. Si le nombre correspond, les données sont recopiées dans la structure eepromData.

Passons à la manipulation proprement dite.

Le montage est donc alimenté en 5V en amont de la diode avec une alimentation de labo.

La manipulation est la suivante :
  • brancher l'alimentation
  • installer un condensateur de 22µF
  • charger le sketch
  • débrancher l'USB
  • couper l'alimentation
  • rebrancher l'USB
  • l'ARDUINO affiche le contenu de l'EEPROM
  • le contenu est correct
  • brancher l'alimentation
  • installer un condensateur de 10µF
  • modifier les données à sauvegarder (le nombre magic par exemple)
  • recharger le sketch
  • débrancher l'USB
  • couper l'alimentation
  • rebrancher l'USB
  • l'ARDUINO affiche le contenu de l'EEPROM
  • le contenu est correct
  • etc.

Avec 4.7µF cela marche encore, mais pas en dessous. Avec un condensateur de 2.2µF, la sauvegarde n'a pas eu lieu. La réserve d'énergie n'est pas assez importante.

Par rapport aux 20µF calculés cela peut paraître étrange que 4.7µF soient suffisants.

Il faut dire que la marge de sécurité prise en adoptant une tension minimale de fonctionnement de 4V est importante, et que la carte utilisée fonctionne encore à 3V. Mais cette marge est obligatoire, car rien ne permet d'affirmer qu'une autre carte fonctionnerait de la même façon dans les mêmes conditions.

5. Alimenter par VIN

Bien entendu, on pourrait faire la même manipulation en alimentant la carte par sa broche VIN.
Cela complique un peu les choses car la tension de chute (drop-out) du régulateur 5V va avoir une influence. Certains régulateurs ont un drop-out de 1V, d'autre plutôt 2V.
Ensuite, il est difficile de dire comment va se comporter le régulateur quand la tension va chuter.

Mais pour assurer la sauvegarde on peut prendre une bonne marge de sécurité, et surtout tester ensuite.

En admettant qu'il faut au minimum 7V sur VIN pour assurer un bon 5V à l'ARDUINO, le pont diviseur doit être recalculé :

Avec 470KΩ et 1MΩ j'obtiens :
Vd2 = 7V * (R1 + R2) / R2 = 7V / (470K + 1M) * 1M = 4.65V
Il n'est même pas nécessaire de refaire les calculs. Le résultat doit être le même, et le test le confirme.

La solution fonctionnera aussi avec du 12V sur VIN, probablement avec un condensateur encore plus petit.

Le pont diviseur doit être recalculé :

Avec 750KΩ et 470KΩ j'obtiens :
Vd2 = 12V * (R1 + R2) / R2 = 12V / (750K + 470K) * 470K = 4.62V

Dans cette configuration, le montage fonctionne presque avec 2.2µF. Seuls les deux derniers octets n'ont pas été écrits lors de la sauvegarde !
Cela ne semble pas anormal. Partant d'une tension plus haute, l'alimentation de l'ARDUINO reste à une valeur élevée plus longtemps.

Comme nous l'avons déjà dit, cette valeur de 4.7µF est une valeur qui convient pour l'exemplaire de carte testée. Par sécurité, nous adopterons donc la valeur de condensateur calculée : 20µF.

Comme il faut toujours choisir une marge de sécurité par rapport au calcul, la valeur de 47µF sera retenue.

Cette valeur a été calculée pour pouvoir sauvegarder 160 octets. Pour sauvegarder plus de données, il suffira d'augmenter la valeur proportionnellement.

6. Choix des composants

Bien entendu la diode a une importance capitale. Avec une 1N5819 l'exemplaire d'ARDUINO UNO que je viens de tester est capable de sauvegarder 160 octets en 620µs avec un condensateur de 4.7µF.
Avec une 1N4001 la valeur du condensateur monte à 33µF pour que la sauvegarde fonctionne, soit 6 fois plus de capacité.
Si l'on prend en compte la valeur calculée, c'est à dire 20µF avec la 1N5919, il faudrait augmenter la valeur du condensateur à 120µF si l'on adoptait une 1N4001.

Le condensateur sera choisi en fonction de la tension maximale présente à ses bornes, avec une marge de sécurité d'environ 25% :
  • 5V : condensateur de 6.3V
  • 7V : condensateur de 10V
  • 12V : condensateur de 16V
Comme il s'agit de condensateurs de forte valeur, un condensateur électrolytique conviendra parfaitement.

Vous aurez du mal à trouver des condensateurs de 10µF en dessous de 16V. Ce n'est pas catastrophique, remplacer un condensateur de 10µF 6.3V introuvable par un 22µF 6.3V ou 10V est envisageable. Qui peut le plus peut le moins.

Parlons de la taille. Un condensateur électrolytique de 47µF 10V a un diamètre de 5mm et fait 11mm de hauteur :

Les modèles récents font 6.3mm x 5mm. Votre réserve d'énergie tient peu de place !

Pour une valeur inférieure à 1µF, un condensateur polyester peut être envisagé. Au delà de 1µF ils occupent un volume important et sont chers (à peu près 10 fois plus chers à capacité égale) :


Jusqu'à quelques µF les condensateurs céramiques multi-couche (MLCC) peuvent être utilisés :

Parlons des résistances du pont diviseur. Celles-ci peuvent avoir une valeur élevée si le montage est destiné à fonctionner en basse consommation. Avec deux résistances de 1MΩ et 100KΩ, la consommation du pont diviseur sera de 4.5µA.

On peut remplacer R2 par une diode zener de 4.2V si des variations de tension d'alimentation sont prévisibles :
On peut utiliser une BZX55C4V2 par exemple. Elle doit normalement être polarisée à l'aide d'un courant de 2.5 à 5mA. Mais pour notre utilisation non critique, on se contentera d'1mA :
  • 5V : R1 vaudra 330Ω
  • 7V : R1 vaudra 1KΩ
  • 12V : R1 vaudra 2.7KΩ
L'inconvénient de la zener : elle n'est pas adaptée à la basse consommation. Hors de question de l'utiliser dans une solution alimentée par batterie.

7. Améliorons avec un CRC

En adoptant un nombre magique sur 32 bits comme vérification d'intégrité des données, il y a une chance d'erreur sur 4294967295, c'est à dire que le nombre magique peut avoir la bonne valeur tout à fait par hasard. Les données pourraient avoir dans ce cas des valeurs incertaines.

C'est une bonne sécurité basique mais l'intégrité des données n'est pas assurée pour autant. En effet le nombre magique peut parfaitement être présent et correct mais rien ne prouve que les données soient valides.

Une autre approche consiste à utiliser une somme de contrôle (CheckSum).
On fait la somme de tous les octets de données pour fabriquer le nombre magique. Cela apporte plus de sécurité mais le simple fait d'inverser deux octets de données ne modifiera pas la somme finale.

Nous allons plutôt utiliser un CRC (Cyclical Redundancy Check)
Si l'on inverse deux octets de données, le CRC ne sera pas le même.

Ce sketch utilise un CRC :

#include <time.h>
#include <EEPROM.h>

#define EEPROM_ADDR 0

struct __attribute__ ((packed)) eeprom_data
{
  time_t timeStamp;
  char data[160];
  unsigned long crc;
};

struct eeprom_data eepromData;

#define DATA_SIZE (sizeof(eepromData) - sizeof(eepromData.crc))

void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  EEPROM.put(EEPROM_ADDR, eepromData);
  eepromData.crc = Crc32((unsigned char *)&eepromData, DATA_SIZE);
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, crc), eepromData.crc);
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

void setup()
{
  struct eeprom_data data;
  time_t t;

  Serial.begin(115200);
  set_system_time(getTimeFromAnywhere());
  time(&t);
  Serial.println(ctime(&t));
  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);
  EEPROM.get(EEPROM_ADDR, data);
  if (Crc32((unsigned char *)&data, DATA_SIZE) != data.crc) {
    Serial.println("No Backup available");
  }
  else {
    Serial.println("Valid Backup data has been found in the EEPROM: ");
    memcpy(&eepromData, &data, sizeof(data));
  }
  Serial.print("CRC: 0x"); Serial.println(data.crc, HEX);
  Serial.print("DATE: "); Serial.println(ctime(&eepromData.timeStamp));
  Serial.print("DATA: "); Serial.println(eepromData.data);
}

void loop()
{
  time(&eepromData.timeStamp);
  strcpy(eepromData.data, "MY-PRECIOUS-DATA");
  Serial.println("Zzzzzzzzzzzzz");
  delay(30000);
}

unsigned long Crc32(const unsigned char *buf, int length)
{
  const unsigned long crc_table[16] = {
    0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
    0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
    0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
    0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
  };

  unsigned long crc = 0L;

  for (int index = 0 ; index < length  ; ++index) {
    crc = crc_table[(crc ^ buf[index]) & 0x0f] ^ (crc >> 4);
    crc = crc_table[(crc ^ (buf[index] >> 4)) & 0x0f] ^ (crc >> 4);
    crc = ~crc;
  }
  return crc;
}

// get time from RTC, NTP, etc.
time_t getTimeFromAnywhere(void)
{
  char s_month[5];
  struct tm t;
  int s, m, h, d, y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";


  memset(&t, 0, sizeof(struct tm));
  sscanf(__DATE__, "%s %d %d", s_month, &d, &y);
  sscanf(__TIME__, "%d:%d:%d", &h, &m, &s);
  t.tm_sec = s;
  t.tm_min = m;
  t.tm_hour = h;
  t.tm_mday = d;
  t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
  t.tm_year = y - 1900;
  t.tm_isdst = -1;
  return mktime(&t);
}


ATTENTION: le calcul du CRC prend du temps. Dans l'exemple précédent la sauvegarde des données prenait 620µs. Avec l'ajout du calcul du CRC, nous passons à 980µs !

Nous augmenterons donc la capacité du condensateur.


8. Gestion de versions

Pour des projets plus complexes il arrive que l'on doive gérer la version des structures de données, parce qu'elles évoluent en fonction des versions du logiciel.

Pour illustrer le principe  je propose un exemple fictif avec deux catégories de données :
  • des données de configuration
  • des données de mesure
Les données de configuration sont stockées en RAM mais sauvegardées à chaque fois qu'elles sont modifiées. Dans l'exemple ces données sont entrées via le terminal :
  • un message de bienvenue
  • une adresse IP
  • un numéro de port
Dans une première version, les données adresse IP et port étaient absentes, elles ont été ajoutées dans la deuxième version du logiciel.

Lors d'un démarrage, la fonction setup() lit la configuration à partir de l'EEPROM.
Si la version de la configuration est égale à 1 la configuration en RAM est mise à jour et sauvegardée en EEPROM avec une adresse IP et un numéro de port par défaut.
Si la version de la configuration est égale à 2 elle est laissée telle quelle.

Les données de mesure peuvent être diverses (vitesse du vent, débit d'eau, température d'un ballon, etc.).

Ces données sont stockées en RAM et sont traitées au fil de l'eau.On peut imaginer beaucoup de choses :
  • calcul de moyenne
  • détection de défaut
  • mise à jour d'une page WEB
  • etc.
Les données de mesures sont sauvegardées uniquement en cas de coupure d'alimentation, car on ne désire pas les sauvegarder à chaque fois qu'elles sont modifiées. Par contre on désire les retrouver après une coupure d'alimentation pour pouvoir continuer nos traitements.

Dans l'exemple ces données sont des nombres aléatoires stockés dans un buffer tampon. Toutes les dix secondes un nouveau numéro est tiré et stocké à la fin du buffer après avoir décalé les précédents pour éliminer le plus ancien.

Dans une première version, il y avait un seul tableau de données, un deuxième tableau a été ajouté dans la deuxième version du logiciel.

Lors d'un démarrage, la fonction setup() lit les données à partir de l'EEPROM.
Si la version de la configuration est égale à 1 les données en RAM du deuxième tableau sont effacées. En effet si on utilisait telles quelles des données de l'EEPROM à partir d'un emplacement n'ayant jamais été initialisé auparavant, on ne serait absolument pas certain du contenu.
Si la version de la configuration est égale à 2 les données sont laissées telle quelles.

Un petit interpréteur de commandes permet d'effectuer certaines actions :
  • welcome=XXXXXXXXX : permet de modifier le message de bienvenue
  • ip=XXX.XXX.XXX.XXX : permet de modifier l'adresse IP
  • port=XXX : permet de modifier le numéro de port
  • drop : permet d'effacer la configuration en EEPROM
  • date : affiche la date
  • conf : affiche la configuration
  • data : affiche les données
  • v1 : enregistre dans l'EEPROM une configuration des données dans leur version 1
La configuration par défaut est :
Message : "WELCOME TO THE ARDUINO WORLD"
Adresse : "192.168.1.1"
Port : 8000

Pour faire marcher tout cela il faut bien sûr le montage décrit plus haut (diode, condensateur, pont diviseur), alimenté en 5V par une alimentation de labo. Le cordon USB peut être laissé branché.

Quelques manipulations :
  • brancher l'alimentation
  • entrer : drop
  • redémarrer la carte (bouton reset)
  • le terminal affiche "No data available "
  • la configuration est celle par défaut
  • les données sont vides
  • entrer : welcome=NOUVEAU MESSAGE DE BIENVENUE 
  • entrer : ip=192.168.1.50
  • entrer : port=9000
  • laisser tourner le logiciel quelques minutes
  • entrer : data
  • le logiciel affiche les données
  • couper l'alimentation
  • les données sont sauvegardées
  • redémarrer la carte
  • le terminal affiche "Valid configuration has been found in the EEPROM "
  • le terminal affiche "Valid data has been found in the EEPROM"
  • la configuration est celle précédement enregistrée
  • les données sont celles ayant été enregistrées lors de la coupure
  • entrer : v1
  • le terminal affiche "CONFIGURATION BACK TO V1"
  • redémarrer la carte
  • le terminal affiche "Valid configuration has been found in the EEPROM"
  • le terminal affiche "V1: Upgrading configuration, using default IP/port "
  • la configuration est celle par défaut (adresse IP + port)
  • le message de bienvenue est celui entré précédement
  • les données du premier tableau sont conservées
  • les données du deuxième tableau sont vides
Voici le sketch :

#include <time.h>
#include <EEPROM.h>

#define CONFIG_ADDR     0
#define DATA_ADDR           256

#define MAGIC                     0xDEADBEEF
#define WELCOME_MAX    80
#define SPEED_MAX           50
#define CMD_MAX               WELCOME_MAX+10
#define IPADDR_MAX          16

#define DEFAULT_WELCOME "WELCOME TO THE ARDUINO WORLD"
#define NULL_IPADDR     "           "
#define DEFAULT_IPADDR  "192.168.1.1"
#define DEFAULT_PORT    8000

struct __attribute__ ((packed)) config_data
{
  unsigned long magic;
  unsigned version;
  time_t timeStamp;
  char welcomeMessage[WELCOME_MAX];
  char ipAddr[IPADDR_MAX];
  uint16_t port;
};

struct config_data configData;

struct __attribute__ ((packed)) measured_data
{
  unsigned long magic;
  unsigned version;
  unsigned speedA[SPEED_MAX];
  unsigned speedB[SPEED_MAX];
};

struct measured_data measuredData;

void eraseConfig(void)
{
  memset(&configData, 0, sizeof(configData));
  EEPROM.put(CONFIG_ADDR, configData);
}

void printConfiguration(void)
{
  Serial.print("DATE: "); Serial.println(ctime(&configData.timeStamp));
  Serial.print("VERSION: "); Serial.println(configData.version);
  Serial.print("WELCOME: "); Serial.println(configData.welcomeMessage);
  Serial.print("IP: "); Serial.println(configData.ipAddr);
  Serial.print("PORT: "); Serial.println(configData.port);
}

void saveConfiguration(void)
{
  configData.magic = 0L;
  time(&configData.timeStamp);
  EEPROM.put(CONFIG_ADDR, configData);
  EEPROM.put(CONFIG_ADDR + offsetof(struct config_data, magic), MAGIC);
}

void printData(void)
{
  Serial.println("SPEEDA:");
  for (int i = 0 ; i < SPEED_MAX ; i++) {
    Serial.print(measuredData.speedA[i]);
    Serial.print(" ");
  }
  Serial.println("\nSPEEDB:");
  for (int i = 0 ; i < SPEED_MAX ; i++) {
    Serial.print(measuredData.speedB[i]);
    Serial.print(" ");
  }
  Serial.println();
}
void saveData(void)
{
  measuredData.magic = 0L;
  EEPROM.put(DATA_ADDR, measuredData);
  EEPROM.put(DATA_ADDR + offsetof(struct measured_data, magic), MAGIC);
}

void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  saveData();
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

void setup()
{
  struct config_data cdata;
  struct measured_data mdata;
  time_t t;

  Serial.begin(115200);
  set_system_time(getTimeFromAnywhere());
  time(&t);
  Serial.println(ctime(&t));
  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);
  EEPROM.get(CONFIG_ADDR, cdata);
  if (cdata.magic != MAGIC) {
    Serial.println("No Backup available, using defaults");
    configData.version = 2;
    strcpy(configData.welcomeMessage, DEFAULT_WELCOME);
    strcpy(configData.ipAddr, DEFAULT_IPADDR);
    configData.port = DEFAULT_PORT;
    saveConfiguration();
    Serial.println("CONFIGURATION SAVED");
  }
  else {
    memcpy(&configData, &cdata, sizeof(cdata));
    Serial.println("Valid configuration has been found in the EEPROM: ");
  }
  switch (configData.version) {
    case 1:
      Serial.println("V1: Upgrading configuration, using default IP/port");
      configData.version = 2;
      strcpy(configData.ipAddr, DEFAULT_IPADDR);
      configData.port = DEFAULT_PORT;
      saveConfiguration();
      Serial.println("CONFIGURATION SAVED");
      break;
    case 2:
      break;
    default:
      break;
  }
  printConfiguration();

  EEPROM.get(DATA_ADDR, mdata);
  if (mdata.magic != MAGIC) {
    Serial.println("No data available");
    measuredData.version = 2;
    memset(&measuredData, 0, sizeof(measuredData));
  }
  else {
    memcpy(&measuredData, &mdata, sizeof(mdata));
    Serial.println("Valid data has been found in the EEPROM: ");
  }
  switch (measuredData.version) {
    case 1:
      Serial.println("V1: Upgrading data");
      measuredData.version = 2;
      memset(&measuredData.speedB, 0, sizeof(measuredData.speedB));
      break;
    case 2:
      break;
    default:
      break;
  }
  printData();
}

void loop()
{
  static unsigned long start;

  commandShell();
  unsigned long now  = millis();
  unsigned long elapsed = now - start;
  if (elapsed >= 10000) {
    // shift data to the left
    memcpy(measuredData.speedA, measuredData.speedA + 1, SPEED_MAX * sizeof(unsigned));
    // new data at right
    measuredData.speedA[SPEED_MAX - 1] = random(3000, 5000);
    // shift data to the left
    memcpy(measuredData.speedB, measuredData.speedB + 1, SPEED_MAX * sizeof(unsigned));
    // new data at right
    measuredData.speedB[SPEED_MAX - 1] = random(2500, 5000);
    start = millis();
  }
}

// get commands from serial line, keyboard, touchscreen, radio, etc.
void commandShell(void)
{
  static char buf[CMD_MAX];
  static int i;
  time_t t;

  if (Serial.available()) {
    int c = Serial.read();
    if (c != -1) {
      if (c == '\n') {
        if (!strncmp(buf, "welcome=", 8)) {
          strncpy(configData.welcomeMessage, buf + 8, WELCOME_MAX);
          saveConfiguration();
          Serial.println("WELCOME MESSAGE SAVED");
          printConfiguration();
        }
        else if (!strncmp(buf, "ip=", 3)) {
          strncpy(configData.ipAddr, buf + 3, IPADDR_MAX);
          saveConfiguration();
          Serial.println("IPADDR SAVED");
          printConfiguration();
        }
        else if (!strncmp(buf, "port=", 5)) {
          configData.port = atoi(buf + 5);
          saveConfiguration();
          Serial.println("PORT SAVED");
          printConfiguration();
        }
        else if (!strcmp(buf, "drop")) {
          eraseConfig();
          Serial.println("CONFIGURATION ERASED");
        }
        else if (!strcmp(buf, "date")) {
          time(&t);
          Serial.println(ctime(&t));
        }
        else if (!strcmp(buf, "conf")) {
          printConfiguration();
        }
        else if (!strcmp(buf, "data")) {
          printData();
        }
        else if (!strcmp(buf, "v1")) {
          configData.version = 1;
          configData.port = 0;
          strcpy(configData.ipAddr, NULL_IPADDR);
          saveConfiguration();
          Serial.println("CONFIGURATION BACK TO V1");
          printConfiguration();
          measuredData.version = 1;
          memset(&measuredData.speedB, 0, sizeof(measuredData.speedB));
          saveData();
          Serial.println("DATA BACK TO V1");
          printData();
        }
        else {
          Serial.print(buf);
          Serial.println(": WHAT'S UP DOC ???");
        }
        i = 0;
        return;
      }
      if (c != '\r') {
        if (i < WELCOME_MAX - 1) {
          buf[i++] = c;
          buf[i] = '\0';
        }
      }
    }
  }
}

// get time from RTC, NTP, etc.
time_t getTimeFromAnywhere(void)
{
  char s_month[5];
  struct tm t;
  int s, m, h, d, y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";


  memset(&t, 0, sizeof(struct tm));
  sscanf(__DATE__, "%s %d %d", s_month, &d, &y);
  sscanf(__TIME__, "%d:%d:%d", &h, &m, &s);
  t.tm_sec = s;
  t.tm_min = m;
  t.tm_hour = h;
  t.tm_mday = d;
  t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
  t.tm_year = y - 1900;
  t.tm_isdst = -1;
  return mktime(&t);
}


9. Conclusion

J'espère que cette petite récréation vous aura amusé.

Il existe des condensateur électrolytiques de gros volume, jusqu'aux super-condensateurs de 10 Farads. Deux condensateurs de 10F 2.7V en série permettraient à un ARDUINO UNO de tenir beaucoup plus longtemps sans alimentation. Si avec 20µF le temps de maintien d'alimentation est de 620µs, avec 10F on obtiendra :

620µs / 20µF * 10F = 310 secondes soit 5 minutes (je l'ai testé).

Cela permettrait de sauvegarder une grosse quantité de données. Si 160 octets sont sauvegardés en 583µs, pour 5 minutes on aura :

q = 160 / 583µs * (5*60*1000000) = 82.332.761 !

Mais on ne parle pas de MégaOctet dans le monde ARDUINO, c'est l'usage.

Cordialement
Henri

10. Mises à jour

09/03/2019 : 7. Améliorons avec un CRC
                     8. Gestion de version
                     1.3. Pont diviseur : petite boulette dans le calcul

25 commentaires:

  1. C'est amusant à expérimenter et à partager.
    merci à vous

    RépondreSupprimer
  2. Bonsoir.
    J'ai développé un projet de nichoir connecté avec un système d'envoi en live sur le direct YouTube de la température et de l'hygrométrie du nichoir mais également du nombre de passages des oiseaux au nichoir.
    Mon système est basé sur un dht22 et 2 barrières infrarouge en 38khz, le tout piloté par une node mcu v3.
    Votre sujet m'intéresse tout particulièrement car je souhaiterais enregistrer dans l'eeprom le nombre d'entrée et de sortie des oiseaux en cas de coupure de courant pour reprendre ces valeurs au rétablissement du courant.
    Étant novice dans tous ces petits calcul d'énergie je souhaitait savoir si vous pourriez m'aider à choisir les composants pour ce faire.
    Merci d'avance.

    RépondreSupprimer
    Réponses
    1. Oui bien entendu. A partir du moment où l'on connaît la consommation du montage et la quantité de données à sauvegarder c'est facile.

      Supprimer
    2. Bonsoir,
      Merci de votre retour.
      Ma nodeMCU consomme 84mA maxi ( entre 77mA et 84mA ) La consommation est élevée car elle alimente mes 2 barrières IR.
      La tension est de 4,98V.
      A ce jour le sketch n'utilise pas d’interruption pour faire l'enregistrement mais enregistre dans l'eeprom dès que le nombre d'entrée est supérieur de 10 ..
      il faudra que je me documente pur faire ce changement....


      exemple actuel :

      " // Sauvegarde du nombre d'entrée dans l'eeprom si " entrée +10
      "
      eeprom = EEPROM.read(1) * 256 + EEPROM.read(2);
      if (eeprom + 10 == entree) {
      EEPROM.write(1, uint8_t(entree / 256));
      EEPROM.write(2, uint8_t(entree % 256));
      EEPROM.commit();
      }
      "
      j'aurais donc a enregistrer 2 informations : le nombre d'entrée et de sorties.
      Merci de votre aide.

      Supprimer
    3. Il faudra réaliser le montage du paragraphe 1 en faisant attention à ce que le pont diviseur abaisse la tension sur la broche D2 à moins de 3.3V.
      Avec R1=100KΩ et R2=180KΩ cela devrait aller : 3.21V

      Ensuite il faut mesurer le temps nécessaire à la sauvegarde.
      Voir 3.2. Durée de sauvegarde

      Ensuite le montage sous 5V consommant - en arrondissant - 100mA est équivalent à une résistance de 50Ω, le condensateur vaudra :
      C = T / R / 0.15 = temps / 50 / 0.15

      Voir ensuite : 4. Les essais

      Supprimer
  3. Bonjour,

    LE temps de sauvegarde ( = delais donné par LOSS POWER )
    Serial.print("LOSS POWER: ");
    Serial.print(stop - start);
    Serial.println("µs");
    est de 70µs .
    cela donnerais un condo de 9.3µF sous 5v c'est cela ? on peut dire 10µF 6.3V ?
    merci de votre retour

    RépondreSupprimer
    Réponses
    1. Il vaut mieux prévoir une marge de sécurité : 15µF ou 22µF
      15µF 6.3V sera difficile à trouver, mais pas impossible.
      15µF ou 22µF 10V ou 16V sera plus facile.

      Supprimer
  4. re,
    oui 10V 22uF est en stock !
    pour la diode je doit rester sur la 1N5819 ?

    RépondreSupprimer
    Réponses
    1. Tout dépend du régulateur 3.3V implanté sur la carte NodeMCU.
      Si c'est un bon LDO il pourra se contenter d'une bonne diode 1N4001 et de sa chute de tension de 0.7V.
      Il faut essayer.

      Supprimer
  5. Bonsoir.
    En 5v ça ne passait pas avec la seul diode en stock 1N4001 !
    J'ai donc alimenté ma nodemcu via la régulation de son support en 12v. J'ai pris un condo 16v 2200uF et la tout est OK.
    Encore un grand merci pour votre patience et astuces....
    Je mettrais bientôt mon blog à jour avec la technologie de mon nichoir... Je vous glisserai un lien.
    Merci

    RépondreSupprimer
  6. bonjour,
    merci pour ce code, qui m'est d'une grande aide!!
    je suis en train de faire un programme, pour faire tourner un moteur pas à pas et afficher sur le lcd arduino, le nombre de cycle que fait le moteur. et donc j'aimerai,en cas de coupure de courant, que l'afficheur affiche le dernier cycle avt la coupure et qu'il reprenne le compte à partir de ce cycle memorisé. est ce possible?
    je suis vraiment novice en programmation Merci d'avance

    RépondreSupprimer
    Réponses
    1. Il suffit de sauvegarder le nombre dans la routine d'interruption powerLoss.
      Dans le cas d'un simple compteur remplacer simplement le membre data de la structure eeprom_data par un entier long.

      Supprimer
  7. J'ai fait le montage décris au chiffre 1. J'alimente en 12V sur VIN. Pour info R1: 100Kohm / R2 : 68Kohm / D1 : 1N5819 / C1 : 100µF Pas de soucis.
    Maintenant je me suis attaqué au soft avec le programme de test du chiffre 4. mais là ça se gâte…. aucun problème pour téléverser. Par contre ça n'a pas l'air de fonctionner.
    Voici ce que je constate :
    - la boucle loop ne s'exécute pas toujours (parfois aucun serial.print ne s'affiche, même après le délai de 30 sec.)
    - la boucle powerloss a l'air de fonctionner car j'ai le temps qui s'affiche. Par contre c'est à la mise sous tension (12V sur VIN) que cela s'exécute
    - la boucle setup : aucune des serial.print ne s'affiche, ni dans le if ni dans le else.

    A part ça ce bout de programme est trop "évolué" pour ce dont j'ai besoin. Moi je cherche juste à mettre un chiffre entre 0 et 255 (voire 999) dans l'EEPROM lorsque la tension chute (le chiffre est stocké dans une variable) puis a le récupérer lorsque la tension revient. Est-ce quelqu'un peut me dire les lignes minimums qu'il faut ?

    D'avance un grand merci

    RépondreSupprimer
    Réponses
    1. Il y a un topic sur le sujet :
      https://forum.arduino.cc/index.php?topic=602342.0
      Il vaut mieux poster le code.

      Supprimer
  8. C'est bien pour sauvegarder les données, mais pour sauvegarder le code, et ne pas avoir à le recharger lors de la remise sous tension (permettant ainsi d'avoir un appareil entièrement autonome, indépendant du PC.
    Merci d'avance.

    RépondreSupprimer
    Réponses
    1. Vous semblez ignorer que le code est chargé en mémoire FLASH, et qu'il est donc non volatile.

      Supprimer
  9. Article excellent !! Peux-ton remplacer les condo ceramiques habituel de 100nF sur la pin d'alim du micro par un condo electrolytique reserve d'energie pour faire la même chose que vous ? Est-ce que cela fonctionnerait ?

    RépondreSupprimer
  10. quand vous faites la lecture du port série, si vous avez vérifié avec un appel à available() qu'au moins un octet est disponible, ce n'est pas la peine lors de la lecture de vérifier que ce n'était pas -1 car -1 n'est retourné que lorsque le buffer d'entrée est vide (et donc que available vaut 0).

    au lieu de

    ```
    if (Serial.available()) {
    int c = Serial.read();
    if (c != -1) {
    ```

    on peut juste faire

    ```
    if (Serial.available()) {
    int c = Serial.read();
    ```

    RépondreSupprimer
    Réponses
    1. C'est un simple réflexe de développeur logiciel . A partir du moment où la fonction retourne une valeur, on teste celle-ci.
      Traiter toutes les erreurs, même improbables, s'appelle de la programmation défensive. On est adepte ou pas, mais cela ne mange pas de pain ...

      Supprimer
  11. si la portabilité est importante, le resultant de offsetof() n'est pas défini par le standard pour les versions avant C++17. C'est pour cela que l'on met souvent le mot magique en début de structure (donc à l'adresse de la structure) mais cela nécessite que l'on a contraint l'organisation interne e la structure avec des attributs comme packed et align (mais là encore c'est spécifique) ou alors on stocke le mot magique séparément de la structure en calculant les adresses "à la main" en fonction du sizeof du mot magique - c'est ce qui est le plus portable, sizeof faisant partie de tous les compilateurs et de la norme.

    RépondreSupprimer
    Réponses
    1. Si l'on devait écrire du code de nos jours en tenant compte des anciens compilateurs ne supportant pas telle ou telle fonctionnalité, on se contenterait d'un subset restreint du langage. Je suis contre, et je tiens à profiter pleinement des avantages des compilateurs récents.
      Ne pas oublier non plus qu'un logiciel n'est rien sans test. Le codage représente la moitié du travail, le test l'autre moitié.
      Donc à partir du moment ou ofsetof() est bien supporté, et que le test confirme ce point, autant l'utiliser.

      Supprimer