← Índice del curso

Sistemas Embebidos · Tema 40 · Bloque 9

Almacenamiento persistente

Un equipo que olvida todo cada vez que se apaga no sirve para casi nada: hay que recordar el brillo que eligió el usuario, la calibración de fábrica, el contador de horas de uso, el código de la cerradura. Guardar un dato en memoria no volátil parece una operación trivial, pero tiene dos enemigos que no existen con la RAM: el desgaste (cada escritura gasta un poco la memoria) y el corte de energía (que puede llegar justo a mitad del guardado y dejar el dato roto). Este tema enseña a guardar datos de manera que sobrevivan a los dos: cómo decidir qué se guarda y dónde, cómo escribir sin que un corte los destruya, cómo repartir el desgaste, cómo escribir menos y cómo hacer evolucionar el formato de lo guardado sin perder lo que ya estaba.

01 · El inventario

Qué se guarda y dónde

Antes de elegir cómo guardar, hay que mirar qué se guarda, porque datos distintos tienen exigencias distintas. Cuatro propiedades los distinguen: cada cuánto cambian (determina el desgaste), cuánto pesan, qué pasa si se pierden (determina la protección) y cuánto deben durar (años de retención):

Clases de datos persistentes de un equipo y sus exigencias.
DatoCuánto cambiaSi se pierdeDónde y cómo
Calibración de fábrica (coeficientes del sensor, número de serie)Una vez en la vidaEl equipo mide mal, o no se puede identificar: críticoZona protegida contra escritura, o memoria OTP; con CRC y copia; fuera de la zona que se actualiza
Configuración del usuario (brillo, idioma, umbrales)Pocas veces: cuando el usuario la cambiaMolesto, pero se puede volver a elegirDoble copia con CRC y secuencia, o registro circular; valores de fábrica como último recurso
Estado que debe sobrevivir (el código de la cerradura, la posición de un actuador, el estado de un ciclo)OcasionalmentePuede ser serio: perder la posición, el código, el progresoIgual que la configuración, con más cuidado en el orden de las escrituras
Contadores (horas de uso, cantidad de ciclos, reinicios totales)Seguido: cada minuto, cada eventoSe pierde un historial, a veces con valor legal o de garantíaSe acumulan en RAM y se guardan cada tanto o al detectar la baja tensión; registro circular con nivelación
Registros de eventos y de datos (historial de mediciones, fallas)ContinuamenteSe pierde el historial más recienteMemoria externa con registro circular (tema 41); no en la flash interna
Estado de recuperación (contadores de reinicios, el motivo del último reinicio)En cada fallaSe pierde evidenciaRAM sin inicializar para lo que debe sobrevivir a un reinicio (tema 37); no escribir en flash en cada arranque

Esa tabla ya decide casi todo. En particular separa dos mundos: lo que cambia poco (configuración, calibración) y se puede proteger con copias y CRC, y lo que cambia seguido (contadores, registros) y exige tratar el desgaste como un recurso. Mezclar los dos en el mismo lugar y con la misma técnica es la fuente de la mayoría de los problemas.

Y sobre dónde: la elección de tecnología la dio el tema 28. La EEPROM (de los AVR, por ejemplo) escribe por byte y aguanta unas 100 000 escrituras por posición; la flash interna se borra por página y aguanta unos 10 000 borrados; la FRAM escribe por byte con una endurancia prácticamente ilimitada; la memoria externa (tema 41) ofrece más capacidad. Lo que sigue se aplica a cualquiera, con la diferencia de qué es lo que se desgasta (un byte, una página).

02 · Un corte, en el peor momento

El enemigo: el corte a mitad de camino

Una escritura en memoria no volátil no es un acto instantáneo: es una secuencia de pasos con duración. Borrar una página de flash lleva decenas de milisegundos; programar cada palabra, decenas de microsegundos; escribir un byte en una EEPROM, unos milisegundos (tema 28). Si la alimentación cae en medio de esa secuencia, la memoria queda en un estado intermedio que ninguna lógica del programa previó:

  • Un borrado interrumpido: la página queda con una mezcla de bits borrados y sin borrar. No es ni el dato viejo ni «vacía».
  • Una programación interrumpida: una palabra a medio escribir, con algunos bits ya cambiados y otros no, o incluso con celdas en un nivel intermedio que se leen distinto cada vez.
  • Una escritura de varios campos a medio hacer: los primeros campos tienen el valor nuevo y los últimos el viejo, y el conjunto es incoherente aunque cada campo, por separado, sea legible.

Y ocurre más de lo que parece, porque los cortes no son un evento raro: el usuario desenchufa el equipo, la pila se agota, hay un brown-out (tema 37), el equipo se reinicia por un watchdog (tema 38) justo cuando guardaba. En una flota grande, lo improbable es seguro: con un millón de equipos que guardan cada minuto, y un guardado que dura 21 ms, la probabilidad de que un corte caiga dentro de un guardado es de 21 ms / 60 s = 0,035 % por corte, o sea unos 350 equipos por millón con la configuración rota, por cada corte que sufran. (El ejercicio 6 hace la cuenta.)

La respuesta a este problema se llama atomicidad: que un guardado ocurra entero o no ocurra, que después de cualquier corte el dato sea o el viejo completo o el nuevo completo, y nunca una mezcla. La memoria, por sí sola, no la da; hay que construirla con el orden de las escrituras y con un criterio para distinguir un dato completo de uno a medias. Hay tres piezas que se combinan:

Las tres piezas de un guardado seguro.
PiezaQué hace
Un CRC (o suma de verificación) que cubre todo el bloquePermite al arranque distinguir un bloque completo de uno a medias: si el CRC no coincide, el bloque no vale (tema 39)
Una secuencia (un contador creciente)Permite saber cuál de dos copias válidas es la más nueva, sin depender de relojes
No pisar nunca la única copia válidaSe escribe el dato nuevo en otro lugar, y recién cuando está completo y verificado deja de importar el viejo
Qué queda válido durante un guardado, según la estrategia Tres líneas de tiempo de un guardado. En la primera, escritura en el lugar: el dato viejo es válido hasta que empieza el borrado, después hay un tramo largo sin ningún dato válido, y recién al terminar es válido el nuevo. En la segunda, doble copia: la copia A es válida todo el tiempo y la copia B se escribe en paralelo, de modo que siempre hay un dato válido. En la tercera, registro circular: los registros anteriores siguen válidos y un registro nuevo a medio escribir se ignora hasta completarse. En el lugarDoble copiaRegistro circular viejo válidoNINGÚN dato válido (corte = pérdida)nuevo válido copia A (vieja): válida todo el tiempo B: intactaB: se escribe (no cuenta)B nueva registros anteriores válidos+ el nuevo ▲ empieza el borrado▲ termina Siempre hay una copia válida: un corte en cualquier punto deja la vieja o la nueva. Un registro nuevo a medio escribir tiene un CRC incorrecto: se ignora y siguen valiendo los anteriores. La idea común: nunca hay un instante en que no exista un dato completo y verificable.

