[Tutorial] Umgang mit dem AVR EEPROM

Umgang mit dem AVR EEPROM in der Arduino Umgebung.
(meine Art damit umzugehen)

Gerne wird das EEPROM für irgendwelche Konfigurationswerte verwendet, welche man nicht im Flash unterbringen möchte. Und genau darum soll es hier gehen.

Doku zur Library: EEPROM
Bitte jedes einzelne Kapitel sorgfältig lesen, sowohl die Beispiele, als auch die Funktionsbeschreibungen.
Ich werde es nicht hier ins deutsche übersetzen.
Auch nicht weiter erläutern.
Aber darauf aufbauen.

Den Fokus möchte ich hier auf die "richtige" Adressierung legen.
Auf dem typischen AVR haben wir drei verschiedene Speicherbereiche.
Flash, der Programmspeicher (10.000 mal beschreibbar)
Ram, der Bereich für veränderliche Daten (ewig oft beschreibbar)
EEPROM, für persistente Daten (100.000 mal beschreibbar)

Auf Addressen in der Ram Section können wir direkt zu greifen.
Für die Flash und auch für die EEPROM Section müssen wir spezielle Zugriffsverfahren verwenden.

In den Library Beispielen wird die Adresse per Hand bestimmt. Das ist recht unglücklich, weil es besondere Aufmerksamkeit erfordert. Einerseits muss man das Verfahren genau verstanden haben, und andererseits ist es selbst dann unübersichtlich und fehlerträchtig.

Dabei kann man den Kompiler recht leicht dazu überreden, die Adressen selbstständig zu berechnen. Dazu muss man dem Kompiler sagen, dass er eine Variable in der EEPROM Section anlegen soll.
Das Vorgehen hat 2 Vorteile:

  1. Der Kompiler legt eine *.epp Datei an, welche man per ISP auf den AVR übertragen kann.
  2. Wir können die Variablen Bezeichner für die Adressierung nutzen.

Alternativ, kann man den Offset in einer Struktur berechnen lassen.

Im folgenden werde ich beide Varianten zeigen.

// -------

Als Beispiel verwende ich hier die interne Referenzspannung. Denn diese hat eine recht große Toleranz, somit macht es Sinn diesen Wert im EEPROM abzulegen. Auch OSCAL Tabellen und Kalibrierwerte für den internen Temperatursensor sind solche Kandidaten. Sowie IP Nummern usw.

Das Beispiel hat ein rudimentäres Menu.
Ein großes P in der seriellen Konsole eingeben, und es zeigt die aktuellen Werte im EEPROM. Beim ersten Lauf können/werden dort noch komische Werte angezeigt, falls man die *.eep Datei nicht aufgespielt hat.

Die Kommandos A und B schreiben dann die jeweiligen Defaultwerte in das EEPROM.

#include <EEPROM.h>


float referenzspannungImEeprom EEMEM = 1.1; // Arduino legt *.eep Datei an
float defaultRef = 1.1;

byte oscalImEeprom EEMEM = 128; // wird von Arduino in der *.eep angelegt
byte defaultOscal = 128;

void printEepromData()
{
  float ref = 0; 
  EEPROM.get((int)&referenzspannungImEeprom,ref); // wert aus dem EEPROM lesen
  Serial.print("Spannung: ");Serial.println(ref);
  
  byte oscal = 0; 
  EEPROM.get((int)&oscalImEeprom,oscal); // wert aus dem EEPROM lesen
  Serial.print("OSCAL: ");Serial.println(oscal);
}

void serialEvent() 
{
  while (Serial.available()) 
  {
    char zeichen = Serial.read();
    switch(zeichen)
    {
      case 'A':   EEPROM.put((int)&referenzspannungImEeprom,defaultRef); // defaultwert schreiben
                  Serial.println("Default Ref geschrieben");
                  break;
                  
      case 'B':   EEPROM.put((int)&oscalImEeprom,defaultOscal); // defaultwert schreiben
                  Serial.println("Default Oscal geschrieben");
                  break;
                  
      case 'P':   printEepromData();
                  break;
    }
  }
}  

