ESP32 LittleFS Tutorial: Dateien im Flash speichern & eigene Webseite ausliefern

Ein Webserver, dessen komplettes HTML als Riesen-String im Sketch klebt? Eine WLAN-SSID, die du bei jeder Änderung neu kompilieren musst? Das muss nicht sein. Dein ESP32 hat ein vollwertiges Dateisystem im Flash-Speicher: LittleFS. Damit legst du HTML-, CSS-, JSON- und CSV-Dateien direkt auf dem Board ab – ganz ohne SD-Karte. In diesem Tutorial schreibst und liest du Dateien, speicherst eine JSON-Konfiguration, lässt einen Webserver echte Dateien ausliefern und baust einen Messwert-Logger mit Download-Link.

Warum ein Dateisystem im Flash?

Der ESP32 hat je nach Modul 4 MB oder mehr Flash-Speicher. Dein Sketch braucht davon vielleicht ein Megabyte – der Rest liegt brach. LittleFS macht aus diesem freien Bereich ein richtiges Dateisystem mit Ordnern, Dateinamen, Öffnen, Schreiben und Anhängen. Genau wie auf dem PC, nur eben in 1,5 MB.

Der Nutzen ist sofort spürbar:

  • Webseiten auslagern: HTML, CSS und JavaScript landen als eigene Dateien im Flash – kein PROGMEM-String-Monster mehr im Sketch.
  • Konfiguration ändern ohne Neukompilieren: WLAN-Daten, Schwellwerte oder Kalibrierung als JSON-Datei.
  • Messwerte loggen: CSV-Datei anhängen und später per Browser herunterladen.
  • Kein Zusatzbauteil: Anders als bei einer SD-Karte brauchst du keine weitere Hardware und keine SPI-Pins.

💡 LittleFS oder SPIFFS? SPIFFS war jahrelang der Standard, ist aber offiziell veraltet: kein echtes Ordner-Konzept, langsam bei vollem Speicher und anfällig für Datenverlust bei Stromausfall während des Schreibens. LittleFS kennt echte Verzeichnisse, ist deutlich schneller und arbeitet mit Wear Leveling. Nutze bei neuen Projekten immer LittleFS.

LittleFS, SD-Karte oder Preferences – was wann?

Kriterium Preferences (NVS) LittleFS Micro-SD-Karte
Kapazität ca. 20 KB 1–2 MB (je nach Partition) mehrere GB
Zusatzhardware keine keine Modul + Karte
Ordner & Dateien nein (nur Key/Value) ja ja
Am PC lesbar nein nur über Upload/Download ja, direkt
Ideal für einzelne Einstellungen Webseiten, Konfig, kleine Logs Langzeit-Datenlogging

Kurz gesagt: Einzelne Zahlen und Zugangsdaten gehören in Preferences, ganze Dateien nach LittleFS, und alles über ein paar hundert Kilobyte auf die SD-Karte.

Das brauchst du

DHT22 (AM2302) Temperatursensor – liefert die Messwerte fürs Logging
1,3 Zoll OLED-Display (I2C) – optional für die IP-Anzeige

Wenn du mehr Platz für große Weboberflächen brauchst, ist das ESP32-S3 Dev Kit mit 16 MB Flash die luxuriöse Variante – dort kannst du LittleFS problemlos mehrere Megabyte groß machen.

1Verkabelung

Für die reinen Dateisystem-Beispiele reicht das nackte Board. Für den Logger kommt der DHT22 dazu:

Bauteil / Pin ESP32 Hinweis
DHT22 VCC (+) 3V3 Versorgung
DHT22 DATA GPIO4 Datenleitung
DHT22 GND (−) GND Masse
OLED SDA / SCL GPIO21 / GPIO22 I2C (optional)
OLED VCC / GND 3V3 / GND Versorgung

2Dateien schreiben, lesen und auflisten

Die LittleFS-Bibliothek ist im ESP32-Core enthalten – du musst nichts installieren. Wichtig ist nur der Parameter true bei begin(): Er formatiert das Dateisystem automatisch, falls es noch nie eingerichtet wurde.

#include <LittleFS.h>

void schreibeDatei(const char *pfad, const char *inhalt) {
  File f = LittleFS.open(pfad, "w");        // "w" = neu anlegen/ueberschreiben
  if (!f) { Serial.println("Schreiben fehlgeschlagen"); return; }
  f.print(inhalt);
  f.close();
}