03 · La técnica de base

Escritura segura: doble copia con secuencia

La técnica más simple y más usada para un bloque de datos chico (la configuración, la calibración) es guardar dos copias en dos páginas distintas, cada una con su secuencia y su CRC. El protocolo tiene una regla de oro: se escribe siempre la copia más vieja, jamás la más nueva. Así, durante la escritura, la copia más nueva (la vigente) está intacta, y un corte en cualquier punto deja una copia válida:

typedef struct { uint32_t secuencia; ConfigDatos datos; uint16_t crc; } ConfigCopia;

/* GUARDAR: siempre en la copia más vieja; cuando termina, pasa a ser la más nueva */
bool config_guardar(const ConfigDatos *nueva) {
    ConfigCopia c[2];
    leer_copia(0, &c[0]);  leer_copia(1, &c[1]);
    bool v0 = copia_valida(&c[0]), v1 = copia_valida(&c[1]);

    int destino;  uint32_t seq;
    if (v0 && v1) {
        bool cero_mas_nueva = (int32_t)(c[0].secuencia - c[1].secuencia) >= 0;      /* diferencia con signo: segura ante el desborde */
        destino = cero_mas_nueva ? 1 : 0;                                          /* se pisa la MÁS VIEJA */
        seq     = (cero_mas_nueva ? c[0].secuencia : c[1].secuencia) + 1;
    } else if (v0) { destino = 1; seq = c[0].secuencia + 1; }                      /* la otra está dañada: se repara */
    else if (v1)   { destino = 0; seq = c[1].secuencia + 1; }
    else           { destino = 0; seq = 1; }                                        /* ninguna válida: se empieza de cero */

    ConfigCopia n = { .secuencia = seq, .datos = *nueva };
    n.crc = crc_de(&n);

    flash_borrar_pagina(PAGINA_CONFIG[destino]);                                    /* un corte aquí: la otra copia sigue válida */
    flash_programar(PAGINA_CONFIG[destino], &n, sizeof n);                          /* un corte aquí: esta copia queda a medias y su CRC la descarta */

    ConfigCopia verif;  leer_copia(destino, &verif);
    return copia_valida(&verif) && verif.secuencia == seq;                       /* verificar lo escrito: la memoria también falla */
}

/* CARGAR (tema 39): de las copias válidas, la de mayor secuencia; si ninguna es válida, valores de fábrica */

La Parte 1 del programa de Python demuestra la propiedad de forma exhaustiva: simula un guardado de 19 bytes (1 secuencia, 16 de datos y 2 de CRC) con 1 borrado y 19 programaciones de un byte, y corta la energía en cada uno de los 21 puntos posibles, desde «antes del primer paso» hasta «después del último». El resultado:

Qué recupera el arranque tras un corte en cada punto de un guardado (resultado de la Parte 1).
EstrategiaPuntos de corteConfig. viejaConfig. nuevaConfiguración perdida
Una copia, escrita en el lugar211119
Dos copias con secuencia212010

La diferencia es total. Con una sola copia, 19 de los 21 puntos de corte destruyen la configuración: el borrado la elimina y los bytes recién programados no completan nada válido hasta el último. Con dos copias, ninguno: en 20 casos se recupera la configuración vieja (completa, válida) y en uno la nueva. Observá además qué no se garantiza: que el dato nuevo se guarde. Un corte antes de terminar deja la configuración vieja, y eso es correcto: atomicidad quiere decir «todo o nada», no «siempre todo». Si el guardado es crítico, el programa debe confirmar el resultado (la función devuelve true solo cuando verificó) y reintentar si no pudo.

Un detalle del código: la comparación de secuencias es una resta con signo, como la de los contadores de tiempo del tema 37: es correcta aunque el contador de 32 bits dé la vuelta. Con una secuencia de 32 bits y un guardado por minuto, el desborde tarda 8000 años; pero con una de 8 o 16 bits (para ahorrar espacio) el desborde llega, y esa resta lo resuelve.

04 · Cuando se guarda seguido

El registro circular: emular una EEPROM en flash

La doble copia es excelente para datos que cambian poco, pero cada guardado cuesta un borrado de página, y una variable que cambia seguido (un contador) agotaría esa página en horas: con 10 000 borrados de endurancia y un guardado por minuto, 6,9 días (Parte 2). Para eso existe la técnica que muchos fabricantes llaman emulación de EEPROM, y que el tema 28 adelantó: en lugar de reescribir el dato en el mismo lugar, se agrega un registro nuevo a una página que funciona como un diario, y el valor vigente es el último registro de esa clave. Se aprovecha la regla de la flash de que programar una posición borrada es posible sin borrar nada: cada actualización consume una ranura libre, y el borrado se paga una vez cada muchas actualizaciones, no en cada una.

Una página de 1 KB con registros de 8 bytes (clave, valor, secuencia y CRC de 2 bytes cada uno) tiene 128 ranuras; una guarda el encabezado, y quedan 127 para registros. Cada actualización ocupa una ranura, y el borrado ocurre cuando la página se llena:

#define PAG_TAM      1024u
#define REG_TAM      8u
#define REG_POR_PAG  (PAG_TAM / REG_TAM - 1u)                      /* la ranura 0 es el encabezado de la página */

typedef struct { uint16_t clave, valor, seq, crc; } Registro;      /* 8 bytes */

static bool reg_vacio(const Registro *r)  { return r->clave == 0xFFFF && r->valor == 0xFFFF && r->seq == 0xFFFF && r->crc == 0xFFFF; }
static bool reg_valido(const Registro *r) { return !reg_vacio(r) && r->crc == crc16_reg(r); }

bool nvs_leer(uint16_t clave, uint16_t *valor) {
    const uint8_t *pag = pagina_activa();                         /* la de mayor secuencia entre las de encabezado válido */
    bool hallado = false;
    for (unsigned i = 1; i <= REG_POR_PAG; i++) {
        const Registro *r = (const Registro *)(pag + i * REG_TAM);
        if (reg_vacio(r)) break;                                  /* de aquí en adelante, todo libre */
        if (reg_valido(r) && r->clave == clave) { *valor = r->valor; hallado = true; }   /* el ÚLTIMO válido gana */
    }
    return hallado;                                               /* un registro a medio escribir no es vacío ni válido: se ignora */
}

bool nvs_escribir(uint16_t clave, uint16_t valor) {
    uint16_t actual;
    if (nvs_leer(clave, &actual) && actual == valor) return true;     /* no escribir si no cambió */

    int libre = primera_ranura_libre(pagina_activa());
    if (libre < 0) { compactar(); libre = primera_ranura_libre(pagina_activa()); }   /* página llena: pasar a la otra */

    Registro r = { clave, valor, siguiente_seq(), 0 };  r.crc = crc16_reg(&r);
    flash_programar(dir_ranura(libre), &r, sizeof r);             /* un solo programar: un corte deja un registro con CRC inválido, que se ignora */
    return true;
}

