Wozu gibt es pages beim EEPROM? Und muss ich das bei Wear Leveling berücksichtigen?

Moin zusammen

ich bin vor kurzem über ein Kommentar von @combie gestolpert (Kommentar), wonach pages beim EEPROM im Zusammenhang mit Wear Leveling irgendwie wichtig zu sein schein.
Für meinen Projekt verwende ich ein Arduino Nano Every mit dem ATmega4809. Im EEPROM möchte zwei Parametersätze abspeichern, für die ich jeweils ein struct angelegt habe. Die beiden sehen so aus:

// Lighting Data including FactorySettings
// Use structs for data stored in EEPROM
struct EESettings {                                     // Will be changed seldom. One address only
  int  colorTMinLED;                                    // lowest  colortemperature of used LED
  int  colorTMaxLED;                                    // hightest  
  byte lmCWLED;                                         // lm/W of cold white LED
  byte lmWWLED;                                         //         warm
  int  colorTMinHCL;                                    // lowest colortemperature in HCL mode
  int  colorTMaxHCL;                                    // hightest
  int  hclTsDelta;                                      // Time Shift, Starting/Min Time, in min
  int  hclTeDelta;                                      //                  End/Max Time
  byte fadingDelay;                                     // Fading HCL <-> Man
  byte powerUpDelay;                                    // Delay for LightOn after PowerON
  byte powerDnDelay;                                    //           LightOff      PowerOFF
  EESettings() {                                        // === FACTORY SETTINGS by constructor ===
    colorTMinLED  = 2700;                               // lowest  colortemperature of used LED
    colorTMaxLED  = 6500;                               // hightest  
    lmCWLED       =  100;                               // lm/W of cold white LED
    lmWWLED       =   90;                               //         warm
    colorTMinHCL  = 2700;                               // lowest colortemperature in HCL mode
    colorTMaxHCL  = 4000;                               // hightest
    hclTsDelta    =  -30;                               // Time Shift, Starting/Min Time, in min
    hclTeDelta    =  105;                               //                  End/Max Time
    fadingDelay   =    6;                               // Fading HCL <-> Man,             ,  500ms
    powerUpDelay  =   17;                               // Delay for LightOn after PowerON , 2500ms 
    powerDnDelay  =   17;                               //           LightOff      PowerOFF, 2500ms
  }
};
struct EERotEncoder {                                   // Will be saved at any power-off
  unsigned long counter = 0;                            // Counter to find latest saved data
  byte rdBrHCLLast;                                     // Absolute Position HCL - Bright
  byte rdCtHCLLast;                                     //                       - ColorT
  byte rdBrManLast;                                     // Absolute Position Man - Bright
  byte rdCtManLast;                                     //                       - ColorT
  EERotEncoder () {                                     // === FACTORY SETTINGS by constructor ===
    counter = 0;                                        // Default is no EEPROM savings yet
    rdBrHCLLast = 128;                                  // HCL: 50% brightness
    rdCtHCLLast = 128;                                  //      colorT at noon = half of HCL range
    rdBrManLast = 128;                                  // Man: 50% brightness
    rdCtManLast = 0;                                    //      lowest colorT
  }
};

Für beide wird nur eine Variable angelegt:

// <General Paramter> are always as adr = 0. Assumed to change seldom (far below 100.000x). They
//                    can be updated at any time because only updated(!) values are actual stored.
// <Rotary Encoder>   are stored in remaining EEPROM using wear leveling technique. Data is put
//                    into a ring buffer, so that ~3Mio writings are obtained in 256byte EEPROM.
//                    It is deemed as sufficient from user-perpective to store in EEPROM at when
//                    light is switched off. It will then properly initialised after loss of 
//                    electrical power
  EESettings eeSettings;
  EERotEncoder eeRotEncoder;                      // Rotoray Encoder Data
  const byte sizeEESettings          = sizeof(eeSettings);    // Size of one Dataset - GeneralPara
  const byte sizeEERotEncoder        = sizeof(EERotEncoder);  //                     - RotEncoder
  const unsigned int eeRotEncAdr0    = sizeEESettings;    // Adr for storing encoder positions: Offset
  unsigned int eeRotEncAdrActual     = eeRotEncAdr0;      //                                    Actual
  unsigned int eeRotEncBufActual     = 1;                 // [1..eeRotEncBufSize]
  const unsigned int eeRotEncBufSize = (EEPROM.length() - sizeEESettings) / sizeEERotEncoder;

