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):
| Dato | Cuánto cambia | Si se pierde | Dónde y cómo |
|---|---|---|---|
| Calibración de fábrica (coeficientes del sensor, número de serie) | Una vez en la vida | El equipo mide mal, o no se puede identificar: crítico | Zona 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 cambia | Molesto, pero se puede volver a elegir | Doble 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) | Ocasionalmente | Puede ser serio: perder la posición, el código, el progreso | Igual 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 evento | Se pierde un historial, a veces con valor legal o de garantía | Se 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) | Continuamente | Se pierde el historial más reciente | Memoria 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 falla | Se pierde evidencia | RAM 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:
| Pieza | Qué hace |
|---|---|
| Un CRC (o suma de verificación) que cubre todo el bloque | Permite 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álida | Se escribe el dato nuevo en otro lugar, y recién cuando está completo y verificado deja de importar el viejo |
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:
| Estrategia | Puntos de corte | Config. vieja | Config. nueva | Configuración perdida |
|---|---|---|---|---|
| Una copia, escrita en el lugar | 21 | 1 | 1 | 19 |
| Dos copias con secuencia | 21 | 20 | 1 | 0 |
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:
| Paso | Qué se hace | Si se corta aquí |
|---|---|---|
| 1 | Borrar la página de destino (la que no está activa) | La página activa sigue intacta; se reintenta al arrancar |
| 2 | Copiar al destino el valor vigente de cada clave, como registros nuevos | El destino no tiene encabezado válido: se ignora; sigue valiendo la activa |
| 3 | Escribir 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 |
| 4 | Borrar la página vieja | Hay 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:
| Estrategia | Borrados tras 100 000 actualizaciones | Actualizaciones hasta el desgaste | Duració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 000 | 6,9 días |
| Registro circular de 2 páginas | 393 y 394 (una por página) | 2 540 000 | 4,8 años |
| Registro circular de 4 páginas | (la mitad por página) | 5 080 000 | 9,7 años |
| Registro circular de 8 páginas | (un cuarto por página) | 10 160 000 | 19,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):
| Política | Escrituras | Por qué |
|---|---|---|
| Guardar en cada cambio | 5000 | Cada movimiento de la perilla es una escritura |
| Guardar solo si el valor es distinto del guardado (leer antes de escribir) | 3360 | Un 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) | 94 | Mientras 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_bytedel AVR lo hace por vos; elnvs_escribirdel 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:
| Caída de tensión permitida | Capacitor mínimo |
|---|---|
| 0,5 V | 617 µF |
| 1,0 V | 308 µF |
| 1,4 V | 220 µ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:
| Opción | Qué ofrece | Cuá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 cortes | Configuración en un ESP32 |
| NVS y «settings» de Zephyr | Almacenes de clave-valor sobre la flash con CRC y compactación | Proyectos con Zephyr |
| littlefs | Un sistema de archivos pensado para microcontroladores: resistente a cortes de energía (copia en escritura), con nivelación del desgaste y poca RAM | Varios 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 desgaste | Registro 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 ilimitada | Contadores 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.
| Propiedad | Có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 mezcla | El contenido recuperado es idéntico a la versión vieja o a la nueva, byte por byte |
| La secuencia nunca retrocede | Tras recuperar, la secuencia es mayor o igual que antes de la operación |
| La recuperación es idempotente | Cortar 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 funcionan | Memoria en blanco (primer arranque), ambas copias dañadas, página llena (compactación), versión desconocida |
| El desgaste está repartido | Tras muchas actualizaciones simuladas, los contadores de borrado de las páginas son parecidos |
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Algunos equipos de la flota pierden la configuración tras un apagón | Escritura en el lugar sin doble copia: el corte cayó en la ventana del guardado | Doble copia con secuencia y CRC; escribir siempre la más vieja |
| El equipo «recuerda» valores absurdos tras un corte | Campos guardados por separado: un corte dejó una combinación incoherente | Guardar el conjunto completo con un solo CRC |
| Un contador se pone a cero o salta tras un corte | Una sola copia, o un guardado que perdió el último cambio sin saberlo | Registro 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 tiempo | La 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ños | El firmware nuevo interpreta el formato viejo con la estructura nueva | Versió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 raro | El firmware viejo interpreta datos de una versión más nueva | Una versión desconocida no se interpreta: valores de fábrica, o formato extensible |
| Un bucle de reinicios que agota la flash | Una 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 gastada | La 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
Estado de la memoria tras el corte (bytes en hexadecimal; FF = borrado)
—
—
Movés el deslizador y mirá qué pasa.
Recorrido guiado
- 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ó.
- 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. - Probá los valores 2, 3, 4, 5, 6: la página tiene el principio de la configuración nueva y
FFen 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. - 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
- Guardá lo que va junto como una unidad, con un solo CRC y una sola secuencia.
- 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.
- Definí los valores de fábrica como último recurso y el aviso que se emite cuando se usan.
- 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.
- 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.
- 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.