Arduino EEPROM & ESP32 Preferences: Einstellungen dauerhaft speichern – auch nach dem Stromausfall
Du stellst die Weckzeit ein, ziehst den Stecker – und beim nächsten Start ist alles wieder auf Null. Nervig, aber leicht zu beheben: Jeder Arduino hat einen kleinen, nichtflüchtigen Speicher an Bord, das EEPROM. Beim ESP32 heißt das Gegenstück Preferences (NVS). In diesem Tutorial lernst du, wie du Zahlen, Texte und ganze Konfigurations-Structs dauerhaft speicherst, wie du beim allerersten Start sinnvolle Startwerte setzt und wie du den Speicher nicht in wenigen Tagen kaputtschreibst.
Warum RAM allein nicht reicht
Ein Mikrocontroller hat drei sehr unterschiedliche Speicher, und der häufigste Anfängerfehler ist, sie zu verwechseln. Deine Variablen leben im RAM – blitzschnell, aber beim Stromausfall komplett weg. Dein Sketch liegt im Flash – bleibt erhalten, wird aber beim Programmieren komplett überschrieben. Und dann gibt es das EEPROM: klein, langsam, aber schreibbar zur Laufzeit und dauerhaft.
Genau dort gehören Dinge hin, die dein Projekt sich merken soll: die eingestellte Zieltemperatur deines Thermostats, der Highscore deines Reaktionsspiels, die Kalibrierwerte deines Bodenfeuchte-Sensors oder schlicht die WLAN-Zugangsdaten.
| Speicher | Größe (typisch) | Übersteht Stromausfall? | Wofür |
|---|---|---|---|
| SRAM | 2 KB (Nano) / 520 KB (ESP32) | nein | Variablen zur Laufzeit |
| Flash | 32 KB (Nano) / 4 MB (ESP32) | ja | dein Programmcode |
| EEPROM (AVR) | 512 B (Nano/UNO: 1 KB) | ja | Einstellungen, Zähler |
| NVS / Preferences (ESP32) | ca. 20 KB nutzbar | ja | Einstellungen, Zugangsdaten |
| Externes I2C-EEPROM | 4 KB (AT24C32) | ja | Logs, große Konfigurationen |
ℹ️ Wie viel EEPROM hat mein Board? Arduino UNO R3 und Nano V3 (ATmega328P) haben 1024 Byte. Der ATmega32U4 hat ebenfalls 1 KB. Der ESP32 und der ESP8266 haben kein echtes EEPROM – dort wird EEPROM in einem reservierten Flash-Bereich nachgebildet. Deshalb ist auf dem ESP32 die Preferences-Bibliothek die bessere Wahl.
Das brauchst du
Für die ersten drei Code-Beispiele reicht ein nacktes Board ohne jede Zusatzhardware – du siehst alles im seriellen Monitor.
1EEPROM-Grundlagen auf dem Arduino
Die EEPROM-Bibliothek ist in der Arduino IDE bereits enthalten. Du sprichst den Speicher byteweise über eine Adresse an – bei einem Nano also Adresse 0 bis 1023. Jede Adresse fasst genau ein Byte (0–255).
#include <EEPROM.h>
const int ADR_HELLIGKEIT = 0; // Adresse fuer unseren Wert
void setup() {
Serial.begin(115200);
delay(300);
byte helligkeit = EEPROM.read(ADR_HELLIGKEIT);
Serial.print("Gespeicherte Helligkeit: ");
Serial.println(helligkeit);
helligkeit++; // Wert veraendern
EEPROM.update(ADR_HELLIGKEIT, helligkeit); // nur schreiben wenn noetig
Serial.print("Neu gespeichert: ");
Serial.println(helligkeit);
Serial.println("Jetzt Reset druecken - der Wert bleibt erhalten.");
}
void loop() {}
Lade den Sketch hoch, öffne den seriellen Monitor und drücke ein paar Mal den Reset-Taster. Der Zähler läuft weiter hoch, obwohl der Sketch keinerlei Variable behält – genau das ist der Punkt.
💡 Immer update() statt write(): EEPROM.write() schreibt bedingungslos und verbraucht damit einen Schreibzyklus. EEPROM.update() liest die Zelle zuerst und schreibt nur, wenn sich der Wert wirklich geändert hat. Das kostet dich nichts und verlängert die Lebensdauer massiv. Es gibt keinen Grund, jemals write() zu nehmen.
2Mehr als ein Byte: put(), get() und structs
Ein Byte reicht selten. Ein float braucht 4 Byte, ein int auf dem AVR 2 Byte. Statt die Bytes selbst zu zerlegen, nimmst du EEPROM.put() und EEPROM.get() – die kopieren beliebige Datentypen an eine Adresse und wieder zurück. Am elegantesten ist ein struct, das alle Einstellungen bündelt:
#include <EEPROM.h>
struct Konfig {
uint8_t magic; // Erkennungsmarke
uint8_t helligkeit; // 0..255
float zieltemp; // z.B. 21.5
uint16_t startCount; // wie oft gestartet
};
const int ADR_KONFIG = 0;
const uint8_t MAGIC = 0x42; // frei waehlbar, aber konstant
Konfig cfg;
void ladeKonfig() {
EEPROM.get(ADR_KONFIG, cfg);
if (cfg.magic != MAGIC) { // EEPROM war noch nie beschrieben
Serial.println("Erststart - setze Standardwerte");
cfg.magic = MAGIC;
cfg.helligkeit = 128;
cfg.zieltemp = 21.0;
cfg.startCount = 0;
EEPROM.put(ADR_KONFIG, cfg);
}
}
void setup() {
Serial.begin(115200);
delay(300);
ladeKonfig();
cfg.startCount++;
EEPROM.put(ADR_KONFIG, cfg); // put() nutzt intern update()
Serial.print("Start Nr. "); Serial.println(cfg.startCount);
Serial.print("Helligkeit: "); Serial.println(cfg.helligkeit);
Serial.print("Zieltemp: "); Serial.println(cfg.zieltemp, 1);
}
void loop() {}
ℹ️ Was ist dieses Magic Byte? Ein fabrikneues EEPROM ist mit 0xFF gefüllt. Ohne Prüfung würdest du beim ersten Start eine Zieltemperatur von rund 3,4 · 10³⁸ °C einlesen. Das Magic Byte ist eine willkürliche Konstante, die du beim ersten Schreiben mit ablegst. Passt sie beim Lesen nicht, weißt du: hier stand noch nie etwas Sinnvolles – also Standardwerte setzen. Änderst du später das struct, erhöhst du einfach die Konstante, und alle Boards initialisieren sich sauber neu.
EEPROM.put() ruft intern für jedes Byte update() auf. Du kannst es also bedenkenlos verwenden, ohne bei jedem Aufruf alle Zellen zu verschleißen.
3ESP32: Preferences statt EEPROM
Auf dem ESP32 gibt es zwar eine EEPROM-Kompatibilitätsschicht, aber sie ist veraltet. Nutze stattdessen Preferences: Die Bibliothek liegt dem ESP32-Core bei, arbeitet mit dem NVS (Non-Volatile Storage) im Flash, bringt Wear-Leveling schon mit und speichert Werte unter Namen statt unter Adressen. Kein Adressgeschiebe mehr:
#include <Preferences.h>
Preferences prefs;
void setup() {
Serial.begin(115200);
delay(300);
prefs.begin("meinprojekt", false); // Namespace, false = lesen+schreiben
// Zweiter Parameter ist der Standardwert, falls der Key fehlt
uint32_t starts = prefs.getUInt("starts", 0);
float zieltemp = prefs.getFloat("zieltemp", 21.0);
String wlanName = prefs.getString("ssid", "nicht gesetzt");
bool nachtmodus= prefs.getBool("nacht", false);
starts++;
prefs.putUInt("starts", starts);
Serial.printf("Start Nr. %u\n", starts);
Serial.printf("Zieltemperatur: %.1f C\n", zieltemp);
Serial.printf("SSID: %s\n", wlanName.c_str());
Serial.printf("Nachtmodus: %s\n", nachtmodus ? "an" : "aus");
prefs.end(); // Namespace schliessen
}
void loop() {}
💡 Kein Magic Byte nötig: Jede get...()-Funktion nimmt den Standardwert direkt als zweiten Parameter. Fehlt der Schlüssel, bekommst du diesen Wert zurück – die Erstinitialisierung erledigt sich damit von selbst. Namespace- und Schlüsselnamen dürfen maximal 15 Zeichen lang sein.
Willst du alles zurücksetzen – zum Beispiel über einen langen Tastendruck als „Werkseinstellungen“ –, geht das in zwei Zeilen:
prefs.begin("meinprojekt", false);
prefs.clear(); // loescht alle Keys in diesem Namespace
// prefs.remove("ssid"); // oder gezielt einen einzelnen Key
prefs.end();
ESP.restart();
4Praxisprojekt: Einstellung drehen, anzeigen, speichern
Jetzt kombinieren wir alles zu etwas Brauchbarem: Mit dem Rotary Encoder stellst du einen Wert ein, das OLED-Display zeigt ihn an, und beim Druck auf den Encoder-Taster wird gespeichert. Nach dem Neustart steht der Wert wieder da.
Verkabelung (Arduino Nano V3)
| Bauteil / Pin | Arduino Nano | Hinweis |
|---|---|---|
| Encoder CLK | D2 | interruptfähiger Pin |
| Encoder DT | D3 | Drehrichtung |
| Encoder SW | D4 | Taster, gegen GND |
| Encoder + / GND | 5V / GND | Versorgung |
| OLED SDA / SCL | A4 / A5 | I2C-Bus |
| OLED VCC / GND | 5V / GND | Versorgung |
Installiere über den Bibliotheksverwalter Adafruit SSD1306 und Adafruit GFX. Der Encoder wird hier ohne Zusatzbibliothek ausgewertet:
#include <EEPROM.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define PIN_CLK 2
#define PIN_DT 3
#define PIN_SW 4
#define ADR_WERT 10
#define ADR_MAGIC 11
#define MAGIC 0x37
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
volatile int wert = 0;
int letzterGespeicherter = 0;
uint8_t letzterClk = HIGH;
unsigned long letzterTasterMs = 0;
void setup() {
pinMode(PIN_CLK, INPUT_PULLUP);
pinMode(PIN_DT, INPUT_PULLUP);
pinMode(PIN_SW, INPUT_PULLUP);
if (EEPROM.read(ADR_MAGIC) != MAGIC) { // Erststart
EEPROM.update(ADR_WERT, 50);
EEPROM.update(ADR_MAGIC, MAGIC);
}
wert = EEPROM.read(ADR_WERT);
letzterGespeicherter = wert;
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
zeichne();
}
void zeichne() {
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.println("Helligkeit");
oled.setTextSize(3);
oled.setCursor(0, 20);
oled.println(wert);
oled.setTextSize(1);
oled.setCursor(0, 54);
oled.println(wert == letzterGespeicherter ? "gespeichert" : "* Taster = speichern");
oled.display();
}
void loop() {
uint8_t clk = digitalRead(PIN_CLK);
if (clk != letzterClk && clk == LOW) { // fallende Flanke
if (digitalRead(PIN_DT) != clk) wert++; else wert--;
wert = constrain(wert, 0, 255);
zeichne();
}
letzterClk = clk;
if (digitalRead(PIN_SW) == LOW && millis() - letzterTasterMs > 250) {
letzterTasterMs = millis();
EEPROM.update(ADR_WERT, (byte)wert); // erst jetzt schreiben!
letzterGespeicherter = wert;
zeichne();
}
}
⚠️ Niemals im loop() speichern! Der naheliegende Reflex wäre, den Wert bei jeder Drehung sofort ins EEPROM zu schreiben. Bei 100.000 garantierten Schreibzyklen und einer Schleife, die tausendfach pro Sekunde läuft, wäre die Zelle in Minuten hinüber. Speichere immer nur bei einem klaren Ereignis: Tastendruck, Menü verlassen oder nach ein paar Sekunden Inaktivität.
Eine elegante Alternative zum Taster ist ein Timer: Ändert sich der Wert, merkst du dir millis(). Sind danach fünf Sekunden ohne weitere Änderung vergangen, schreibst du einmal. Wie du solche Zeitabläufe sauber ohne delay() baust, zeigt dir unser Artikel zu millis() statt delay().
5Mehr Platz: externes I2C-EEPROM (AT24C32)
1 KB ist schnell voll, wenn du Messreihen speichern willst. Praktisch: Auf fast jedem DS3231-RTC-Modul sitzt zusätzlich ein AT24C32 mit 4096 Byte. Er hängt am selben I2C-Bus wie die Uhr, aber unter einer eigenen Adresse (meist 0x57, die RTC selbst liegt auf 0x68). Ansprechen kannst du ihn ganz ohne Zusatzbibliothek:
#include <Wire.h>
#define EEP_ADDR 0x57 // AT24C32 auf dem DS3231-Modul
void eepSchreibe(uint16_t adresse, byte daten) {
Wire.beginTransmission(EEP_ADDR);
Wire.write((byte)(adresse >> 8)); // Adresse High-Byte
Wire.write((byte)(adresse & 0xFF)); // Adresse Low-Byte
Wire.write(daten);
Wire.endTransmission();
delay(6); // Schreibzyklus abwarten (5 ms)
}
byte eepLese(uint16_t adresse) {
Wire.beginTransmission(EEP_ADDR);
Wire.write((byte)(adresse >> 8));
Wire.write((byte)(adresse & 0xFF));
Wire.endTransmission();
Wire.requestFrom(EEP_ADDR, 1);
return Wire.available() ? Wire.read() : 0xFF;
}
void setup() {
Serial.begin(115200);
Wire.begin();
delay(300);
eepSchreibe(100, 123);
Serial.print("Gelesen von Adresse 100: ");
Serial.println(eepLese(100));
}
void loop() {}
ℹ️ Adresse unbekannt? Manche Module liegen je nach Stellung der A0/A1/A2-Lötbrücken auf 0x50 bis 0x57. Lass einfach einen I2C-Scanner laufen – wie das geht, steht im Artikel I2C-Bus mit Arduino verstehen. Das delay(6) nach dem Schreiben ist Pflicht: Der Chip ist während des internen Schreibzyklus nicht ansprechbar.
Für richtig große Datenmengen – etwa einen Langzeit-Datenlogger mit Zeitstempel – ist eine Micro-SD-Karte die bessere Wahl. Den kompletten Aufbau dazu findest du im Datenlogger-Tutorial.
Lebensdauer: was du wissen musst
EEPROM-Zellen halten nicht ewig. Der Hersteller garantiert für den ATmega328P 100.000 Schreibzyklen pro Zelle – gelesen werden darf dagegen unbegrenzt oft. Das klingt viel, ist aber schnell aufgebraucht, wenn du unbedacht schreibst.
| Schreibmuster | Schreibvorgänge | Haltbarkeit |
|---|---|---|
| Einmal pro Tastendruck | ca. 20 pro Tag | über 13 Jahre |
| Einmal pro Minute | 1.440 pro Tag | ca. 69 Tage |
| Einmal pro Sekunde | 86.400 pro Tag | gut 1 Tag |
| In jedem loop()-Durchlauf | Tausende pro Sekunde | Minuten |
Zwei einfache Strategien halten dich auf der sicheren Seite: Erstens immer update() bzw. put() nutzen, damit identische Werte gar nicht erst geschrieben werden. Zweitens Wear Leveling – wenn du wirklich häufig schreiben musst, verteile die Schreibvorgänge über mehrere Adressen:
// Betriebsstundenzaehler ueber 100 Zellen verteilt:
// Statt einer Zelle mit 100.000 Zyklen hast du 10.000.000.
const int BASIS = 200;
const int SLOTS = 100;
void speichereZaehler(uint16_t wert) {
static int slot = -1;
if (slot < 0) slot = findeAktuellenSlot();
slot = (slot + 1) % SLOTS;
EEPROM.put(BASIS + slot * 4, wert); // 2 Byte Wert + 2 Byte Sequenz
EEPROM.put(BASIS + slot * 4 + 2, (uint16_t)(millis() / 1000));
}
⚠️ Der klassische Anfängerfehler: EEPROM.write() in loop(), um „immer den aktuellen Stand zu sichern“. Das Ergebnis ist ein Board, das nach ein paar Stunden Betrieb scheinbar zufällige Werte liefert – die Zelle ist verschlissen. Schreibe nur bei Ereignissen, nie zyklisch.
Troubleshooting
⚠️ Nach dem Erststart stehen absurde Werte drin (z. B. 3.4e38 oder 65535): Ein leeres EEPROM ist mit 0xFF gefüllt. Nutze das Magic Byte aus Schritt 2, um den Erststart zu erkennen und Standardwerte zu setzen.
⚠️ Werte gehen beim Neuprogrammieren verloren: Das sollte nicht passieren – prüfe in der Arduino IDE unter Tools, ob eine Option wie „Erase All Flash Before Sketch Upload“ (ESP32) aktiviert ist. Beim AVR bleibt das EEPROM erhalten, solange die EESAVE-Fuse gesetzt ist; die Standard-Bootloader ändern das nicht.
⚠️ ESP32: EEPROM.commit() vergessen: Nutzt du auf dem ESP32 doch die EEPROM-Kompatibilitätsschicht, wird ohne abschließendes EEPROM.commit() gar nichts in den Flash geschrieben. Genau deshalb ist Preferences die entspanntere Lösung.
⚠️ Struct passt nicht mehr: Hast du ein Feld im struct ergänzt, liest get() Müll aus der alten Speicherbelegung. Lösung: Erhöhe die Magic-Konstante, dann werden beim nächsten Start automatisch Standardwerte gesetzt.
⚠️ Preferences liefert immer den Standardwert: Namespace oder Key länger als 15 Zeichen, oder begin() wurde mit true (read-only) aufgerufen. Beides prüfen.
⚠️ Externes EEPROM antwortet nicht: Falsche I2C-Adresse oder fehlende Pull-ups. Auf dem DS3231-Modul sind die Pull-ups bereits verbaut – Scanner laufen lassen und Adresse verifizieren.
🚀 Alles für dein Speicher-Projekt
Diese Bauteile brauchst du für die Beispiele aus diesem Tutorial:
- Arduino Nano V3 (USB-C) – 1 KB EEPROM an Bord
- ESP32 Entwicklungsboard – für Preferences & NVS
- DS3231 RTC mit AT24C32 – 4 KB externes I2C-EEPROM
- Rotary Encoder KY-040 und 0,96 Zoll OLED-Display
- Breadboard 400 Kontakte und Dupont-Jumperkabel
Fazit
Dauerhaftes Speichern ist keine Raketenwissenschaft – aber es gibt drei Details, an denen fast jeder einmal hängenbleibt: das Magic Byte für den Erststart, update() statt write() und die eiserne Regel, niemals im loop() zu schreiben. Wenn du diese drei Punkte beherzigst, hält dein EEPROM länger als das Projekt selbst.
Auf dem AVR nimmst du EEPROM.put() mit einem Konfigurations-struct, auf dem ESP32 die deutlich bequemere Preferences-API mit Namen statt Adressen. Reicht der Platz nicht, hängst du ein AT24C32 an den I2C-Bus – und für echte Messreihen greifst du gleich zur SD-Karte. Damit merkt sich dein nächstes Projekt seine Einstellungen genauso selbstverständlich wie ein fertiges Gerät aus dem Laden. Viel Spaß beim Bauen!