Die Daten in eeSettings ändern sich nur selten. Wear Leveling ist nicht notwendig. Anders bei eeERotEncoder, weswegen es den Zähler eeERotEncoder.counter gibt. Er wird beim jedem Schreiben um eins erhöht und bei der Programminitialisierung wird der zuletzt abgespeicherte Wert eben über den höchsten Zählerstand gefunden.

Wie sich (hoffentlich) erahnen lässt, liegt eeSettings bei der Adresse Null und der allererste EIntrag von eeERotEncoder folgt unmittelbar danach.
Im weiteren wird gemäß der Größe von eeERotEncoder als auch dem EEPROM bei jedem Speichervorgang die Adresse beim nächsten Schreibvorgang erhöht, bis das "Ende" vom EEPROM erreicht wurde, und das Spiel beginnt wieder bei eeRotEncAdr0. Für das Schreiben verwende dich die Methode put() aus der EEPROM.h, also, im Fall von eeERotEncoder:

OpError ShutDownPowerStage() {
  OpError opErr = OpError::None;                          // Assume as Default
  // (1) Collect required inputs
  // (none)
  // (2) Recovery
  // Copy latest Last-Values to rotary encoder related data struct
  // EncoderData (now stored in eeRotEncoder)
    eeRotEncoder.counter++;                               // Increase counter to mark latest entry
    eeRotEncoder.rdBrHCLLast = rdBrHCLLast;               // HCL - Brightness
    eeRotEncoder.rdCtHCLLast = rdCtHCLLast;               //     - color temperature at noon
    eeRotEncoder.rdBrManLast = rdBrManLast;               // Man - Brightness
    eeRotEncoder.rdCtManLast = rdCtManLast;               //     - color temperature
  // Calculate next address for EEPROM write at shutoff (for rotary encoder), and write EEPROM
    if (eeRotEncBufActual == eeRotEncBufSize) eeRotEncBufActual = 1;
    else                                      eeRotEncBufActual++;
    eeRotEncAdrActual = eeRotEncAdr0 + (eeRotEncBufActual-1) * sizeEERotEncoder;
    EEPROM.put(eeRotEncAdrActual, eeRotEncoder);          // Store in EEPROM at "soft turn-off"
  // (3) Actual values
  // (none)
  // (4) Send Command: CMD-Byte = Light Off, Power Off, Off Delay, lowest colorT [K], zero brightness
    (void) SetPowerCMD(powerCMD, 0b00000000, colorTMinLED, 0, powerDnDelay);
    if (!SendCMD(powerCMD)) {                             // Send CMD by default, but may be blocked
      opErr = OpError::I2C;                               // I2C Error in case it was not successful
    }
  // (5) Manage OLED Display. 
    runDisplay(DisplayMode::Off);
  return(opErr);
}

Soweit so gut, mein Code funktioniert. Nur, habe ich damit auch ein Wear Leveling implementiert? Wie sich im Code erkennen läßt, verwende ich nirgends die Page-Größe es eingesetzten ATmega4809 vom Arduino Board.
Der ATmega4809 hat ein 256Byte großes EEPROM, die page ist 64Byte groß.

Ich Datenblatt des ATmega4809 konnte ich folgendes finden:
9.3.1.2 EEPROM: The EEPROM is divided into a set of pages where one page consists of multiple bytes. The EEPROM has byte granularity on the erase/write. Within one page, only the bytes marked to be updated will be erased/written. The byte is marked by writing a new value to the page buffer for that address location.