La parte delicada es la compactación, el momento en que la página se llena y hay que pasar a la otra. Es otra secuencia de pasos que un corte puede interrumpir, y su protocolo repite la lección de la doble copia: el commit es un único paso al final, y antes de él todo se puede abandonar sin consecuencias:

Protocolo de compactación a prueba de cortes.
PasoQué se haceSi se corta aquí
1Borrar la página de destino (la que no está activa)La página activa sigue intacta; se reintenta al arrancar
2Copiar al destino el valor vigente de cada clave, como registros nuevosEl destino no tiene encabezado válido: se ignora; sigue valiendo la activa
3Escribir el encabezado del destino, con estado «activa» y secuencia de página + 1 (el commit)Si no se completó, el encabezado es inválido y se ignora (como el paso 2). Si se completó, el destino pasó a ser la activa
4Borrar la página viejaHay dos páginas válidas: el arranque elige la de mayor secuencia y borra la otra

En el arranque, un procedimiento de recuperación examina las dos páginas: si hay una con encabezado válido, es la activa; si hay dos, la de mayor secuencia es la activa y la otra se borra (quedó de una compactación interrumpida); si no hay ninguna, el almacenamiento se inicializa vacío. Toda la técnica descansa en lo mismo que la doble copia: un solo paso decide cuál es el estado vigente, y ese paso es atómico porque es corto y está protegido por un CRC.

05 · Repartir el golpe

Desgaste y nivelación

El registro circular reduce muchísimo el desgaste, y lo mide la Parte 2 del programa de Python con una variable que se actualiza 100 000 veces:

Desgaste de una variable que cambia seguido (resultado de la Parte 2; endurancia de 10 000 borrados por página).
EstrategiaBorrados tras 100 000 actualizacionesActualizaciones hasta el desgasteDuración a 1 actualización por minuto
Reescribir en el lugar (un borrado por actualización)100 000 (sobre una sola página: ya superó la endurancia)10 0006,9 días
Registro circular de 2 páginas393 y 394 (una por página)2 540 0004,8 años
Registro circular de 4 páginas(la mitad por página)5 080 0009,7 años
Registro circular de 8 páginas(un cuarto por página)10 160 00019,3 años

Dos mensajes. El primero: el registro circular multiplica la vida útil por el número de registros por página (127 en el ejemplo): cada borrado se «reparte» entre 127 actualizaciones. El segundo: sumar páginas multiplica la vida útil por el número de páginas, porque el borrado se reparte entre más páginas. Esa idea, repartir las escrituras de modo que ninguna zona de la memoria se desgaste antes que las demás, se llama nivelación del desgaste (wear leveling), y tiene dos variantes:

  • Nivelación dinámica: las escrituras nuevas se dirigen siempre a los bloques menos usados entre los que están libres. Es lo que hace el registro circular al rotar entre páginas. No toca los datos que ya están guardados y no cambian.
  • Nivelación estática: además, los datos que casi nunca cambian (la calibración, el código del programa) se mueven periódicamente a bloques muy usados, y liberan los bloques poco usados para que reciban escrituras. Se necesita cuando una parte grande de la memoria guarda datos fijos y el desgaste se concentra en la parte restante; la usan los sistemas de archivos y las tarjetas SD y SSD.

Un detalle que importa: el desgaste se cuenta y conviene registrarlo. Un contador de borrados por página (guardado en el propio encabezado) permite saber, en el campo, cuánta vida le queda a la memoria, anticipar un fallo y mostrarlo en la telemetría (tema 37). Sin contador, el desgaste es una incógnita que se descubre cuando el equipo deja de recordar.

Y una advertencia sobre la retención: la flash gastada no solo deja de aceptar borrados, sino que retiene los datos por menos tiempo (tema 28). Un sector cerca del límite de endurancia puede perder un dato en meses en lugar de en años, y el CRC lo detectará, pero ya no habrá nada con qué reemplazarlo. Por eso el margen de desgaste de un diseño no se agota hasta el último ciclo: se diseña para usar una fracción de la endurancia (diez por ciento es un criterio frecuente).

06 · La escritura más barata

Escribir menos

La mejor manera de cuidar la memoria es no escribirla. Muchas escrituras son evitables con tres técnicas simples, que se pueden combinar con todo lo anterior. La Parte 4 del programa las mide con un caso concreto: un usuario ajusta un brillo con una perilla (100 sesiones de 50 cambios, cada uno de −1, 0 o +1, separados 0,1 s):

Escrituras según la política de guardado (resultado de la Parte 4).
PolíticaEscriturasPor qué
Guardar en cada cambio5000Cada movimiento de la perilla es una escritura
Guardar solo si el valor es distinto del guardado (leer antes de escribir)3360Un tercio de los cambios son de 0: el valor no cambia; se ahorra el 33 %
Guardar una vez, tras 2 s sin cambios (solo el valor final de cada sesión)94Mientras el usuario mueve la perilla, el valor intermedio no importa: solo el final. Un ahorro del 98 %

Cada técnica tiene su regla:

  • Leer antes de escribir. Es la más barata y casi siempre correcta: si el valor ya es el que se quiere guardar, no se escribe (eeprom_update_byte del AVR lo hace por vos; el nvs_escribir del apartado 4 también).
  • Diferir y agrupar (write-back, o debounce del guardado): el valor vive en RAM, y se guarda recién cuando pasa un tiempo sin cambios, o cuando el usuario confirma. Se agrupan los campos que cambiaron juntos en un solo guardado. El precio es una ventana en la que el dato está solo en RAM: si la energía se corta dentro de ella, se pierde el último cambio. Para datos de configuración, es un precio aceptable.
  • Acumular en RAM y guardar de a tandas (para los contadores): contar en una variable de RAM y escribirla cada N incrementos o cada cierto tiempo, a costa de perder hasta N cuentas en un corte. Si la precisión importa, se la complementa con la siguiente técnica.
  • Guardar al detectar la caída de energía. Con un aviso de baja tensión (PVD, tema 37), el programa tiene unos milisegundos para guardar el estado crítico antes de caer. Es lo que permite no escribir en cada cambio y aun así no perder nada: los datos vivos están en RAM y se vuelcan en el último instante. Requiere que haya energía de reserva para terminar el guardado.

La energía para terminar el guardado

Guardar con el equipo ya sin alimentación externa exige un capacitor de reserva que mantenga la tensión del chip por encima de su mínimo durante todo el guardado, y se dimensiona con la carga que consume la secuencia: C ≥ Q / ΔV, donde Q es la carga del guardado y ΔV, cuánto puede caer la tensión. La Parte 3 lo calcula para un borrado de 20 ms a 15 mA y 10 programaciones de 70 µs a 12 mA:

Capacitor de reserva para un guardado (resultado de la Parte 3).
Caída de tensión permitidaCapacitor mínimo
0,5 V617 µF
1,0 V308 µF
1,4 V220 µF

