zephyr-almacenamiento-nvs-flash

Almacenamiento NVS en Zephyr: datos persistentes

  • 5 min

NVS es un sistema de almacenamiento para guardar valores identificados por números en memoria flash con rotación entre sectores y detección de datos válidos.

En los viejos tiempos, usábamos una memoria EEPROM externa o una zona EEPROM interna del microcontrolador. Pero los microcontroladores modernos (como el ESP32 o la serie nRF52) raramente traen EEPROM interna. En su lugar, usamos la propia Memoria Flash del programa para guardar datos.

Escribir en flash requiere borrar por bloques, lleva tiempo y desgasta las celdas, que admiten un número limitado de ciclos.

Para solucionar esto, Zephyr nos ofrece NVS (Non-Volatile Storage).

¿Qué es NVS?

NVS es un sistema de archivos simplificado diseñado específicamente para memorias Flash. No funciona con nombres de archivo (como “config.txt”), sino con IDs numéricos (pares Clave-Valor).

Sus grandes ventajas son:

  1. Wear Leveling (Nivelación de desgaste): NVS no escribe siempre en la misma celda de memoria. Va rotando los datos dentro de la partición asignada para que la Flash dure años.
  2. Tolerancia a fallos de energía: NVS escribe datos y metadatos por separado y utiliza CRC para detectar entradas incompletas. Tras un corte puede recuperar la última entrada válida.

La partición en Devicetree

Antes de escribir nada, Zephyr necesita saber dónde puede escribir. No queremos sobrescribir nuestro propio código ejecutable por accidente.

Para ello, usamos el mapa de particiones de la Flash. La mayoría de placas soportadas por Zephyr ya vienen con una partición definida por defecto llamada storage_partition o nvs_partition.

Podemos verificarlo en el archivo .dts de nuestra placa, pero generalmente se ve algo así:

/* Esto suele venir definido en el DTS de la placa, no hay que tocarlo
   a menos que usemos una placa custom */
partitions {
    compatible = "fixed-partitions";
    /* ... bootloader, app ... */
    storage_partition: partition@fa000 {
        label = "storage";
        reg = <0x000fa000 0x00006000>; /* Dirección y Tamaño */
    };
};
Copied!

Configuración (prj.conf)

Necesitamos activar los drivers de Flash y el subsistema NVS.

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

# Opcional: Activar logging para ver qué hace NVS
CONFIG_LOG=y
CONFIG_NVS_LOG_LEVEL_DBG=y
Copied!

Inicialización y uso desde C

Trabajar con NVS implica tres pasos: obtener el dispositivo Flash, inicializar el sistema de archivos (nvs_mount) y luego leer/escribir.

Vamos a crear un ejemplo clásico: un Contador de Reinicios. Cada vez que encendamos la placa, leeremos el valor guardado, lo incrementaremos y lo volveremos a guardar.

#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);

/* Definimos una estructura NVS global */
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)

/* Definimos los IDs para nuestras variables (arbitrarios) */
#define NVS_ID_REBOOT_COUNT 1
#define NVS_ID_WIFI_CONFIG  2

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

    /* Configuración del sistema de archivos NVS */
    fs.flash_device = NVS_PARTITION_DEVICE;
    fs.offset = NVS_PARTITION_OFFSET;

    if (!device_is_ready(fs.flash_device)) {
        LOG_ERR("El dispositivo flash no está listo");
        return -ENODEV;
    }
    
    /* Obtenemos información del sector de la flash (tamaño de página) */
    rc = flash_get_page_info_by_offs(fs.flash_device, fs.offset, &info);
    if (rc) {
        LOG_ERR("No se pudo consultar el sector flash (%d)", rc);
        return rc;
    }
    fs.sector_size = info.size;
    fs.sector_count = FIXED_PARTITION_SIZE(NVS_PARTITION) / fs.sector_size;

    /* 2. Montamos el sistema de archivos */
    rc = nvs_mount(&fs);
    if (rc) {
        LOG_ERR("Fallo al inicializar NVS (Error %d)", rc);
        return rc;
    }

    /* 3. Lectura del dato */
    uint32_t reboot_counter = 0;
    
    /* nvs_read devuelve los bytes leídos, o error negativo si no existe */
    rc = nvs_read(&fs, NVS_ID_REBOOT_COUNT, &reboot_counter, sizeof(reboot_counter));
    
    if (rc == sizeof(reboot_counter)) {
        LOG_INF("Valor encontrado en NVS. Reinicios: %u", reboot_counter);
    } else if (rc == -ENOENT) {
        LOG_INF("No se encontró valor (Primer arranque). Inicializando a 0.");
        reboot_counter = 0;
    } else {
        LOG_ERR("Fallo al leer NVS (%d)", rc);
        return rc < 0 ? rc : -EIO;
    }

    /* 4. Incrementamos y Guardamos */
    reboot_counter++;
    
    rc = nvs_write(&fs, NVS_ID_REBOOT_COUNT, &reboot_counter, sizeof(reboot_counter));
    if (rc < 0) {
        LOG_ERR("Fallo al guardar en NVS");
    } else {
        LOG_INF("Guardado nuevo valor: %u", reboot_counter);
    }

    return 0;
}
Copied!

Análisis del código

  1. flash_area_open: Es la forma moderna de Zephyr de buscar particiones. Busca la etiqueta storage_partition del Devicetree.
  2. sector_size: NVS necesita saber el tamaño físico del sector de borrado de la flash (normalmente 4KB en chips externos o 1KB/2KB en internos). Usamos flash_get_page_info_by_offs para no tener que adivinarlo.
  3. nvs_read: Fíjate que devuelve la cantidad de bytes leídos. Si devuelve un número negativo (ej. -ENOENT), significa que el ID no existe (es la primera vez que arrancamos la placa y la memoria está vacía).
  4. nvs_write: Guarda el dato. Si el dato que intentamos guardar es idéntico al que ya hay, NVS es inteligente y no hace nada para no desgastar la flash.

Tipos de datos soportados

Aunque en el ejemplo hemos guardado un uint32_t, NVS guarda bloques de bytes. Puedes guardar estructuras completas, strings o arrays.

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

struct wifi_config_t mi_config = { "MiCasa", "1234" };

/* Guardar estructura */
nvs_write(&fs, NVS_ID_WIFI_CONFIG, &mi_config, sizeof(mi_config));

/* Leer estructura */
nvs_read(&fs, NVS_ID_WIFI_CONFIG, &mi_config, sizeof(mi_config));
Copied!

Precaución: Si cambias la definición de la struct en tu código (añades un campo), al leer los datos viejos de la flash tendrás un desajuste de tamaño o datos corruptos. Gestionar las versiones de configuración es responsabilidad de la aplicación.