Speichern von Daten in einem EEPROM auf einem Arduino transaktional

Das Vorhandensein von EEPROM bietet Entwicklern ein praktisches Tool zum Speichern von Konfigurationsparametern oder eines sich langsam ändernden Zustands, in dem ein Stromausfall überleben sollte. In diesem Artikel werden wir uns ansehen, wie dies so sicher und bequem wie möglich gemacht werden kann, um nichts zu vergessen und uns nicht daran zu erinnern, was nicht da war.

Angenommen, wir haben eine Variable und möchten sie in einem EEPROM speichern. Es scheint, dass alle Werkzeuge dafür in unseren Händen sind:

#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); 

Ein genauerer Blick zeigt jedoch, dass dieser Ansatz mehr Probleme schafft als löst. Wir werden sie der Reihe nach besprechen.

1. Wie können wir sicherstellen, dass wir genau das lesen, was wir aufgeschrieben haben (um die Integrität zu gewährleisten)? Stellen Sie sich folgendes Bild vor. Wir schreiben uns einen Brief für den Fall unseres plötzlichen Todes aufgrund eines Stromausfalls oder eines Rücksetzsignals und legen ihn in eine Schreibtischschublade. Im nächsten Leben öffnen wir die Schreibtischschublade, nehmen ein Stück Papier heraus, lesen die Botschaft und setzen unsere Mission fort. Das Problem ist, dass in der Box immer Blätter mit zufälligem Text gekritzelt sind. Wir brauchen also eine Möglichkeit, die richtige Nachricht von der zufälligen zu unterscheiden. Man könnte ihm einen Notar zusichern, aber im einfachsten Fall würde seine Unterschrift ausreichen, wenn wir die Richtigkeit überprüfen könnten. Beispielsweise können wir das Ergebnis eines mathematischen Ausdrucks in Abhängigkeit vom Text als Signatur verwenden, so dass die Wahrscheinlichkeit eines zufälligen Zusammentreffens ausreichend gering ist. Im einfachsten Fall ist dies ein CRC oder eine Prüfsumme. Es schützt uns nicht nur vor dem Lesen dessen, was wir nicht geschrieben haben, sondern auch vor dem Lesen einer beschädigten Nachricht. Schließlich verblasst der Text mit der Zeit und die Elektronen im isolierten Shutter sind noch weniger langlebig - ein Partikel fliegt mit ausreichender Energie aus dem Weltraum, und das Bit ändert sich. Es gibt aber auch eine andere Möglichkeit, eine beschädigte Nachricht zu erhalten: Fügen Sie sie nicht am Ende hinzu. Es ist nicht so exotisch, weil zum Zeitpunkt der Aufnahme der Stromverbrauch stark ansteigt, was einen vorzeitigen Tod des Schriftstellers hervorrufen kann.

2. Angenommen, wir waren von der Richtigkeit der Nachricht überzeugt, aber wie kann ich sicherstellen, dass ich es war, der sie geschrieben hat (um die Authentizität zu gewährleisten) ? Wie das Sprichwort sagt, bin ich anders. Plötzlich saß vor meiner Reinkarnation noch jemand an diesem Tisch, und er hatte eine andere Mission, und aus welchem ​​Grund werde ich mich jetzt von seinen Botschaften leiten lassen? Wenn wir unsere Notizen mit einem bestimmten Etikett versehen würden, könnten wir unsere leichter von Fremden unterscheiden. Ein solches Label könnte beispielsweise der Name der Variablen sein, die wir speichern. Das einzige Problem besteht darin, dass im EEPROM nicht viel Platz für Variablennamen vorhanden ist, und dies ist unpraktisch, da sie unterschiedlich lang sind. Glücklicherweise gibt es jedoch eine einfachere Möglichkeit: Sie können die Prüfsumme im Namen der Variablen berechnen und als Verknüpfung verwenden. Gleichzeitig ist es nützlich, die Größe der Variablen in Bytes zu dieser Prüfsumme hinzuzufügen, um nicht versehentlich die falsche Menge zu lesen. Der Vollständigkeit halber fügen wir dort einen weiteren numerischen Bezeichner hinzu, um zu gewährleisten, dass unsere Variable von einer anderen Person unterschieden wird, auch wenn sie gleich heißt. Wir nennen diese Nummer die Instanzkennung (inspiriert von OOP, wenn der Variablenname als Objektfeld betrachtet wird). Wenn wir unsere Mission jemals auf eine radikal neue Version aktualisieren, sodass dieses Update alles, was die alte gespeichert hat, sinnlos macht, müssen wir nur die Instanz-ID ändern, um alles, was in der alten Version gespeichert ist, ungültig zu machen.

3. Wie kann ich einen unvollständigen Schreibvorgang ausführen, ohne den alten gespeicherten Wert zu ändern? Das heißt, der Speichervorgang sollte entweder erfolgreich sein oder überhaupt keine beobachtbaren Auswirkungen haben. Mit anderen Worten, es sollte atomar oder transaktional sein, wenn es sich um eine Transaktion handelt, die auf eine bedingungslose Aktualisierung eines einzelnen Werts hinausläuft. Offensichtlich können wir nicht sicherstellen, dass der Datensatz atomar ist, indem wir den vorherigen Wert überschreiben. Wir müssen an eine neue Stelle schreiben, damit der alte gespeicherte Wert erhalten bleibt, zumindest bis die Aufzeichnung des neuen Werts abgeschlossen ist. Diese Technik wird oft als "Copy-on-Write" bezeichnet, wenn nur ein Teil des gespeicherten Werts aktualisiert wird, der unveränderte Teil jedoch noch kopiert und an eine neue Position geschrieben wird. Ausgehend von unserer Analogie werden wir uns selbst Briefe schreiben, wobei die alten unangetastet bleiben, aber jeden Brief mit einer zunehmenden Seriennummer versehen, damit wir in unserem nächsten Leben die Möglichkeit haben, den letzten Brief zu finden, den wir geschrieben haben. Gleichzeitig tritt jedoch ein neues Problem auf - die Stelle in der Box, an der wir die Buchstaben ablegen, endet früher oder später, wenn wir alte, irrelevant gewordene Buchstaben nicht wegwerfen. Es ist leicht zu verstehen, dass es ausreicht, nur zwei Buchstaben zu speichern - einen alten und einen neuen. Möglicherweise wird gerade geschrieben. Dementsprechend benötigt die Buchstabennummer auch nicht viele Bits.