La carga total del guardado es de 0,308 mC (308 µC), casi toda del borrado (el 97 %). Dos consecuencias de diseño: elegir una técnica que borre poco (el registro circular borra una vez cada 127 actualizaciones, y el guardado típico es solo una programación de 8 bytes, de microcoulombs, con una reserva mucho menor), y no empezar un borrado cuando ya se detectó la caída si no hay energía para completarlo: se pospone al próximo arranque. El capacitor debe ser de baja fuga y aguantar la temperatura; un electrolítico viejo pierde capacidad con los años y el margen que alcanzaba en el prototipo no alcanza en el campo.

07 · Lo guardado también envejece

Coherencia y versionado del formato

Hay dos problemas de diseño que no tienen que ver con la memoria sino con el contenido y que suelen ignorarse hasta que ocurren.

Guardar juntos lo que va junto

Una configuración con varios campos relacionados (un umbral alto y uno bajo, una hora de inicio y una de fin) tiene una coherencia interna: algunos valores solo tienen sentido en conjunto. Si cada campo se guarda por separado (una escritura por campo), un corte entre dos escrituras deja los campos incoherentes: el umbral nuevo con el viejo, una combinación que el usuario nunca eligió y quizá inválida. La regla es guardar el conjunto completo como una unidad, con un único CRC y una única secuencia, de modo que la atomicidad cubra todo. Para datos independientes (el brillo y el idioma), los registros por clave están bien: no tienen una relación que proteger.

Hacer evolucionar el formato

Un firmware se actualiza (tema 42), y la versión nueva suele necesitar campos que la vieja no tenía. Pero la memoria no volátil conserva lo que la versión anterior guardó: tras la actualización, el firmware nuevo se encuentra con datos en el formato viejo. Si ignora la diferencia e interpreta los bytes con la estructura nueva, lee basura: el campo nuevo contiene lo que sea que haya quedado en esa posición. La solución es incluir una versión en el bloque y una función de migración:

typedef struct { uint8_t version; uint8_t brillo; uint8_t volumen; } ConfigV1;
typedef struct { uint8_t version; uint8_t brillo; uint8_t volumen; uint16_t umbral_alarma; } ConfigV2;

static const ConfigV2 POR_DEFECTO_V2 = { 2, 50, 30, 400 };

/* Las versiones nuevas leen las viejas: los campos que faltan toman su valor por defecto */
static ConfigV2 migrar(const uint8_t *bloque, size_t largo) {
    ConfigV2 c = POR_DEFECTO_V2;
    if (largo < 1) return c;
    switch (bloque[0]) {                                        /* el primer byte siempre es la versión */
        case 1:
            if (largo >= sizeof(ConfigV1)) { c.brillo = bloque[1]; c.volumen = bloque[2]; }   /* umbral_alarma queda con su valor por defecto */
            break;
        case 2:
            if (largo >= sizeof(ConfigV2)) { c.brillo = bloque[1]; c.volumen = bloque[2]; c.umbral_alarma = (uint16_t)(bloque[3] | (bloque[4] << 8)); }
            break;
        default:
            return POR_DEFECTO_V2;                              /* versión desconocida (más nueva): no se interpreta */
    }
    return c;
}

Tres reglas completan el cuadro. Serializar campo por campo (como en el ejemplo: byte a byte, con un orden fijo y little-endian para los enteros) en lugar de volcar la struct a la memoria: el volcado depende del relleno que inserte el compilador (tema 32), del orden de bytes del procesador y de la versión del compilador, y se rompe cuando cualquiera cambia. Los campos nuevos se agregan al final y con un valor por defecto sensato, de modo que las versiones viejas, si el formato lo permite, puedan seguir leyendo lo que conocen. Y una versión desconocida no se interpreta: ocurre cuando se vuelve a un firmware anterior (tema 42) que encuentra datos guardados por uno más nuevo, y lo único seguro es usar valores de fábrica o ignorar los campos que no entiende. Esta compatibilidad hacia atrás y hacia adelante es lo que permite actualizar y volver atrás sin perder la configuración de los usuarios.

08 · Lo que ya está hecho

No reinventar: bibliotecas y sistemas de archivos

Las técnicas de este tema son delicadas, y una implementación propia sin pruebas exhaustivas de cortes de energía casi siempre tiene un hueco. En la práctica, conviene usar una biblioteca probada y entender cómo funciona, que es lo que enseñan los apartados anteriores. Las opciones habituales:

Opciones de almacenamiento persistente más usadas (orientativo; consultar la documentación de cada una).
OpciónQué ofreceCuándo
EEPROM del AVR (eeprom_update_*)Escritura por byte, con «actualizar» que evita escribir lo que no cambióPocos datos en un AVR; sin protección contra cortes: la atomicidad hay que construirla encima
Emulación de EEPROM del fabricante (por ejemplo, la de ST para sus STM32)Un registro circular sobre dos o más páginas de flash, con una interfaz de «clave y valor»Chips sin EEPROM, con pocos parámetros
NVS del ESP32 (Non-Volatile Storage)Un almacén de pares clave-valor sobre la flash con nivelación del desgaste y tolerancia a cortesConfiguración en un ESP32
NVS y «settings» de ZephyrAlmacenes de clave-valor sobre la flash con CRC y compactaciónProyectos con Zephyr
littlefsUn sistema de archivos pensado para microcontroladores: resistente a cortes de energía (copia en escritura), con nivelación del desgaste y poca RAMVarios archivos, registros, datos de tamaño variable en flash interna o externa
Sistemas de archivos de tarjetas SD (FAT)Compatibilidad con la PC; la tarjeta trae su propia nivelación del desgasteRegistro de datos masivo (tema 41); sin garantías ante cortes si no se toman precauciones (cierre de archivos, flush)
FRAM (externa o integrada)Escritura por byte, rápida, endurancia práctica ilimitadaContadores que cambian muy seguido, sin preocuparse por el desgaste

Sea cual sea la elección, tres preguntas la ponen a prueba: ¿qué pasa si se corta la energía en cada paso de una escritura? ¿Cómo reparte el desgaste y cuánto dura con mi frecuencia de escritura? ¿Qué pasa al actualizar el formato? Una biblioteca que no responde a las tres no sirve para un producto. Y una advertencia sobre la FAT en una tarjeta SD: es la fuente clásica de «se me corrompió la tarjeta». Un archivo FAT abierto en el momento de un corte puede quedar con la tabla de asignación inconsistente; la defensa es cerrar los archivos (o hacer flush) tras cada bloque, escribir de a bloques completos y verificar el sistema de archivos al arrancar.

09 · Comprobar que funciona

Verificación y diagnóstico