void setup() 
{
  Serial.begin(9600);
  Serial.println();
  
  Serial.print("Adresse Ref: ");Serial.println((int)&referenzspannungImEeprom);
  Serial.print("Adresse Oscal: ");Serial.println((int)&oscalImEeprom);
  Serial.println();
    
  Serial.println("Menue:");
  Serial.println("A Default Referenz ins EEPROM schreiben");
  Serial.println("B Default OSCAL Wert ins EEPROM schreiben");
  Serial.println("P Print, zeige EEPROM Daten auf der Konsole");
  Serial.println();
}

void loop() 
{
}

Wie man sieht, verwende ich hier drei Repräsentationen der EEPROM Daten:

  1. Im Flash, die Default Repräsentation
  2. Im EEPROM die persistente Repräsentation
  3. Im RAM die volatile Repräsentation "Die Arbeitskopie"

Ein verwenden der *.eep Datei würde einem die Repräsentation der Default Daten im Flash ersparen.

// -------

Aber es geht auch, ohne Variablen im EEPROM anzulegen. Dazu müssen alle Varablen in einer Struktur abgelegt werden.

Hier noch mal das gleiche Beispiel, nach diesem Prinzp:

#include <EEPROM.h>


struct ConfData
{
  float ref;
  byte oscal;
};


ConfData defaultData = {1.1,128};

void printEepromData()
{
  float ref = 0; 
  EEPROM.get(offsetof(ConfData,ref),ref); // wert aus dem EEPROM lesen
  Serial.print("Spannung: ");Serial.println(ref);
  
  byte oscal = 0; 
  EEPROM.get(offsetof(ConfData,oscal),oscal); // wert aus dem EEPROM lesen
  Serial.print("OSCAL: ");Serial.println(oscal);
}

void serialEvent() 
{
  while (Serial.available()) 
  {
    char zeichen = Serial.read();
    switch(zeichen)
    {
      case 'A':   EEPROM.put(offsetof(ConfData,ref),defaultData.ref); // defaultwert schreiben
                  Serial.println("Default Ref geschrieben");
                  break;
                  
      case 'B':   EEPROM.put(offsetof(ConfData,oscal),defaultData.oscal); // defaultwert schreiben
                  Serial.println("Default Oscal geschrieben");
                  break;
                  
      case 'P':   printEepromData();
                  break;
    }
  }
}  

void setup() 
{
  Serial.begin(9600);
  Serial.println();
  
  Serial.print("Offset Ref: ");Serial.println(offsetof(ConfData,ref));
  Serial.print("Offset Oscal: ");Serial.println(offsetof(ConfData,oscal));
  Serial.println();
    
  Serial.println("Menue:");
  Serial.println("A Default Referenz ins EEPROM schreiben");
  Serial.println("B Default OSCAL Wert ins EEPROM schreiben");
  Serial.println("P Print, zeige EEPROM Daten auf der Konsole");
  Serial.println();
}

void loop() 
{
}

Hier wird jetzt nur eine leere *.eep Datei erzeugt.

// -------

Fallstricke:
Spannung und Takt müssen erhalten bleiben, sonst kann es beim Schreiben versagen.
BOD ist empfehlenswert. (Ist auch Arduino default)
EESAVE Fuse zeigt Wirkung

// -------

War das hilfreich?
Vorschläge für Erweiterungen?
Kritik?

1 Like

Fortsetzung:

Wie bekommen wir die gewünschten *.eep Daten ins EEPROM?

So sehr uns die Arduino IDE dabei hilft, die *.eep Datei zu erzeugen, sowenig ist sie dabei behilflich sie auf den µC zu schreiben.
Über den Bootloader geht es nicht.

Arduino trägt avrdude in sich. Damit, und einem ISP Programmer, geht das.

Die Kommandozeile wie man Avrdude aufruft, sieht man, wenn man die ausführlichen Meldungen in der IDE aktiviert und dann "Upload mit Programmer" drückt.
Das ist eine gute Vorlage für das eigene EEPROM Schreib Kommando.
Einfach das flash schreiben, durch das eeprom schreiben ersetzen.

Aus meiner persönlichen Sicht, ist die die IDE Funktion "Upload mit Programmer" sowieso problematisch, wenn nicht sogar defekt. Sie überschreibt den Bootloader. Und das, ohne dessen Ressourcen frei zu geben.
Die Fuses bleiben falsch stehen, der reservierte Flash Bereich kann nicht von der Anwendung genutzt werden.
Bei anderen Boarddefinitionen sieht das manchmal anders aus. Aber die Originalen Arduino Boarddefinitionen tragen das Problem in sich.

