Cloud, DevOps e IoT en español

Parte 6 de 22 de la serie Domótica con ESP32 y AWS desde cero

NVS y Preferences en ESP32: guardar configuración que sobrevive cortes

4 de octubre de 2026 · Steven Carvajal · Tutoriales

Código de este artículo en GitHub →

El primer prototipo de casi cualquier proyecto con ESP32 tiene todo escrito en el código: la red Wi-Fi, la contraseña, el certificado de la nube, la MAC de la otra placa. Funciona, pero cada dispositivo nuevo es un firmware distinto que hay que compilar y cargar con el cable.

La alternativa es un solo firmware para todos los dispositivos del mismo tipo, y lo que cambia entre ellos se guarda después de flashear en la memoria NVS, donde sobrevive a cortes de luz y reinicios. En esta parte vemos cómo hacerlo con la librería Preferences.

Qué es NVS

NVS (Non-Volatile Storage) es una partición de la memoria flash del ESP32 pensada para guardar pares clave-valor pequeños: números, textos y bloques de bytes. Viene en la tabla de particiones por defecto, así que no tienes que configurar nada.

En Arduino se usa con la librería Preferences, incluida en el core de ESP32. Los datos se organizan en espacios de nombres (namespaces) y, dentro de cada uno, en claves.

ConceptoLímite
Nombre del namespace15 caracteres
Nombre de la clave15 caracteres
Texto (putString)Pensado para textos cortos o medianos; un certificado cabe
Bloque de bytes (putBytes)Útil para estructuras y arreglos

Si superas los 15 caracteres en una clave, la escritura falla. Usa claves cortas: ssid, pass, cert.

Lo básico: abrir, escribir, leer, cerrar

#include <Preferences.h>

Preferences prefs;

void setup() {
  Serial.begin(115200);

  // Lectura: el segundo parámetro en true abre en modo solo lectura
  prefs.begin("config", true);
  unsigned int arranques = prefs.getUInt("boots", 0);  // 0 si la clave no existe
  prefs.end();

  arranques++;

  // Escritura
  prefs.begin("config", false);
  prefs.putUInt("boots", arranques);
  prefs.end();

  Serial.printf("Este ESP32 ha arrancado %u veces\n", arranques);
}

void loop() {}

Tres buenas costumbres de este ejemplo:

Monitor Serie del ESP32 mostrando el contador de arranques guardado en NVS aumentando tras cada reinicio y corte de energía

Salida esperada del ejemplo: el contador sobrevive a los reinicios y a los cortes de energía porque vive en la flash.

Ejemplo 1: un firmware que decide según lo que encuentra

Un dispositivo conectado a la nube necesita, como mínimo, la red Wi-Fi y sus credenciales. Si las guarda en NVS la primera vez (por ejemplo, desde un portal de configuración, parte 16), el mismo binario sirve para todas las placas:

String ssid, pass, cert, llave;
bool configurado = false;

void guardarConfig(const String& s, const String& p, const String& c, const String& k) {
  prefs.begin("config", false);
  prefs.putString("ssid", s);
  prefs.putString("pass", p);
  prefs.putString("cert", c);
  prefs.putString("key", k);
  prefs.end();
}

void cargarConfig() {
  prefs.begin("config", true);
  ssid  = prefs.getString("ssid", "");
  pass  = prefs.getString("pass", "");
  cert  = prefs.getString("cert", "");
  llave = prefs.getString("key", "");
  prefs.end();

  configurado = ssid.length() > 0 && cert.length() > 0 && llave.length() > 0;
}

En setup(), si configurado es falso, el dispositivo entra en modo configuración; si es verdadero, se conecta. El mismo binario se comporta distinto según lo que encuentra en la memoria.

Un detalle con los certificados: WiFiClientSecure::setCertificate() guarda el puntero, no copia el texto. La variable String tiene que seguir viva mientras dure la conexión; por eso en el ejemplo son globales.

Ejemplo 2: un arreglo de estructuras con putBytes

Para datos con forma fija, como una lista de horarios programados, putBytes guarda el arreglo tal cual:

#define MAX_HORARIOS 10

