Wear Leveling auf dem EEPROM eines Mikrocontrollers

Zum Beispiel: Das Datenblatt für ATtiny2313 (wie die meisten Atmel AVR-Datenblätter) besagt:

128 Bytes im System programmierbares EEPROM Lebensdauer: 100.000 Schreib-/Löschzyklen

Stellen Sie sich vor, ein Programm benötigt nur zwei Bytes, um eine Konfiguration zu speichern, die anderen 126 Bytes werden effektiv verschwendet. Was mich beunruhigt ist, dass regelmäßige Updates der beiden Konfigurationsbytes das EEPROM des Geräts verschleißen und unbrauchbar machen können. Das ganze Gerät würde unzuverlässig werden, weil man zu einem bestimmten Zeitpunkt einfach nicht mehr verfolgen kann, welche Bytes im EEPROM unzuverlässig sind.

Gibt es eine intelligente Möglichkeit, Wear Leveling auf dem EEPROM eines Mikrocontrollers durchzuführen, wenn Sie effektiv nur ein oder zwei Bytes von verfügbaren 128 verwenden?

Wenn 100.000 Schreibzyklen eine Einschränkung wären, wäre es dann sinnvoll, stattdessen eine andere Technologie zu verwenden? Entweder ein Mechanismus, der eine interne Nivellierung beinhaltet, oder etwas mit einer Größenordnung oder größerer Ausdauer?
@AnindoGhosh Ich möchte meinen kleinen Bestand an Mikrocontrollern einfach nicht verschwenden, nur weil das EEPROM abgenutzt ist, weil ich einen Proof of Concept getestet habe. Ich möchte mir keine Gedanken darüber machen, welches Byte ich bei einem früheren Projekt verwendet habe, wenn ich den Controller wiederverwende. Und es fühlt sich einfach gut an zu wissen, dass ich die vorhandene Hardware optimal ausnutze.
Vielleicht schaust du dir meine Antwort bei stackoverflow an .
Werfen Sie einen Blick auf die MSP430 FRAM-Serie von TI... 10^13 schreibt!!!

Antworten (5)

Die Technik, die ich normalerweise verwende, besteht darin, den Daten eine fortlaufende 4-Byte-Sequenznummer voranzustellen, wobei die größte Zahl den letzten / aktuellen Wert darstellt. Im Fall der Speicherung von 2 Bytes tatsächlicher Daten, die insgesamt 6 Bytes ergeben würden, würde ich dann eine kreisförmige Warteschlangenanordnung bilden, sodass 128 Bytes EEPROM 21 Einträge enthalten und die Lebensdauer um das 21-fache erhöhen würden.

Dann kann beim Booten die größte Sequenznummer verwendet werden, um sowohl die nächste zu verwendende Sequenznummer als auch das aktuelle Ende der Warteschlange zu bestimmen. Der folgende C-Pseudocode demonstriert, dies setzt voraus, dass der EEPROM-Bereich bei der Erstprogrammierung auf Werte von 0xFF gelöscht wurde, sodass ich eine Sequenznummer von 0xFFFF ignoriere:

struct
{
  uint32_t sequence_no;
  uint16_t my_data;
} QUEUE_ENTRY;

#define EEPROM_SIZE 128
#define QUEUE_ENTRIES (EEPROM_SIZE / sizeof(QUEUE_ENTRY))

uint32_t last_sequence_no;
uint8_t queue_tail;
uint16_t current_value;

// Called at startup
void load_queue()
{
  int i;

  last_sequence_no = 0;
  queue_tail = 0;
  current_value = 0;
  for (i=0; i < QUEUE_ENTRIES; i++)
  {
    // Following assumes you've written a function where the parameters
    // are address, pointer to data, bytes to read
    read_EEPROM(i * sizeof(QUEUE_ENTRY), &QUEUE_ENTRY, sizeof(QUEUE_ENTRY));
    if ((QUEUE_ENTRY.sequence_no > last_sequence_no) && (QUEUE_ENTRY.sequence_no != 0xFFFF))
    {
      queue_tail = i;
      last_sequence_no = QUEUE_ENTRY.sequence_no;
      current_value = QUEUE_ENTRY.my_data;
    }
  }
}