Mein Vorschlag ist:
Reparieren des Menupunktes "Upload mit Programmer"!

Hier gehe ich davon aus:

  1. Dass man einen der üblichen AVR Arduinos, oder einen der vielen kompatiblen verwendet.
  2. Dass man den Bootloader behalten möchte.
  3. Auch die *.eep Datei übertragen möchte

Die Anweisung, welche ausgeführt wird, wenn man auf "Upload mit Programmer" drückt findet sich in der jeweiligen platform.txt.
Den Arduino Entwicklern sei Dank, werden die nötigen Dateien "Anwendung mit Bootloader" und *.eep schon für uns erzeugt.
Sie finden sich im jeweiligen Build Ordner.

Es reicht eine platform.local.txt mit einer Zeile anzulegen, damit wir das Ziel erreichen.

tools.avrdude.program.pattern="{cmd.path}" "-C{config.path}" {program.verbose} {program.verify} -p{build.mcu} -c{protocol} {program.extra_params} "-Uflash:w:{build.path}/{build.project_name}.with_bootloader.hex:i" "-Ueeprom:w:{build.path}/{build.project_name}.eep:i"

Der original Eintrag in der platform.txt kommt nicht mehr zur Geltung, wenn ein gleich benannter Eintrag in der platform.local.txt existiert.

Wenn man die ausführlichen Meldungen beim kompilieren aktiviert, zeigt die IDE, welche Hardwaredefinition sie verwendet, und dort muss diese platform.local.txt angelegt werden.

Zur Klärung:
Hier wird nur die Funktion "Upload mit Programmer" geändert.
Der normale Upload ändert sich nicht.
Auch das "Bootloader brennen" bleibt unberührt erhalten.

2 Likes

combie:
Vorschläge für Erweiterungen?

An anderer Stelle hattest Du schon mal beschrieben, wie es mit eep-Datei geht. Das könntest Du doch hierher "kopieren", dann wäre das Thema vollständig.

Ich habe meinen kleinen Beitrag gelöscht, danke bitte gerne, dann kannst Du #1 für die Fortsetzung nutzen :slight_smile:

Im laufe der Zeit haben sich bei mir ein paar mehr Verfahren und Ansichten zum Umgang mit dem AVR EEPROM entwickelt.
Diese möchte ich hier mal so nach und nach vorstellen.
Verbesserungen? Erweiterungen? Her damit!

z.B. ein Äquivalent zum F() Makro
Das F() Makro dient dazu Zeichenketten ins Flash zu stopfen, um das RAM zu entlasten. Es macht also richtig Sinn, wenn es im RAM knapp wird.
Aber was ist zu tun, wenn es dann auch im Flash knapp wird?
Klaro: Einen Teil der Zeichenketten ins EEPROM auslagern!

Drum hier ein E() Makro, welches streng dem F() Makro nachempfunden ist. Also Zeichenketten im EEPROM bunkert.

Um mit Strings im Flash umzugehen gibts die incomplete __FlashStringHelper Klasse.
Hier wird ein Äquivalent dazu vorgestellt, die ebenso incomplete __EepromStringHelper Klasse.

Problem damit:
Print, also auch Serial.print() kann nicht mit __EepromStringHelper umgehen, ohne den Quellcode der Print Klasse zu ändern. Darum bleibt die Verwendung von __EepromStringHelper dem Streaming vorbehalten, denn dafür lässt sich der nötige << Operator jederzeit hinzufügen.

#include <Streaming.h> // die Lib findest du selber ;-)
Print &cout = Serial; // cout Emulation für "Arme"

#include <EEPROM.h> 

using FString = const __FlashStringHelper *; 

class __EepromStringHelper ; // incompete class definition 
using EString = __EepromStringHelper *; 

#define ESTR(s) (__extension__({static char __c[] EEMEM = (s); &__c[0];}))
#define E(string_literal) (reinterpret_cast<EString>(ESTR(string_literal)))

Print &operator <<(Print &p, EString estr)
{ 
  int i = reinterpret_cast<int>(estr);
  char c; while((c = EEPROM[i++])) p.print(c); // C++11
  //while(char c = EEPROM[i++]) p.print(c); // ab C++17 moeglich
  return p;
}








