EEPROM Teil 1 – AVR interne EEPROMs

Über den Beitrag

Entwicklungsboards auf Basis von AVR Mikrocontrollern, wie der Arduino UNO, MEGA2560, Nano, Pro Mini und einige andere, besitzen einen kleinen EEPROM (Electrically Erasable Programmable Read-Only Memory). Der EEPROM ist ideal zum Speichern kleiner Datenmengen, die der Mikrocontroller auch nach dem Ausschalten nicht vergessen soll.

Auch die ESP32 und ESP8266 Boards verfügen über Festspeicher, die sich mit der EEPROM Bibliothek ansteuern lassen.

Wer meinen Beitrag über die HX711 basierte Waage gelesen hat, erinnert sich vielleicht, dass dort die Kalibrierwerte auf dem EEPROM gespeichert werden konnten. Das ist wesentlich bequemer, als sie manuell in den Programmcode zu übertragen.

In diesem Beitrag werde ich zeigen, mit welchen Funktionen ihr den EEPROM beschreibt und auslest. Das ist für Datentypen mit definierter Größe, wie Byte, Integer oder Float ausgesprochen simpel. Ein wenig anspruchsvoller ist der Umgang mit Zeichenketten wie Strings oder Character Arrays. Deswegen gehe ich darauf besonders intensiv ein.

Der Beitrag ist wie folgt strukturiert:

Einführung – was ist ein EEPROM?

Kurze Entwicklungsgeschichte des EEPROM

Relikt aus einer anderen Zeit: ein EPROM
Relikt aus einer anderen Zeit: ein EPROM

Die Entwicklungsgeschichte des EEPROMs steckt in seinem Namen. Die Evolution verlief folgendermaßen:

  • ROMRead Only Memory: diese Art von Speicher ist unveränderlich. Der Speicherinhalt wird bei der Fertigung festgelegt.
  • PROMProgrammable Read Only Memory: dieser Speicher kann ein einziges Mal programmiert werden.
  • EPROMErasable Programmable Read Only Memory: mit dieser Weiterentwicklung konnte der ROM auch wieder gelöscht werden, z.B. mit UV-Licht.
  • EEPROMElectrically Erasable Programmable Read Only Memory: diese modernste Variante ist elektrisch löschbar, also auch per Programm.

Flash-Speicher, wie sie beispielsweise in SD-Karten zum Einsatz kommen, basieren auch auf der EEPROM Technologie. Ein wesentlicher Unterschied ist, dass der EEPROM byteweise beschrieben und gelöscht werden kann, während das auf dem Flash-Speicher sektorenweise geschieht. Der Flash-Speicher ist unter anderem deshalb wesentlich schneller.

Eigenschaften von EEPROMs

EEPROMs kommen zum Einsatz, wenn kleine Datenmengen permanent gespeichert werden sollen und keinen häufigen Änderungen unterliegen. Ihr Nachteil ist eine begrenzte Zahl von Schreibvorgängen. Hersteller garantieren zwischen 10.000 und 1.000.000 Schreibzyklen. Überdies ist der Schreibvorgang ausgesprochen langsam – dazu kommen wir noch. 

Der EEPROM Speicher im Arduino

Der EEPROM Speicher ist Bestandteil des zugrundeliegenden Mikrocontrollers. Der Arduino Mega 2560 basiert auf dem ATmega2560, der einen EEPROM von 4 kB besitzt. Die auf dem ATmega328P basierenden Boards, wie beispielsweise der Arduino UNO, haben einen 1 kB EEPROM. Bei Arduinos, die mit unterschiedlichen Mikrocontrollern erhältlich sind, wie der Arduino Pro Mini (Atmega328P oder Atmega168) müsst ihr genauer hinschauen. Die Lebensdauer beträgt 100.000 Schreibzyklen.

Die Funktionen der EEPROM Bibliothek

put(), get() und length()

Wir starten mit einer ganz einfachen Aufgabe. Drei Integer-Werte sollen erst auf den EEPROM geschrieben, dann ausgelesen und schließlich auf dem seriellen Monitor ausgegeben werden.

Zunächst müssen wir dazu die EEPROM Bibliothek, die zur Standardausstattung der Arduino IDE für die AVR Boards gehört, einbinden. Es ist nicht nötig, dass wir ein EEPROM Objekt erzeugen. Allerdings ist es beim ESP32 und dem ESP8266 notwendig, den EEPROM mit EEPROM.begin(size in byte); zu initialisieren. Ich schreibe das nur in den ersten Beispielsketch. Bei den anderen Sketchen müsst die Zeile selbst hinzufügen.