void write_value(uint16_t v)
{
  queue_tail++;
  if (queue_tail >= QUEUE_ENTRIES)
    queue_tail = 0;
  last_sequence_no++;
  QUEUE_ENTRY.sequence_no = last_sequence_no;
  QUEUE_ENTRY.my_data = v;
  // Following assumes you've written a function where the parameters
  // are address, pointer to data, bytes to write
  write_EEPROM(queue_tail * sizeof(QUEUE_ENTRY), &QUEUE_ENTRY, sizeof(QUEUE_ENTRY));
  current_value = v;
}

Für ein kleineres EEPROM wäre eine 3-Byte-Sequenz effizienter, würde jedoch ein wenig Bit-Slicing erfordern, anstatt Standarddatentypen zu verwenden.

+1, Schöner Ansatz. Kann der Speicher ein wenig optimiert werden, indem weniger „Tag“-Bytes verwendet werden und möglicherweise von einer Form von Hash-Bucket-Mechanismus abhängt, um eine zusätzliche Verteilung bereitzustellen? Eine Mischung aus No-Leveling und Ihrem Ansatz?
@ AnindoGhosh, ja, ich glaube, es könnte. Ich habe diesen Ansatz normalerweise auf kleinen Mikros zur Vereinfachung des Codes verwendet und persönlich habe ich ihn hauptsächlich auf größeren Geräten wie DataFLASH verwendet. Eine andere einfache Idee, die mir in den Sinn kommt, ist, dass die Sequenznummern periodisch verringert werden könnten, um sie auf kleineren Werten zu halten.
Der von @m.Alin erwähnte Atmel Application Note hat eine clevere Vereinfachung: Nach einem RESET ist es dann möglich, den [...] Puffer zu durchsuchen und das letzte [...] geänderte Pufferelement zu finden, indem man die Stelle findet, an der die Unterschied zwischen einem Pufferelement und dem nächsten Pufferelement ist größer als 1 .
Sollte write_value() den Eintrag nicht bei queue_tail*sizeof(QUEUE_ENTRY) ablegen? Ich werde beim ersten Mal richtig liegen, aber sollte es nicht weiter voranschreiten, wenn mehrere Schreibvorgänge vorhanden sind? i wird außerhalb von load_queue() nicht inkrementiert.
@marshaul, danke ja, das war ein Tippfehler und es hätte so sein sollen, ich habe es gerade aktualisiert.
Die Antwort von PeterJ wird funktionieren, bis "last_sequence_no" auf 0 zurückgesetzt wird. Die Funktion "load_queue()" bleibt dann bei 4294967295 hängen, da der nächste Index auf 0 heruntergesetzt wird und die (QUEUE_ENTRY.sequence_no > last_sequence_no ) prüfen.
@ DWORD32: Ja, das ist technisch korrekt, aber in der Praxis irrelevant. Bis dahin ist die Verschleißgrenze des EEPROMs um den Faktor 2000 überschritten!
Auf der anderen Seite, wenn Sie Rollover richtig handhaben, können Sie das Tag auf 1 oder 2 Bytes reduzieren, was in einem kleinen EEProm wie diesem eine große Einsparung wäre, wenn Ihre Datengröße nur 2 Bytes beträgt.
@jippie Aber das scheint teuer zu sein. [...] Man sollte sich darüber im Klaren sein, dass diese Methode, zusätzliche Ausdauer für die Parameterspeicherung bereitzustellen, speicherhungrig ist. [...]

Es folgt eine Methode, die Buckets und etwa ein Overhead-Byte pro Bucket verwendet. Die Bucket-Bytes und Overhead-Bytes werden etwa gleich stark beansprucht. Im vorliegenden Beispiel weist dieses Verfahren bei gegebenen 128 EEPROM-Bytes 42 2-Byte-Buckets und 44 Statusbytes zu, was die Verschleißfähigkeit um das 42-fache erhöht.