Dort steht: The EEPROM has byte granularity on the erase/write - was mich glauben lässt, pages sind egal... Also mein Code schont in der Tat das EEPROM. Und ich kann auch eeSettings und eeERotEncoder auf der ersten page abspeichern, ohne das beim mehrfachen Schreiben von eeERotEncoder an verschieden Adressen der ersten Page die Daten von eeSettings irgendwie mitmanipuliert werden.

Oder hätte ich doch pages berücksichtigen müssen. Und falls ja, wie?

Mein dortiger Beitrag bezog sich auf die EEPROM Emulation auf dem ESP8266.
Denn dieser emuliert das EEPROM im Flash.

Dabei gilt:
Flash wird Pageweise, schlagartig gelöscht, als wenn da ein Blitz einschlägt. Daher stammt auch der Name: Flash.
Drum muss man sich beim Flash Wear Leveling auch auf die Pages konzentrieren. Die einzelnen Bytes spielen da eher keine Rolle.

EEPROM sind dagegen Zellenorientiert. Also andere Bedingungen, als beim Flash.
Ja, auch viele EEPROM funktionieren mit Pages, aber das sind nur Schreib-Lese Adressbuffer, sind für das Prinzip, des Wear Leveling, recht uninteressant.

Zu deinem ATmega4809 kann ich nicht viel sagen, .....

Danke für die Klarstellung

Ok. Also kann ich die Pages beiseite legen. Und meinen Code so lassen....

Pages sind bei EEPROMs schon zu beachten.

Wenn man Page Write benutzt, also einmal die Anfangsadresse übermitteln und dann mehrere Byte Daten schickt, dann muß man die Pages beachten.
Obwohl eine Page zB 32 Bit groß ist, also theoretisch 32 Bytes geschrieben werden könnten, ist die Anfangsadresse zu beachten. Wenn die Anfangsadresse nicht mit der Startadresse der Page übereinstimmt so werden die zusätzlichen Bytes nicht in die nächste Page geschrieben, sondern an den Anfang der aktuellen Page und somit der Inhalt der Page komprimitiert.

Grüße Uwe

Natürlich, ist die interne Struktur beim lesen und schreiben zu beachten.

  1. das hat nichts mit dem wear leveling zu tun
  2. erledigen das die genutzten Libs gleich mit. (sollten sie zumindest)

Hier ist ein interessantes Dokument zum Thema wear leveling auf AVR EEPROMs:

Ich wollte nur zu bedenken geben, daß Pages auch abseits von wear leveling Probleme geben können.

Grüße Uwe

Reingeschaut, danke. Ok, ein Ringbuffer für die zuletzt genutzte Adresse, um damit, so verstehe ich das, eine Grundlage zu schaffen ohne allzu schlimm Speicher im EEPROM für Wear Leveling zu verbrauchen.
Bei mir hingegen ist der Nutzungsgrad denkbar schlecht: eine unsigned long für das Wear Leveling je 4 Byte Nutzdaten, also nur 50%. Vielleicht nicht supertoll, reicht aber für meine Anwendung (in meinem Fall ergeben sich etwa 3Mio Schreibzyklen, was, abgeschätzt aus der Nutzung des Gerätes, für mehrere Jahrzehnte reicht)

Äh, ja, das war ein bisschen meine Frage: Wie denn? Bei meinen Tests konnte ich nichts feststellen. Ich verwende die Arduino-EEPROM-Lib; wenn die das nicht ohnehin schon machen, dann wie soll ich das machen? Also wie @combie schreibt:

Es gibt eine Byte-weises Speichern wo man die Adresse und 1 Byte angibt.
Es gibt Page-Speichern wo man die Adresse angibt und dann mehrere Byte Daten nachschickt.
Grüße Uwe

I2C EEPROM?

Ich habe das so verstanden, dass wir beim EEPROM eines AVRs sind.

Ist das bei dem auch so, wie bei den üblichen I2C Dingern?

Nachtrag, habe es gerade mal nachgelesen..... ca ab Seite 74 wirds spannend.
Ist schon interessant!
Ganz anders als bei den 328, oder so...