Los guardados seguros no se prueban cortando la energía «un par de veces»: un corte a mano cae casi siempre fuera de la ventana de escritura, que dura milisegundos. Hay dos técnicas, que se complementan:

  • Prueba exhaustiva en la PC: el módulo de almacenamiento se compila para el escritorio con una flash simulada (un arreglo de bytes que obedece las reglas de la flash: borrar a 0xFF, programar solo baja bits). El simulador cuenta los pasos de cada escritura y puede abortar en cualquiera. Un bucle corre la operación cortándola en cada paso posible, reinicia el módulo con la memoria en ese estado, y comprueba las propiedades. Es exactamente lo que hace la Parte 1 del programa de este tema.
  • Prueba en el equipo con cortes aleatorios: un montaje con un relé (o un interruptor controlado por otro microcontrolador) que corta la alimentación en instantes aleatorios durante los guardados, miles de veces, y verifica tras cada reinicio que la configuración es válida. Descubre lo que la simulación no modela: el comportamiento real de las celdas, los tiempos y el efecto de la tensión que cae.
Propiedades que deben cumplirse tras un corte en cualquier punto.
PropiedadCómo se comprueba
Siempre hay una configuración válida (la vieja o la nueva)Tras cada corte simulado, el arranque encuentra una copia con CRC correcto
Nunca una mezclaEl contenido recuperado es idéntico a la versión vieja o a la nueva, byte por byte
La secuencia nunca retrocedeTras recuperar, la secuencia es mayor o igual que antes de la operación
La recuperación es idempotenteCortar la energía durante el arranque (en la limpieza de la compactación, por ejemplo) y volver a arrancar da el mismo resultado
Los casos límite funcionanMemoria en blanco (primer arranque), ambas copias dañadas, página llena (compactación), versión desconocida
El desgaste está repartidoTras muchas actualizaciones simuladas, los contadores de borrado de las páginas son parecidos
Síntomas frecuentes y qué revisar.
SíntomaCausa probableQué hacer
Algunos equipos de la flota pierden la configuración tras un apagónEscritura en el lugar sin doble copia: el corte cayó en la ventana del guardadoDoble copia con secuencia y CRC; escribir siempre la más vieja
El equipo «recuerda» valores absurdos tras un corteCampos guardados por separado: un corte dejó una combinación incoherenteGuardar el conjunto completo con un solo CRC
Un contador se pone a cero o salta tras un corteUna sola copia, o un guardado que perdió el último cambio sin saberloRegistro circular; guardar al detectar la baja tensión; aceptar y documentar la pérdida máxima
El equipo deja de recordar al cabo de un tiempoLa página de guardado se desgastó (guardado en el lugar y muy frecuente)Registro circular con nivelación; escribir solo si cambió; diferir; medir el desgaste
Tras una actualización, la configuración contiene valores extrañosEl firmware nuevo interpreta el formato viejo con la estructura nuevaVersión en el bloque y migración; serializar campo por campo
Tras volver a un firmware anterior, el equipo pierde la configuración o se comporta raroEl firmware viejo interpreta datos de una versión más nuevaUna versión desconocida no se interpreta: valores de fábrica, o formato extensible
Un bucle de reinicios que agota la flashUna escritura en cada arranque (tema 37)No escribir en cada arranque; contadores en RAM sin inicializar para lo transitorio
El guardado funciona en la mesa y falla con la batería gastadaLa tensión baja durante el borrado o la programación (tema 37)No guardar con tensión baja; capacitor de reserva; verificar lo escrito

10 · Analogía con programación

Probar el modelo en Python

El programa tiene cuatro partes: la prueba exhaustiva de cortes de energía sobre una flash simulada (cada uno de los 21 puntos de un guardado de 19 bytes, con una copia en el lugar y con dos copias), la comparación de desgaste entre reescribir en el lugar y un registro circular con distintos números de páginas, el cálculo del capacitor de reserva para terminar un guardado, y la medición de cuántas escrituras se pueden evitar con tres políticas de guardado.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

# Almacenamiento persistente: cortes de energía en cada paso, desgaste con registro circular, capacitor de reserva y escrituras evitables.
import random

def crc16(datos):
    crc = 0xFFFF
    for b in datos:
        crc ^= b << 8
        for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
    return crc

# ---- Parte 1: cortar la energía en cada paso de un guardado ----------------------------------------------------------
# Una copia = secuencia (1 byte) + 16 bytes de datos + CRC (2 bytes) = 19 bytes. Se programa byte a byte (cada byte es un paso).
TAM = 19
def armar(seq, datos): 
    cuerpo = bytes([seq]) + datos
    return cuerpo + crc16(cuerpo).to_bytes(2, "big")
def valida(pagina):
    return len(pagina) == TAM and pagina[:1] != b"\xff" and crc16(pagina[:-2]) == int.from_bytes(pagina[-2:], "big")

viejo, nuevo = armar(1, bytes(range(16))), armar(2, bytes(range(100, 116)))
pasos_por_escritura = 1 + TAM                       # 1 borrado + 19 bytes programados

def estado_tras(paginas, indice, pasos):
    """Aplica 'pasos' pasos de escribir 'nuevo' sobre la página 'indice' (borrar y luego programar byte a byte)."""
    p = [bytearray(x) for x in paginas]
    if pasos >= 1: p[indice] = bytearray(b"\xff" * TAM)
    for k in range(max(0, pasos - 1)): p[indice][k] = nuevo[k]
    return [bytes(x) for x in p]

def recuperar(paginas):
    candidatas = [(p[0], p) for p in paginas if valida(p)]
    if not candidatas: return "PERDIDA"
    elegida = max(candidatas, key=lambda c: c[0])[1]
    return "NUEVA" if elegida == nuevo else "VIEJA" if elegida == viejo else "OTRA"

print("Parte 1: un corte de energía en cada uno de los 20 pasos de un guardado (1 borrado + 19 bytes programados)")
res_lugar, res_doble = [], []
for pasos in range(pasos_por_escritura + 1):
    res_lugar.append(recuperar(estado_tras([viejo], 0, pasos)))                       # una sola copia, escrita en el lugar
    res_doble.append(recuperar(estado_tras([viejo, armar(0, bytes(16))], 1, pasos)))  # dos copias: se escribe la MÁS VIEJA (la segunda)
for nombre, res in (("una copia, en el lugar", res_lugar), ("dos copias con secuencia", res_doble)):
    cuenta = {k: res.count(k) for k in ("VIEJA", "NUEVA", "PERDIDA")}
    print(f"  {nombre:<26}: {len(res)} puntos de corte -> vieja {cuenta['VIEJA']}, nueva {cuenta['NUEVA']}, PERDIDA {cuenta['PERDIDA']}")

# ---- Parte 2: desgaste, en el lugar o con un registro circular --------------------------------------------------------
ENDURANCIA, PAGINA, REGISTRO = 10_000, 1024, 8
por_pagina = PAGINA // REGISTRO - 1                  # una ranura de la página guarda el encabezado
print()
print(f"Parte 2: una variable que cambia seguido; endurancia de {ENDURANCIA} borrados por página, páginas de {PAGINA} B, registros de {REGISTRO} B ({por_pagina} útiles)")
def simular_log(actualizaciones, paginas):
    borrados, activa, usados = [0] * paginas, 0, 1
    for _ in range(actualizaciones):
        if usados > por_pagina:                        # la página se llenó: se compacta en la siguiente y se borra la vieja
            activa = (activa + 1) % paginas; borrados[activa] += 1; usados = 1
        usados += 1
    return borrados
