ESP32 Pico und DevKitC

ESP32: Programmkonfiguration speichern mit der Preferences-Bibliothek

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 PT_I8, PT_U8, PT_I16, PT_U16, PT_I32, PT_U32, PT_I64, PT_U64, PT_STR, PT_BLOB, PT_INVALID zurück:

  • 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
  1. /* ------------------------------------------------------------
  2.  * Ablegen von Konfigurationsdaten für den WLAN-Zugang im NVS
  3.  * 
  4.  * Name des NVS-Namespace: wifi
  5.  * Daten:
  6.  * - ssid:      Name des WLANs
  7.  * - wifipwd:   WLAN-Passwort
  8.  * - hostname:  "sprechender" Name des Geräts im Netzwerk
  9.  * - timeout:   max. Zeit (in Sekunden) für Verbindungsaufbau
  10.  * - ntpserver: Name des NTP-Servers
  11.  * - ntpfreq:   Intervall (in Stunden) für Abfrage des NTP-Servers
  12.  * - timezone:  Zeitzone
  13.  * 
  14.  * 2021-05-09 Heiko / unsinnsbasis.de
  15.  * ------------------------------------------------------------ */
  16.  
  17. /* ------------------------------------------------------------
  18.  *  Einbinden der benötigten Bibliotheken, 
  19.  *  Defintion von Konstanten,
  20.  *  Anlegen der Datenobjekte
  21.  * ------------------------------------------------------------ */
  22. // Übertragungsrate für Ausgabe zum seriellen Monitor
  23. #define SERIAL_BAUDRATE 115200
  24.  
  25. #include <Preferences.h>
  26. // Datenobjekt für einen NVS-Namespace
  27. Preferences prefs;
  28. // Name des Namespace für die WiFi-Konfigurstion
  29. const char* wifi_namespace = "wifi";
  30.  
  31. /* ------------------------------------------------------------
  32.  * Einstellungen für WiFi (NTP) und Datum/Zeit
  33.  * ------------------------------------------------------------ */
  34. const char* ssid = "HS Gastzugang";
  35. const char* password = "dein WLAN-Passwort";
  36. const char* hostname = "ESP32-Test-A";
  37. const char* ntpServer = "fritz.box";    // z.B. (de.)pool.ntp.org
  38.   // wie lange (in Sekunden) soll Verbindungsaufbau versucht werden
  39. #define WIFI_TIMEOUT 60
  40.   // Häufigkeit (in Stunden) der NTP-Server-Abfrage
  41. #define NTP_FREQ 24
  42. // lokale Zeitzone definieren
  43. // s. auch https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  44. #define TIMEZONE PSTR("CET-1CEST,M3.5.0,M10.5.0/3")  // Europa/Berlin
  45.  
  46.  
  47. void setup() {
  48.   bool f_error = false;  //Fehler-Flag
  49.  
  50.   Serial.begin(SERIAL_BAUDRATE);
  51.   delay(500);
  52.   // Namespace zum Schreiben öffnen (zweiter Parameter "readonly"
  53.   // kann entfallen, da per Default auf "false")
  54.   if (!prefs.begin(wifi_namespace)) {
  55.     Serial.println("Fehler beim Öffnen des NVS-Namespace");
  56.     for (;;);  // leere Dauerschleife -> Ende
  57.   }
  58.   Serial.printf("Anzahl freier Einträge: %d\n", prefs.freeEntries());
  59.   prefs.clear();
  60.   Serial.printf("Anzahl freier Einträge nach dem Init./Löschen: %d\n", prefs.freeEntries());
  61.   if (prefs.putString("ssid", ssid) == 0)
  62.     f_error = true;
  63.   if (prefs.putString("wifipwd", password) == 0)
  64.     f_error = true;
  65.   if (prefs.putString("hostname", hostname) == 0)
  66.     f_error = true;
  67.   if (prefs.putInt("timeout", WIFI_TIMEOUT) == 0)
  68.     f_error = true;
  69.   if (prefs.putString("ntpserver", ntpServer) == 0)
  70.     f_error = true;
  71.   if (prefs.putInt("ntpfreq", NTP_FREQ) == 0)
  72.     f_error = true;
  73.   if (prefs.putString("timezone", TIMEZONE) == 0)
  74.     f_error = true;
  75.  
  76.   if (f_error)
  77.     Serial.println("Fehler beim Speichern mindestens eines Eintrags");
  78.  
  79.   Serial.printf("Anzahl freier Einträge nach dem Speichern: %d\n", prefs.freeEntries());
  80.   prefs.end();
  81. }
  82.  
  83. void loop() {
  84. }

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:

  1.   if (!prefs.begin(wifi_namespace, false)) {
  1.   if (!prefs.begin(wifi_namespace)) {
Preferences-Bibliothek: Freie Einträge beim Speichern im NVS
Preferences-Bibliothek: Freie Einträge beim Speichern im NVS

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.

Ein NTP-Server ist ein Server im Internet, der anderen Rechnern per NTP (network time protocol) eine Abfrage der aktuellen Uhrzeit ermöglicht. Hierdurch können Geräte mit Internetzugang ihre Uhrzeit synchronisieren, ohne dass eine batteriegepufferte, korrekt eingestellte Echtzeituhr nötig ist. Oft kann der Router im lokalen Netz diese Funktion übernehmen, wie bei mir die Fritz!Box. Da NTP-Server die Zeit der Zeitzone UTC (früher: GMT = Greenwich Mean Time) liefern, sollte man außerdem die eigene Zeitzone einstellen – dann wird z.B. auch die automatische Umstellung Sommer-/Winterzeit berücksichtigt. Ausführliche Informationen zu NTP-Servern und zum Einstellen der lokalen Zeitzone enthält der Beitrag zur TM1637-LED-Anzeige.
  1. /* ------------------------------------------------------------
  2.  * Konfigurationsdaten für den WLAN-Zugang aus NVS lesen
  3.  *
  4.  * Name des NVS-Namespace: wifi
  5.  * Daten:
  6.  * - ssid:      Name des WLANs
  7.  * - wifipwd:   WLAN-Passwort
  8.  * - hostname:  "sprechender" Name des Geräts im Netzwerk
  9.  * - timeout:   max. Zeit (in Sekunden) für Verbindungsaufbau
  10.  * - ntpserver: Name des NTP-Servers
  11.  * - ntpfreq:   Intervall (in Stunden) für Abfrage des NTP-Servers
  12.  * - timezone:  Zeitzone
  13.  *
  14.  * 2021-05-09 Heiko / unsinnsbasis.de
  15.  * ------------------------------------------------------------ */
  16.  
  17. /* ------------------------------------------------------------
  18.  *  Einbinden der benötigten Bibliotheken,
  19.  *  Defintion von Konstanten,
  20.  *  Anlegen der Datenobjekte
  21.  * ------------------------------------------------------------ */
  22. // Übertragungsrate für Ausgabe zum seriellen Monitor
  23. #define SERIAL_BAUDRATE 115200
  24.  
  25. #include <Preferences.h>
  26. // Datenobjekt für einen NVS-Namespace
  27. Preferences prefs;
  28. // Name des Namespace für die WiFi-Konfigurstion
  29. const char* wifi_namespace = "wifi";
  30.  
  31. /* ------------------------------------------------------------
  32.  * WiFi (NTP) und Datum/Zeit
  33.  * ------------------------------------------------------------ */
  34. #include <WiFi.h>
  35. #include <time.h>
  36. // Default-Werte für einige Einstellungen
  37. // werden genutzt, wenn kein passender Wert im NVS gefunden wid
  38. #define DEFAULT_WIFI_TIMEOUT 60
  39. #define DEFAULT_NTP_SERVER PSTR("de.pool.ntp.org")
  40. #define DEFAULT_TIMEZONE PSTR("CET-1CEST,M3.5.0,M10.5.0/3")  // Europa/Berlin
  41. #define DEFAULT_NTP_FREQ 24
  42. int ntp_freq;
  43.  
  44. /* Timer für verschiedene Aufgaben:
  45.  * - alle n Stunden die Uhrzeit per NTP-Abfrage aktualisieren
  46.  * - ggf. weitere Timer, z.B. alle n Sekunden Sensoren abfragen,
  47.  *   eine Anzeige aktualisieren etc.
  48.  */
  49. unsigned long timer_ntp=0,   //NTP-Server abfragen
  50.               timer_time=0;  // Uhrzeit anzeigen
  51.  
  52. /* ------------------------------------------------------------ */
  53.  
  54. void setup() {
  55.   bool f_error = false;  //Fehler-Flag
  56.  
  57.   Serial.begin(SERIAL_BAUDRATE);
  58.   delay(500);
  59.     // Namespace zum Lesen öffnen
  60.   if (!prefs.begin(wifi_namespace, true)) {
  61.     Serial.println("Fehler beim Öffnen des NVS-Namespace");
  62.     for (;;);  // leere Dauerschleife -> Ende
  63.   }
  64.   // ... Initialisieren von Sensoren etc.
  65. }
  66.  
  67. void loop() {
  68.   unsigned long timer;  // Systemtimer in Sekunden 
  69.   time_t now;
  70.   struct tm tdata;
  71.  
  72.   timer = millis() / 1000;
  73.   // alle paar Stunden die Zeit von einem NTP-Server holen
  74.   // (auch ganz am Anfang, da ist timer_ntp = 0)
  75.   if (timer >= timer_ntp) {
  76.     if (get_time_from_ntp_server()) {  // Zeit einstellen
  77.       timer_ntp += 3600 * ntp_freq;  // nächste Sync. in x Stunden
  78.     } else {
  79.       // im Fehlerfall nach einiger Zeit (n Sekunden) nochmal versuchen
  80.       timer_ntp += 3600;
  81.     }
  82.   }
  83.  
  84.   // alle 30 Sekunden die Uhrzeit anzeigen
  85.   if (timer >= timer_time) {
  86.     now = time(NULL);
  87.     localtime_r(&now, &tdata);
  88.     Serial.printf("%04d-%02d-%02d %02d:%02d:%02d\n", 
  89.         tdata.tm_year+1900, tdata.tm_mon+1, tdata.tm_mday,
  90.         tdata.tm_hour, tdata.tm_min, tdata.tm_sec);
  91.     timer_time += 30;
  92.   }
  93.  
  94.   delay(100);
  95. }
  96.  
  97. /* ------------------------------------------------------------
  98.  *  Verbindung zum WLAN herstellen
  99.  *  - SSID und Passwort aus NVS lesen (müssen vorhanden sein)
  100.  *  - max. Dauer für Verbindungsaufbau aus NVS lesen
  101.  *    (bei Fehler, z.B. nicht vorhanden) Defaultwert verwenden
  102.  *
  103.  *  Returncode:
  104.  *  - true:  WLAN-Verbindung wurde hergestellt
  105.  *  - false: Verbindungsaufbau fehlgeschlagen
  106.  * ------------------------------------------------------------ */
  107. bool connect_WiFi() {
  108.   struct tm tdata;
  109.   unsigned long timer;
  110.   // Variablen für die Werte aus dem NVS
  111.   char ssid[33];      // Maximallänge lt. Standard: 32
  112.   char wifipwd[64];   // Maximallänge lt. Standard: 63 (mind. 8)
  113.   char hostname[64];  // Maximallänge lt. Standard: 63
  114.   int wifi_timeout;
  115.  
  116.   if ((prefs.getString("ssid", ssid, 32) == 0) ||
  117.       (prefs.getString("wifipwd", wifipwd, 63) == 0)) {
  118.     return false;
  119.   }
  120.   wifi_timeout = prefs.getInt("timeout",  DEFAULT_WIFI_TIMEOUT);
  121.   // Hostnamen setzen, wenn im NVS einer angegeben ist
  122.   // (muss vor WiFi.begin() erfolgen)
  123.   if (prefs.isKey("hostname")) {
  124.     prefs.getString("hostname", hostname, 63);
  125.     WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
  126.     WiFi.setHostname(hostname);
  127.   }
  128.   // mit dem WLAN verbinden
  129.   Serial.printf("Verbindung herstellen mit %s ", ssid);
  130.   timer = millis() / 1000;
  131.   WiFi.mode(WIFI_STA);
  132.   WiFi.begin(ssid, wifipwd);
  133.   while (WiFi.status() != WL_CONNECTED && (millis() / 1000) < timer + wifi_timeout) {
  134.       delay(500);
  135.       Serial.print(".");
  136.   }
  137.   if (WiFi.status() != WL_CONNECTED) {
  138.     Serial.println("\nTimeout bei Verbindungsaufbau!");
  139.     return false;
  140.   } else {
  141.     Serial.print("\nVerbunden! IP-Adresse: ");
  142.     Serial.println(WiFi.localIP());
  143.     return true;
  144.   }
  145. }
  146.  
  147. /* ------------------------------------------------------------
  148.  *  Aktuelle Zeit von einem NTP-Server holen
  149.  *  - dazu ggf. Wifi aktivieren
  150.  *  - Returncode false, wenn keine Wifi-Verbindung zustande kommt
  151.  * ------------------------------------------------------------ */
  152. bool get_time_from_ntp_server() {
  153.   struct tm tdata;
  154.   String ntp_server, timezone;
  155.  
  156.   // NTP-Server-Name und Zeitzone aus NVS ermitteln oder
  157.   // Defaultwerte verwenden
  158.   ntp_server = prefs.getString("ntpserver", DEFAULT_NTP_SERVER);
  159.   timezone   = prefs.getString("timezone", DEFAULT_TIMEZONE);
  160.  
  161.   if (WiFi.status() != WL_CONNECTED) {
  162.     // ggf. mit dem WLAN verbinden (falls noch nicht geschehen)
  163.     connect_WiFi();
  164.   }
  165.   if (WiFi.status() != WL_CONNECTED) {
  166.     // keine WLAN-Verbindung -> zurück mit Fehler
  167.     return false;
  168.   } else {
  169.     // Zeit vom NTP-Server holen und Zeitzone einstellen
  170.     configTzTime(timezone.c_str(), ntp_server.c_str());
  171.     // einmal getLocalTime() aufrufen; sonst wird die Zeit nicht
  172.     // übernommen
  173.     getLocalTime(&tdata);
  174.     return true;
  175.   }
  176. }

Einige Erläuterungen zum Programm:

  1. /* Timer für verschiedene Aufgaben:
  2.  * - alle n Stunden die Uhrzeit per NTP-Abfrage aktualisieren
  3.  * - ggf. weitere Timer, z.B. alle n Sekunden Sensoren abfragen,
  4.  *   eine Anzeige aktualisieren etc.
  5.  */
  6. unsigned long timer_ntp, timer_time;
  1.   timer = millis() / 1000;
  2.   // alle paar Stunden die Zeit von einem NTP-Server holen
  3.   // (auch ganz am Anfang, da ist timer_ntp = 0)
  4.   if (timer >= timer_ntp) {
  5.     if (get_time_from_ntp_server()) {  // Zeit einstellen
  6.       timer_ntp += 3600 * ntp_freq;  // nächste Sync. in x Stunden
  7.     } else {
  8.       // im Fehlerfall nach einiger Zeit (n Sekunden) nochmal versuchen
  9.       timer_ntp += 600;
  10.     }
  11.   }

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:

  1. bool connect_WiFi() {
  2.   struct tm tdata;
  3.   unsigned long timer;
  4.   // Variablen für die Werte aus dem NVS
  5.   char ssid[33];      // Maximallänge lt. Standard: 32
  6.   char wifipwd[64];   // Maximallänge lt. Standard: 63
  7.   // ...
  8.   // ...
  9.  
  10.   if ((prefs.getString("ssid", ssid, 32) == 0) ||
  11.       (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).

  1. bool get_time_from_ntp_server() {
  2.   struct tm tdata;
  3.   String ntp_server, timezone;
  4.  
  5.   // NTP-Server-Name und Zeitzone aus NVS ermitteln oder
  6.   // Defaultwerte verwenden
  7.   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.

  1.     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.

ESP32-Hostname in der Router-Ansicht. Standard- und individueller Name
ESP32-Hostname in der Router-Ansicht. Standard- und individueller Name

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«.

Fußnoten

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.

Ein Kommentar

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht.