Mit den Funktionen der Bibliothek »Preferences« kann man im Flash-Speicher des ESP32 Daten zur Programm-Konfiguration ablegen
, z.B. die WLAN-Zugangsdaten, aber auch Daten des laufenden Programms, bspw. ob ein Taster gedrückt war und eine LED an- oder ausgeschaltet hat. So kann man im Fall eines Neustarts, z.B. nach einem Ausfall der Versorgungsspannung oder einem Absturz des ESP32, einen früheren Programmzustand wiederherstellen. Außerdem muss man die Daten nicht in jedem Programm angeben, das sie nutzen soll; insbes. Passwörter kann man so aus den Quelltexten heraushalten.
Die Preferences-Bibliothek gehört zu den Standard-Bibliotheken des ESP32 in der Arduino-IDE, muss also nicht zusätzlich installiert werden. Sie nutzt zum Speichern der Daten einen kleinen Teil des auf dem ESP32 integrierten Flash-Speichers, den sog. NVS (non volatile storage). Sie ist nur für den ESP32 implementiert und setzt voraus, dass man zum Entwickeln die Arduino-IDE verwendet – für andere Entwicklungsumgebungen (wie das espressif-SDK oder Platform.io) oder andere Mikrocontroller folgen weiter unten Hinweise zu Alternativen.
Flash-Speicher des ESP32
Je nach Typ des ESP32 und der damit bestückten Boards kann dem ESP32 unterschiedlich viel Flash-Speicher zur Verfügung stehen. Die folgenden Angaben beziehen sich auf die Standard-Developer Boards (DevKitC) mit dem Modul ESPWROOM32, die 4 MB Flash besitzen. Es gibt auch ESP32-Boards mit 8 oder 16 MB Flash – mehr als 16 MB kann der ESP32 nicht adressieren.
Im Flash wird u.a. das Programm (der vom Compiler erzeugte Objektcode oder binary) gespeichert, andere Teile können als Dateisystem eingerichtet und zum Speichern von Daten genutzt werden – vergleichbar mit dem Speichern auf einer SD-Karte. Hierzu wird der Flash-Speicher in verschiedene Bereiche – die Partitionen – unterteilt.
Die Standard-Aufteilung der Partitionen sieht beim ESP32 so aus,[1]Siehe Datei default.csv im Partitions-Ordner des ESP32-Pakets für die Arduino-IDE; bei einer Standard-Windows-Installation im Verzeichnis … Continue reading wobei die Partitionen in Sektoren oder Seiten von 4096 Bytes aufgeteilt sind.
- NVS: Konfigurationsdaten; 20 KiB[2]Gemeint sind hier ausdrücklich die sog. Kibibytes (s. Wikipedia), d.h. 210 = 1.024 Bytes; die NVS-Partition ist also standardmäßig 20.480 Bytes groß.
- OTADATA: Angabe der Partition, aus der beim Booten das Programm geladen wird (8 KiB)
- APP0: Programm, das standardmäßig gestartet wird; 1.280 KiB
- APP1: alternatives Programm bei Nutzung der OTA-Funktionalität des ESP32[3]Mit OTA ist die Installation eines Programm-Updates im laufenden Betrieb möglich. Hierbei kann der Inhalt einer nicht zum Booten genutzten Programm-Partition z.B. per WiFi oder Bluetooth … Continue reading; 1.280 KiB
- SPIFFS: Dateisystem zum Speichern von Daten, z.B. Sensordaten oder Logs; 1.472 KiB
Wie man die Partitionierung ändert und ein Dateisystem nutzt, wird Inhalt weiterer Beiträge. Im folgenden geht es um die Nutzung der NVS-Partition.
Vorbild: INI-Dateien zum Speichern von Einstellungen
INI-Dateien werden seit Jahrzehnten verwendet, um im Dateisystem Daten zu speichern, die zum Initialisieren bestimmter Variablen eines Programms genutzt werden. Es sind einfache Text-Dateien, in denen Paare von Schlüsseln und Werten gespeichert sind.[4]Siehe auch die Beschreibung bei Wikipedia: Initialisierungsdatei
INI-Dateien haben folgenden Aufbau:
[Sektion1] schluessel1=wert1 schluessel2=wert2 [Sektion2] ; Kommentar schluessel=wert schluessel2=wert3
Vielen PC-Anwendern und Programmierern sind solche Dateien wahrscheinlich schon begegnet. Auch wenn in Windows die meisten Einstellungen inzwischen in der Registry (Registrier-Datenbank) abgelegt werden, findet man doch noch immer viele solche Dateien auf einem Rechner. In der Entwicklungsumgebung Platform.io werden dort bspw. Board-Informationen abgelegt. Im Beitrag zum Longan Nano findet sich folgendes Beispiel:
; PlatformIO Project Configuration File [env:sipeed-longan-nano] platform = gd32v framework = arduino ; alternativ: framework = gd32vf103-sdk board = sipeed-longan-nano upload_protocol = dfu monitor_speed = 115200
INI-Dateien sind in verschiedene Abschnitte oder Sektionen (engl. section) gegliedert, deren Namen in eckigen Klammern stehen. Leere Sektionen sind möglich; meist enthalten sie aber ein oder mehrere Schlüssel-Wert-Paare.
Innerhalb einer Sektion muss ein Schlüssel (key) eindeutig sein; verschiedene Sektionen können aber gleichlautende Schlüssel enthalten, denen gleiche oder verschiedene Werte (value) zugewiesen werden können (im oberen Beispiel schluessel2
).
Ähnliche Möglichkeiten bietet das JSON-Format, mit dem strukturierte Daten in Textdateien abgelegt werden können. Es wird zwar häufig zum Datenaustausch zwischen verschiedenen Systemen eingesetzt, kann aber auch ähnlich wie eine INI-Datei genutzt werden. Erläuterungen und ein Beispiel finden sich in Wikipedia.
Daten im NVS
Wie oben beschrieben, ist der NVS eine kleine Partition des auf dem ESP32 integrierten Flash-Speichers. »NVS« steht für Non Volatile Storage, d.h. nicht-flüchtiger Speicher. Im Gegensatz zum RAM-Speicher bleiben hier gespeicherte Daten nach dem Ausschalten des ESP32 erhalten. (Natürlich ist nicht nur der NVS-Speicherbereich „nicht-flüchtig”, sondern der gesamte Flash.[5]Nicht-flüchtig: Der Speicherinhalt bleibt auch beim Ausschalten der Versorgungsspannung erhalten. Beispiele: ROM, EEPROM, Flash-Speicher (z.B. in USB-Sticks oder SD-Karten), außerdem externe … Continue reading)
Einen ähnlichen Mechanismus wie eine INI-Datei stellt die Bibliothek »Preferences« für den NVS bereit. Was dort die Sektionen sind, sind hier die sog. namespaces bzw. Namensräume. Schlüssel sind ASCII-Zeichenketten mit einer maximalen Länge von 15; auch die Bezeichner der Namespaces haben diese Maximallänge. Als Werte können beliebige Datentypen gespeichert werden – neben „normalen” Strings (null-terminierten Zeichenketten) auch Binärdaten: boolsche Werte, verschiedene Integer-Typen, Fließkommazahlen und sog. BLOBs – binary large objects (mehr oder weniger große Binärdaten, d.h. (fast) beliebige Datenstrukturen).[6]Die der Preferences-Bibliothek zugrunde liegenden NVS-Funktionen unterstützen Fließkommazahlen nicht direkt; die Preferences-Bibliothek speichert float und double deshalb intern als – kleine … Continue reading
Der NVS (bzw. der gesamte Flash) ist in Speicherseiten (pages) von 4096 Bytes Größe organisiert. Die Standardgröße des NVS von 20 KiB entspricht also 5 Seiten. Jede Speicherseite bietet Platz für 126 Einträge[7]Ein Eintrag belegt 32 Bytes: 16 für den Schlüssel (15 Zeichen plus Null-Terminierung), 8 für den Wert, 4 für den Datentyp und weitere Verwaltungsinformationen und weitere 4 für eine Prüfsumme. … Continue reading (entries; entsprechen den Schlüssel-Wert-Paaren), wobei Strings und BLOBs je nach Größe mehrere Einträge belegen können. Ist man sparsam mit langen Einträgen, reicht der Platz für mehrere Hundert Schlüssel-Wert-Paare.
Es gibt folgende Obergrenzen: Zeichenketten (strings) dürfen maximal 4.000 Zeichen lang sein (inkl. des abschließenden Null-Bytes), BLOBs 508.000 Bytes oder 97,6% der Partitionsgröße (je nachdem, welcher Wert kleiner ist).[8]Siehe die NVS-Dokumentation: „String values are currently limited to 4000 bytes. This includes the null terminator. Blob values are limited to 508000 bytes or 97.6% of the partition size … Continue reading Gedacht ist der NVS aber zum Speichern kleiner Datenmengen. Längere Zeichenkeiten und BLOBs sollten lt. Dokumentation besser im Dateisystem abgelegt werden:
NVS works best for storing many small values, rather than a few large values of the type ‘string’ and ‘blob’. If you need to store large blobs or strings, consider using the facilities provided by the FAT filesystem on top of the wear levelling library.
Wie jeder Flash-Speicher kann auch der NVS nicht beliebig oft beschrieben werden. espressif geht von 100.000 Schreibzugriffen aus und errechnet daraus, dass selbst bei minütlichem Speichern eines 4 Byte großen Werts diese Zugriffszahl innerhalb einer Nutzungsdauer von 10 Jahren bei weitem nicht erreicht wird.[9]Siehe die Storage-FAQ: If data needs to be stored or updated to flash every minute, can ESP32 NVS meet this requirement? Solange man nicht exzessiv häufig Daten speichert, muss man sich um einen Datenverlust durch Ausfall des Flash keine Sorgen machen.
Im Code eines von espressif bereitgestellten Beispielprogramms[10]Das Beispielprogramm Prefs2Struct.ino liegt bei einer Windows-Installation des ESP32 in der Arduino-IDE auch hier: … Continue reading findet sich außerdem folgender Hinweis:
nvs has signifcant overhead, so should not be used for data that will change often.
Es ist also nicht unbedingt sinnvoll, häufige Zustandsänderungen eines Programms zu speichern, sondern eher selten auftretende. Aber wie so oft: es kommt drauf an …
Die Bibliotheksfunktionen stellen sicher, dass der NVS-Speicher keinen inkonsistenten Zustand erreicht, selbst wenn der ESP32 unerwartet vom Strom getrennt wird. Maximal geht ein Wert verloren, wenn er genau in diesem Moment geschrieben wird, in dem die Versorgungsspannung ausfällt. Der bisherige Inhalt der NVS-Partition sollte nach einem erneuten Einschalten unbeschädigt zur Verfügung stehen.[11]Abschnitt »Security, tampering, and robustness« der Dokumentation: „The library does try to recover from conditions when flash memory is in an inconsistent state. In particular, one … Continue reading
Funktionen der Bibliothek Preferences
Die Quelltexte der Bibliotheksdateien preferences.cpp
und preferences.h
sind sparsam kommentiert und bieten wenig zusätzliche Dokumentation.[12]Sie sind unter Windows im SRC-Ordner des Verzeichnis C:\Users\<benutzername>\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\Preferences zu finden; online im … Continue reading Folgende Funktionen bzw. Methoden sind vorhanden:
bool begin(const char * name, bool readOnly=false, const char* partition_label=NULL);
öffnet einen Namespace; name
ist der Bezeichner (Name) des Namespaces (maximal 15 Zeichen lang);readonly
gibt an, ob der Namespace nur zum Lesen geöffnet wird (true
) oder ob auch Daten geschrieben werden sollen (false
; Standard).partition_label
muss nur angegeben werden, wenn der Name der NVS-Partition nicht »nvs« lautet, d.h. wenn eine eigene Partitionstabelle mit Nicht-Standard-Namen und mglw. mehreren NVS-Partitionen genutzt wird – i.d.R. kann dieser Parameter entfallen.
void end();
schließt den Namespace
bool clear();
initialisiert den Namespace und löscht alle darin gespeicherten Daten; Ergebnis ist ein leerer Namespace, d.h. der Namespace selbst wird nicht gelöscht.
Eine Funktion zum vollständigen Löschen eines Namespace oder aller Namespaces im NVS stellt die Preferences-Bibliothek nicht zur Verfügung.
bool remove(const char * key);
entfernt den Schlüssel namens key
aus dem Namespace
size_t putChar(const char* key, int8_t value);
size_t putUChar(const char* key, uint8_t value);
size_t putShort(const char* key, int16_t value);
size_t putUShort(const char* key, uint16_t value);
size_t putInt(const char* key, int32_t value);
size_t putUInt(const char* key, uint32_t value);
size_t putLong(const char* key, int32_t value);
size_t putULong(const char* key, uint32_t value);
size_t putLong64(const char* key, int64_t value);
size_t putULong64(const char* key, uint64_t value);
size_t putFloat(const char* key, float_t value);
size_t putDouble(const char* key, double_t value);
size_t putBool(const char* key, bool value);
size_t putString(const char* key, const char* value);
size_t putString(const char* key, String value);
size_t putBytes(const char* key, const void* value, size_t len);
schreibt ein Schlüssel-Wert-Paar des jeweiligen Datentyps in den Namespace; die Länge des Schlüssel-Namens key
darf maximal 15 Zeichen betragen
Rückgabewert: Anzahl der gespeicherten Bytes; 0 im Fehlerfall
bool isKey(const char* key);
existiert der Schlüssel key
im Namespace?
PreferenceType getType(const char* key);
ermittelt den Datentyp des Schlüssels key
und gibt einen der Werte
- Integer der Länge 8, 16, 32 oder 64 Bit (jeweils mit und ohne Vorzeichen; Präfix »I« bzw. »U«)
- String
- BLOB
- ungültig
- boolsche Werte werden als vorzeichenlose Integer der Länge 8 gespeichert
- Fließkommazahlen werden als BLOB abgelegt
int8_t getChar(const char* key, int8_t defaultValue = 0);
uint8_t getUChar(const char* key, uint8_t defaultValue = 0);
int16_t getShort(const char* key, int16_t defaultValue = 0);
uint16_t getUShort(const char* key, uint16_t defaultValue = 0);
int32_t getInt(const char* key, int32_t defaultValue = 0);
uint32_t getUInt(const char* key, uint32_t defaultValue = 0);
int32_t getLong(const char* key, int32_t defaultValue = 0);
uint32_t getULong(const char* key, uint32_t defaultValue = 0);
int64_t getLong64(const char* key, int64_t defaultValue = 0);
uint64_t getULong64(const char* key, uint64_t defaultValue = 0);
float_t getFloat(const char* key, float_t defaultValue = NAN);
double_t getDouble(const char* key, double_t defaultValue = NAN);
bool getBool(const char* key, bool defaultValue = false);
liest den Wert des Schlüssels key
aus dem Namespace und liefert einen Wert des jeweilgen Datentyps als Rückgabewert der Funktion. Als zweiter Parameter kann ein Standardwert angegeben werden, der im Fehlerfall zurückgegeben wird (voreingestellt ist 0).
size_t getString(const char* key, char* value, size_t maxLen);
String getString(const char* key, String defaultValue = String());
Für Zeichenketten gibt es keine Bibliotheksfunktion, um deren Länge zu bestimmen, wie es für BLOBs mit getBytesLength()
möglich ist.[13]Verwendet man nicht die Preferences-Bibliothek, sondern direkt die NVS-Funktionen, kann man dafür die Funktion nvs_get_str()
nutzen. Entweder muss man wissen, was für einen Wert man erwartet, und einen ausreichend großen Puffer anlegen (oberer Aufruf) oder man verwendet die String
-Klasse (unterer Aufruf). Beide Methoden kommen im zweiten Beispielprogramm zum Einsatz.
size_t getBytesLength(const char* key);
size_t getBytes(const char* key, void * buf, size_t maxLen);
Für BLOBs unbekannter Größe kann mit der Methode getBytesLength()
die Größe des BLOBs ermittelt werden, um dann einen passenden Puffer anzulegen.
Beim Lesen eines BLOBs mit getBytes()
muss ein ausreichend großer Puffer buf
bereitgestellt werden, dessen Länge in maxLen
übergeben wird; ist dîe Puffergröße maxLen
kleiner als die BLOB-Größe, wird 0 zurückgegeben – es ist nicht möglich (und i.d.R. auch nicht sinnvoll), nur einen Teil eines BLOBS auszulesen.
size_t freeEntries();
ermittelt die Anzahl freier Einträge im NVS
Lt. NVS-Dokumentation ist die Änderung (Aktualisierung) eines Wertes nur möglich, wenn der Datentyp gleich bleibt. Andernfalls wird ein Fehler erzeugt. Abhilfe müsste möglich sein, indem man bei Änderung des Datentyps erst den alten Schlüssel löscht und dann einen neuen Schlüssel (mit dem alten Namen) und dem geänderten Datentyp und -wert speichert.
Beispielprogramme
Speichern von Konfigurationsdaten: WiFi-Zugangsdaten
Im NVS wird ein Namespace „wifi” angelegt, in dem die wichtigsten Daten zur Nutzung des WLANs gespeichert sind:
- Name des Netzwerks (SSID)
- Passwort
- Hostname – ein eindeutiger, sprechender Name eines Geräts im Netzwerk (maximal 63 Zeichen; erlaubt sind a-z, 0-9 und das Minuszeichen (keine Leerzeichen oder Unterstriche; Groß-/Kleinschreibung ist egal); Beispiele: „esp-keller”, „temperatur-buero”, „OfficeTemp”[14]Standard-Hostname ist „esp32-arduino” – jedenfalls wenn man zum Entwickeln die Arduino-IDE verwendet; das espressif-SDK setzt ihn wohl auf „espressif”.)
- Timeout – wie lange (in Sekunden) soll versucht werden, eine Verbindung zum WLAN herzustellen
- NTP-Server – ein Zeitserver, von dem der ESP2 die aktuelle Uhrzeit beziehen kann, um seine interne Uhr zu stellen (z.B. der Router im lokalen Netz oder ein Server aus dem Pool deutscher –
de.pool.ntp.org
– oder internationaler NTP-Server –ntp.pool.org
; siehe unten) - Frequenz – wie häufig (in Stunden) soll der NTP-Server abgefragt werden
- Zeitzone – die eigene Zeitzone, damit beim Einstellen der Uhrzeit auch Sommer-/Winterzeit berücksichtigt wird
/* ------------------------------------------------------------
* Ablegen von Konfigurationsdaten für den WLAN-Zugang im NVS
*
* Name des NVS-Namespace: wifi
* Daten:
* - ssid: Name des WLANs
* - wifipwd: WLAN-Passwort
* - hostname: "sprechender" Name des Geräts im Netzwerk
* - timeout: max. Zeit (in Sekunden) für Verbindungsaufbau
* - ntpserver: Name des NTP-Servers
* - ntpfreq: Intervall (in Stunden) für Abfrage des NTP-Servers
* - timezone: Zeitzone
*
* 2021-05-09 Heiko / unsinnsbasis.de
* ------------------------------------------------------------ */
/* ------------------------------------------------------------
* Einbinden der benötigten Bibliotheken,
* Defintion von Konstanten,
* Anlegen der Datenobjekte
* ------------------------------------------------------------ */
// Übertragungsrate für Ausgabe zum seriellen Monitor
#define SERIAL_BAUDRATE 115200
#include <Preferences.h>
// Datenobjekt für einen NVS-Namespace
Preferences prefs;
// Name des Namespace für die WiFi-Konfigurstion
const char* wifi_namespace = "wifi";
/* ------------------------------------------------------------
* Einstellungen für WiFi (NTP) und Datum/Zeit
* ------------------------------------------------------------ */
const char* ssid = "HS Gastzugang";
const char* password = "dein WLAN-Passwort";
const char* hostname = "ESP32-Test-A";
const char* ntpServer = "fritz.box"; // z.B. (de.)pool.ntp.org
// wie lange (in Sekunden) soll Verbindungsaufbau versucht werden
#define WIFI_TIMEOUT 60
// Häufigkeit (in Stunden) der NTP-Server-Abfrage
#define NTP_FREQ 24
// lokale Zeitzone definieren
// s. auch https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define TIMEZONE PSTR("CET-1CEST,M3.5.0,M10.5.0/3") // Europa/Berlin
void setup() {
bool f_error = false; //Fehler-Flag
Serial.begin(SERIAL_BAUDRATE);
delay(500);
// Namespace zum Schreiben öffnen (zweiter Parameter "readonly"
// kann entfallen, da per Default auf "false")
if (!prefs.begin(wifi_namespace)) {
Serial.println("Fehler beim Öffnen des NVS-Namespace");
for (;;); // leere Dauerschleife -> Ende
}
Serial.printf("Anzahl freier Einträge: %d\n", prefs.freeEntries());
prefs.clear();
Serial.printf("Anzahl freier Einträge nach dem Init./Löschen: %d\n", prefs.freeEntries());
if (prefs.putString("ssid", ssid) == 0)
f_error = true;
if (prefs.putString("wifipwd", password) == 0)
f_error = true;
if (prefs.putString("hostname", hostname) == 0)
f_error = true;
if (prefs.putInt("timeout", WIFI_TIMEOUT) == 0)
f_error = true;
if (prefs.putString("ntpserver", ntpServer) == 0)
f_error = true;
if (prefs.putInt("ntpfreq", NTP_FREQ) == 0)
f_error = true;
if (prefs.putString("timezone", TIMEZONE) == 0)
f_error = true;
if (f_error)
Serial.println("Fehler beim Speichern mindestens eines Eintrags");
Serial.printf("Anzahl freier Einträge nach dem Speichern: %d\n", prefs.freeEntries());
prefs.end();
}
void loop() {
}
Das Programm ist (hoffentlich) gut genug kommentiert, damit keine weiteren Erläuterungen nötig sind. Alle Aktionen zum Schreiben von Daten in den NVS werden nur einmal durchgeführt und sind deshalb Bestandteil der setup()
-Funktion. Die Hauptschleife loop()
bleibt leer.
Beim Öffnen des Namespace zum Schreiben kann die Angabe des zweiten Parameters readonly
entfallen, da er per Default auf false
gesetzt ist); die folgenden beiden Zeilen sind also gleichbedeutend:
if (!prefs.begin(wifi_namespace, false)) {
if (!prefs.begin(wifi_namespace)) {

Wie man an der Ausgabe im seriellen Monitor sieht, ist nach dem Initialisieren die Anzahl freier Einträge deutlich kleiner als (von mir jedenfalls) gedacht. Erwartet hatte ich etwa 600 Einträge (5 Pages mit je 126 Einträgen), tatsächlich sind es weniger als 500. NVS arbeitet wohl mit einigem Overhead, der Platz belegt.[15]Bspw. gibt es einen Namespace mit Index 0, der die Namen und Adressen aller anderen Namespaces enthält sowie die Namen der dort jeweils gespeicherten Schlüssel. Aber auch mit den verfügbaren 475 Einträgen sollte man normalerweise lange auskommen. Reicht das wirklich nicht, muss man den Flash-Speicher selbst partitionieren und einen etwas größeren NVS-Bereich anlegen.
Man sieht auch, dass die 7 gespeicherten Werte 12 Einträge belegen: die Zeichenketten jeweils 2, die Zahlenwerte je einen.
Lesen der Konfigurationsdaten: Anmelden im WLAN
Das folgende Programm liest die vorher im NVS gespeicherten Werte aus. Wesentlich sind Netzwerk-Name (SSID) und WLAN-Passwort. Werden andere Daten nicht im NVS gefunden, greift das Programm auf Standardwerte zurück. Bei der WLAN-Anmeldung ist das Setzen des Host-Namens optional.
Nach der WLAN-Anmeldung wird die interne Uhrzeit des ESP mit der Uhrzeit eines NTP-Servers synchronisiert.
/* ------------------------------------------------------------
* Konfigurationsdaten für den WLAN-Zugang aus NVS lesen
*
* Name des NVS-Namespace: wifi
* Daten:
* - ssid: Name des WLANs
* - wifipwd: WLAN-Passwort
* - hostname: "sprechender" Name des Geräts im Netzwerk
* - timeout: max. Zeit (in Sekunden) für Verbindungsaufbau
* - ntpserver: Name des NTP-Servers
* - ntpfreq: Intervall (in Stunden) für Abfrage des NTP-Servers
* - timezone: Zeitzone
*
* 2021-05-09 Heiko / unsinnsbasis.de
* ------------------------------------------------------------ */
/* ------------------------------------------------------------
* Einbinden der benötigten Bibliotheken,
* Defintion von Konstanten,
* Anlegen der Datenobjekte
* ------------------------------------------------------------ */
// Übertragungsrate für Ausgabe zum seriellen Monitor
#define SERIAL_BAUDRATE 115200
#include <Preferences.h>
// Datenobjekt für einen NVS-Namespace
Preferences prefs;
// Name des Namespace für die WiFi-Konfigurstion
const char* wifi_namespace = "wifi";
/* ------------------------------------------------------------
* WiFi (NTP) und Datum/Zeit
* ------------------------------------------------------------ */
#include <WiFi.h>
#include <time.h>
// Default-Werte für einige Einstellungen
// werden genutzt, wenn kein passender Wert im NVS gefunden wid
#define DEFAULT_WIFI_TIMEOUT 60
#define DEFAULT_NTP_SERVER PSTR("de.pool.ntp.org")
#define DEFAULT_TIMEZONE PSTR("CET-1CEST,M3.5.0,M10.5.0/3") // Europa/Berlin
#define DEFAULT_NTP_FREQ 24
int ntp_freq;
/* Timer für verschiedene Aufgaben:
* - alle n Stunden die Uhrzeit per NTP-Abfrage aktualisieren
* - ggf. weitere Timer, z.B. alle n Sekunden Sensoren abfragen,
* eine Anzeige aktualisieren etc.
*/
unsigned long timer_ntp=0, //NTP-Server abfragen
timer_time=0; // Uhrzeit anzeigen
/* ------------------------------------------------------------ */
void setup() {
bool f_error = false; //Fehler-Flag
Serial.begin(SERIAL_BAUDRATE);
delay(500);
// Namespace zum Lesen öffnen
if (!prefs.begin(wifi_namespace, true)) {
Serial.println("Fehler beim Öffnen des NVS-Namespace");
for (;;); // leere Dauerschleife -> Ende
}
// ... Initialisieren von Sensoren etc.
}
void loop() {
unsigned long timer; // Systemtimer in Sekunden
time_t now;
struct tm tdata;
timer = millis() / 1000;
// alle paar Stunden die Zeit von einem NTP-Server holen
// (auch ganz am Anfang, da ist timer_ntp = 0)
if (timer >= timer_ntp) {
if (get_time_from_ntp_server()) { // Zeit einstellen
timer_ntp += 3600 * ntp_freq; // nächste Sync. in x Stunden
} else {
// im Fehlerfall nach einiger Zeit (n Sekunden) nochmal versuchen
timer_ntp += 3600;
}
}
// alle 30 Sekunden die Uhrzeit anzeigen
if (timer >= timer_time) {
now = time(NULL);
localtime_r(&now, &tdata);
Serial.printf("%04d-%02d-%02d %02d:%02d:%02d\n",
tdata.tm_year+1900, tdata.tm_mon+1, tdata.tm_mday,
tdata.tm_hour, tdata.tm_min, tdata.tm_sec);
timer_time += 30;
}
delay(100);
}
/* ------------------------------------------------------------
* Verbindung zum WLAN herstellen
* - SSID und Passwort aus NVS lesen (müssen vorhanden sein)
* - max. Dauer für Verbindungsaufbau aus NVS lesen
* (bei Fehler, z.B. nicht vorhanden) Defaultwert verwenden
*
* Returncode:
* - true: WLAN-Verbindung wurde hergestellt
* - false: Verbindungsaufbau fehlgeschlagen
* ------------------------------------------------------------ */
bool connect_WiFi() {
struct tm tdata;
unsigned long timer;
// Variablen für die Werte aus dem NVS
char ssid[33]; // Maximallänge lt. Standard: 32
char wifipwd[64]; // Maximallänge lt. Standard: 63 (mind. 8)
char hostname[64]; // Maximallänge lt. Standard: 63
int wifi_timeout;
if ((prefs.getString("ssid", ssid, 32) == 0) ||
(prefs.getString("wifipwd", wifipwd, 63) == 0)) {
return false;
}
wifi_timeout = prefs.getInt("timeout", DEFAULT_WIFI_TIMEOUT);
// Hostnamen setzen, wenn im NVS einer angegeben ist
// (muss vor WiFi.begin() erfolgen)
if (prefs.isKey("hostname")) {
prefs.getString("hostname", hostname, 63);
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
WiFi.setHostname(hostname);
}
// mit dem WLAN verbinden
Serial.printf("Verbindung herstellen mit %s ", ssid);
timer = millis() / 1000;
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, wifipwd);
while (WiFi.status() != WL_CONNECTED && (millis() / 1000) < timer + wifi_timeout) {
delay(500);
Serial.print(".");
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("\nTimeout bei Verbindungsaufbau!");
return false;
} else {
Serial.print("\nVerbunden! IP-Adresse: ");
Serial.println(WiFi.localIP());
return true;
}
}
/* ------------------------------------------------------------
* Aktuelle Zeit von einem NTP-Server holen
* - dazu ggf. Wifi aktivieren
* - Returncode false, wenn keine Wifi-Verbindung zustande kommt
* ------------------------------------------------------------ */
bool get_time_from_ntp_server() {
struct tm tdata;
String ntp_server, timezone;
// NTP-Server-Name und Zeitzone aus NVS ermitteln oder
// Defaultwerte verwenden
ntp_server = prefs.getString("ntpserver", DEFAULT_NTP_SERVER);
timezone = prefs.getString("timezone", DEFAULT_TIMEZONE);
if (WiFi.status() != WL_CONNECTED) {
// ggf. mit dem WLAN verbinden (falls noch nicht geschehen)
connect_WiFi();
}
if (WiFi.status() != WL_CONNECTED) {
// keine WLAN-Verbindung -> zurück mit Fehler
return false;
} else {
// Zeit vom NTP-Server holen und Zeitzone einstellen
configTzTime(timezone.c_str(), ntp_server.c_str());
// einmal getLocalTime() aufrufen; sonst wird die Zeit nicht
// übernommen
getLocalTime(&tdata);
return true;
}
}
Einige Erläuterungen zum Programm:
/* Timer für verschiedene Aufgaben:
* - alle n Stunden die Uhrzeit per NTP-Abfrage aktualisieren
* - ggf. weitere Timer, z.B. alle n Sekunden Sensoren abfragen,
* eine Anzeige aktualisieren etc.
*/
unsigned long timer_ntp, timer_time;
timer = millis() / 1000;
// alle paar Stunden die Zeit von einem NTP-Server holen
// (auch ganz am Anfang, da ist timer_ntp = 0)
if (timer >= timer_ntp) {
if (get_time_from_ntp_server()) { // Zeit einstellen
timer_ntp += 3600 * ntp_freq; // nächste Sync. in x Stunden
} else {
// im Fehlerfall nach einiger Zeit (n Sekunden) nochmal versuchen
timer_ntp += 600;
}
}
Es werden verschiedene Timer angelegt, die angeben, wann vom Programm bestimmte Aktionen durchgeführt werden sollen:timer_ntp
gibt an, wann die nächste Synchronisation der Uhrzeit mit dem NTP-Server erfolgen soll;timer_time
steuert die Ausgabe der Uhrzeit (im Programm alle 30 Sekunden).
Im Hauptprogramm loop()
werden nicht bei jedem Durchlauf bestimmte Aktionen erledigt, sondern die Aufgaben werden zeitgesteuert bzw. in Intervallen ausgeführt. Wenn die aktuelle Systemzeit größer oder gleich dem jeweiligen Timer ist, wird die entsprechende Aktion durchgeführt.[16]Sinnvoll wäre es, dabei auch den Überlauf der vom System gemessenen Millisekunden nach knapp 50 Tagen zu berücksichtigen. Das habe ich mir der einfacheren Darstellung wegen im Programmbeispiel … Continue reading Anschließend wird der Timer hochgesetzt auf den Zeitpunkt, wann die Aktion das nächste Mal geplant ist.
Im Programm wird beispielhaft der NTP-Server abgefragt. Hat das geklappt, erfolgt die nächste Abfrage nach der in den Einstellungen gespeicherten Anzahl Stunden. Konnte keine WLAN-Verbindung aufgebaut werden, wird nach einer Stunde der nächste Versuch gestartet.
So kann man für verschiedene Aufgaben Intervalle definieren, z.B. fragt man jede Minute die Daten eines Sensors ab, sammelt ein paar Daten und schreibt sie alle 10 Minuten in eine Datei im Flash oder auf einer SD-Karte usw.
Im Programm werden beide Wege zum Auslesen einer Zeichenkette aus dem NVS mit der Methode getString()
genutzt:
bool connect_WiFi() {
struct tm tdata;
unsigned long timer;
// Variablen für die Werte aus dem NVS
char ssid[33]; // Maximallänge lt. Standard: 32
char wifipwd[64]; // Maximallänge lt. Standard: 63
// ...
// ...
if ((prefs.getString("ssid", ssid, 32) == 0) ||
(prefs.getString("wifipwd", wifipwd, 63) == 0)) {
In der Funktion connect_WiFi()
werden für SSID und Passwort entprechende Puffer als char
-Array angelegt. Die Puffer haben die laut WiFi-Standard maximal möglichen Längen: 32 Zeichen für die SSID, 63 Zeichen für das Passwort (jeweils plus das abschließende Null-Byte). getString()
wird dann mit Name des Schlüssels sowie Adresse und Länge des Puffers als Parametern aufgerufen. Es sind also drei Parameter, Rückgabewert ist die Länge der Zeichenkette (wird hier nicht verwendet).
bool get_time_from_ntp_server() {
struct tm tdata;
String ntp_server, timezone;
// NTP-Server-Name und Zeitzone aus NVS ermitteln oder
// Defaultwerte verwenden
ntp_server = prefs.getString("ntpserver", DEFAULT_NTP_SERVER);
In get_time_from_ntp_server()
werden ntp_server
und timezone
als String-Objekte definiert. getString()
werden der Name des Schlüssels und ein Standard-Rückgabewert als Parameter übergeben. Hier sind es zwei Parameter und die Methode liefert als Ergebnis ein String-Objekt.
configTzTime(timezone.c_str(), ntp_server.c_str());
Damit man die in den String-Objekten gespeicherten Zeichenketten später wie C-konforme Zeichenketten nutzen kann, muss man die Methode c_str()
der String-Klasse verwenden. String-Objekte bedeuten im Programm viel Overhead, scheinen mir aber die einzige Möglichkeit zu sein, Zeichenketten zu verarbeiten, deren Länge variabel oder unbekannt ist, da es in der Preference-Bibliothek keine Methode zum Ermitteln der Länge einer gespeicherten Zeichenkette gibt.
In der Funktion connect_WiFi()
wird der Hostname für den ESP32 nur gesetzt, wenn ein Eintrag im NVS-Namespace gefunden wurde. Das folgende Bild zeigt, wie der ESP32 im Router erscheint: Im Hintergrund (hellrot umrahmt) mit dem Namen „esp32-arduino”, wenn man selber keinen Namen setzt und der von der Arduino-IDE vergebene Standard-Hostname verwendet wird; im Vordergrund (leuchtend roter Rahmen) wurde mit „ESP32-Test-A” ein indivueller Hostname definiert.
Speichern und Wiederherstellen von Programmzuständen
Neben den Grundeinstellungen kann man den NVS auch nutzen, um Informationen zum Zustand des laufenden Programms zu speichern.
Bspw. ist es denkbar, dass der ESP ein bestimmtes Ereignis registriert hat (z.B. eine Benutzerinteraktion wie das Drücken eines Tasters) und deshalb (über ein Relais) einen Motor, eine Pumpe oder Lampe eingeschaltet hat. Nach einem Stromausfall und Neustart des ESP32 möchte man evtl., dass auch der Motor etc. wieder angeschaltet wird. Um das zu ermöglichen, kann man sich in einem Eintrag im NVS jede Zustandsänderung »Motor an/aus« merken und nach einem Neustart des ESP32 wiederherstellen. Im Endeffekt läuft das darauf hinaus, dass man den Zustand bestimmter GPIO-Pins im NVS speichert.
Oder man merkt sich den Zeitpunkt, an dem eine bestimmte Aktion, z.B. das tägliche Bewässern des Blumenbeets, zuletzt durchgeführt wurde. Erkennt der ESP32 nach einem Neustart anhand des im NVS gespeicherten Timestamps, dass die Aktion an diesem Tag bereits erledigt wurde, führt er sie nicht noch einmal aus. Andernfalls (bspw. nach einem etwas längeren Ausfall der Versorgungsspannung) kann er sie nachholen.
Ein einfacher Fall wird in einem der installierten Beispielprogramme gezeigt: im NVS wird ein Zähler gespeichert, der bei jedem Programmstart inkrementiert (und angezeigt) wird, d.h. es wird gezählt, wie oft der ESP (mit diesem Programm) gestartet wurde.
Man findet das Programm im Menü Datei→ Beispiele → Beispiele für ES32 Dev Module / Preferences → StartCounter
. Das Programm wird sehr ausführlich auf www.tutorialspoint.com (in Englisch) kommentiert.
Alternative Bibliotheken
Wie zu Beginn geschrieben, steht die Preferences-Bibliothek nur für den ESP32 bei Nutzung der Arduino-IDE zur Verfügung.
Auch die Arduino-Modelle bieten in Form eines EEPROMs die Möglichkeit, kleine Datenmengen nicht-flüchtig zu speichern. Beim Arduino Uno und Nano bzw. dem darauf verbauten Mikrocontroller-Chip ATmega328P sind das 1024 Bytes, bei anderen Arduino-Modellen sind zwischen 512 und 4096 Bytes EEPROM vorhanden. Um dort Daten zu speichern, kann man die EEPROM-Bibliothek nutzen.
Der ESP8266 (jedenfalls der ESP8266-12F, der auf den NodeMCUs verbaut ist, sowie der fast identische ESP8266-12E) besitzt wie der ESP32 einen Flash-Speicher von 4 MB Größe. Auch hier gibt es unter den Systembibliotheken eine EEPROM-Library, mit der man bis zu 4 KiB Daten (einen Sektor) im Flash verwalten kann. Trotz gleichen Namens ist sie an die Arduino-Bibliothek nur angelehnt, aber nicht gleich in der Anwendung.
Außerdem existieren einige Bibliotheken, die im Dateisystem auf dem Flash INI- oder JSON-Dateien schreiben und lesen können. Diese Bibliotheken muss man selbst manuell oder über die Arduino-Bibliotheksverwaltung installieren. Ihr Vorteil ist, dass man sie auf verschiedenen Mikrocontrollern nutzen kann, um portable Programme zu schreiben.
Als Beispiele seien hier ArduinoJSON und IniFile genannt.
Weitere findet man über die Suchmaschine der Wahl, bspw. mit einer Suche nach den Begriffen »arduino ini library«.
Links
- espressif stellt eine Dokumentation zur Programmierung des NVS bereit: docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/nvs_flash.html. Auf diesen Funktionen basiert die Preferences-Bibliothek.
Hier findet man auch Funktionen, die die Bibliothek nicht bereitstellt und viele Informationen zur Funktionsweise des NVS. - bei randomnerdtutorials.com gibt es einen ausführlichen Text zur Preferences-Bibliothek mit mehreren Programmbeispielen (englisch): randomnerdtutorials.com/esp32-save-data-permanently-preferences/
Fußnoten
1↑ | Siehe Datei default.csv im Partitions-Ordner des ESP32-Pakets für die Arduino-IDE; bei einer Standard-Windows-Installation im Verzeichnis C:\Users\<benutzername>\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\tools\partitions |
---|---|
2↑ | Gemeint sind hier ausdrücklich die sog. Kibibytes (s. Wikipedia), d.h. 210 = 1.024 Bytes; die NVS-Partition ist also standardmäßig 20.480 Bytes groß. |
3↑ | Mit OTA ist die Installation eines Programm-Updates im laufenden Betrieb möglich. Hierbei kann der Inhalt einer nicht zum Booten genutzten Programm-Partition z.B. per WiFi oder Bluetooth aktualisiert werden (Over The Air) statt des Hochladens über USB. (Siehe die Dokumentation bei espressif.) |
4↑ | Siehe auch die Beschreibung bei Wikipedia: Initialisierungsdatei |
5↑ | Nicht-flüchtig: Der Speicherinhalt bleibt auch beim Ausschalten der Versorgungsspannung erhalten. Beispiele: ROM, EEPROM, Flash-Speicher (z.B. in USB-Sticks oder SD-Karten), außerdem externe Speichermedien wie DVDs, SSDs oder magnetische Festplatten uva. Im Gegensatz dazu verliert flüchtiger Speicher bei einem Wegfallen der Spannung seinen Inhalt. Er kommt als Hauptspeicher (RAM) zum Einsatz und enthält u.a. während der Programmlaufzeit verwendete Daten. |
6↑ | Die der Preferences-Bibliothek zugrunde liegenden NVS-Funktionen unterstützen Fließkommazahlen nicht direkt; die Preferences-Bibliothek speichert float und double deshalb intern als – kleine – BLOBs. |
7↑ | Ein Eintrag belegt 32 Bytes: 16 für den Schlüssel (15 Zeichen plus Null-Terminierung), 8 für den Wert, 4 für den Datentyp und weitere Verwaltungsinformationen und weitere 4 für eine Prüfsumme. Die restlichen 64 Bytes der Seite (126 * 32 = 4032; Seitengröße: 4096 Bytes) werden für den Header und eine Bitmap genutzt, die die Belegung der Einträge abbildet. Zu den Details siehe die Dokumentation. |
8↑ | Siehe die NVS-Dokumentation: „String values are currently limited to 4000 bytes. This includes the null terminator. Blob values are limited to 508000 bytes or 97.6% of the partition size – 4000 bytes, whichever is lower.“ |
9↑ | Siehe die Storage-FAQ: If data needs to be stored or updated to flash every minute, can ESP32 NVS meet this requirement? |
10↑ | Das Beispielprogramm Prefs2Struct.ino liegt bei einer Windows-Installation des ESP32 in der Arduino-IDE auch hier: C:\Users\<benutzername>\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\Preferences\examples\Prefs2Struct\Prefs2Struct.ino; die Versionsnummer „1.0.6” kann je nach installierter ESP32-Paketversion anders sein; beim Schreiben dieses Beitrags im Mai 2021 ist 1.0.6 aktuell. |
11↑ | Abschnitt »Security, tampering, and robustness« der Dokumentation: „The library does try to recover from conditions when flash memory is in an inconsistent state. In particular, one should be able to power off the device at any point and time and then power it back on. This should not result in loss of data, except for the new key-value pair if it was being written at the moment of powering off. The library should also be able to initialize properly with any random data present in flash memory.“ |
12↑ | Sie sind unter Windows im SRC-Ordner des Verzeichnis C:\Users\<benutzername>\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6\libraries\Preferences zu finden; online im Github-Repository von espressif: github.com/espressif/arduino-esp32/tree/master/libraries/Preferences. |
13↑ | Verwendet man nicht die Preferences-Bibliothek, sondern direkt die NVS-Funktionen, kann man dafür die Funktion nvs_get_str() nutzen. |
14↑ | Standard-Hostname ist „esp32-arduino” – jedenfalls wenn man zum Entwickeln die Arduino-IDE verwendet; das espressif-SDK setzt ihn wohl auf „espressif”. |
15↑ | Bspw. gibt es einen Namespace mit Index 0, der die Namen und Adressen aller anderen Namespaces enthält sowie die Namen der dort jeweils gespeicherten Schlüssel. |
16↑ | Sinnvoll wäre es, dabei auch den Überlauf der vom System gemessenen Millisekunden nach knapp 50 Tagen zu berücksichtigen. Das habe ich mir der einfacheren Darstellung wegen im Programmbeispiel gespart. Da die aufgabenbezogenen Timer wie der timer_ntp im Beispiel mit Sekunden arbeiten, laufen sie nicht über, erreichen aber irgendwann gegen Ende der 50 Tage einen Wert, den der Systemtimer wegen des Überlaufs niemals erreichen wird, d.h. sie werden nie mehr ausgelöst.Beispiel: Der Systemtimer ist ein unsigned long -Integer und kann maximal den Wert 232-1 = 4.294.967.295 erreichen. (Dieser Wert ist in der vordefinierten Konstanten ULONG_MAX gespeichert; die Konstante kann der Einfachheit halber in den folgenden Rechnungen statt des Werts 232 verwendet werden.)Nach 49 Tagen sind 49*86.400*1000 = 4.233.600.000 Millisekunden vergangen. Der NTP-Timer zählt in Sekunden und hat dann den Wert 4.233.600. Einen Tag später soll der NTP-Timer wieder auslösen, wird also um 86.400 auf 4.320.000 erhöht. Der Systemtimer hat aber nach 50 Tagen nicht den Wert 4.320.000.000, sondern wegen des Überlaufs 4.320.000.000 – 232 = 25.032.704 Millisekunden bzw. 25.032 Sekunden. Das ist viel weniger als der NTP-Timer, weshalb der nach 50 Tagen (und auch danach) nicht auslöst. Man muss also bei einem Überlauf des Systemtimers auch alle anderen Timer im Programm entsprechend reduzieren, und zwar um ULONG_MAX/1000. Dann hätte der NTP-Timer den Wert 4.320.000 – ULONG_MAX/1000 = 25.032 und würde auch am 50. Tag wieder auslösen. |
Themenverwandte Beiträge
Durch einen Artikel im Heise Newsticker bin ich auf den Longan Nano der chinesischen Firma Sipeed ge...
Wie der HC-SR501 sind auch der HC-SR505, der AM312 und der SR602 pyroelektrische Infrarotsensoren (a...
Für viele Einsatzzwecke von ESP32, Arduino und Konsorten muss man die Basisfunktionalitäten, z.B. zu...
Im Januar 2019 wurden wieder einmal größere Mengen gestohlener Benutzerdaten im Darkweb angeboten – ...
Man kann Fritzboxen auch als NTP-server benutzen. Das finde ich sehr praktisch
Ja, das nutze ich in meinem lokalen Netz auch. Wie das geht, beschreibt der Hersteller AVM hier: https://avm.de/service/wissensdatenbank/dok/FRITZ-Box-7590/336_Zeitsynchronisation-NTP-fur-FRITZ-Box-und-Netzwerkgerate-einrichten/
Hallo Heiko,
vielen Dank für deinen Super Artikel zum Thema NVS / Preferences auf dem ESP32.
Hierzu habe ich ein Frage. Ich machte gerne in einem Namespace eine veränderliche Anzahl von key – value Paaren speichern. Hierzu soll bei einem Neustart des Controllers dieser Namespace ausgelesen werden und die Keys mit den Namen von beim Neustart detektierten Sensoren verglichen werden (und dann die im NVS hinterlegte Liste geupdated werden).
Preferences.h gibt hierfür keine Möglichkeit, aber anscheinend gibt es eine Möglichkeit, einen Iterator zu deklarieren, der in nvs.h (oder nvs_flash.h) definiert ist. Aber ich bin zu doof dazu.
Falls du Zeit und Lust hast, dich mit mir hierüber auszutauschen und mir zu helfen, wäre ich hocherfreut…
Vielen lieben Dank auf jeden Fall schon mal!