borr = simular_log(100_000, 2)
print(f"  tras 100 000 actualizaciones: en el lugar = 100 000 borrados de una página; registro circular de 2 páginas = {borr[0]} y {borr[1]} borrados")
print(f"  {'páginas':>13} | {'actualizaciones hasta el desgaste':>34} | 1 por minuto")
print(f"  {'(en el lugar)':>13} | {ENDURANCIA:>34,} | {ENDURANCIA / 1440:5.1f} días".replace(",", " "))
for p in (2, 4, 8):
    total = ENDURANCIA * por_pagina * p
    print(f"  {p:>13} | {total:>34,} | {total / 525_600:5.1f} años".replace(",", " "))

# ---- Parte 3: capacitor de reserva para terminar un guardado al cortarse la alimentación -----------------------------
print()
print("Parte 3: carga de un guardado (borrado de 20 ms a 15 mA y 10 programaciones de 70 µs a 12 mA) y capacitor de reserva")
q_borrado = 15e-3 * 20e-3
q_prog = 10 * 12e-3 * 70e-6
q = q_borrado + q_prog
print(f"  carga total: {q * 1e3:.4f} mC ({q * 1e6:.0f} µC)")
for dv in (0.5, 1.0, 1.4):
    print(f"  la tensión puede caer {dv:.1f} V durante el guardado -> capacitor mínimo {q / dv * 1e6:5.0f} µF")

# ---- Parte 4: cuántas escrituras se pueden evitar -------------------------------------------------------------------------
print()
print("Parte 4: un usuario ajusta un brillo: 100 sesiones de 50 cambios (cada uno de -1, 0 o +1), 0,1 s entre cambios")
rnd = random.Random(8)
cambios, valor, guardado = [], 50, 50
sesiones = []
for s in range(100):
    sesion = []
    for _ in range(50):
        valor = max(0, min(100, valor + rnd.choice((-1, 0, 1))))
        sesion.append(valor)
    sesiones.append(sesion)
todos = sum(len(s) for s in sesiones)
def distintos():
    n, ultimo = 0, 50
    for s in sesiones:
        for v in s:
            if v != ultimo: n += 1; ultimo = v
    return n
def con_pausa():
    n, ultimo = 0, 50
    for s in sesiones:
        if s[-1] != ultimo: n += 1; ultimo = s[-1]
    return n
print(f"  guardar en cada cambio: {todos} escrituras")
print(f"  guardar solo si el valor es distinto del guardado: {distintos()} escrituras")
print(f"  guardar una vez, tras 2 s sin cambios (solo el valor final de cada sesión, si cambió): {con_pausa()} escrituras")

Resultado, parte 1: de los 21 puntos de corte de un guardado (1 borrado y 19 bytes), la escritura en el lugar con una sola copia deja la configuración vieja en 1, la nueva en 1 y perdida en 19; con dos copias con secuencia, la vieja en 20, la nueva en 1 y perdida en 0. Parte 2: una variable que se actualiza 100 000 veces reescribiendo en el lugar acumula 100 000 borrados en una página (10 veces su endurancia de 10 000); con un registro circular de dos páginas, solo 393 y 394 borrados. La vida útil a una actualización por minuto va de 6,9 días (en el lugar) a 4,8 años (2 páginas), 9,7 años (4) y 19,3 años (8), con 127 registros por página. Parte 3: la carga de un guardado de 20 ms a 15 mA más diez programaciones de 70 µs a 12 mA es de 308 µC, y pide un capacitor de 617 µF si la tensión puede caer 0,5 V, de 308 µF si cae 1,0 V y de 220 µF si cae 1,4 V. Parte 4: 5000 cambios de una perilla producen 5000 escrituras si se guarda en cada uno, 3360 si solo se guarda lo distinto y 94 si se guarda una vez tras 2 s sin cambios.

Probá cambiar TAM y los pasos de la Parte 1 para ver que la propiedad se mantiene con cualquier tamaño de bloque, o sacar la secuencia del arranque (elegir siempre la copia A) para ver cómo reaparece la pérdida: la secuencia no es un adorno. La idea que enseña el programa es que la atomicidad se puede verificar de forma exhaustiva: no hace falta suerte ni miles de cortes reales para saber que un diseño es correcto, hace falta recorrer todos los puntos de corte posibles.

11 · Exploración interactiva

Cortar la energía

El laboratorio simula el guardado de una configuración (secuencia, cuatro datos y una suma de verificación: 6 bytes) con dos estrategias: una sola copia escrita en el lugar y dos copias, escribiendo la más vieja. Un guardado tiene 7 pasos: borrar la página y programar los 6 bytes. Con el deslizador elegís en qué paso se corta la energía, y el laboratorio muestra cómo queda la memoria y qué configuración recupera el arranque. La configuración vieja tiene secuencia 1 y datos 10, 20, 30, 40; la nueva, secuencia 2 y datos 11, 22, 33, 44.

Un corte en cada paso del guardado

El corte ocurre—
Qué recupera el arranque—

Estado de la memoria tras el corte (bytes en hexadecimal; FF = borrado)

—
—

Movés el deslizador y mirá qué pasa.

Recorrido guiado

  1. Con una sola copia y el deslizador en 0: se corta antes de empezar y recuperás la configuración vieja (secuencia 1). No se perdió nada, pero tampoco se guardó.
  2. Subí el deslizador a 1: la página ya se borró (todos los bytes valen FF) y el guardado nuevo no empezó: la configuración está perdida. El equipo arrancaría con valores de fábrica.
  3. Probá los valores 2, 3, 4, 5, 6: la página tiene el principio de la configuración nueva y FF en el resto; la suma de verificación no coincide y el bloque es inválido. En los seis puntos intermedios (del 1 al 6) se pierde todo.
  4. Subí a 7: ahora están los 6 bytes y la configuración nueva es válida. El único punto de éxito es el último; el de «nada perdido» es el primero. En medio, el desastre.
  5. Cambiá a dos copias y repetí el recorrido de 0 a 7: nunca se pierde nada. La copia A conserva la configuración vieja (siempre válida), y la copia B (la más vieja) se va escribiendo: mientras está a medias, su suma de verificación la descarta, y el arranque usa A.
  6. Con dos copias y el deslizador en 7: ahora las dos son válidas y el arranque elige la de mayor secuencia (la B, de secuencia 2). Tenés la prueba de lo que dice el tema: el dato nuevo queda guardado solo al final, pero no se arriesga el viejo, y en este punto de corte se recupera la nueva.
Desafío: ¿qué pasaría con dos copias si el arranque escribiera siempre la copia A, sin mirar cuál es la más vieja?