Seltsamerweise konnte der Autor keine einzige Implementierung finden, die die Organisation der Datenspeicherung im EEPROM ermöglicht und gleichzeitig Integrität, Authentizität und Atomizität gewährleistet. Ich musste selbst an github.com/olegv142/NvTx schreiben

Um jede Variable im EEPROM zu speichern, werden 2 aufeinanderfolgende Bereiche verwendet - Zellen mit derselben Struktur. In die ersten 2 Bytes wird die Kennung der Variablen geschrieben, die anhand ihrer Größe, Textbezeichnung und Instanzkennung berechnet wird. Als nächstes werden Daten geschrieben, gefolgt von 2 Bytes der Prüfsumme. Im allerersten Byte haben zwei Bits einen besonderen Zweck. Das höchstwertige Bit ist das Korrektheitsflag, das beim Schreiben immer auf eins gesetzt wird. Das niederwertige Bit wird als Einzelbit-Nummer der Ära verwendet und zum Auffinden der letzten Nachricht benötigt. Die Aufzeichnung erfolgt in Zellen 'in einem Kreis'. Die Nummer der Ära ändert sich jedes Mal, wenn ein Datensatz in der ersten Zelle erstellt wird. Daher der Algorithmus zur Bestimmung der zuletzt aufgezeichneten Zelle: Wenn die Epochen der Zellen gleich sind, wird die zweite zuletzt geschrieben, wenn sie unterschiedlich ist - dann die erste.

Das Korrektheitsbit erscheint redundant, hat aber eine wichtige Funktion. Zunächst lesen wir die gespeicherten Daten und überprüfen die Richtigkeit beider Zellen. Wenn die Zelle die Prüfung auf die richtige Kennung oder Prüfsumme nicht besteht, setzen wir das Korrektheitsbit zurück. Nachfolgende Schreiboperationen prüfen möglicherweise nicht die Richtigkeit der Zellen, sondern verlassen sich auf dieses Flag, wodurch der Overhead um das Zweifache reduziert wird.

Diejenigen, die sich mit den Implementierungsdetails befassen möchten, können die Bilder und den Code im Repository sehen . Ich gehe weiter, um den Leser nicht zu langweilen. Die Funktionen zum Schreiben / Lesen von Daten erhalten jeweils 5 Parameter, sodass die Benutzerfreundlichkeit zugunsten der Flexibilität beeinträchtigt wird. Es wird jedoch großzügig durch zwei Sätze von Makros kompensiert, die die Verwendung der Bibliothek so einfach wie im Fall von EEPROM.get / put machen. Die erste Gruppe von Makros wird verwendet, wenn Sie die Variable nur unter der angegebenen Adresse speichern möchten:

 #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); 

Wenn mehrere Variablen gespeichert werden sollen, muss jede die Adresse ermitteln und gleichzeitig die Größe korrekt berücksichtigen, damit sich die Speicherbereiche, in denen die Variablen gespeichert sind, nicht überlappen. Um die Aufgabe zu vereinfachen, implementiert die zweite Gruppe von Makros die automatische Adresszuweisung, und zwar zum Zeitpunkt der Kompilierung . Beispielsweise kann die Arduino-EEPROMEx-Bibliothek zur Laufzeit Speicher zuweisen, während sie die Adresse für jede gespeicherte Variable im RAM speichert. Die NvTx- Bibliothek reserviert Speicherplatz im EEPROM, ohne den ausführbaren Code oder den Inhalt des RAM zu verändern.

 #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); 

Das Makro NvPlace legt die Startadresse des EEPROM-Bereichs fest, in dem die Variablen und die Instanzkennung gespeichert werden. Das NvAfter-Makro reserviert einen Speicherbereich, um sein erstes Argument unmittelbar nach dem für den zweiten reservierten Speicherbereich zu speichern. Bei der Zuweisung von Speicher wird außerdem überprüft, dass die verfügbare EEPROM-Größe nicht überschritten wurde und dass überlappende Speicherbereiche nicht reserviert wurden (dies kann vorkommen, wenn zwei NvAfter-Makros dasselbe zweite Argument haben). Bei einem Verstoß gegen eine der beiden angegebenen Bedingungen wird das Programm einfach nicht kompiliert. Diejenigen, die sich mit dem Speicherzuweisungsmechanismus befassen möchten, finden ihn in der Header-Datei NvTx.h. Alles, was die Makros NvPlace und NvAfter tun, ist, die Aufzählungen zu definieren, ihre Namen auf der Grundlage der Variablennamen zu bilden und auch die sehr nützliche idiomatische Konstruktion der Kompilierungszeit zu verwenden .

Hoffentlich hilft die NvTx- Bibliothek den Lesern dabei, zuverlässigen Code in Industriequalität zu schreiben.

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


All Articles