Die Funktion EEPROM.put(addr, var) schreibt die Variable var auf den EEPROM. Sie beginnt damit an der Adresse addr. Im Beispielsketch ist das die Adresse 0. Da ein Integer zwei Bytes umfasst, werden die Speicherplätze 0 und 1 belegt. Den nächsten Integer-Wert legen wir an der Adresse 2 ab und belegen dadurch die Speicherplätze 2 und 3. Ihr müsst beim Beschreiben also „mitzählen“.

Falls ihr euch nicht sicher seid, wie viele Bytes eine Variable belegt, könnt ihr das mit der Funktion sizeof(var) ermitteln.

Der put()-Funktion ist es egal, welche Adresse ihr wählt. Ihr könntet auch bei 357 beginnen. Und ob ein Speicherplatz belegt ist oder nicht, interessiert die Funktion nicht. Wenn ihr beispielsweise den zweiten Integer auf den Speicherplätzen 1 und 2 ablegen würdet, gäbe es keine Beschwerde. Der erste Integer-Wert wäre futsch.

Die Funktion EEPROM.get(addr, var) liest den EEPROM an der Adresse addr aus. Den Inhalt speichert sie in der Variable var. Der Variablentyp bestimmt, wie viele Speicherplätze die Funktion liest. 

Die Funktion EEPROM.length() liefert die Größe des EEPROM Speichers zurück.

Hier der Sketch:

#include <EEPROM.h>
void setup(){
  Serial.begin(9600);
//  EEPROM.begin(1024); // uncomment if you use an ESP32 or ESP8266
//  delay(1000); 
  int int0 = 42, int1 = 9999, int2 = -13756;
  int readInt = 0;      
  Serial.print("Size of EEPROM: ");
  Serial.println(EEPROM.length());

  unsigned int address = 0;
  unsigned int increment = (sizeof(int));
  Serial.print("increment: ");
  Serial.println(increment);
  
  // Write:
  EEPROM.put(address, int0);
  address += increment;
  EEPROM.put(address, int1);
  address += increment;
  EEPROM.put(address, int2);

  // Read:
  address = 0;
  EEPROM.get(address, readInt);
  Serial.print("1. Integer: ");
  Serial.println(readInt);

  address += increment;
  EEPROM.get(address, readInt);
  Serial.print("2. Integer: ");
  Serial.println(readInt);

  address += increment;
  EEPROM.get(address, readInt);
  Serial.print("3. Integer: ");
  Serial.println(readInt);
}

void loop(){}

 

Und hier die Ausgabe:

Ausgabe von eeprom_basic.ino
Ausgabe von eeprom_basic.ino

write(), read(), update() und EEPROM[]

Alternativ beschreibt ihr den EEPROM mit der Funktion EEPROM.write(addr, var). Dabei ist var allerdings zwingender Maßen eine Variable vom Typ byte. Um mit dieser Funktion einen Integer-Wert zu speichern, müsstet ihr ihn zunächst in seine zwei Bytes zerlegen (4 Bytes beim ESP32 und ESP8266) und diese einzeln in den EEPROM schreiben.

Die Funktion EEPROM.update(addr, var) bewirkt dasselbe wie EEPROM.write(addr, var), schreibt var aber nur dann, wenn sich der Wert vom aktuellen Wert an dieser Speicheradresse unterscheidet. Das bringt zum einen Geschwindigkeitsvorteile, zum anderen erhöht es die Lebensdauer des EEPROMs. Allerdings macht sich das nur bemerkbar, wenn lediglich ein kleiner Anteil dessen, was ihr auf den EEPROM schreibt, tatsächlich neu ist. Ansonsten ist die Chance 1 : 256, dass der richtige Werte schon im Speicher steht. Bei zu speicherndem Text sieht es etwas besser aus.

Das Gegenstück zu EEPROM.write() ist EEPROM.read(addr). Die Funktion liest ein einzelnes Byte an der Adresse addr.

Mit EEPROM[addr] sprecht ihr den EEPROM als Array an. EEPROM[addr] = var schreibt einen Wert, var = EEPROM[addr] liest einen Wert. Und wieder ist var zwingend vom Datentyp byte.