Perdería la garantía. Supongamos que A tiene la secuencia 5 (la más nueva) y B la 4. Un guardado que borra y reescribe A deja, durante la escritura, solo a B válida: un corte en ese momento devuelve la configuración 4 en lugar de la 5, un dato más viejo que el último guardado. Y si B además estuviera dañada (por ejemplo, por un guardado anterior interrumpido), la configuración se perdería por completo. Elegir la copia más vieja garantiza que la más nueva esté intacta durante toda la escritura, y que lo peor que pueda pasar sea no guardar el cambio último, pero nunca retroceder ni perderlo todo. Por eso la secuencia no es un adorno: sin ella no se puede decidir cuál de las dos es la más vieja.

12 · Método de trabajo

Método: guardar sin perder

  1. Hacé el inventario de los datos persistentes: qué se guarda, cada cuánto cambia, cuánto pesa, qué pasa si se pierde y cuánto debe durar. Cada clase tendrá su técnica.
  2. Calculá la vida útil con la endurancia de tu memoria y la frecuencia real de escritura (tema 28). Si no alcanza (casi nunca, para un contador), reducí las escrituras y repartí el desgaste.
  3. Escribí menos: solo si cambió, diferida hasta que el valor se asiente, agrupando campos y, para los contadores, acumulando en RAM y guardando al detectar la caída de tensión.
  4. Para los datos que cambian poco: doble copia, con secuencia y CRC, escribiendo siempre la más vieja y verificando lo escrito. Para los que cambian seguido: registro circular sobre varias páginas, con compactación a prueba de cortes.
  5. Guardá lo que va junto como una unidad, con un solo CRC y una sola secuencia.
  6. Poné una versión en el bloque y serializá campo por campo, con un orden y una endianidad fijos. Las versiones nuevas leen las viejas con valores por defecto; una versión desconocida no se interpreta.
  7. Definí los valores de fábrica como último recurso y el aviso que se emite cuando se usan.
  8. Prevé la energía para terminar el guardado si se guarda al detectar la caída: aviso de baja tensión, capacitor de reserva dimensionado con la carga del guardado, y no iniciar un borrado sin energía para completarlo.
  9. Probá todos los puntos de corte con una flash simulada en la PC, y con cortes aleatorios en el equipo. Verificá: siempre hay un dato válido, nunca una mezcla, la secuencia no retrocede, la recuperación es idempotente.
  10. Medí el desgaste en el campo (contadores de borrados por página en la telemetría) y no escribas en memoria no volátil en cada arranque.

13 · Comprensión

Confusiones frecuentes

«Con un CRC, el guardado ya es seguro»

El CRC permite detectar que un bloque quedó roto, pero si era la única copia, lo detecta y no hay nada con qué reemplazarlo. La seguridad frente a cortes viene de no pisar nunca la única copia válida: dos copias, o un registro circular. El CRC es una de las tres piezas, no la solución.

«Un corte de energía justo durante el guardado es tan improbable que no vale la pena preocuparse»

En un equipo, quizá; en una flota, es seguro. Con un guardado por minuto y 21 ms de duración, 350 de cada millón de equipos pierden la configuración por cada corte que sufran. Y los cortes no son raros: pilas que se agotan, usuarios que desenchufan, brown-outs y reinicios. Lo improbable, multiplicado por la cantidad de equipos y de años, ocurre.

«Escribo la copia nueva encima de la vieja; con otra copia de respaldo alcanza»

Depende de en qué orden. Si se escribe primero la más nueva (y se la pisa), durante la escritura la única copia buena es la de respaldo, que es más vieja: un corte hace retroceder la configuración. Si se escribe la más vieja, siempre queda intacta la más reciente. El orden de escritura es el diseño.

«La flash aguanta 100 000 ciclos, así que no me tengo que preocupar»

El número es de ciclos de borrado por página, no por byte, y con una página de 1 KB que se reescribe en cada guardado, un guardado por minuto la agota en días. Hay que hacer la cuenta con la frecuencia real y, para lo que cambia seguido, usar un registro circular o una tecnología sin desgaste práctico (FRAM).

«Escribir solo si cambió es una optimización menor»

Es de las mayores, y gratuita: en el ejemplo de la perilla ahorra un tercio de las escrituras, y combinada con el guardado diferido, el 98 %. Se escribe tan poco como se pueda, porque cada escritura gasta vida útil y ofrece una oportunidad más de que un corte lo arruine.

«Guardo la struct directamente en memoria con un memcpy y listo»

Funciona hasta que cambia el relleno del compilador, el orden de bytes, la versión del compilador o la estructura misma tras una actualización. Se serializa campo por campo, con un orden fijo, y con una versión en el bloque para poder migrarlo.

«Si actualizo el firmware, la configuración se pierde y no pasa nada»

Para el usuario pasa: pierde el idioma, los umbrales, el código. Un producto que borra la configuración de sus usuarios en cada actualización es un producto que la gente deja de actualizar. La versión en el bloque y la migración son lo que permite actualizar sin perder, y volver atrás sin romper.

«Probé cortando la energía varias veces y siempre se recuperó, así que está bien»

La ventana de escritura dura milisegundos, y un corte manual casi siempre cae fuera de ella. Una prueba así puede pasar cien veces sin tocar la zona crítica. Se prueba cortando en cada paso posible, de manera exhaustiva con una flash simulada, y con cortes automáticos y aleatorios en el equipo real.

14 · Práctica guiada

Ejercicios

1. La vida útil de un contador

Un equipo guarda un contador cada 30 s. La endurancia de la flash es de 10 000 borrados por página y los registros son de 8 bytes en páginas de 1 KB (127 útiles). ¿Cuánto dura si se reescribe en el lugar? ¿Y con un registro circular de 2 páginas? ¿Y de 8?

Ver solución

En el lugar: 10 000 borrados × 30 s = 300 000 s = 3,5 días. Registro circular de 2 páginas: 2 × 10 000 × 127 = 2 540 000 actualizaciones × 30 s = 76 200 000 s ≈ 2,4 años. De 8 páginas: 8 × 10 000 × 127 = 10 160 000 actualizaciones × 30 s = 304 800 000 s ≈ 9,7 años. Y si el producto debe durar diez años con margen, ni siquiera 8 páginas alcanzan para un guardado cada 30 s: hay que guardar menos seguido (cada vez que cambia más de un umbral, o al apagarse), acumulando en RAM.

2. El capacitor de reserva

Un guardado consiste en un borrado de 40 ms a 10 mA y 20 programaciones de 60 µs a 8 mA. La tensión del chip puede caer 0,8 V. ¿Qué carga consume y qué capacitor hace falta?

Ver solución

Borrado: 10 mA × 40 ms = 400 µC. Programación: 20 × 8 mA × 60 µs = 9,6 µC. Total: 409,6 µC. C ≥ Q / ΔV = 409,6 µC / 0,8 V = 512 µF; en la práctica, un valor normalizado superior (680 µF), que cubra también el envejecimiento y la tolerancia del capacitor. Observá que el borrado es el 98 % de la carga: si el diseño evita borrar en el momento de la caída (registro circular, que borra una vez cada 127 actualizaciones y guarda con una sola programación), el capacitor necesario se reduce a unos pocos µF.