Aus dem Datasheet hat ich obiges Zitat gefischt, wonach Bytes im EEPROM individuell geschrieben werden können. Aber gerne nochmal reingeschaut.

  • gleicher Controller für Flash und EEPROM, und noch anderen Kram

  • Flash kann nur page-weise beschrieben werden

  • EEPROM kann auch page-weise beschrieben werden, wobei hierzu die komplette page auch mit Werten gefüllt werden müsste. Es werden nämliche nur die Bytes von dem Buffer des (internen) Nonvolatile Memory Controllers (NVMCTRL) ins EEPROM geschrieben, die eben zuvor im Buffer abgelegt wurden (automatisches marking). D.h. wird nur z. B. nur ein Teil der page beschrieben, dann wird auch nur diese in EEPROM übertragen

  • Es sieht mir danach aus, als ob mit der NVMCTRL ein pageweises Löschen des EEPROM gar nicht unterstützt (ausser man beschreibt zu Fuss die ganze Page)

Außerdem hat mir das Internet verraten, dass wohl ein (Bit) ändern von 1->0 ohne Verschleiß funktioniert, wohingegen 0->1 verschleißbehaftet ist. Was irgendwie erklärt, warum EEPROMs mit 0xFF intialisiert sind. Ansonsten wohl eher theoretischer Natur, es hilft beim ersten Schreiben...

Außerdem habe ich mal ein Blick in die EEPROM.h geworfen (sind bei avr und megaavr bis auf die Methode length() identisch), muss aber zugegeben, dass ich gar nicht so viele weiße Fahnen habe, wie ich beim Betrachten des Codes schwenken möchte.... ich verstehe gar nix.
Damit ihr nicht selber suchen müßt einnmal die EEPROM.h - hilft das?

/*
  EEPROM.h - EEPROM library
  Original Copyright (c) 2006 David A. Mellis.  All right reserved.
  New version by Christopher Andrews 2015.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#ifndef EEPROM_h
#define EEPROM_h

#include <inttypes.h>
#include <avr/eeprom.h>
#include <avr/io.h>

/***
    EERef class.
    
    This object references an EEPROM cell.
    Its purpose is to mimic a typical byte of RAM, however its storage is the EEPROM.
    This class has an overhead of two bytes, similar to storing a pointer to an EEPROM cell.
***/

struct EERef{

    EERef( const int index )
        : index( index )                 {}
    
    //Access/read members.
    uint8_t operator*() const            { return eeprom_read_byte( (uint8_t*) index ); }
    operator uint8_t() const             { return **this; }
    
    //Assignment/write members.
    EERef &operator=( const EERef &ref ) { return *this = *ref; }
    EERef &operator=( uint8_t in )       { return eeprom_write_byte( (uint8_t*) index, in ), *this;  }
    EERef &operator +=( uint8_t in )     { return *this = **this + in; }
    EERef &operator -=( uint8_t in )     { return *this = **this - in; }
    EERef &operator *=( uint8_t in )     { return *this = **this * in; }
    EERef &operator /=( uint8_t in )     { return *this = **this / in; }
    EERef &operator ^=( uint8_t in )     { return *this = **this ^ in; }
    EERef &operator %=( uint8_t in )     { return *this = **this % in; }
    EERef &operator &=( uint8_t in )     { return *this = **this & in; }
    EERef &operator |=( uint8_t in )     { return *this = **this | in; }
    EERef &operator <<=( uint8_t in )    { return *this = **this << in; }
    EERef &operator >>=( uint8_t in )    { return *this = **this >> in; }
    
    EERef &update( uint8_t in )          { return  in != *this ? *this = in : *this; }
    
    /** Prefix increment/decrement **/
    EERef& operator++()                  { return *this += 1; }
    EERef& operator--()                  { return *this -= 1; }
    
    /** Postfix increment/decrement **/
    uint8_t operator++ (int){ 
        uint8_t ret = **this;
        return ++(*this), ret;
    }