struct Horario {
  uint8_t hora;
  uint8_t minuto;
  uint8_t salida;  // qué relé
  uint8_t valor;   // 0 = apagar, 1 = encender
};

Horario horarios[MAX_HORARIOS];
size_t totalHorarios = 0;

void guardarHorarios() {
  prefs.begin("config", false);
  prefs.putUChar("n_hor", (uint8_t)totalHorarios);
  prefs.putBytes("horarios", horarios, totalHorarios * sizeof(Horario));
  prefs.end();
}

void cargarHorarios() {
  prefs.begin("config", true);
  size_t n = prefs.getUChar("n_hor", 0);
  if (n > MAX_HORARIOS) n = MAX_HORARIOS;  // nunca confíes en lo que hay en memoria
  if (n > 0) prefs.getBytes("horarios", horarios, n * sizeof(Horario));
  totalHorarios = n;
  prefs.end();
}

La cantidad va en una clave aparte para saber cuántos bytes leer, y se limita a MAX_HORARIOS antes de usarla. Si algún día la memoria tuviera un valor corrupto, el código no escribiría fuera del arreglo.

Si cambias la estructura en una versión nueva del firmware, los bytes guardados ya no encajan. Una solución simple es guardar también un número de versión y descartar los datos si no coincide.

Otro detalle: si defines la estructura directamente en el .ino, Arduino puede fallar con 'Horario' does not name a type. El generador automático de prototipos pone las declaraciones de funciones antes de tu estructura. La solución es mover la estructura a un archivo .h propio e incluirlo.

No desgastes la flash

La flash soporta un número limitado de ciclos de borrado. NVS reparte las escrituras para no gastar siempre el mismo sector, pero aun así conviene no escribir de más.

Un caso típico: el dispositivo recibe su configuración desde la nube cada vez que se reconecta. Si la guarda cada vez, escribe en la flash en cada reconexión aunque nada haya cambiado. Compara primero:

void aplicarHorarios(const Horario* nuevos, size_t n) {
  bool cambio = n != totalHorarios || memcmp(nuevos, horarios, n * sizeof(Horario)) != 0;
  if (!cambio) return;  // nada cambió: no se toca la flash

  memcpy(horarios, nuevos, n * sizeof(Horario));
  totalHorarios = n;
  guardarHorarios();
}

Reglas prácticas:

Cómo borrar la configuración

Para devolver un dispositivo al estado de fábrica tienes dos caminos:

prefs.begin("config", false);
prefs.clear();  // borra todas las claves del namespace
prefs.end();

O borrar la flash completa al instalar el firmware. Con ESP Web Tools (parte 20) puedes ofrecer una instalación que borre todo, incluida la partición NVS, y la placa arranca como nueva.

Ojo: subir un sketch nuevo desde Arduino IDE no borra NVS por defecto. Si cambias de firmware y ves comportamientos raros, puede que esté leyendo datos viejos. En Arduino IDE, la opción Tools → Erase All Flash Before Sketch Upload lo borra todo.

Seguridad: ¿es seguro guardar una llave privada en NVS?

Por defecto, NVS no está cifrado. Alguien con acceso físico a la placa y las herramientas adecuadas podría leer la flash, incluida la llave privada del certificado.

Cómo reducir el riesgo:

Preguntas frecuentes

¿Cuál es la diferencia entre NVS y EEPROM en el ESP32?

El ESP32 no tiene EEPROM real. La librería EEPROM de Arduino para ESP32 la emula sobre la flash. Preferences usa NVS directamente, maneja claves con nombre y reparte el desgaste. Para proyectos nuevos, usa Preferences.

¿Los datos de NVS sobreviven a una actualización de firmware?

Sí, si la actualización no borra la partición NVS. Subir un sketch desde Arduino IDE o una actualización OTA la conservan. Una instalación con borrado completo no.

¿Cuánto puedo guardar?

La partición NVS por defecto mide 20 KB. Para configuraciones, certificados y tablas pequeñas sobra. Para archivos grandes, usa LittleFS.

¿Por qué getString me devuelve vacío si guardé el valor?

Revisa que la clave tenga 15 caracteres o menos, que uses el mismo namespace al leer y al escribir, y que hayas abierto en modo escritura (false) al guardar.

Sigue leyendo