3. Qué copia se escribe

Al arrancar, la copia A tiene la secuencia 12 y es válida; la B es inválida. Se quiere guardar una configuración nueva: ¿en qué copia y con qué secuencia? ¿Y si las dos son válidas con las secuencias 7 (A) y 8 (B)? ¿Y si ninguna es válida?

Ver solución

A válida (12) y B inválida: se escribe B con la secuencia 13: se repara la copia dañada y se conserva A intacta durante la escritura. Las dos válidas, A = 7 y B = 8: la más vieja es A; se escribe A con la secuencia 9 (la mayor, 8, más uno). Ninguna válida: no hay copia que proteger; se escribe, por ejemplo, A con la secuencia 1, y se registra el evento de «configuración perdida» (el equipo estaba con valores de fábrica). En el segundo caso, la comparación debe hacerse con una resta con signo, para que siga siendo correcta si la secuencia da la vuelta.

4. Escrituras evitables y años

Un equipo se usa en 20 sesiones por día. En cada sesión el usuario hace 50 cambios de un parámetro. Si se guarda en cada cambio, ¿cuántas escrituras por día? ¿Cuánto dura una página reescrita en el lugar (10 000 borrados)? ¿Y si se guarda una vez por sesión? ¿Y con un registro circular de 2 páginas (2 540 000 actualizaciones) guardando una vez por sesión?

Ver solución

En cada cambio: 20 × 50 = 1000 escrituras por día; una página en el lugar dura 10 000 / 1000 = 10 días. Una vez por sesión: 20 escrituras por día; 10 000 / 20 = 500 días ≈ 1,4 años. Con un registro circular de 2 páginas guardando una vez por sesión: 2 540 000 / 20 = 127 000 días ≈ 348 años: el desgaste deja de ser un problema (el límite pasa a ser la retención de la flash y la vida del producto). La combinación de las dos técnicas, diferir y registro circular, convierte un problema de días en uno inexistente.

5. Versiones

La versión 1 de un firmware guardaba brillo y volumen. La versión 2 agrega un umbral de alarma. Un usuario actualiza de la v1 a la v2, y después vuelve a la v1. ¿Qué debe hacer la v2 al leer los datos de la v1? ¿Y la v1 al leer datos de la v2?

Ver solución

La v2 al leer datos de la v1 los migra: conserva el brillo y el volumen del usuario y completa el umbral de alarma con su valor por defecto; el usuario no pierde nada. La v1 al leer datos de la v2 ve una versión que no conoce: si el formato lo permite (los campos nuevos al final, con un largo declarado), puede leer los dos primeros campos y ignorar el resto; si no, la regla segura es no interpretar lo que no entiende y usar valores de fábrica. Para que la vuelta atrás no pierda la configuración, hay que haber previsto en la v1 que el formato fuera extensible. Si cada versión conservara también los campos que no conoce al volver a guardar, aun mejor: al volver a la v2 el umbral seguiría ahí.

6. Un millón de equipos

Un guardado en el lugar tiene una ventana vulnerable de 21 ms, y el equipo guarda una vez por minuto. Un corte de energía ocurre en un instante al azar. ¿Qué probabilidad hay de que caiga dentro de un guardado? En una flota de un millón de equipos que sufren un corte, ¿cuántos pierden la configuración? ¿Y con guardado diario?

Ver solución

Con un guardado por minuto: la probabilidad es 21 ms / 60 000 ms = 0,035 % (3,5 × 10−4). Con un millón de equipos y un corte cada uno: 1 000 000 × 0,00035 = 350 equipos. Con un guardado por día: 21 ms / 86 400 000 ms = 2,4 × 10−7, o sea 0,24 equipos (casi ninguno por cada millón y por corte). La conclusión: el riesgo es proporcional a la frecuencia de guardado, y por eso escribir menos no solo cuida el desgaste, también reduce la exposición a los cortes. Y con doble copia, el riesgo es cero en cualquiera de los dos casos.

15 · Síntesis

Ideas para recordar

  • Los datos persistentes se clasifican por cuánto cambian, cuánto pesan, qué pasa si se pierden y cuánto deben durar: calibración (casi no cambia), configuración (poco), contadores (seguido) y registros (continuamente). Cada clase pide una técnica distinta.
  • Guardar tiene dos enemigos: el desgaste (cada borrado gasta la memoria) y el corte de energía (que interrumpe una secuencia con duración y deja datos a medias). En una flota, lo improbable es seguro: con un guardado por minuto de 21 ms, 350 de cada millón de equipos pierden la configuración por corte.
  • Un guardado seguro es atómico (todo o nada) y se construye con tres piezas: un CRC que distingue un bloque completo de uno a medias, una secuencia que dice cuál es el más nuevo, y la regla de no pisar nunca la única copia válida.
  • La doble copia con secuencia, escribiendo siempre la más vieja, garantiza una copia válida tras un corte en cualquier punto: con una copia en el lugar, 19 de 21 puntos de corte destruyen la configuración; con dos, ninguno. Se verifica lo escrito.
  • El registro circular (emulación de EEPROM) agrega un registro por actualización y paga el borrado una vez cada muchas (127 con páginas de 1 KB y registros de 8 B). Su compactación tiene un único paso de commit (el encabezado de la página nueva), y la recuperación del arranque elige la página de mayor secuencia.
  • La nivelación del desgaste reparte las escrituras (dinámica: entre los bloques libres; estática: moviendo también los datos fijos). La vida útil se multiplica por los registros por página y por el número de páginas: de 6,9 días a 19,3 años en el ejemplo. Se mide con contadores de borrado y se diseña para usar solo una fracción de la endurancia.
  • Escribir menos es la mejor protección: solo si cambió (−33 %), diferida hasta que el valor se asiente (−98 %), agrupando campos, acumulando contadores en RAM y guardando al detectar la baja tensión, con un capacitor de reserva dimensionado como C ≥ Q / ΔV.
  • Los datos se guardan como una unidad coherente y con una versión: serializados campo por campo, las versiones nuevas leen las viejas con valores por defecto y una versión desconocida no se interpreta. Se usan bibliotecas probadas (NVS, emulación de EEPROM, littlefs) entendiendo cómo funcionan, y se prueban todos los puntos de corte con una flash simulada y con cortes aleatorios en el equipo.

¿Listo para el tema siguiente?

Antes de avanzar, tomá un producto que guarde algo (aunque sea el brillo de una pantalla) y respondé: ¿qué datos persisten y cada cuánto cambian?, ¿qué pasa si se corta la energía a mitad de cada guardado?, ¿cuánto dura la memoria con la frecuencia real?, ¿qué pasa con esos datos al actualizar el firmware? Si alguna respuesta es «no sé», probá cortar la energía en cada paso (aunque sea con un simulador en la PC) y mirá qué queda.

En el próximo tema salimos del chip: memorias externas y registro de datos. La flash SPI, las tarjetas SD, los sistemas de archivos livianos y los registros circulares para datos que no caben (o no deben ir) en la flash interna.