    uint8_t operator-- (int){ 
        uint8_t ret = **this;
        return --(*this), ret;
    }
    
    int index; //Index of current EEPROM cell.
};

/***
    EEPtr class.
    
    This object is a bidirectional pointer to EEPROM cells represented by EERef objects.
    Just like a normal pointer type, this can be dereferenced and repositioned using 
    increment/decrement operators.
***/

struct EEPtr{

    EEPtr( const int index )
        : index( index )                {}
        
    operator int() const                { return index; }
    EEPtr &operator=( int in )          { return index = in, *this; }
    
    //Iterator functionality.
    bool operator!=( const EEPtr &ptr ) { return index != ptr.index; }
    EERef operator*()                   { return index; }
    
    /** Prefix & Postfix increment/decrement **/
    EEPtr& operator++()                 { return ++index, *this; }
    EEPtr& operator--()                 { return --index, *this; }
    EEPtr operator++ (int)              { return index++; }
    EEPtr operator-- (int)              { return index--; }

    int index; //Index of current EEPROM cell.
};

/***
    EEPROMClass class.
    
    This object represents the entire EEPROM space.
    It wraps the functionality of EEPtr and EERef into a basic interface.
    This class is also 100% backwards compatible with earlier Arduino core releases.
***/

struct EEPROMClass{

    //Basic user access methods.
    EERef operator[]( const int idx )    { return idx; }
    uint8_t read( int idx )              { return EERef( idx ); }
    void write( int idx, uint8_t val )   { (EERef( idx )) = val; }
    void update( int idx, uint8_t val )  { EERef( idx ).update( val ); }
    
    //STL and C++11 iteration capability.
    EEPtr begin()                        { return 0x00; }
    EEPtr end()                          { return length(); } //Standards requires this to be the item after the last valid entry. The returned pointer is invalid.
    uint16_t length()                    { return EEPROM_SIZE; }
    
    //Functionality to 'get' and 'put' objects to and from EEPROM.
    template< typename T > T &get( int idx, T &t ){
        EEPtr e = idx;
        uint8_t *ptr = (uint8_t*) &t;
        for( int count = sizeof(T) ; count ; --count, ++e )  *ptr++ = *e;
        return t;
    }
    
    template< typename T > const T &put( int idx, const T &t ){
        EEPtr e = idx;
        const uint8_t *ptr = (const uint8_t*) &t;
        for( int count = sizeof(T) ; count ; --count, ++e )  (*e).update( *ptr++ );
        return t;
    }
};

static EEPROMClass EEPROM;
#endif

Von der Methode put() wird behauptet, dass sie nur dann ins EEPROM schreibt, wenn der Wert tatsächlich neu ist (und sich hierzu der Methode update() bedient). Wie gesagt, ich verstehe gar nix, da ich noch nicht mal den dazu notwendigen Vergleich von gespeicherten zu neuem Wert im Code finde ...

Auch andere Libraries zum WearLeveling scheinen die page size nicht auszuwerten (etwa die von Peter Rosenberg https://github.com/PRosenb/EEPROMWearLevel) ...

Keine Ahnung. Das ist hier alles mein erstes µController-Projekt überhaupt. Ich habe zwar Erfahrung in der Programmierung von Steuergeräten (im Automobilbau), ist aber auch schon lange her, und die Low-Level-Funktionen haben wir damals nicht gemacht. Kurz: ich verwende das interne EEPROM, weil es da ist und von der Größe reicht.

Ja, die greifen da schon recht tief in die C++ Trickkiste.
Die zum Durchblick erforderlichen Grundlagen findet man in jedem etwas besserem modernen dicken C++ Buch.
Auch hier: cppreference.com

Gegen suchen hilft das.
Gegen was, soll das denn überhaupt helfen?

Gegen dein Page Problem?

Die EEPROM.h verwendet avr-libc: <avr/eeprom.h>: EEPROM handling

Ja. Bzw. der Frage, ob ich überhaupt eines habe.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.