Hier ein kleiner Sketch dazu:

#include <EEPROM.h>

void setup(){
  Serial.begin(9600);
  byte readVal = 0;
  
  // write:
  EEPROM.write(0,255);
  EEPROM.update(523,42);
  EEPROM[37] = 111;

  // read and print:
  readVal = EEPROM[0];
  Serial.print("Address 0:   ");
  Serial.println(readVal);
  
  readVal = EEPROM.read(523);
  Serial.print("Address 523: ");
  Serial.println(readVal);
  
  EEPROM.get(37, readVal);
  Serial.print("Address 37:  ");
  Serial.println(readVal); 
}

void loop(){}

 

Und so sieht dann die Ausgabe aus:

EEPROM Funktionen: Ausgabe von eeprom_write_read_update.ino
Ausgabe von eeprom_write_read_update.ino

Ich werde im Folgenden hauptsächlich die Funktionen put() und get() benutzen, auch zum Schreiben einzelner Bytes.

Schreib- und Lesegeschwindigkeit

Das Beschreiben von EEPROMs ist ein ausgesprochen langsamer Prozess. Das habe ich mit dem folgenden Sketch „gemessen“.

#include <EEPROM.h>

void setup(){
  Serial.begin(9600);
  
  unsigned long duration = 0;
  unsigned long start = millis();
  
  for(int address=0; address<1024; address++){
    EEPROM.write(address,42);
  }
  duration = millis() - start;

  Serial.print("duration [ms]: ");
  Serial.println(duration);
}

void loop(){}

Es benötigte ganze 3.5 Sekunden, um den EEPROM einmal vollständig zu beschreiben:

EEPROM Geschwindigkeitstest: 
Ausgabe von eeprom_write_speed.ino
Ausgabe von eeprom_write_speed.ino

Ihr braucht also ca. 3.4 Millisekunden, um ein einzelnes Byte zu schreiben. Um das einzuordnen: Wolltet ihr einen Spielfilm von 4 GB in dieser Geschwindigkeit speichern, würde das ca. 157 Tage dauern!

Der Lesevorgang ist fast 2000 Mal schneller. Mit dem folgenden Sketch könnt ihr das nachvollziehen:

#include <EEPROM.h>

void setup(){
  Serial.begin(9600);
  
  byte b = 0;
  unsigned long a = 0;
  unsigned long duration1 = 0;
  unsigned long duration2 = 0;
  unsigned long duration = 0;
  unsigned long start = micros();
  
  for(int address=0; address<1024; address++){
    EEPROM.get(address,b);
    a += b; // dummy use of b
  }
  duration1 = micros() - start;

  a = 0;
  start = micros();
  for(int address=0; address<1024; address++){
    a += b;
  }
  duration2 = micros() - start;

  duration = duration1 - duration2;

  Serial.print("duration1 [µs]: ");
  Serial.println(duration1);
  Serial.print("duration2 [µs]: ");
  Serial.println(duration2);
  Serial.print("duration [µs]: ");
  Serial.println(duration);
  Serial.print("dummy: ");
  Serial.println(a); 
}
void loop(){}

 

Der Sketch sieht recht komplex aus für das, was er tun soll. Ein reines Auslesen des Speicherinhaltes ohne Verwendung der Daten führt aber dazu, dass der Compiler den Vorgang schlicht „wegoptimiert“.

Da die Schleife und die Addition auch etwas Zeit in Anspruch nimmt, habe ich diese Zeit separat noch einmal gemessen und abgezogen.

EEPROM Geschwindigkeitstest: Ausgabe eeprom_read_speed.ino
Ausgabe eeprom_read_speed.ino

Das Auslesen des gesamten Speichers nimmt 1924 µs in Anspruch, also ca. 1.88 µs pro Byte.

Vertiefung

Der folgende Sketch schreibt die Zahlen 65 bis 74 als Bytes auf die Speicherplätze 0 bis 9. Zur Überprüfung werden die Zahlen ausgelesen und auf dem seriellen Monitor ausgegeben.

#include <EEPROM.h>