void leseDatei(const char *pfad) {
  File f = LittleFS.open(pfad, "r");
  if (!f) { Serial.println("Datei nicht gefunden"); return; }
  Serial.printf("Inhalt von %s (%u Byte):\n", pfad, f.size());
  while (f.available()) Serial.write(f.read());
  Serial.println();
  f.close();
}

void listeAuf(const char *ordner) {
  File root = LittleFS.open(ordner);
  File datei = root.openNextFile();
  while (datei) {
    Serial.printf("  %-20s %6u Byte\n", datei.name(), datei.size());
    datei = root.openNextFile();
  }
}

void setup() {
  Serial.begin(115200);
  delay(300);

  if (!LittleFS.begin(true)) {              // true = bei Bedarf formatieren
    Serial.println("LittleFS-Start fehlgeschlagen");
    return;
  }

  Serial.printf("Gesamt: %u Byte, belegt: %u Byte\n",
                LittleFS.totalBytes(), LittleFS.usedBytes());

  schreibeDatei("/hallo.txt", "Hallo von MAKEROO!");
  leseDatei("/hallo.txt");

  File f = LittleFS.open("/hallo.txt", "a");   // "a" = anhaengen
  f.println("\nZweite Zeile.");
  f.close();
  leseDatei("/hallo.txt");

  Serial.println("Dateien im Wurzelverzeichnis:");
  listeAuf("/");
}

void loop() {}

ℹ️ Die drei Modi: "r" liest, "w" legt neu an und löscht dabei den alten Inhalt, "a" hängt hinten an. Verwechselst du w und a, ist dein Logfile nach jedem Schreibvorgang wieder leer – ein Klassiker. Pfade beginnen beim ESP32 immer mit einem Schrägstrich.

Weitere nützliche Funktionen: LittleFS.exists("/datei.txt") prüft die Existenz, LittleFS.remove() löscht, LittleFS.rename() benennt um, LittleFS.mkdir("/logs") legt einen Ordner an und LittleFS.format() setzt alles zurück.

3Konfiguration als JSON speichern

Statt WLAN-Zugangsdaten und Schwellwerte fest in den Sketch zu schreiben, legst du sie in eine JSON-Datei. Installiere dafür über den Bibliotheksverwalter ArduinoJson von Benoît Blanchon (Version 7).

#include <LittleFS.h>
#include <ArduinoJson.h>

const char *KONFIG_PFAD = "/config.json";

String  wlanSsid;
String  wlanPass;
float   schwelle;
uint32_t intervall;

void speichereKonfig() {
  JsonDocument doc;
  doc["ssid"]      = wlanSsid;
  doc["pass"]      = wlanPass;
  doc["schwelle"]  = schwelle;
  doc["intervall"] = intervall;

  File f = LittleFS.open(KONFIG_PFAD, "w");
  serializeJsonPretty(doc, f);      // schreibt lesbar formatiert
  f.close();
}

bool ladeKonfig() {
  File f = LittleFS.open(KONFIG_PFAD, "r");
  if (!f) return false;

  JsonDocument doc;
  DeserializationError fehler = deserializeJson(doc, f);
  f.close();
  if (fehler) { Serial.println("Config defekt"); return false; }

  wlanSsid  = doc["ssid"]      | "";       // | setzt den Standardwert
  wlanPass  = doc["pass"]      | "";
  schwelle  = doc["schwelle"]  | 25.0;
  intervall = doc["intervall"] | 60000;
  return true;
}

void setup() {
  Serial.begin(115200);
  delay(300);
  LittleFS.begin(true);

  if (!ladeKonfig()) {
    Serial.println("Keine Config - lege Standardwerte an");
    wlanSsid = "MeinWLAN"; wlanPass = "geheim";
    schwelle = 25.0; intervall = 60000;
    speichereKonfig();
  }

  Serial.printf("SSID: %s, Schwelle: %.1f, Intervall: %u ms\n",
                wlanSsid.c_str(), schwelle, intervall);
}

void loop() {}

💡 Der Pipe-Operator: doc["schwelle"] | 25.0 liefert 25.0, falls der Schlüssel fehlt oder den falschen Typ hat. Damit überlebt dein Sketch auch eine unvollständige Konfigurationsdatei, ohne mit Nullwerten weiterzurechnen.

4Webserver: HTML und CSS aus LittleFS ausliefern

Jetzt wird es richtig praktisch. Statt HTML als String im Sketch zu haben, liefert der Webserver echte Dateien aus. Damit die Dateien überhaupt aufs Board kommen, gibt es zwei Wege – wir gehen den, der ohne Zusatz-Plugin funktioniert: Der Sketch legt die Dateien beim ersten Start selbst an.

