Preferences and NVS
A key-value store in flash that survives reboots and firmware updates. It looks like a dictionary, which is exactly why people write to it in a loop and wear the flash out in a fortnight.
How fast you can wear it out
getInt("n") on a key you saved with putFloat returns the default and no error, which is an afternoon most people spend once.What belongs in NVS
Settings, credentials, calibration constants, a device name, a boot counter. Small things, written rarely, read at startup.
Not: readings, logs, anything that accumulates. Those go to a file or a microSD card — and if they arrive faster than once a minute, they do not go in flash at all without a buffer in front of them.
The pattern worth copying
Read once at boot into a struct in RAM. Use the struct everywhere. Write back only on an actual change, or on a timer that batches several changes into one write. That turns a thousand writes a day into ten, which is the difference between a decade and a fortnight.
The code
Open a namespace, read with a default, write only when the value changed. That last condition is the whole article - without it a setting saved every loop erases the flash on a schedule.
#include <Preferences.h>
Preferences prefs;
int setpoint = 21;
void save(int v) {
prefs.begin("config", false);
if (prefs.getInt("setpoint", -999) != v) // only if it changed
prefs.putInt("setpoint", v);
prefs.end();
}
void setup() {
Serial.begin(115200);
prefs.begin("config", true); // true = read only
setpoint = prefs.getInt("setpoint", 21); // 21 is the default
prefs.end();
Serial.printf("setpoint %d\n", setpoint);
}
void loop() {}The type must match. getInt on a key stored with putFloat returns the default and reports nothing, which is an afternoon most people spend exactly once.
MicroPython exposes NVS directly, or you can keep a JSON file on the filesystem. JSON is easier to inspect; NVS survives a filesystem format.
import json, os
DEFAULTS = {'setpoint': 21, 'name': 'greenhouse'}
def load():
try:
with open('config.json') as f:
return {**DEFAULTS, **json.load(f)}
except (OSError, ValueError):
return dict(DEFAULTS)
def save(cfg):
with open('config.tmp', 'w') as f:
json.dump(cfg, f)
os.rename('config.tmp', 'config.json') # atomic
cfg = load()
print(cfg['setpoint'])Write the file to a temporary name and rename it. A power cut halfway through a rewrite otherwise leaves you with a truncated config and a board that will not start.
When it does not work
The type does not match what was stored, or the namespace name differs by a character. NVS reports neither - it just hands you the default.
The NVS partition is full or worn out. Erase it with nvs_flash_erase, and then look at how often you are writing.
NVS is its own partition, so flashing an app does not touch it. Clearing it has to be deliberate.
That is the limit, and keys are capped at 15 too. Longer names fail at begin, quietly.
When kilobytes are not enough. A card is gigabytes, an SPI bus, and a set of failure modes flash does not have.
microSD cards →Edit this page — content/esp32/preferences-and-nvs.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.