Methode:

Unterteilen Sie den EEPROM-Adressraum in k Buckets, wobei k =⌊ E /( n +1)⌋, mit n = Setup-Data-Array-Größe = Bucket-Größe und E = EEPROM-Größe (oder allgemeiner die Anzahl der EEPROMs). Zellen, die dieser Datenstruktur gewidmet werden sollen).

Initialisiere ein Verzeichnis, ein Array von m Bytes, die alle auf k gesetzt sind , mit m = En·k . Wenn Ihr Gerät hochfährt, liest es das Verzeichnis durch, bis es den aktuellen Eintrag findet, der ein Byte ungleich k ist . [Wenn alle Verzeichniseinträge gleich k sind, initialisiere den ersten Verzeichniseintrag auf 0 und mache von dort aus weiter.]

Wenn der aktuelle Verzeichniseintrag j enthält , enthält Bucket j aktuelle Daten. Wenn Sie einen neuen Setup-Dateneintrag schreiben müssen, speichern Sie j +1 im aktuellen Verzeichniseintrag; Wenn es dadurch gleich k wird, initialisiere den nächsten Verzeichniseintrag mit 0 und mache von dort aus weiter.

Beachten Sie, dass Verzeichnis-Bytes ungefähr den gleichen Verschleiß erfahren wie Bucket-Bytes, weil 2 · k > mk .

(Ich habe das Obige aus meiner Antwort auf die Arduino SE-Frage 34189 angepasst , Wie kann die Lebensdauer des EEPROM verlängert werden? .)

Für mich war es ein bisschen schwer, die Beschreibung zu verstehen, aber nachdem ich simuliert hatte, wie es in meinem Kopf ablaufen würde, wurde es klar. Die Lösung ist der akzeptierten Antwort in Bezug auf Speichereffizienz / Verschleißreduzierung überlegen. Mit 21 Buckets wie in der akzeptierten Antwort können Sie beispielsweise 5 Bytes Daten anstelle von 2 speichern.

Es gibt ein paar Optionen, abhängig von der Art des EEPROMs, das Sie haben, und der Größe Ihrer Daten.

  1. Wenn Ihr EEPROM einzeln löschbare Seiten hat und Sie 1 Seite (oder mehr) verwenden, lassen Sie einfach alle Seiten außer den verwendeten gelöscht und verwenden Sie die Seiten im Kreis wieder.

  2. Wenn Sie nur einen Bruchteil einer Seite verwenden, der sofort gelöscht werden muss, teilen Sie diese Seite in Dateneinträge auf. Verwenden Sie jedes Mal einen sauberen Eintrag, wenn Sie schreiben, und löschen Sie ihn, wenn Ihnen die sauberen Einträge ausgehen.

Verwenden Sie bei Bedarf ein "dirty"-Bit, um zwischen sauberen und schmutzigen Einträgen zu unterscheiden (normalerweise haben Sie mindestens ein Byte, das sich garantiert von 0xFF unterscheidet, das zum Verfolgen von schmutzigen Einträgen verwendet werden kann).

Wenn Ihre EEPROM-Bibliothek die Löschfunktion nicht verfügbar macht (wie Arduino), ist hier ein netter Trick für Algorithmus Nr. 2: Da Ihr erster EEPROM-Eintrag immer verwendet wird, können Sie den Wert des "schmutzigen" Bits bestimmen, indem Sie ihn lesen. Sobald Ihnen die sauberen Einträge ausgehen, fangen Sie einfach wieder beim ersten Eintrag an, invertieren das "Dirty"-Bit, und der Rest Ihrer Einträge wird automatisch als "sauber" markiert.

Sequenznummern und Kataloge sind Platzverschwendung, es sei denn, Sie möchten fehlerhafte Seiten verfolgen oder verschiedene Teile Ihrer EEPROM-Daten unabhängig voneinander aktualisieren.

Ich habe dafür eine fortlaufende Sequenznummer verwendet (ähnlich wie Peters Antwort). Die Sequenznummer kann tatsächlich nur 1 Bit betragen, vorausgesetzt, die Anzahl der Elemente im Cue ist ungerade. Kopf und Schwanz werden dann durch die 2 aufeinanderfolgenden Einsen oder Nullen gekennzeichnet

Wenn Sie beispielsweise 5 Elemente durchlaufen möchten, wären die Sequenznummern:

{01010} (auf 0 schreiben) {11010} (auf 1 schreiben) {10010} (auf 2 schreiben) {10110} (auf 3 schreiben) {10100} (auf 4 schreiben) {10101} (auf 5 schreiben)

Ich glaube, es gibt eine einfache Möglichkeit, nur n Zellen und ein einziges dediziertes Bitmuster zu verwenden, um eine n/2-Erhöhung der Haltbarkeit zu erreichen.

Das dedizierte Bitmuster (z. B. 0xffff) ist ein Marker, der eine ungenutzte Zelle anzeigt, und im Normalbetrieb hat höchstens eine Zelle gleichzeitig andere Werte als diesen Markerwert:

uint16_t readWord (const uint16_t *address);
void writeWord (uint16_t *address, uint16_t value);

#define CELL_COUNT   42
#define BASE_ADDRESS 12
#define MARKER       0xffff  // Best to use EEPROM default for this

static uint8_t curCell = 0;

static void wlInit (void)
{
  for ( uint8_t ii = 0 ; ii < CELL_COUNT ; ii++ ) {
    if ( readWord (BASE_ADDRESS + ii) != MARKER ) {
      curCell = ii;
      // No early return/break here to keep init() execution time 
      // constant.  Could also check for corruption here by checking for
      // > 1 cells without MARKER (see below).
    }
  }
  // If we didn't find a non-marker cell we'll end up with curCell = 0 
  // per the initial value of curCell
}

static uint16_t wlRead (void)
{
  return readWord (BASE_ADDRESS + curCell);
}

static void wlWrite (uint16_t value)
{
  assert (value != MARKER);   // Storing marker value is big no-no

  uint8_t nextCell = (curCell + 1) % CELL_COUNT;
  writeWord (BASE_ADDRESS + nextCell, value);
  writeWord (BASE_ADDRESS + curCell, MARKER);
  curCell = nextCell;
}

Derselbe Ansatz kann mit strukturierten Daten verwendet werden, vorausgesetzt, dass ein Bitmuster reserviert werden kann (entweder in einem dedizierten Byte oder von einem anderen Feld, das ein Ersatzmuster hat).

Dieser Ansatz hat auch eine nützliche Eigenschaft in Bezug auf unterbrochene (z. B. Stromausfall) Schreibvorgänge: Wenn jemals zwei Zellen ohne den MARKER-Wert vorhanden sind, ist ein unvollständiger (dh beschädigter) Schreibvorgang aufgetreten. Dies erscheint sinnvoll, da z. B. das ATmega328P-Datenblatt den folgenden unklaren und nicht besonders beruhigenden Absatz über die Verwendung der Brown-Out-Erkennung zur Vermeidung von Korruption enthält:

Halten Sie AVR RESET aktiv (low) während Perioden mit unzureichender Versorgungsspannung. Dies kann durch Aktivieren des internen Brown-Out-Detektors (BOD) erfolgen. Wenn der Erkennungspegel des internen BOD nicht mit dem erforderlichen Erkennungspegel übereinstimmt, kann eine externe Schutzschaltung zum Zurücksetzen auf niedrige V CC verwendet werden. Wenn ein Reset auftritt, während ein Schreibvorgang abläuft, wird der Schreibvorgang abgeschlossen, vorausgesetzt, dass die Versorgungsspannung ausreichend ist.