Chapter 22: Non-Volatile Storage (NVS) Library in ESP-IDF
Chapter Objectives
- Understand the concept of non-volatile storage and its importance.
- Learn about the ESP-IDF Non-Volatile Storage (NVS) library.
- Understand the key-value pair structure used by NVS.
- Learn how to use namespaces to organize data within NVS.
- Initialize the NVS flash partition.
- Open and close NVS handles.
- Read and write basic data types (integers, strings) to NVS.
- Read and write binary data blobs to NVS.
- Understand the importance of committing changes to NVS.
- Implement error handling for NVS operations.
- Learn how to erase NVS data.
Introduction
In our journey so far, we’ve worked primarily with data stored in the ESP32’s RAM (Random Access Memory). RAM is fast and essential for program execution, but it has a crucial limitation: it’s volatile. This means that whenever the ESP32 loses power or is reset, all data stored in RAM is lost.
For many applications, we need to store information persistently, so it survives reboots. Examples include:
- Device configuration settings (e.g., WiFi credentials, calibration data).
- Application state (e.g., user preferences, last known state).
- Usage counters (e.g., number of boot cycles, operational hours).
- Small amounts of sensor logs or cached data.
This is where non-volatile storage comes in. The ESP32’s onboard flash memory provides this capability, and the ESP-IDF offers a convenient library called Non-Volatile Storage (NVS) specifically designed for storing small pieces of data in a structured way. This chapter explores how to effectively use the NVS library to give your applications memory that persists.
Theory
What is Non-Volatile Storage?
Non-volatile storage refers to memory that retains its contents even when power is removed. The primary non-volatile storage medium on the ESP32 is its internal SPI flash memory chip – the same chip where your program firmware is stored.
While you could theoretically read and write directly to raw flash memory locations, it’s complex and risky. Raw flash has limitations:
- Erase Cycles: Flash memory has a limited number of erase/write cycles per sector (typically 10,000 to 100,000). Frequent raw writes to the same location can wear it out quickly.
- Erase Granularity: Flash must be erased in larger blocks (sectors or pages, typically 4KB) before individual bytes can be rewritten to ‘0’. Writing directly requires careful management of these erase operations.
- Power Loss Corruption: A power loss during a raw write operation can leave the flash sector in a corrupted state.
The NVS Library Solution
The ESP-IDF NVS library provides a higher-level abstraction over the raw flash, specifically designed for storing key-value data efficiently and safely. It addresses the limitations of raw flash access:
- Wear Leveling: NVS distributes writes across the flash partition to minimize wear on any single sector, extending the lifespan of the flash memory.
- Abstraction: It hides the complexities of flash sector erasing and writing.
- Atomicity & Recovery: NVS is designed to minimize the chance of data loss due to sudden power cuts during write operations. It uses journaling and redundant entries to ensure data integrity.
- Key-Value Store: It organizes data using familiar key-value pairs, making it easy to store and retrieve individual pieces of information.
NVS Structure: Partitions, Namespaces, and Key-Value Pairs
The NVS library operates on a dedicated region of the flash memory defined in the partition table. By default, ESP-IDF projects include an NVS partition labeled "nvs"
.
Within the NVS partition, data is organized hierarchically:
- Namespaces: Think of namespaces as folders or directories within the NVS partition. They group related key-value pairs together, preventing key name collisions between different parts of your application or different libraries. Each namespace has a unique name (string, up to 15 characters).
- Keys: Within each namespace, data is stored using unique keys (strings, up to 15 characters).
- Values: Associated with each key is the actual data value. NVS supports storing various data types, including:
- Integers (8-bit, 16-bit, 32-bit, 64-bit, signed and unsigned)
- Strings (null-terminated C strings)
- Binary Large Objects (BLOBs – arbitrary blocks of binary data)
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#EDE9FE', 'primaryTextColor': '#5B21B6', 'primaryBorderColor': '#5B21B6', /* Purple for partition */ 'secondaryColor': '#DBEAFE', 'secondaryTextColor': '#1E40AF', 'secondaryBorderColor': '#2563EB', /* Blue for namespace */ 'tertiaryColor': '#FEF3C7', 'tertiaryTextColor': '#92400E', 'tertiaryBorderColor': '#D97706', /* Amber for key/value */ 'lineColor': '#A78BFA', 'textColor': '#1F2937', 'mainBkg': '#FFFFFF', 'nodeBorder': '#A78BFA', 'fontFamily': '"Open Sans", sans-serif' } } }%% graph LR NVS["NVS Partition<br>(On SPI Flash)"]:::partitionStyle; NVS --> NS1["Namespace: 'wifi_config'"]:::namespaceStyle; NVS --> NS2["Namespace: 'device_stats'"]:::namespaceStyle; NVS --> NS3["Namespace: 'user_prefs'"]:::namespaceStyle; subgraph "Namespace: 'wifi_config'" direction LR NS1_Key1["Key: 'ssid'"]:::keyStyle; NS1_Val1["Value: 'MyNetwork' (String)"]:::valueStyle; NS1_Key2["Key: 'password'"]:::keyStyle; NS1_Val2["Value: 'secret123' (String)"]:::valueStyle; NS1_Key3["Key: 'channel'"]:::keyStyle; NS1_Val3["Value: 6 (Integer)"]:::valueStyle; NS1_Key1 --- NS1_Val1; NS1_Key2 --- NS1_Val2; NS1_Key3 --- NS1_Val3; end subgraph "Namespace: 'device_stats'" direction LR NS2_Key1["Key: 'boot_count'"]:::keyStyle; NS2_Val1["Value: 105 (Integer)"]:::valueStyle; NS2_Key2["Key: 'cal_data'"]:::keyStyle; NS2_Val2["Value: {0xABC..} (Blob)"]:::valueStyle; NS2_Key1 --- NS2_Val1; NS2_Key2 --- NS2_Val2; end subgraph "Namespace: 'user_prefs'" direction LR NS3_Key1["Key: 'theme'"]:::keyStyle; NS3_Val1["Value: 'dark' (String)"]:::valueStyle; NS3_Key1 --- NS3_Val1; end %% Styling classDef partitionStyle fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6,font-weight:bold; classDef namespaceStyle fill:#DBEAFE,stroke:#2563EB,stroke-width:1.5px,color:#1E40AF,font-weight:bold; classDef keyStyle fill:#FEF9C3,stroke:#F59E0B,stroke-width:1px,color:#B45309; classDef valueStyle fill:#F3F4F6,stroke:#9CA3AF,stroke-width:1px,color:#374151;
Core NVS API Concepts (nvs_flash.h
and nvs.h
)
Working with NVS generally involves these steps:
- Initialization (
nvs_flash_init
): Before any NVS operations can occur, the NVS flash partition must be initialized. This function checks the partition’s state and prepares it for use. It should typically be called once early in your application’s startup sequence (app_main
). - Opening a Namespace (
nvs_open
): To read or write data, you need to “open” the desired namespace. This function takes the namespace name and the desired access mode (NVS_READONLY
orNVS_READWRITE
) and returns a handle (nvs_handle_t
). This handle is used for subsequent read/write operations within that namespace. - Reading/Writing Data (
nvs_get_...
,nvs_set_...
): Use functions likenvs_get_i32
,nvs_set_i32
,nvs_get_str
,nvs_set_str
,nvs_get_blob
,nvs_set_blob
to read or write values associated with specific keys within the opened namespace. These functions require the namespace handle, the key name, and a pointer to store/retrieve the value. For string and blob types, you also need to handle buffer sizes. - Committing Changes (
nvs_commit
): Write operations (nvs_set_...
) are often cached in RAM initially for performance. To ensure the changes are physically written to the flash memory and become persistent, you must callnvs_commit()
using the namespace handle after making modifications. Forgetting to commit is a common source of errors where data seems to disappear after a reboot. - Closing the Handle (
nvs_close
): Once you are finished reading/writing within a namespace, you must close the handle usingnvs_close()
. This releases resources associated with the opened namespace. It’s crucial to close handles to prevent resource leaks.
Function | Header | Purpose |
---|---|---|
nvs_flash_init() |
nvs_flash.h |
Initializes the default NVS partition. Must be called once before any other NVS operations. Handles partition checking and recovery setup. |
nvs_flash_erase() |
nvs_flash.h |
Erases the entire default NVS partition. Use with caution, as it deletes all stored data. Often used during development or if nvs_flash_init fails with specific errors. |
nvs_open() |
nvs.h |
Opens a specific namespace within the NVS partition. Requires namespace name and access mode (NVS_READONLY or NVS_READWRITE ). Returns an nvs_handle_t required for subsequent operations. |
nvs_get_type() (e.g., nvs_get_i32 , nvs_get_str , nvs_get_blob ) |
nvs.h |
Reads a value of a specific type associated with a key from an opened namespace. Requires handle, key name, and a pointer to store the retrieved value. For strings/blobs, handles buffer sizing. |
nvs_set_type() (e.g., nvs_set_i32 , nvs_set_str , nvs_set_blob ) |
nvs.h |
Writes a value of a specific type associated with a key to an opened namespace (must be opened ReadWrite). Requires handle, key name, and the value/data to store. Changes might be cached initially. |
nvs_commit() |
nvs.h |
Writes any pending changes (from nvs_set_... calls) for the given handle from the cache to the physical flash storage, making them persistent. Crucial step after writing data. |
nvs_close() |
nvs.h |
Closes an opened NVS handle, releasing associated resources. Must be called for every handle obtained via nvs_open . |
nvs_erase_key() |
nvs.h |
Removes a specific key-value pair from the namespace. Requires handle and key name. Needs nvs_commit() afterwards to make the deletion persistent. |
nvs_erase_all() |
nvs.h |
Removes all key-value pairs within the opened namespace. Requires handle. Needs nvs_commit() afterwards to make the deletion persistent. |
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB', /* Blue */ 'secondaryColor': '#FEF3C7', 'secondaryTextColor': '#92400E', 'secondaryBorderColor': '#D97706', /* Amber */ 'tertiaryColor': '#D1FAE5', 'tertiaryTextColor': '#065F46', 'tertiaryBorderColor': '#059669', /* Green */ 'errorColor': '#FEE2E2', 'errorTextColor': '#991B1B', 'errorBorderColor': '#DC2626', /* Red */ 'lineColor': '#A78BFA', 'textColor': '#1F2937', 'mainBkg': '#FFFFFF', 'nodeBorder': '#A78BFA', 'fontFamily': '"Open Sans", sans-serif' } } }%% graph TD Start["Start Write Operation"]:::startNode --> OpenNVS["nvs_open(namespace, NVS_READWRITE, &handle)"]:::processNode; OpenNVS -- OK --> SetValue["nvs_set_<i>type</i>(handle, key, value)"]:::processNode; OpenNVS -- Error --> HandleOpenError["Handle/Log Open Error"]:::errorNode; HandleOpenError --> Stop1([Stop/Fail]); SetValue -- OK --> Commit["nvs_commit(handle)"]:::commitNode; SetValue -- Error --> HandleSetError["Handle/Log Set Error"]:::errorNode; HandleSetError --> CloseNVS["nvs_close(handle)"]:::processNode; Commit -- OK --> CloseNVS; Commit -- Error --> HandleCommitError["Handle/Log Commit Error"]:::errorNode; HandleCommitError --> CloseNVS; CloseNVS --> EndWrite([Write Sequence Complete]):::endNode; Stop1 --> EndWrite; %% Styling classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef commitNode fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#92400E,font-weight:bold; classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB', /* Blue */ 'secondaryColor': '#FEF3C7', 'secondaryTextColor': '#92400E', 'secondaryBorderColor': '#D97706', /* Amber */ 'tertiaryColor': '#D1FAE5', 'tertiaryTextColor': '#065F46', 'tertiaryBorderColor': '#059669', /* Green */ 'errorColor': '#FEE2E2', 'errorTextColor': '#991B1B', 'errorBorderColor': '#DC2626', /* Red */ 'lineColor': '#A78BFA', 'textColor': '#1F2937', 'mainBkg': '#FFFFFF', 'nodeBorder': '#A78BFA', 'fontFamily': '"Open Sans", sans-serif' } } }%% graph TD Start["Start Read Operation"]:::startNode --> OpenNVS["nvs_open(namespace, NVS_READONLY or NVS_READWRITE, &handle)"]:::processNode; OpenNVS -- OK --> GetValue["nvs_get_<i>type</i>(handle, key, &buffer, [optional &size])"]:::processNode; OpenNVS -- Error --> HandleOpenError["Handle/Log Open Error"]:::errorNode; HandleOpenError --> Stop1([Stop/Fail]); GetValue -- ESP_OK --> UseValue["Use the read value (buffer)"]:::endNode; GetValue -- ESP_ERR_NVS_NOT_FOUND --> HandleNotFound["Handle Not Found<br>(e.g., use default value)"]:::notFoundNode; GetValue -- "Other Error" --> HandleGetError["Handle/Log Get Error"]:::errorNode; HandleNotFound --> CloseNVS["nvs_close(handle)"]:::processNode; HandleGetError --> CloseNVS; UseValue --> CloseNVS; CloseNVS --> EndRead([Read Sequence Complete]):::endNode; Stop1 --> EndRead; %% Styling classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef notFoundNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
Error Handling
Almost all NVS API functions return an esp_err_t
value. It is essential to check these return codes after every NVS operation. Common error codes include:
Error Code | Meaning | Common Causes / Notes |
---|---|---|
ESP_OK |
Success | The operation completed successfully. |
ESP_ERR_NVS_NOT_INITIALIZED |
NVS Not Initialized | nvs_flash_init() was not called or failed. |
ESP_ERR_NVS_NOT_FOUND |
Key Not Found | Returned by nvs_get_... or nvs_erase_key when the specified key does not exist in the namespace. Often expected on first boot; handle by providing default values. |
ESP_ERR_NVS_INVALID_HANDLE |
Invalid Handle | The provided nvs_handle_t is NULL, invalid, or was already closed via nvs_close() . |
ESP_ERR_NVS_INVALID_NAME |
Invalid Name | Namespace or key name is NULL, empty, too long (> 15 chars), or contains invalid characters. |
ESP_ERR_NVS_INVALID_LENGTH |
Invalid Length | Buffer too small for reading string/blob, or data size mismatch. Check required size first. |
ESP_ERR_NVS_READ_ONLY |
Namespace Read-Only | A write operation (nvs_set_... , nvs_commit , nvs_erase_... ) was attempted on a handle opened with NVS_READONLY mode. |
ESP_ERR_NVS_NO_FREE_PAGES |
No Free Pages | NVS partition is full or too fragmented. May occur during nvs_flash_init (requires erase) or during write/commit operations. Consider increasing partition size or reducing data stored. |
ESP_ERR_NVS_NEW_VERSION_FOUND |
New NVS Version | The NVS partition contains data from an older, incompatible NVS library version. Returned by nvs_flash_init() . Requires erasing the partition (nvs_flash_erase() ) before retrying init. |
ESP_FAIL |
Generic Failure | An unspecified internal error occurred. |
Proper error handling allows your application to react gracefully, perhaps by using default values if a key isn’t found or by logging critical errors if NVS fails entirely.
NVS Partition Size
The size of the NVS partition is defined in the project’s partition table (usually partitions.csv
). The default size is often around 24KB, but this can be adjusted in menuconfig
(Partition Table
-> Partition Table
-> (Custom partition table CSV)
). The amount of data you can store depends on this size, the overhead of the NVS structure (page headers, entry headers), and the number/size of your key-value pairs.
Practical Examples
NVS Write/Commit/Read Simulation
RAM Value (Current)
NVS Value (Persistent)
Project Setup:
- Use a standard ESP-IDF project template.
- Ensure VS Code with the ESP-IDF extension is set up.
- No additional hardware is needed.
Common Includes:
Add these includes to your main C file:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h" // Required for esp_restart
#include "nvs_flash.h" // Required for NVS initialization
#include "nvs.h" // Required for NVS operations
#include "esp_log.h"
static const char *TAG = "NVS_EXAMPLE";
Example 1: Initializing NVS and Handling Errors
This example shows the basic initialization sequence, including handling potential errors that might require erasing the NVS partition (e.g., if it’s corrupted or from an old incompatible version).
void app_main(void)
{
ESP_LOGI(TAG, "Initializing NVS...");
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_LOGW(TAG, "NVS init failed (%s), erasing NVS...", esp_err_to_name(ret));
ESP_ERROR_CHECK(nvs_flash_erase()); // Erase the partition
ret = nvs_flash_init(); // Retry initialization
}
ESP_ERROR_CHECK(ret); // Check the result of the initialization (or retry)
ESP_LOGI(TAG, "NVS Initialized Successfully.");
// --- Your application code using NVS would go here ---
ESP_LOGI(TAG, "Example finished. Restarting in 5 seconds...");
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart(); // Restart to demonstrate persistence in later examples
}
Build, Flash, and Monitor:
- Build:
idf.py build
- Flash:
idf.py -p <YOUR_PORT> flash
- Monitor:
idf.py -p <YOUR_PORT> monitor
Expected Output:
On the first run (or after nvs_flash_erase
), you should see:
I (xxx) NVS_EXAMPLE: Initializing NVS...
I (xxx) NVS_EXAMPLE: NVS Initialized Successfully.
I (xxx) NVS_EXAMPLE: Example finished. Restarting in 5 seconds...
If the NVS partition had issues, you might see the warning about erasing it first. After the restart, subsequent runs should just show the successful initialization message.
Example 2: Storing and Retrieving a Boot Counter
This example demonstrates reading and writing an integer value (int32_t
) to track how many times the device has booted.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
static const char *TAG = "NVS_BOOT_COUNTER";
const char* NVS_NAMESPACE = "storage"; // Define a namespace
const char* BOOT_COUNT_KEY = "boot_count"; // Define a key
void app_main(void)
{
// Initialize NVS (same as Example 1)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS init failed (%s), erasing NVS...", esp_err_to_name(ret));
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS Initialized.");
// --- NVS Operations ---
nvs_handle_t my_handle;
int32_t boot_count = 0; // Default value if not found
// 1. Open NVS Namespace
ESP_LOGI(TAG, "Opening NVS namespace: %s", NVS_NAMESPACE);
ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &my_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "NVS handle opened successfully.");
// 2. Read the boot count
ESP_LOGI(TAG, "Reading boot count from NVS key: %s", BOOT_COUNT_KEY);
ret = nvs_get_i32(my_handle, BOOT_COUNT_KEY, &boot_count);
switch (ret) {
case ESP_OK:
ESP_LOGI(TAG, "Successfully read boot_count = %" PRIi32, boot_count);
break;
case ESP_ERR_NVS_NOT_FOUND:
ESP_LOGW(TAG, "The value is not initialized yet!");
// boot_count remains 0 (our default)
break;
default :
ESP_LOGE(TAG, "Error (%s) reading!", esp_err_to_name(ret));
}
// 3. Increment the boot count
boot_count++;
ESP_LOGI(TAG, "Current boot count is: %" PRIi32, boot_count);
// 4. Write the new boot count
ESP_LOGI(TAG, "Writing updated boot count (%d) to NVS...", (int)boot_count);
ret = nvs_set_i32(my_handle, BOOT_COUNT_KEY, boot_count);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) writing!", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Write successful.");
}
// 5. Commit written value
ESP_LOGI(TAG, "Committing updates in NVS...");
ret = nvs_commit(my_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) committing updates!", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Commit successful.");
}
// 6. Close NVS
nvs_close(my_handle);
ESP_LOGI(TAG, "NVS handle closed.");
}
ESP_LOGI(TAG, "Restarting in 10 seconds to see updated count...");
vTaskDelay(pdMS_TO_TICKS(10000));
esp_restart();
}
Build, Flash, and Monitor:
- Build, flash, and monitor as before.
- Let the device run and restart automatically. Observe the monitor output each time.
Expected Output:
- First Run: It will likely log
ESP_ERR_NVS_NOT_FOUND
when reading, then write and commitboot_count = 1
. - Second Run: It should successfully read
boot_count = 1
, then write and commitboot_count = 2
. - Subsequent Runs: The boot count will continue to increment each time the device restarts.
Example 3: Storing and Retrieving a String
This example shows how to store and load a configuration string, handling potential buffer size issues.
#include <stdio.h>
#include <string.h> // For strlen, strcpy
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
static const char *TAG = "NVS_STRING_EXAMPLE";
const char* NVS_NAMESPACE = "config"; // Use a different namespace
const char* DEVICE_ID_KEY = "device_id"; // Key for the string
void app_main(void)
{
// Initialize NVS (same as Example 1)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS init failed (%s), erasing NVS...", esp_err_to_name(ret));
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS Initialized.");
nvs_handle_t config_handle;
// Open NVS Namespace
ESP_LOGI(TAG, "Opening NVS namespace: %s", NVS_NAMESPACE);
ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &config_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(ret));
return; // Cannot proceed
} else {
ESP_LOGI(TAG, "NVS handle opened successfully.");
// --- Write a default string if not found ---
size_t required_size = 0;
// First, check if the key exists by querying the size with a NULL buffer
ret = nvs_get_str(config_handle, DEVICE_ID_KEY, NULL, &required_size);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGW(TAG, "Device ID not found in NVS. Writing default value.");
const char* default_device_id = "ESP32-Default-XYZ";
ret = nvs_set_str(config_handle, DEVICE_ID_KEY, default_device_id);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) writing default device ID!", esp_err_to_name(ret));
} else {
// Commit the newly written default value
ret = nvs_commit(config_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) committing default device ID!", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Default device ID written and committed.");
}
}
} else if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) checking for device ID!", esp_err_to_name(ret));
}
// --- Read the string value ---
// Determine required size again (in case it was just written or already existed)
ret = nvs_get_str(config_handle, DEVICE_ID_KEY, NULL, &required_size);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Device ID found, required size = %d", required_size);
// Allocate buffer (+1 for null terminator)
char* device_id_buffer = malloc(required_size);
if (device_id_buffer == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for device ID buffer!");
} else {
// Read the string into the buffer
ret = nvs_get_str(config_handle, DEVICE_ID_KEY, device_id_buffer, &required_size);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Successfully read Device ID = '%s'", device_id_buffer);
} else {
ESP_LOGE(TAG, "Error (%s) reading device ID!", esp_err_to_name(ret));
}
free(device_id_buffer); // Free the allocated buffer
}
} else if (ret == ESP_ERR_NVS_NOT_FOUND) {
// This case should ideally not happen if we wrote the default above, but good practice to handle
ESP_LOGW(TAG, "Device ID key disappeared unexpectedly!");
}
else {
ESP_LOGE(TAG, "Error (%s) getting size for device ID!", esp_err_to_name(ret));
}
// Close NVS
nvs_close(config_handle);
ESP_LOGI(TAG, "NVS handle closed.");
}
ESP_LOGI(TAG, "Example finished.");
// No restart this time
}
Build, Flash, and Monitor:
- Build, flash, and monitor.
Expected Output:
- First Run: It will log
ESP_ERR_NVS_NOT_FOUND
when checking, then write the default string “ESP32-Default-XYZ”, commit it, and finally read and print it. - Subsequent Runs: It will find the key, determine the size, allocate a buffer, read the string “ESP32-Default-XYZ”, and print it.
Example 4: Storing and Retrieving a Binary Blob (Struct)
This example shows how to store arbitrary binary data, like a C struct.
#include <stdio.h>
#include <string.h> // For memcpy
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
static const char *TAG = "NVS_BLOB_EXAMPLE";
const char* NVS_NAMESPACE = "calibration"; // Yet another namespace
const char* CAL_DATA_KEY = "cal_data"; // Key for the blob
// Example structure to store
typedef struct {
uint32_t sensor_id;
float offset;
float scale_factor;
uint8_t checksum; // Example field
} calibration_data_t;
void app_main(void)
{
// Initialize NVS (same as Example 1)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS init failed (%s), erasing NVS...", esp_err_to_name(ret));
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS Initialized.");
nvs_handle_t cal_handle;
// Open NVS Namespace
ESP_LOGI(TAG, "Opening NVS namespace: %s", NVS_NAMESPACE);
ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &cal_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(ret));
return;
} else {
ESP_LOGI(TAG, "NVS handle opened successfully.");
calibration_data_t cal_write = {
.sensor_id = 0xABCDEF01,
.offset = 1.23f,
.scale_factor = 0.98f,
.checksum = 0x5A
};
calibration_data_t cal_read = {0}; // Initialize read buffer to zeros
// --- Write the blob ---
ESP_LOGI(TAG, "Writing calibration data blob...");
ret = nvs_set_blob(cal_handle, CAL_DATA_KEY, &cal_write, sizeof(calibration_data_t));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) writing blob!", esp_err_to_name(ret));
} else {
// Commit the change
ret = nvs_commit(cal_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) committing blob!", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Blob written and committed successfully.");
}
}
// --- Read the blob ---
size_t required_size = 0;
// Get the size first
ret = nvs_get_blob(cal_handle, CAL_DATA_KEY, NULL, &required_size);
if (ret == ESP_OK) {
if (required_size == sizeof(calibration_data_t)) {
ESP_LOGI(TAG, "Blob found, size matches (%d bytes). Reading...", required_size);
// Read into the cal_read struct
ret = nvs_get_blob(cal_handle, CAL_DATA_KEY, &cal_read, &required_size);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Successfully read blob:");
ESP_LOGI(TAG, " Sensor ID: 0x%08" PRIX32, cal_read.sensor_id);
ESP_LOGI(TAG, " Offset: %.2f", cal_read.offset);
ESP_LOGI(TAG, " Scale Factor: %.2f", cal_read.scale_factor);
ESP_LOGI(TAG, " Checksum: 0x%02X", cal_read.checksum);
// Simple validation
if (memcmp(&cal_write, &cal_read, sizeof(calibration_data_t)) == 0) {
ESP_LOGI(TAG, "Read data matches written data.");
} else {
ESP_LOGW(TAG, "Read data DOES NOT match written data!");
}
} else {
ESP_LOGE(TAG, "Error (%s) reading blob data!", esp_err_to_name(ret));
}
} else {
ESP_LOGW(TAG, "Blob found, but size mismatch! Expected %d, got %d", sizeof(calibration_data_t), required_size);
}
} else if (ret == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGW(TAG, "Calibration data blob not found.");
} else {
ESP_LOGE(TAG, "Error (%s) getting blob size!", esp_err_to_name(ret));
}
// Close NVS
nvs_close(cal_handle);
ESP_LOGI(TAG, "NVS handle closed.");
}
ESP_LOGI(TAG, "Example finished.");
}
Build, Flash, and Monitor:
- Build, flash, and monitor.
Expected Output:
- It will write the
calibration_data_t
struct as a binary blob, commit it, then read it back and print the individual fields. It should also confirm that the read data matches the written data. On subsequent runs, it will overwrite the existing blob with the same data.
Variant Notes
The NVS library API and functionality are consistent across all ESP32 variants supported by ESP-IDF v5.x (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2).
- Partition Size: The main potential difference is the default size of the
"nvs"
partition defined in the default partition table for each variant. While the API is the same, the total amount of data you can store might vary slightly depending on the specific board support package or default configuration. You can always customize the partition table viamenuconfig
if you need more (or less) NVS space. Check thepartitions.csv
file in your project or the output duringidf.py build
to see the configured size.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Forgetting nvs_commit() Writing data but not saving it persistently. |
Data written with nvs_set_... is lost after reboot. Old value or “Not Found” error on next read. |
Always call nvs_commit(handle) after writing data you want to save permanently before closing the handle or rebooting. |
Not Closing Handles (nvs_close() )Leaking NVS resources. |
Resource leak. Future nvs_open() calls may fail over time. |
Ensure every successful nvs_open() has a matching nvs_close(handle) when done with the namespace. |
Ignoring Error Codes (esp_err_t )Assuming NVS operations always succeed. |
Unpredictable behavior, crashes, using incorrect/default data because an NVS operation failed silently. | Check the return value of every NVS function. Handle expected errors (like ESP_ERR_NVS_NOT_FOUND ) gracefully. Log or handle unexpected errors. Use ESP_ERROR_CHECK() for critical init steps. |
Incorrect Buffer Size (Strings/Blobs) Buffer too small for read, or misinterpreting blob data. |
Returns ESP_ERR_NVS_INVALID_LENGTH . Potential buffer overflows or reading incomplete data. Crashes if treating non-null-terminated blob as string. |
Call nvs_get_str/blob first with NULL buffer to get required_size . Allocate buffer of that size (+1 for string null terminator if needed). Read again into allocated buffer. Remember blobs are not null-terminated. |
Namespace/Key Name Issues Collisions, invalid characters, or too long (> 15 chars). |
Unintentionally overwriting data. Functions return ESP_ERR_NVS_INVALID_NAME . |
Use distinct namespaces. Use clear, unique key names within namespaces. Adhere to 15-character limit for both. |
Forgetting nvs_flash_init() Trying NVS operations before initialization. |
All NVS operations fail, likely returning ESP_ERR_NVS_NOT_INITIALIZED . |
Call nvs_flash_init() (and check its return code) once early in app_main before any other NVS function calls. |
NVS Partition Full Exceeding storage capacity. |
Writes/commits fail, often returning ESP_ERR_NVS_NO_FREE_PAGES . |
Check data storage needs. Increase NVS partition size in partitions.csv (via menuconfig) if necessary. Erase unused keys/namespaces. Consider alternative storage (SPIFFS/LittleFS) for larger data. |
Exercises
- Multiple Data Types: Extend Example 2. In addition to the
boot_count
(i32), store auint8_t
value named"status_flags"
and auint64_t
value named"last_active_time"
(you can useesp_timer_get_time()
for a mock timestamp) within the same"storage"
namespace. Read them back and print them on each boot. - Multiple Namespaces: Create two separate functions. One function (
save_wifi_creds
) saves mock “ssid” and “password” strings to a"wifi"
namespace. Another function (save_device_settings
) saves a mock “brightness”uint8_t
and “volume”uint8_t
to a"settings"
namespace. Call both functions fromapp_main
after initializing NVS. Add code to read back the values from both namespaces and print them. - Error Handling Practice: Modify Example 3 (string read/write). Intentionally try to read the string into a buffer that is too small (e.g., allocate only 10 bytes). Check the return code from
nvs_get_str
and log an appropriate error message whenESP_ERR_NVS_INVALID_LENGTH
occurs. - NVS Erase Function: Add a function
erase_all_nvs_data()
that callsnvs_flash_erase()
and thennvs_flash_init()
again. Inapp_main
(perhaps using a GPIO button press from Chapter 20 if you want to combine concepts, or just conditionally based on a boot count check likeif (boot_count % 10 == 0)
), call this erase function. Observe how all stored values (boot count, strings, etc.) are reset to their default/not-found state after the erase operation. Warning:nvs_flash_erase()
erases the entire NVS partition, including data stored by other components (like WiFi credentials saved by the framework if configured). Use with caution.
Summary
- Non-Volatile Storage (NVS) allows data to persist across device reboots.
- ESP-IDF provides the NVS library (
nvs_flash.h
,nvs.h
) for safe and efficient key-value storage on the flash partition. - NVS uses namespaces and key-value pairs to organize data.
- Supported data types include integers, strings, and binary blobs.
- Initialization (
nvs_flash_init
) is required once at startup. - Operations require opening a namespace (
nvs_open
) to get a handle. - Use
nvs_get_...
andnvs_set_...
functions to read/write data. - Changes written with
nvs_set_...
must be saved usingnvs_commit()
. - Handles must be closed using
nvs_close()
to release resources. - Error checking (
esp_err_t
) after every NVS call is crucial. - NVS handles wear leveling and helps protect against data corruption from power loss.
Further Reading
- ESP-IDF Programming Guide – NVS: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/storage/nvs_flash.html (Primary API reference).
- ESP-IDF Programming Guide – Partition Tables: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-guides/partition-tables.html (Understanding where NVS data lives).