#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <DHT.h>

#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

const char *SSID = "DEIN_WLAN";
const char *PASS = "DEIN_PASSWORT";

WebServer server(80);

const char *INDEX_HTML =
  "<!DOCTYPE html><html lang='de'><head><meta charset='utf-8'>"
  "<meta name='viewport' content='width=device-width,initial-scale=1'>"
  "<link rel='stylesheet' href='/style.css'><title>ESP32 Monitor</title></head><body>"
  "<h1>ESP32 Monitor</h1>"
  "<div class='karte'>Temperatur: <span id='t'>--</span> C</div>"
  "<div class='karte'>Luftfeuchte: <span id='h'>--</span> %</div>"
  "<p><a href='/log.csv'>Messwerte herunterladen</a></p>"
  "<script>setInterval(function(){fetch('/daten').then(r=>r.json()).then(d=>{"
  "document.getElementById('t').textContent=d.t.toFixed(1);"
  "document.getElementById('h').textContent=d.h.toFixed(1);});},2000);</script>"
  "</body></html>";

const char *STYLE_CSS =
  "body{font-family:sans-serif;background:#101520;color:#e0e6ed;text-align:center;padding:2rem}"
  "h1{color:#fff}.karte{background:#1a2332;border-radius:12px;padding:1.5rem;margin:1rem auto;"
  "max-width:320px;font-size:1.4rem}a{color:#3b82f6}";

void legeDateienAn() {
  if (!LittleFS.exists("/index.html")) {
    File f = LittleFS.open("/index.html", "w"); f.print(INDEX_HTML); f.close();
  }
  if (!LittleFS.exists("/style.css")) {
    File f = LittleFS.open("/style.css", "w"); f.print(STYLE_CSS); f.close();
  }
}

void setup() {
  Serial.begin(115200);
  dht.begin();
  LittleFS.begin(true);
  legeDateienAn();

  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
  Serial.println();
  Serial.print("Bereit unter http://");
  Serial.println(WiFi.localIP());

  // JSON-Endpunkt fuer die Live-Werte
  server.on("/daten", []() {
    float t = dht.readTemperature();
    float h = dht.readHumidity();
    char puffer[64];
    snprintf(puffer, sizeof(puffer), "{\"t\":%.1f,\"h\":%.1f}",
             isnan(t) ? 0.0 : t, isnan(h) ? 0.0 : h);
    server.send(200, "application/json", puffer);
  });

  // Alles andere direkt aus LittleFS ausliefern
  server.serveStatic("/", LittleFS, "/");
  server.serveStatic("/index.html", LittleFS, "/index.html");
  server.onNotFound([]() { server.send(404, "text/plain", "Nicht gefunden"); });

  server.begin();
}

void loop() { server.handleClient(); }

ℹ️ Der eigentliche Trick ist serveStatic(). Eine Zeile genügt, und der Webserver liefert jede Datei aus LittleFS aus – inklusive passendem MIME-Typ für .html, .css, .js und Bilder. Du kannst deine Oberfläche damit beliebig ausbauen, ohne den Sketch anzufassen. Grundlagen zum Webserver findest du im Artikel ESP32 Webserver einrichten.

5Messwerte als CSV loggen und herunterladen

Der letzte Baustein: Alle fünf Minuten einen Messwert an eine CSV-Datei anhängen, damit du sie später im Browser öffnen und in Excel auswerten kannst. Diesen Block ergänzt du im Sketch aus Schritt 4:

const char *LOG_PFAD = "/log.csv";
const unsigned long LOG_INTERVALL = 300000UL;   // 5 Minuten
unsigned long letzterLog = 0;

void logZeile(float t, float h) {
  // Kopfzeile nur einmal schreiben
  if (!LittleFS.exists(LOG_PFAD)) {
    File neu = LittleFS.open(LOG_PFAD, "w");
    neu.println("sekunden;temperatur;feuchte");
    neu.close();
  }
  // Schutz vor vollem Flash: ab 200 KB neu beginnen
  File pruef = LittleFS.open(LOG_PFAD, "r");
  bool zuGross = pruef && pruef.size() > 200000;
  if (pruef) pruef.close();
  if (zuGross) LittleFS.remove(LOG_PFAD);

  File f = LittleFS.open(LOG_PFAD, "a");        // anhaengen!
  if (!f) return;
  f.printf("%lu;%.1f;%.1f\n", millis() / 1000, t, h);
  f.close();
}

// im loop() ergaenzen:
void loggeWennFaellig() {
  if (millis() - letzterLog < LOG_INTERVALL) return;
  letzterLog = millis();
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (!isnan(t) && !isnan(h)) logZeile(t, h);
}

Weil serveStatic("/", LittleFS, "/") bereits aktiv ist, kannst du die Datei sofort unter http://<IP-des-ESP32>/log.csv im Browser abrufen oder herunterladen. Ein Klick auf den Link in der Weboberfläche genügt.

💡 Echte Zeitstempel: millis() beginnt nach jedem Neustart wieder bei Null. Für auswertbare Logs holst du dir die Uhrzeit per NTP aus dem Internet – wie das geht, zeigt der Artikel ESP32 Uhrzeit per NTP holen. Dann schreibst du statt der Sekunden ein sauberes Datum in die erste Spalte.

Praxistipps

  • Nicht zu oft schreiben: Flash-Speicher hat begrenzte Löschzyklen (typisch 10.000 bis 100.000 pro Sektor). LittleFS verteilt die Zugriffe zwar, aber ein Log im Sekundentakt über Monate ist trotzdem keine gute Idee. Alle paar Minuten reicht für die meisten Umweltdaten völlig.
  • Partition anpassen: Unter Tools → Partition Scheme wählst du aus, wie viel Flash für Programm und wie viel fürs Dateisystem reserviert wird. „Default 4MB with spiffs“ gibt dir rund 1,5 MB Dateisystem. Achtung: Ein Wechsel des Schemas löscht das Dateisystem.
  • Immer close(): Eine offen gelassene Datei kann bei einem Reset unvollständig bleiben. Schließe jede Datei direkt nach dem Schreiben.
  • Speicher überwachen: LittleFS.totalBytes() - LittleFS.usedBytes() zeigt dir den freien Platz. Prüfe ihn, bevor du große Dateien schreibst.
  • Kombination mit OTA: Zusammen mit OTA-Updates wird dein Projekt komplett kabellos wartbar – neue Firmware per WLAN, neue Konfiguration per Weboberfläche.

Troubleshooting

⚠️ LittleFS.begin() gibt false zurück: Das Dateisystem ist noch nicht formatiert. Rufe LittleFS.begin(true) auf – der Parameter formatiert bei Bedarf automatisch.

⚠️ Datei ist nach jedem Schreiben wieder leer: Du hast "w" statt "a" benutzt. "w" löscht den bisherigen Inhalt.

⚠️ Nach dem Sketch-Upload sind alle Dateien weg: Du hast das Partition Scheme gewechselt oder „Erase All Flash Before Sketch Upload“ aktiviert. Beides formatiert das Dateisystem mit.

⚠️ Webseite lädt, aber ohne Stil: Die CSS-Datei fehlt oder der Pfad stimmt nicht. Liste mit listeAuf("/") auf, was wirklich im Dateisystem liegt – der Pfad muss exakt passen, inklusive führendem Schrägstrich.

⚠️ Schreiben schlägt irgendwann fehl: Das Dateisystem ist voll. Prüfe usedBytes() und rotiere deine Logdateien, so wie im Beispiel oben mit der 200-KB-Grenze.

⚠️ Kompilierfehler „LittleFS.h: No such file“: Dein ESP32-Core ist zu alt. Aktualisiere ihn im Boardverwalter auf Version 2.0 oder neuer.

🚀 Bauteile für dieses Projekt

Alles, was du für den Dateisystem-Webserver brauchst:

Fazit

Mit LittleFS wird aus dem ESP32 ein kleiner Server mit eigenem Dateisystem. Du hast Dateien geschrieben, gelesen und aufgelistet, eine Konfiguration als JSON abgelegt, eine Weboberfläche aus echten HTML- und CSS-Dateien ausgeliefert und Messwerte als CSV geloggt, die du per Browser herunterladen kannst – alles ohne ein einziges Zusatzbauteil.

Die wichtigsten Merksätze: begin(true) zum automatischen Formatieren, "a" statt "w" beim Anhängen, jede Datei sauber schließen und nicht im Sekundentakt schreiben. Von hier aus kannst du eine Konfigurationsseite bauen, auf der man WLAN-Daten im Browser einträgt, oder deine Weboberfläche mit Diagrammen aufwerten. Viel Spaß beim Bauen!

Zurück zum Blog