char testA[] EEMEM = "Drei rote Rueben.";
char testB[] EEMEM = "Zwo rote Rueben.";
char testC[] EEMEM = "19 rote Rueben.";
const char testP[] PROGMEM = "Ein Frosch.";



void setup() 
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;
  
  cout << E("Dieses ist ein Beispieltext im EEPROM")  << endl
       << E("Und dieser auch")                        << endl
       << E("Ein dritter darf nicht Fehlen.")         << endl
       << F("Dieses ist ein Beispieltext im Flash")   << endl; 
  
  cout << EString(testA) << endl
       << EString(testB) << endl
       << EString(testC) << endl
       << FString(testP) << endl;
  
  cout << endl;
  
}

void loop() 
{

}
2 Likes

Nach den Strings in der vorherigen Folge, möchte ich mich hier mal um einzelne Bytes kümmern.
Am Beispiel eines Reset Zählers. So ein Ding kann ja mal interessant sein, um die Anzahl Stromausfälle zu beobachten, oder WDT Resets.
Der Fokus liegt natürlich wieder auf dem EEPROM.

Zuerst mal anhand des, ich nenne ihn hier mal so, EEPROM Array Access Operators.

#include <EEPROM.h>
#include <Streaming.h>

/**
 * Simpler 8 Bit breiter Reset Counter 
 */

// Counter vorbesetzen, mit Eintag in *.eep Datei
byte resetCounter EEMEM {0};


void serialEvent()
{
  if('R' == Serial.read())
  {
    EEPROM[int(&resetCounter)] = 0;
    Serial << F("ResetCounter auf Null gesetzt ") << endl;
  }
}


void setup() 
{
 Serial.begin(9600);
 Serial << F("Start") << endl;
 Serial << F("Gib R ein, um den Counter zurueck zu setzen") << endl;

 
 EEPROM[int(&resetCounter)]++;

 Serial << F("ResetCounter: ")<< EEPROM[int(&resetCounter)] << endl;
 
}

void loop() 
{

}

Das ist doch schon recht praktisch. Man kann also das gesamte EEPROM wie ein Array von Bytes behandeln. Wie das intern abläuft, dazu später mehr.

Was uns die originale EEPROM Klasse auch noch bietet, sind Iteratoren. Hier zeige ich mal wie man damit das gesamte EEPROM ausgibt. Zugegeben, nicht sonderlich wichtig, aber doch schon für Diagnose Zwecke interessant und auch ausbaufähig.

#include <EEPROM.h>
#include <Streaming.h>

void setup() 
{
 Serial.begin(9600);
 Serial << F("Start") << endl;

 for(byte b:EEPROM) Serial << b << endl;
}

void loop() {}

Jetzt zum etwas spannenderem, zu Referenzen ins EEPROM.
Denn zusätzlich finden wir in der EEPROM Lib solche Referenzen auf Bytes. Also kann man sie auch nutzen.

#include <EEPROM.h>
#include <Streaming.h>


// Counter vorbesetzen, mit Eintag in *.eep Datei
byte resetCounter EEMEM {0};



void setup() 
{
 Serial.begin(9600);
 Serial << F("Start") << endl;

 // Referenz auf 1 Byte erstellen
 ERef resetCounterRef = int(&resetCounter);
// EERef resetCounterRef = EEPROM[int(&resetCounter)]; // alternativ


 // inhalt zeigen
 Serial << resetCounterRef << endl;
 
 resetCounterRef++; // inhalt erhöhen
 
 // neuen inhalt zeigen
 Serial << resetCounterRef << endl;
}

void loop() {}

Diese Referenz erlaubt uns Bytevariablen im EEPROM so zu nutzen, als würden sie im RAM liegen.
Leider beglückt uns der Kompiler mit eine Warnung:

warning: 'EEPROM' defined but not used [-Wunused-variable]

Damit hat er vollkommen recht, muss uns aber nicht jucken, denn schließlich benutzen wir hier die EEPROM Instanz wirklich/absichtlich nicht.

So!
Das soll für dieses Kapitel reichen...