void setup(){
  Serial.begin(9600);
  byte byteArray[10];
  byte startValue = 65;
  
  for(int i=0; i<10; i++){
    EEPROM.put(i, startValue + i);
  }
  
  for(int i=0; i<10; i++){
    EEPROM.get(i, byteArray[i]);
  }
  for(int i=0; i<10; i++){
    Serial.print("byteArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(byteArray[i]);
  }
}

void loop(){}

Den Daten auf dem EEPROM ist nicht anzusehen, um welche Datentypen es sich handelt oder an welcher Adresse ein Wert anfängt und wo er aufhört. Mit dem folgenden Sketch werden die Daten als Bytes, Characters, Integers, Float und Long unterschiedlich interpretiert.

#include <EEPROM.h>

void setup(){
  Serial.begin(9600);
  byte byteArray[10];
  char charArray[10];
  unsigned int uintArray[5];
  float floatVal = 0.0;
  long longVal = 0;
  int intArray[5];
  
  for(int i=0; i<10; i++){
    EEPROM.get(i, byteArray[i]);
  }
  for(int i=0; i<10; i++){
    Serial.print("byteArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.print(byteArray[i]);
    Serial.print(", 0x");
    Serial.println(byteArray[i], HEX);
  }
  for(int i=0; i<10; i++){
    EEPROM.get(i, charArray[i]);
  }
  for(int i=0; i<10; i++){
    Serial.print("charArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(charArray[i]);
  }
  for(int i=0; i<5; i++){
    EEPROM.get(2*i, uintArray[i]); 
  }
  for(int i=0; i<5; i++){
    Serial.print("uintArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.print(uintArray[i]);
    Serial.print(", 0x");
    Serial.println(uintArray[i], HEX);
  }
  for(int i=0; i<5; i++){
    EEPROM.get(2*i, intArray[i]); 
  }
  EEPROM.get(0, floatVal);
  Serial.print("Float Value:  ");
  Serial.println(floatVal);
  EEPROM.get(4, longVal);
  Serial.print("Long Value:   ");
  Serial.println(longVal);
}
void loop(){}

 

Und so sieht die Ausgabe aus:

Ausgabe von eeprom_different_interpretation.ino
Ausgabe von eeprom_different_interpretation.ino

Dieser letzte Sketch sollte noch einmal verdeutlichen, dass ihr wissen müsst, welche Datentypen an welcher Stelle im Speicher stehen. Ohne das befindet sich dort nur irgendein Datensalat.

Character Arrays und Strings auf dem EEPROM speichern

Für die Deklaration von Zeichenketten (Strings) gibt es zwei Möglichkeiten. Entweder, ihr benutzt Character Arrays (array of chars) oder String-Objekte. Character Arrays benötigen weniger Programmspeicher und RAM, für Strings hingegen gibt es sehr komfortable Funktionen.

Der Unterschied zwischen Zeichenketten und den bisher verwendeten Variablentypen ist ihre variable Länge. Man muss also nicht nur wissen, an welcher Adresse sie im EEPROM beginnen, sondern auch wo sie aufhören. Nur wie? Die Antwort lautet: Zeichenketten werden mit dem Nullzeichen (null character) '\0', abgeschlossen. Das Nullzeichen (ASCII-Code 0) ist nicht mit der Null (ASCII-Code 48) zu verwechseln. Bei der Deklaration eines Character Arrays muss aufgrund der Null-Terminierung immer ein Element mehr berücksichtigt werden, als lesbare Zeichen vorhanden sind, zum Beispiel:

char myCharArray[5] = "four";

Character Arrays

Allgemeines über Character Arrays

Im letzten Abschnitt hatten wir die Zeichen „ABCDEFGHIJ“ auf dem EEPROM gespeichert. Die Zeichenfolge belegt die Adressen 0 bis 9. Der folgende Sketch fügt das Nullzeichen an der Adresse 10 hinzu. Nach dieser Operation lässt sich die Zeichenfolge in einem Rutsch auslesen, auch wenn die Länge nicht bekannt ist. Um das zu verdeutlichen, habe ich dem Character Array myCharArray in dem folgenden Sketch absichtlich mehr Zeichen zur Verfügung gestellt als notwendig. 

#include <EEPROM.h>

void setup(){
  Serial.begin(9600);
  EEPROM.put(10,'\0');
  
  char myCharArray[20];
  EEPROM.get(0,myCharArray);
  
  Serial.print("myCharArray: ");
  Serial.println(myCharArray);
  Serial.print("Length: ");
  Serial.println(sizeof(myCharArray));

  String myString = String(myCharArray);
  Serial.print("myString:    ");
  Serial.println(myString);
  Serial.print("Length: ");
  Serial.println(myString.length());
}

void loop(){}

So sieht die Ausgabe aus:

Obwohl myCharArray 20 Zeichen reserviert bekommen hat, wird es über die println() Funktion korrekt dargestellt. Bei der Umwandlung in ein String Objekt werden die Zeichen nach dem Nullzeichen ignoriert.

Mehrere Character Arrays auf den EEPROM schreiben

Jetzt möchte ich zeigen, wie Ihr mehrere Character Arrays im EEPROM ablegt. Im folgenden Sketch werden die Character Arrays zunächst deklariert und dann nacheinander an eine Funktion übergeben, die die Arrays zeichenweise in den EEPROM schreibt. Dabei muss über die Adresse „Buch geführt“ werden.

#include <EEPROM.h>

void setup(){
  int address = 0;
  char myCharArray0[] = "Arduino's";
  char myCharArray1[] = "EEPROM";
  char myCharArray2[] = "can";
  char myCharArray3[] = "be very";
  char myCharArray4[] = "useful";
  
  Serial.begin(9600);

  address = writeToEEPROM(address, myCharArray0);
  address = writeToEEPROM(address, myCharArray1);
  address = writeToEEPROM(address, myCharArray2);
  address = writeToEEPROM(address, myCharArray3);
  address = writeToEEPROM(address, myCharArray4);
}

void loop(){}

int writeToEEPROM(int addr, char *cArr){
  int i=0; 
  
  do{
    EEPROM.put(addr + i, cArr[i]);
    i++;   
  }while(cArr[i-1] != '\0');
  
  addr += i;  
  return addr;
}

Da Arrays viel Speicherplatz einnehmen können, arbeitet man in Funktionen, an die sie übergeben werden, nicht mit Kopien, sondern mit den Originalen. Der Name eines Character Arrays ist der Zeiger auf sein erstes Element. Damit wird in writeToEEPROM(address, myCharArray0); das Array myCharArray0 als Zeiger übergeben. Entsprechend muss es in der Funktion writeToEEPROM() als Zeiger entgegengenommen werden, was durch das „*“-Zeichen gekennzeichnet wird.

Mehrere Character Arrays vom EEPROM lesen

Das Auslesen funktioniert ähnlich:

#include <EEPROM.h>

void setup(){
  char myCharArray[30]; // max 29 characters expected
  Serial.begin(9600);

  for(int i=0; i<5; i++){
    readNextCharArray(myCharArray);
    Serial.println(myCharArray);
  }
}

void loop(){}

void readNextCharArray(char* cArr){
  static int address = 0;
  int i = 0;
  cArr[i] = ' ';

  do{
    EEPROM.get(address + i, cArr[i]);
    i++;
  }while(cArr[i-1] != '\0');
  
  address += i;
}

Mit char myCharArray[30] deklariere ich ein Character Array für bis zu 29 Zeichen plus Nullzeichen. Das Array wird als Zeiger an readNextCharArray() übergeben und dort sozusagen mit Leben gefüllt. Die Buchführung für die Adresse habe ich hier zur Abwechslung anders gelöst. Sie wird als „static int“ in der Schreibfunktion berechnet.

Ausgabe von eeprom_read_several_char_arrays.ino
Ausgabe von eeprom_read_several_char_arrays.ino

Warum zeichenweises Schreiben und Lesen?

Weiter oben hatte ich das „ABCDEFGHIJ“ – Array „in einem Rutsch“ ausgelesen und nicht zeichenweise. Warum nicht in den letzten Beispielen? Das funktioniert leider nicht so ohne Weiteres nach der Übergabe in die Funktion. Dazu folgender Test:

#include <EEPROM.h>

void setup(){
  char myCharArray[30]; // max 29 characters expected
  Serial.begin(9600);

  EEPROM.get(0,myCharArray);
  
  Serial.print("Char Array (setup): ");
  Serial.println(myCharArray);
  readCharArray(myCharArray); 
}

void loop(){}

void readCharArray(char* cArr){
  char myLocalCharArray[30];
  Serial.print("Char Array (passed to readCharArray): ");
  Serial.println(cArr);
  EEPROM.get(0,cArr);
  Serial.print("Char Array (read from EEPROM in readCharArray): ");
  Serial.println(cArr);
  EEPROM.get(0,myLocalCharArray);
  Serial.print("Char Array (read from EEPROM in readCharArray as local array of chars): ");
  Serial.println(myLocalCharArray);
}

Der Serial.println() Befehl funktioniert nach der Übergabe, das Auslesen über EEPROM.get(0, cArr) hingegen nicht:

Ausgabe von eeprom_read_test.ino
Ausgabe von eeprom_read_test.ino

Eine Alternative

Anstelle des Nullzeichens könntet ihr auch die Länge des zu speichernden Character Arrays jeweils vor das Character Array schreiben. Dann wisst ihr, wie viele Zeichen ihr lesen müsst, bevor das nächste Character Array beginnt. Das ersetzt das Nullzeichen auf dem EEPROM. Der Platzbedarf ist also derselbe.

Strings

Mehrere Strings auf den EEPROM schreiben

Auch bei der Übergabe von String Objekten solltet ihr in Funktionen mit den Originalen arbeiten und keine lokalen Kopien anlegen. Dazu wird dem String Objekt in der Funktionsdeklaration einfach nur der Adressoperator „&“ vorangestellt. Ansonsten ist die Vorgehensweise ähnlich:

#include <EEPROM.h>

void setup(){
  int address = 0;
  String myString0 = "Arduino's";
  String myString1 = "EEPROM";
  String myString2 = "can";
  String myString3 = "be very";
  String myString4 = "useful";
  Serial.begin(9600);

  address = writeToEEPROM(address, myString0);
  address = writeToEEPROM(address, myString1);
  address = writeToEEPROM(address, myString2);
  address = writeToEEPROM(address, myString3);
  address = writeToEEPROM(address, myString4);
}

void loop(){}

int writeToEEPROM(int address, String &stringToWrite){
  for(unsigned int i=0; i<stringToWrite.length(); i++){
    EEPROM.put(address, stringToWrite[i]);
    address++;
  }
  EEPROM.put(address, '\0');
  address++;
  return address;
}

Wie ihr seht, speichern wir das String Objekt nicht als solches, sondern wieder als Character Array ab.

Mehrere Strings vom EEPROM lesen

Da die Strings als Character Array auf dem EEPROM gespeichert wurden, könntet ihr sie einfach mit dem Sketch eeprom_read_multiple_char_arrays.ino auslesen. Ich möchte hier aber einmal zeigen, wie man mit Strings anstelle von Character Arrays arbeitet.

#include <EEPROM.h>

void setup(){
  int address = 0;
  String myString = "";
  Serial.begin(9600);

  for(int i=0; i<5; i++){
    readNextString(myString);
    Serial.println(myString);
  }
}

void loop(){}

void readNextString(String &stringToRead){
  static int address = 0;
  stringToRead = "";
  char c = ' ';

  while(c != '\0'){
    EEPROM.get(address,c);
    address++;
    stringToRead += c; 
  }
}

 

Die Ausgabe ist natürlich dieselbe wie beim Auslesen unter Verwendung von Character Arrays:

Ausgabe von eeprom_read_multiple_strings.ino
Ausgabe von eeprom_read_multiple_strings.ino

Anwendungsbeispiel: Telefonverzeichnis mit vorgefertigten SMS

Vor ein paar Wochen hat mich ein Leser gefragt, wie man es mithilfe des EEPROM und des SIM800L Moduls realisieren könne, vorgefertigte SMS per Knopfdruck zu versenden. Das war übrigens auch der Anlass, diesen Beitrag zu schreiben.

Diese Aufgabe ist nun keine große Herausforderung mehr. Zunächst werden die Telefonnummern und die vorgefertigten SMS wie folgt auf dem EEPROM gespeichert.

#include <EEPROM.h>

void setup(){
  int address = 0;
  String Herbert = "+491731234567,Hi Herbert, let's have a beer this evening, do you have time?";
  String John = "+492323223323,Hi John, can you call me tomorrow?";
  String Grandma = "+497776665554444,Hi Grandma, don't forget to take your medicine!";
  String Tina = "+49876543210,Hi Tina, I will arrive in 30 minutes";
  String Pizza = "+492345654321,Hi Pizza-Service, No. 18 with extra garlic, please";
  
  Serial.begin(9600);

  address = writeToEEPROM(address, Herbert);
  address = writeToEEPROM(address, John);
  address = writeToEEPROM(address, Grandma);
  address = writeToEEPROM(address, Tina);
  address = writeToEEPROM(address, Pizza);
}

void loop(){}

int writeToEEPROM(int address, String &stringToWrite){
  for(unsigned int i=0; i<stringToWrite.length(); i++){
    EEPROM.put(address, stringToWrite[i]);
    address++;
  }
  EEPROM.put(address, '\0');
  address++;
  return address;
}

Dieser Beitrag hat den EEPROM zum Thema und nicht das SIM800L Modul. Deswegen konzentriere ich mich auf nur darauf, wie die Einträge vom EEPROM gelesen werden. Die Empfänger der SMS werden als enum „smsRecipient“ definiert. Die Reihenfolge muss dabei der Reihenfolge der Einträge auf dem EEPROM entsprechen. Wenn ihr nun eine SMS an John schicken möchtet, kommt ihr folgendermaßen an die Telefonnummer und die Nachricht:

#include <EEPROM.h>

enum smsRecipient {Herbert, John, Grandma, Tina, Pizza}; 

void setup(){
  String smsString = "";
  smsRecipient sendTo = John;
  Serial.begin(9600);

  readSmsString(smsString, sendTo);
  Serial.println(smsString);
  String phone = smsString.substring(0, smsString.indexOf(","));
  String message = smsString.substring(smsString.indexOf(",")+1, smsString.length());
  Serial.println("Phone:");
  Serial.println(phone);
  Serial.println("Message:");
  Serial.println(message);
}

void loop(){}

void readSmsString(String &stringToRead, smsRecipient sendTo){
  unsigned int address = 0;
  stringToRead = "";
  char c = ' ';
  unsigned int counter = 0;
  
  while(counter < sendTo){
    address++;
    EEPROM.get(address,c);
    if(c == '\0'){
      counter++;
    }
  }
  c = ' ';
  while(c != '\0'){
    address++;
    EEPROM.get(address,c);
    stringToRead += c; 
  }
}

Herbert ist der Eintrag 0, John ist der Eintrag 1. Also muss auf dem EEPROM nach dem ersten Nullzeichen gesucht werden. Alles zwischen dem ersten und dem zweiten Nullzeichen wird in einem String gespeichert. Das erste Komma des Strings trennt die Telefonnummer von der Nachricht. Über die komfortablen substring() und indexOf() Funktionen lassen sich die beiden Teile in separate Strings trennen. In diesem Beispiel gebe ich erst den gesamten String, dann die Nummer und die Nachricht auf dem seriellen Monitor aus:

Ausgabe von eeprom_get_entry_from_telephone_directory.ino

Um den Sketch zu vervollständigen, fehlt nicht viel:  Ein paar Taster werden in einer Schleife abgefragt, ob sie gedrückt wurden. Die Taster ordnet man SMS Empfängern zu, beispielsweise: „Wenn Taste 1 gedrückt, dann smsRecipient = John“. Wie ihr SMS Nachrichten mit dem SIM800L Modul verschickt, habe ich in meinem Beitrag über das SIM800L Modul beschrieben.

Ausblick

Wenn ihr größere Datenmengen speichern müsst, dann kommt ihr mit dem internen EEPROM nicht sehr weit, zumindest gilt das für die AVR-basierten Boards. Deshalb werde ich in einem zweiten Teil zeigen, wie ihr externe IC2 basierte EEPROMs beschreibt und Daten von ihnen lest. In einem dritten Teil wende ich mich den SPI basierten EEPROMs zu.

8 thoughts on “EEPROM Teil 1 – AVR interne EEPROMs

  1. Huhu,
    wenn mit dem EEPROM rum gespielt wird, dann die Daten dort IMMER in einer struct definieren, weil nur dann die Adressen kontinuierlich vergeben werden UND die Initialisierung via Arduino flashing einfach möglich ist.

    EEMEM struct {uint8_t value1, value2; char msg[6]; float tp;}
    __attribute__ ((aligned (1), packed)) ee = {.value1=42, .msg=“Alarm“, .tp=42.42};

    Die Initwerte werden beim Flashing (wenn ausgewählt) mit ins EEPROM übertragen!
    Die Adressen sind dann bestimmbar und in der Reihenfolge wie festgelegt, … &ee.msg_alarm oder &ee.tp .

    Ohne struct geht es irgendwann in die Küche des Teufels, GCC kann die Adressen sonst beliebig jedesmal beim compilieren neu festlegen und die Reihenfolge ist nicht garantiert.

    1. Und nochmal vielen Dank.

      Allerdings: was soll ich sagen? Funktioniert alles auch so absolut zuverlässig. Bisher sind immer alle Daten dort gelandet, wo ich sie haben wollte und entsprechend habe ich sie dort auch wiedergefunden. Ich habe es bisher auch nirgendwo mit der struct-Methode gesehen (was natürlich nicht heißt, dass es nicht besser wäre, es so zu machen!). Nur habe ich das Problem noch nicht erkannt, weil es wie gesagt bisher noch keines gab. Unter welchen Bedingungen treten deiner Meinung bzw. Erfahrung Probleme auf?

  2. Es wurde zwar erwähnt, dass die Schreibzyklen bei den Eeproms begrenzt sind.
    Aber es ist vielleicht für den ein oder anderen hilfreich zu wissen, dass man aufpassen sollte, den Schreibbefehlt nicht ausversehen in einer Endlosschleife zu platzieren, da man damit innerhalb von Minuten denEeprom zerstören kann.

    Die alten Eprom mit dem Sichtfenster kenne ich auch noch. Die habe ich damals mit Hilfe eines C64 programmiert, um mit dem Z80 oder 8051 verwenden zu können. Gelöscht habe ich die Bausteine mit dem UV Bräuner meiner Mutter. 🙂

  3. Hallo Wolfgang,
    ich verstehe zwar nicht warum nur für ESP aber speziell für den ESP32 ist Preferences.h zu empfehlen.
    Man braucht sich da dann nicht mehr mit Adressen herum schlagen. Funktioniert über Key Words.
    Ich nutze das für eine größere Anwendung und bin damit sehr glücklich!
    Grüße
    Jörg

    1. Hallo, danke für den Kommentar. Da hätte ich vielleicht drauf eingehen sollen. Ich werde dazu noch etwas nachtragen, zumindest einen Hinweis. Preferences.h und EEPROM.h sprechen sehr spezifische Hardwarekomponenten an. Das lässt sich nicht so ohne Weiteres allgemeingültig programmieren. VG, Wolfgang

  4. Hallo Ewald,

    Ich würde gerne wissen ob und wie es gehen würde wenn man ein paar soundfiles auf einen eeprom speichert und abruft um damit z.b. einen df Player Mini zu ersetzen, wenn man für sein Projekt nur 3 oder 4 Sounds benötigt die ca 1sek lang sind…. da ist ein df Player mit micro sd card doch eigentlich resourcen Verschwendung…und wäre ein eeprom da nicht auch platzsparende? Frage ist dann aber auch…wie schließe ich dann die Lautsprecher an…

    1. Hallo, man braucht schon etwas, das mp3 files lesen und verarbeiten kann und diese Aufgabe übernehmen Module wie der DFPlayer. Ich wüsste nicht, dass man so etwas auf einem Arduino programmieren könnte. VG, Wolfgang

      1. Die AVRs haben gleich mehrere Grenzen, die einem mp3-Dekoder nicht genügen: 8Bit, 16MHz genügt schlicht nicht, egal in welcher Qualität. Auch das RAM ist zu wenig für den mathematischen Tabellenteil des Dekoders.
        Für den ESP32 gibt es dagegen eine hervorragende Library (ESP32-audioI2S von Wolle schrfeibfaul1), die das ganz ohne Dekodier-Hardware schafft. Für Miniqualität reichen 80MHz Prozessortakt, für 44kHz Stereo Qualität aktiviert man die 240MHz (setCpuFrequencyMhz(240);). Audioausgabe geht dann bequem über I2S-Anschluss auf einen I2S-Verstärker wie MAX98357A. Einfacher gehts nicht.
        Was das Speichern von Audiodaten auf einem EEPROM angeht: technisch ist das machbar, notfalls muss man das vor dem Abspielen ins RAM kopieren. 1kB würde bei 3kHz 8Bit Mono(Telefonqualität) mp3 wenige Sekunden reichen, als WAV nur 300ms. Die 4kB EEPROM des ESP32 reissen da auch nicht viel raus, der hat aber ohnehin das ein oder andere MB SPI-Flash eingebaut, das man u.U. nutzen kann.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert