Enregistrement de données dans une EEPROM sur un Arduino de manière transactionnelle

La présence d'EEPROM offre aux développeurs un outil pratique pour enregistrer les paramètres de configuration ou un état changeant lentement qu'une panne de courant devrait survivre. Dans cet article, nous verrons comment le faire de manière aussi sûre et pratique que possible afin de ne rien oublier et de ne pas se souvenir de ce qui n'était pas là.

Supposons que nous ayons une variable et que nous voulons la stocker dans une EEPROM. Il semblerait que tous les outils pour cela soient entre nos mains:

#include <EEPROM.h> int my_var = DEFAULT_VALUE; EEPROM.get(MY_VAR_ADDR, my_var); my_var = NEW_VALUE; EEPROM.put(MY_VAR_ADDR, my_var); 

Cependant, un examen plus approfondi révèle que cette approche crée plus de problèmes qu'elle n'en résout. Nous en discuterons dans l'ordre.

1. Comment s'assurer que nous lisons exactement ce que nous avons écrit (pour garantir l' intégrité )? Imaginez l'image suivante. Nous nous écrivons une lettre en cas de mort subite due à une panne de courant ou à un signal de réinitialisation et la mettons dans un tiroir de bureau. Dans la prochaine vie, nous ouvrons le tiroir du bureau, sortons un morceau de papier, lisons le message et continuons notre mission. Le problème est que dans la boîte il y a toujours des feuilles de papier gribouillées avec du texte aléatoire. Nous avons donc besoin d'un moyen de distinguer le message correct du message aléatoire. On pourrait l'assurer d'un notaire public, mais dans le cas le plus simple, sa signature suffirait si nous avons un moyen de vérifier son exactitude. Par exemple, nous pouvons utiliser le résultat d'une expression mathématique en fonction du texte comme signature, de sorte que la probabilité de coïncidence aléatoire soit suffisamment petite. Dans le cas le plus simple, il s'agit d'un CRC ou d'une somme de contrôle. Cela nous protégera non seulement de la lecture de ce que nous n'avons pas écrit, mais aussi de la lecture d'un message endommagé. Après tout, le texte s'estompe avec le temps et les électrons dans l'obturateur isolé sont encore moins durables - une particule volera de l'espace avec suffisamment d'énergie et le bit changera. Mais il existe un autre moyen d'obtenir un message endommagé - ce n'est pas de l'ajouter à la fin. Ce n'est pas si exotique, car au moment de l'enregistrement, la consommation actuelle augmente fortement, ce qui peut provoquer une mort prématurée de l'écrivain.

2. Supposons que nous soyons convaincus de l'exactitude du message, mais comment puis-je m'assurer que c'est moi qui l'ai écrit (pour garantir l' authenticité ). Comme dit le proverbe, je suis différent. Soudain, quelqu'un d'autre était assis à cette table avant ma réincarnation, et il avait une mission différente, et pour quelle raison vais-je maintenant être guidé par ses messages? Si nous fournissions à nos notes une certaine étiquette, il nous serait plus facile de distinguer les nôtres des étrangers. Par exemple, une telle étiquette pourrait être le nom de la variable que nous enregistrons. Le seul problème est qu'il n'y a pas beaucoup d'espace dans l'EEPROM pour y mettre des noms de variables, et il n'est pas pratique de le faire, car ils sont de longueurs différentes. Mais heureusement, il existe un moyen plus simple - vous pouvez calculer la somme de contrôle au nom de la variable et l'utiliser comme raccourci. Dans le même temps, il est utile d'ajouter la taille de la variable en octets à cette somme de contrôle afin de ne pas lire accidentellement le mauvais montant. Eh bien, par souci d'exhaustivité, nous y ajoutons un autre identifiant numérique, afin de garantir distinguer notre variable de quelqu'un d'autre, même s'ils sont appelés les mêmes. Nous appelons ce numéro l'identificateur d'instance (inspiré de la POO si le nom de la variable est considéré comme un champ d'objet). Si jamais nous mettons à jour notre mission vers une version radicalement nouvelle, de sorte que cette mise à jour rend insensé tout ce que l'ancien a enregistré, alors nous avons juste besoin de changer l'identifiant d'instance pour invalider tout ce qui a été enregistré par l'ancienne version.

3. Comment puis-je faire en sorte qu'une opération d'écriture incomplète laisse inchangée l'ancienne valeur stockée? Autrement dit, l'opération de sauvegarde doit réussir ou ne pas avoir d'effet observable du tout. En d'autres termes, il devrait être atomique ou transactionnel si nous parlons d'une transaction qui se résume à une mise à jour inconditionnelle d'une valeur unique. De toute évidence, nous ne pouvons pas garantir l'atomicité de l'enregistrement en réécrivant la valeur précédente, nous devons écrire dans un nouvel emplacement afin que l'ancienne valeur stockée reste intacte, au moins jusqu'à la fin de l'enregistrement de la nouvelle. Cette technique est souvent appelée «copie sur écriture» si seule une partie de la valeur enregistrée est mise à jour, mais la partie qui reste inchangée est toujours copiée et écrite dans un nouvel emplacement. En développant notre analogie, nous nous écrirons des lettres, en laissant les anciennes intactes, mais en fournissant à chaque lettre un numéro de série croissant afin que dans notre prochaine vie, nous ayons la possibilité de trouver la dernière lettre que nous avons écrite. Cependant, un nouveau problème se pose - la place dans la boîte où nous mettons les lettres se terminera tôt ou tard si nous ne jetons pas les vieilles lettres qui ne sont plus pertinentes. Il est facile de comprendre qu'il suffit de stocker seulement 2 lettres - une ancienne et une nouvelle, elle peut être en cours d'écriture. Par conséquent, le numéro de lettre n'a également pas besoin de beaucoup de bits.

Curieusement, l'auteur n'a pas pu trouver une seule implémentation qui permettrait l'organisation du stockage des données en EEPROM, tout en garantissant l'intégrité, l'authenticité et l'atomicité. J'ai dû écrire à github.com/olegv142/NvTx moi-même

Pour enregistrer chaque variable dans l'EEPROM, 2 zones consécutives sont utilisées - des cellules avec la même structure. L'identifiant de la variable calculé sur la base de sa taille, de son étiquette de texte et de son identifiant d'instance est écrit dans les 2 premiers octets. Ensuite, les données sont écrites, suivies de 2 octets de la somme de contrôle. Dans le tout premier octet, deux bits ont une fonction particulière. Le bit le plus significatif est l'indicateur de correction; lors de l'écriture, il est toujours défini sur un. Le bit de poids faible est utilisé comme un seul bit de l'époque, il est nécessaire pour trouver le dernier message. L'enregistrement se fait dans des cellules «en cercle». Le numéro de l'ère change chaque fois qu'un enregistrement est effectué dans la première cellule. D'où l'algorithme de détermination de la dernière cellule enregistrée: si les époques des cellules sont les mêmes, alors la seconde est écrite en dernier, si différente - alors la première.

Le bit de correction semble redondant, mais il a une fonction importante. Tout d'abord, nous lisons les données stockées et vérifions l'exactitude des deux cellules. Si la cellule ne réussit pas la vérification de l'identifiant ou de la somme de contrôle correct, nous réinitialisons le bit d'exactitude. Les opérations d'écriture suivantes peuvent ne pas vérifier l'exactitude des cellules, mais s'appuyer sur cet indicateur, ce qui réduit les frais généraux d'environ 2 fois.

Ceux qui veulent se plonger dans les détails de l'implémentation peuvent voir les images et le code dans le référentiel . Moi, pour ne pas ennuyer le lecteur, je passe à l'usage. Les fonctions d'écriture / lecture de données reçoivent chacune 5 paramètres, de sorte que la commodité de leur utilisation est sacrifiée au profit de la flexibilité. Mais il est généreusement compensé par deux ensembles de macros, qui rendent l'utilisation de la bibliothèque aussi simple que dans le cas d'EEPROM.get / put. Le premier ensemble de macros est utilisé si vous souhaitez simplement enregistrer la variable à l'adresse donnée:

 #include <NvTx.h> int my_var = DEFAULT_VALUE; bool have_my_var = NvTxGetAt(my_var, MY_VAR_ADDR); my_var = NEW_VALUE; NvTxPutAt(my_var, MY_VAR_ADDR); 

S'il y a plusieurs variables à enregistrer, chacune devra déterminer l'adresse et en même temps considérer correctement la taille afin que les zones de mémoire où les variables sont stockées ne se chevauchent pas. Pour simplifier la tâche, le deuxième ensemble de macros implémente l'allocation automatique d'adresses, et le fait au moment de la compilation . Par exemple, la bibliothèque Arduino-EEPROMEx peut allouer de la mémoire au moment de l'exécution, tandis qu'elle stocke l'adresse en RAM pour chaque variable stockée. La bibliothèque NvTx alloue de l'espace dans l'EEPROM sans rien ajouter au code exécutable ou au contenu de la RAM.

 #include <NvTx.h> int my_var = DEFAULT_VALUE; char my_string[16] = ""; NvPlace(my_var, MY_START_ADDR, MY_INST_ID); NvAfter(my_string, my_var); bool have_my_var = NvTxGet(my_var); my_var = NEW_VALUE; NvTxPut(my_var); 

La macro NvPlace définit l'adresse de départ de la zone EEPROM, où nous allons stocker les variables et l'identifiant d'instance. La macro NvAfter réserve une région de mémoire pour stocker son premier argument immédiatement après la région de mémoire réservée au second. Lors de l'allocation de mémoire, il est également vérifié que nous n'avons pas dépassé la taille EEPROM disponible, et que nous n'avons pas réservé de zones de mémoire qui se chevauchent (cela peut se produire si deux macros NvAfter ont le même deuxième argument). En cas de violation de l'une des deux conditions spécifiées, le programme ne compile tout simplement pas. Ceux qui veulent gérer le mécanisme d'allocation de mémoire le trouveront dans le fichier d'en-tête NvTx.h. Toutes les macros NvPlace et NvAfter permettent de définir les énumérations, de former leurs noms en fonction des noms de variables, et d'utiliser également la construction idiomatique très utile de l' assertion de temps de compilation .

Espérons que la bibliothèque NvTx aidera les lecteurs à écrire du code fiable de qualité industrielle.

Source: https://habr.com/ru/post/fr482918/


All Articles