Referenzen auf Bytes und überhaupt Byte Zugriffe sind ja schon nett und wichtig.
Aber die Welt besteht doch aus viel mehr Datentypen.
Also hier jetzt Referenzen ins EEPROM für (fast) beliebige Datentypen.

#include <EEPROM.h> 
#include <Streaming.h> // die Lib findest du selber ;-)
Print &cout = Serial; // cout Emulation für "Arme"

template<typename T> class EepRef
{
  private:
  T &value;
  
  public:
  EepRef( T &value):value(value){}
  T operator=(T v)
  {
    EEPROM.put(int(&value),v);
    return v;
  }
  
  operator T() const
  {
    T temp;
    EEPROM.get(int(&value),temp);
    return temp;
  }
};
 

// Variable im EEPROM anlegen 
// und in *.eep Datei vorbesetzen
unsigned resetCounterEep EEMEM {0};

// eine Referenz auf den resetCounter im EEPROM erstellen
EepRef<decltype(resetCounterEep)> resetCounter {resetCounterEep};


void setup() 
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;

 // inhalt zeigen
 Serial << resetCounter << endl;
 
 resetCounter = resetCounter +1; // inhalt erhöhen
 
 // neuen inhalt zeigen
 Serial << resetCounter << endl;

}

void loop() 
{

}

Mögliche Verbesserungen:

  1. Für numerische Datentypen die nötigen Operatoren erstellen, wie es auch EERef tut. z.B. ++
  2. Die Erstellung der Variablen und der Referenz zusammen fassen

Punkt 2 kann man recht einfach mit Hilfe eines Makros erledigen.

#include <EEPROM.h> 
#include <Streaming.h> // die Lib findest du selber ;-)
Print &cout = Serial; // cout Emulation für "Arme"

template<typename T> class EepRef
{
  private:
  T &value;
  
  public:
  EepRef( T &value):value(value){}
  T operator=(T v)
  {
    EEPROM.put(int(&value),v);
    return v;
  }
  
  operator T() const
  {
    T temp;
    EEPROM.get(int(&value),temp);
    return temp;
  }
};
 

#define EEPDATA(datentype,bezeichner,defaultvalue) \
datentype bezeichner##eep EEMEM {defaultvalue};    \
EepRef<datentype> bezeichner {bezeichner##eep};



// Variable im EEPROM anlegen 
// und in *.eep Datei vorbesetzen
// eine Referenz auf den resetCounter im EEPROM erstellen
EEPDATA(uint32_t,resetCounter,0);

void setup() 
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;

 // inhalt zeigen
 Serial << resetCounter << endl;
 
 resetCounter = resetCounter + 1; // inhalt erhöhen
 
 // neuen inhalt zeigen
 Serial << resetCounter << endl;

}

void loop() 
{

}

Das funktioniert auch mit Strukturen, wenn man eine lokale Instanz der Struktur nutzt:

#include <EEPROM.h> 
#include <Streaming.h> // die Lib findest du selber ;-)
Print &cout = Serial; // cout Emulation für "Arme"

template<typename T> class EepRef
{
  private:
  T &value;
  
  public:
  EepRef( T &value):value(value){}
  T operator=(T v)
  {
    EEPROM.put(int(&value),v);
    return v;
  }
  
  operator T() const
  {
    T temp;
    EEPROM.get(int(&value),temp);
    return temp;
  }
};
 
#define EEPDATA(datentype,bezeichner,defaultvalue) \
datentype bezeichner##eep EEMEM {defaultvalue};    \
EepRef<datentype> bezeichner {bezeichner##eep};

struct Daten
{
   int  a;
   byte b;
   long c;
};

EEPDATA(double,testVar,3.14);
EEPDATA(int,testVar3,77);
EEPDATA(Daten,data,Daten(1,2,3));


void setup() 
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;

  // lokale Instanz erzeugen 
  // und aus dem EEPROM initialisieren
  Daten data = ::data;

  cout << F("data.a:   ") << data.a << endl; 
  data.a += 12; // daten manipulieren

  // und hier die Struktur wieder ins EEPROM schreiben
  ::data = data;


  cout << F("testVar:   ") << testVar << endl;

  testVar = 42.44;
  
  cout << F("testVar:   ") << testVar << endl;
  
}

void loop() 
{

}

Ich hoffe, damit erstmal alle Klarheiten beseitigt zu haben!
Fragen?