zephyr-almacenamiento-nvs-flash

NVS Storage in Zephyr: Persistent Data

  • 5 min

NVS is a storage system for saving values identified by numbers in flash memory with rotation between sectors and valid data detection.

In the old days, we used an external EEPROM memory or an internal EEPROM area of the microcontroller. However, modern microcontrollers (like the ESP32 or the nRF52 series) rarely come with internal EEPROM. Instead, we use the program’s own Flash Memory to store data.

Writing to flash requires erasing by blocks, takes time, and wears out the cells, which have a limited number of cycles.

To solve this, Zephyr offers us NVS (Non-Volatile Storage).

What is NVS?

NVS is a simplified file system designed specifically for Flash memories. It doesn’t work with filenames (like “config.txt”), but with numerical IDs (Key-Value pairs).

Its main advantages are:

  1. Wear Leveling: NVS doesn’t always write to the same memory cell. It rotates the data within the allocated partition so that the Flash lasts for years.
  2. Power-failure tolerance: NVS writes data and metadata separately and uses CRC to detect incomplete entries. After a power cut, it can recover the last valid entry.

The Partition in Devicetree

Before writing anything, Zephyr needs to know where it can write. We don’t want to accidentally overwrite our own executable code.

For this, we use the Flash partition map. Most boards supported by Zephyr already come with a default partition defined called storage_partition or nvs_partition.

We can verify this in the .dts file of our board, but generally it looks something like this:

/* This is usually defined in the board's DTS, no need to modify
   unless we are using a custom board */
partitions {
    compatible = "fixed-partitions";
    /* ... bootloader, app ... */
    storage_partition: partition@fa000 {
        label = "storage";
        reg = <0x000fa000 0x00006000>; /* Address and Size */
    };
};
Copied!

Configuration (prj.conf)

We need to enable the Flash drivers and the NVS subsystem.

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

# Optional: Enable logging to see what NVS does
CONFIG_LOG=y
CONFIG_NVS_LOG_LEVEL_DBG=y
Copied!

Initialization and usage from C

Working with NVS involves three steps: get the Flash device, initialize the filesystem (nvs_mount), and then read/write.

Let’s create a classic example: a Reboot Counter. Each time we power on the board, we will read the saved value, increment it, and save it again.

#include <zephyr/kernel.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/fs/nvs.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/logging/log.h>
#include <errno.h>

LOG_MODULE_REGISTER(demo_nvs, LOG_LEVEL_INF);

/* Define a global NVS structure */
static struct nvs_fs fs;

#define NVS_PARTITION         storage_partition
#define NVS_PARTITION_DEVICE  FIXED_PARTITION_DEVICE(NVS_PARTITION)
#define NVS_PARTITION_OFFSET  FIXED_PARTITION_OFFSET(NVS_PARTITION)

/* Define IDs for our variables (arbitrary) */
#define NVS_ID_REBOOT_COUNT 1
#define NVS_ID_WIFI_CONFIG  2

int main(void)
{
    int rc;
    struct flash_pages_info info;

    /* NVS filesystem configuration */
    fs.flash_device = NVS_PARTITION_DEVICE;
    fs.offset = NVS_PARTITION_OFFSET;

    if (!device_is_ready(fs.flash_device)) {
        LOG_ERR("Flash device is not ready");
        return -ENODEV;
    }
    
    /* Get flash sector information (page size) */
    rc = flash_get_page_info_by_offs(fs.flash_device, fs.offset, &info);
    if (rc) {
        LOG_ERR("Could not query flash sector (%d)", rc);
        return rc;
    }
    fs.sector_size = info.size;
    fs.sector_count = FIXED_PARTITION_SIZE(NVS_PARTITION) / fs.sector_size;

    /* 2. Mount the filesystem */
    rc = nvs_mount(&fs);
    if (rc) {
        LOG_ERR("Failed to initialize NVS (Error %d)", rc);
        return rc;
    }

    /* 3. Read the data */
    uint32_t reboot_counter = 0;
    
    /* nvs_read returns the number of bytes read, or a negative error if it doesn't exist */
    rc = nvs_read(&fs, NVS_ID_REBOOT_COUNT, &reboot_counter, sizeof(reboot_counter));
    
    if (rc == sizeof(reboot_counter)) {
        LOG_INF("Value found in NVS. Rebounds: %u", reboot_counter);
    } else if (rc == -ENOENT) {
        LOG_INF("No value found (First boot). Initializing to 0.");
        reboot_counter = 0;
    } else {
        LOG_ERR("Failed to read NVS (%d)", rc);
        return rc < 0 ? rc : -EIO;
    }

    /* 4. Increment and Save */
    reboot_counter++;
    
    rc = nvs_write(&fs, NVS_ID_REBOOT_COUNT, &reboot_counter, sizeof(reboot_counter));
    if (rc < 0) {
        LOG_ERR("Failed to save to NVS");
    } else {
        LOG_INF("Saved new value: %u", reboot_counter);
    }

    return 0;
}
Copied!

Code Analysis

  1. flash_area_open: This is the modern Zephyr way to find partitions. It looks for the storage_partition label from the Devicetree.
  2. sector_size: NVS needs to know the physical erase sector size of the flash (usually 4KB on external chips or 1KB/2KB on internal ones). We use flash_get_page_info_by_offs so we don’t have to guess it.
  3. nvs_read: Notice it returns the number of bytes read. If it returns a negative number (e.g., -ENOENT), it means the ID does not exist (it’s the first time we booted the board and the memory is empty).
  4. nvs_write: Saves the data. If the data we’re trying to save is identical to what’s already there, NVS is smart and does nothing to avoid wearing out the flash.

Supported Data Types

Although we saved a uint32_t in the example, NVS saves byte blocks. You can save complete structures, strings, or arrays.

struct wifi_config_t {
    char ssid[32];
    char pass[32];
};

struct wifi_config_t my_config = { "MyHouse", "1234" };

/* Save structure */
nvs_write(&fs, NVS_ID_WIFI_CONFIG, &my_config, sizeof(my_config));

/* Read structure */
nvs_read(&fs, NVS_ID_WIFI_CONFIG, &my_config, sizeof(my_config));
Copied!

Caution: If you change the definition of the struct in your code (add a field), when you read the old data from flash you’ll have a size mismatch or corrupted data. Managing configuration versions is the application’s responsibility.