01 · El problema
Cuándo hace falta una memoria externa
Una memoria externa no es un lujo: agrega un chip, pines, consumo y complejidad, y por eso se la usa solo cuando la flash interna no alcanza o no es el lugar adecuado. Hay cuatro motivos habituales, cada uno con sus exigencias:
| Motivo | Ejemplo | Qué se le exige |
|---|---|---|
| Registro de datos (data logging) | Un sensor de campo que mide cada segundo y descarga los datos cada tanto | Capacidad, escritura continua, vida útil y resistencia a cortes de energía |
| Recursos grandes y fijos | Imágenes, fuentes, sonidos y textos de una pantalla | Capacidad y velocidad de lectura; se escribe poco |
| Ampliar el firmware o actualizarlo | Guardar la imagen nueva antes de instalarla (tema 42), o ejecutar código desde una flash externa | Capacidad suficiente para una imagen completa, y lectura rápida |
| Más datos persistentes | Configuraciones numerosas, tablas de calibración grandes, una base de datos pequeña | Escritura por bloques y tolerancia a cortes |
En todos los casos aparece una diferencia con la flash interna que cambia el diseño: la memoria externa vive al otro lado de un bus (I²C, SPI, SDIO), con su propio protocolo, su propio tiempo de escritura y sus propias fallas (un cable flojo, un chip que no responde, una tarjeta que se retira en caliente). Para el programa, no es memoria sino un periférico: se la accede mediante comandos, no con un puntero, y cada acceso puede fallar y tarda mucho más que uno a RAM. Eso se refleja en el diseño: la lectura y la escritura pasan por una capa de acceso con comprobación de errores y tiempos máximos (tema 39), y los datos se aplican con un buffer en RAM entre el programa y la memoria.
02 · El menú
Las opciones y cómo se comparan
| Tipo | Capacidad típica | Interfaz | Escritura | Desgaste | Para qué |
|---|---|---|---|---|---|
| EEPROM I²C (por ejemplo, la 24LC256) | De 128 B a 256 KB | I²C (hasta 400 kHz o 1 MHz) | Por byte o por página de 32 a 128 B; unos 5 ms por escritura | De 105 a 106 escrituras por byte | Configuración, calibración, números de serie |
| FRAM (I²C o SPI) | De 4 KB a 1 MB | I²C o SPI | Por byte, a la velocidad del bus, sin espera | Más de 1012 ciclos | Contadores muy frecuentes, registros chicos sin desgaste práctico |
| Flash NOR SPI (por ejemplo, la serie W25Q) | De 1 a 128 MB | SPI (decenas de MHz); QSPI de 4 líneas | Programar páginas de 256 B; borrar sectores de 4 KB | Unos 105 borrados por sector | Registro de datos, recursos, imágenes de firmware, código con ejecución directa (XIP) |
| Flash NAND (SPI o paralelo) | De 128 MB a varios GB | SPI o bus paralelo | Páginas de 2 a 4 KB; bloques de 128 a 256 KB | Menor (103 a 105), con bloques defectuosos y corrección de errores (ECC) | Almacenamiento masivo con un sistema de archivos; necesita una capa de gestión |
| Tarjeta SD / microSD | De 1 GB a 1 TB | SPI (simple) o SDIO (rápido) | Bloques de 512 B; la tarjeta gestiona todo | La tarjeta nivela el desgaste internamente | Registro masivo, intercambio con una PC; extraíble |
| eMMC | De 4 a 128 GB | Bus de 8 bits | Como una SD soldada | Gestionado internamente | Sistemas con Linux embebido |
Una distinción importa más que todas: memoria cruda frente a memoria gestionada. La EEPROM, la FRAM y la flash NOR son «crudas»: el programa maneja directamente los bytes, los borrados y el desgaste, y también las consecuencias de un corte. Una tarjeta SD o una eMMC son «gestionadas»: llevan un controlador interno que traduce los bloques lógicos de 512 B a la flash NAND real, reparte el desgaste, reemplaza los bloques defectuosos y corrige errores. Eso las hace más fáciles de usar y mucho menos predecibles: el controlador decide cuándo hace su limpieza interna, y esa limpieza es la causa de las pausas de cientos de milisegundos del apartado 7.
Y la interfaz fija la velocidad máxima. Con I²C a 400 kHz, un byte de datos ocupa 9 pulsos (8 de dato y uno de reconocimiento), de modo que el máximo teórico es de unos 44 KB/s, y en la práctica, con las direcciones y las pausas, unos 30 a 40 KB/s. Con SPI a 10 MHz hay 1,25 MB/s de ancho de banda en el bus; la memoria misma es el cuello de botella (la Parte 5 del programa de Python calcula que escribir 1 MB en una flash SPI lleva entre 15 y 23 segundos, es decir, entre 45 y 70 KB/s, porque dominan los tiempos de programación y de borrado, no la transferencia).
03 · La más usada
La flash SPI por dentro
La flash NOR con interfaz SPI es la memoria externa más difundida: barata, rápida de leer, con pocos pines y con un conjunto de comandos casi estándar entre fabricantes. Se maneja con comandos de un byte seguidos de una dirección y datos, y tiene las mismas reglas físicas que la flash interna (tema 28): borrada es 0xFF, programar solo baja bits y se borra por sector. Los comandos que hay que conocer:
| Comando | Código | Qué hace |
|---|---|---|
| Read JEDEC ID | 0x9F | Devuelve el fabricante y el tipo de chip: es la primera prueba de que la conexión funciona |
| Write Enable (WREN) | 0x06 | Habilita una operación de escritura o borrado: hay que enviarlo antes de cada una |
| Read Status | 0x05 | Lee el registro de estado: el bit WIP (write in progress) indica que la memoria está ocupada |
| Read Data | 0x03 | Lee datos desde una dirección; sin límite de página, se puede leer de corrido toda la memoria |
| Page Program | 0x02 | Programa hasta 256 bytes dentro de una página |
| Sector Erase | 0x20 | Borra un sector de 4 KB (también hay borrado de bloques de 32 y 64 KB y de todo el chip) |
| Deep Power-Down / Release | 0xB9 / 0xAB | Reduce el consumo a un µA aproximadamente; hay que «despertarlo» antes de usarlo |
Hay tres reglas que hacen fallar el primer driver de casi todos:
- Cada escritura o borrado necesita un
WRENantes. Si se olvida, la operación se ignora en silencio: el programa cree haber escrito y la memoria no cambió. ElWRENse «consume» con la operación. - Tras iniciar una escritura o un borrado, hay que esperar a que termine consultando el bit
WIP(con un tiempo máximo, tema 39). Durante ese tiempo, la memoria ignora los comandos de escritura. Los tiempos son del orden de 1 ms por página y de decenas de milisegundos por sector (en el peor caso, cientos). - La programación de una página da la vuelta. Si una escritura de varios bytes cruza el final de una página de 256 B, los bytes que sobran no pasan a la página siguiente: vuelven a escribirse al principio de la misma página, pisando lo que había. Es el error más característico de estas memorias.
#define CMD_WREN 0x06
#define CMD_RDSR 0x05
#define CMD_READ 0x03
#define CMD_PP 0x02
#define CMD_SE 0x20
#define SR_WIP 0x01
#define PAGINA 256u
static void cmd_con_direccion(uint8_t cmd, uint32_t dir) {
spi_enviar(cmd); spi_enviar((uint8_t)(dir >> 16)); spi_enviar((uint8_t)(dir >> 8)); spi_enviar((uint8_t)dir);
}
static bool esperar_listo(uint32_t timeout_ms) { /* con tiempo máximo: una flash que no responde no bloquea el programa */
uint32_t t0 = ahora_ms();
for (;;) {
cs_bajo(); spi_enviar(CMD_RDSR); uint8_t sr = spi_recibir(); cs_alto();
if (!(sr & SR_WIP)) return true;
if ((int32_t)(ahora_ms() - t0) >= (int32_t)timeout_ms) return false;
}
}
static void habilitar_escritura(void) { cs_bajo(); spi_enviar(CMD_WREN); cs_alto(); }
bool flash_borrar_sector(uint32_t dir) {
habilitar_escritura();
cs_bajo(); cmd_con_direccion(CMD_SE, dir); cs_alto();
return esperar_listo(500); /* típico 45 ms; peor caso, cientos de ms */
}
/* Programar un bloque de cualquier tamaño: se parte en pedazos que NO crucen el final de una página */
bool flash_programar(uint32_t dir, const uint8_t *datos, size_t n) {
while (n > 0) {
size_t cabe = PAGINA - (dir & (PAGINA - 1)); /* lo que falta hasta el final de la página actual */
size_t k = (n < cabe) ? n : cabe;
habilitar_escritura();
cs_bajo(); cmd_con_direccion(CMD_PP, dir); spi_enviar_buf(datos, k); cs_alto();
if (!esperar_listo(5)) return false; /* típico 0,7 ms; máximo 3 ms */
dir += (uint32_t)k; datos += k; n -= k;
}
return true;
}
La Parte 4 del programa de Python muestra qué pasa con el error de la página: al escribir 20 bytes a partir de la posición 250 de una página de 256 B, el driver ingenuo deja los primeros 6 bytes en su lugar (250 a 255) y los 14 restantes (los valores 7 a 20) al principio de la misma página, en las posiciones 0 a 13, pisando lo que hubiera; el correcto los ubica en la página siguiente. El resultado ingenuo parece correcto en una prueba con datos que no cruzan un límite y falla en producción con el primer registro que lo cruza, y ese registro, además, corrompe un dato anterior sin dejar ningún error. Por eso un buen diseño elige registros cuyo tamaño divide a la página (16 o 32 bytes en una de 256): ninguno cruza jamás un límite.
Antes de confiar en nada, el driver debe comprobar el chip: leer el JEDEC ID y compararlo con el esperado. Un ID de 0xFFFFFF o 0x000000 es el síntoma clásico de un bus mal conectado (chip sin alimentación, selección de chip mal cableada, líneas cruzadas) y evita horas de depuración de «escribe pero no lee».
04 · Gran capacidad, poca previsibilidad
Las tarjetas SD
Una tarjeta SD ofrece gigabytes por poco dinero y se puede sacar y llevar a una PC, lo que la hace la elección natural para registros masivos. Se accede en bloques de 512 bytes por dos interfaces: SPI (más simple, cuatro pines y cualquier microcontrolador la soporta, pero más lenta) y SDIO (con 1 o 4 líneas de datos y mucha más velocidad, que requiere un periférico específico). Sobre ella casi siempre se monta un sistema de archivos FAT, con la biblioteca FatFs (de ChaN) como opción más difundida.
Una SD tiene una forma de comportarse que el programador de firmware debe conocer, porque no se parece a la de una flash cruda:
| Particularidad | Consecuencia | Qué hacer |
|---|---|---|
| Pausas de escritura impredecibles: el controlador interno hace limpieza (garbage collection) cuando quiere; la especificación admite que una escritura tarde hasta 250 ms | Una escritura que casi siempre tarda 1 ms puede tardar 250 de golpe: si el programa espera, se pierden muestras | Buffer en RAM dimensionado para la pausa máxima (apartado 7); escritura desde una tarea aparte |
| Consumo alto al escribir: decenas de mA, y varios mA aun en reposo según la tarjeta | Pesa en un equipo a batería (tema 33) | Escribir de a bloques grandes y poco seguido; alimentar la tarjeta a través de un interruptor y apagarla entre escrituras |
| FAT frágil ante cortes: la tabla de asignación se actualiza aparte de los datos | Un corte con un archivo abierto puede dejar la tabla inconsistente o perder el final del archivo | Sincronizar (f_sync) cada cierto tiempo; escribir bloques completos; preasignar el archivo; verificar el sistema de archivos al arrancar |
| Extracción en caliente | Escrituras que fallan; archivos cortados | Detectar la tarjeta (pin de detección o errores de lectura), cerrar y volver a montar; no bloquearse |
| Calidad muy variable: las baratas aguantan pocos ciclos y se comportan mal bajo escritura continua | Fallos «misteriosos» en el campo con la tarjeta equivocada | Tarjetas industriales de modelo conocido; probar la tarjeta que realmente se va a usar |
| Inicialización con ritual (en modo SPI) | Si se omite un paso, la tarjeta no responde | Seguir la secuencia: al menos 74 pulsos de reloj con la selección inactiva, luego los comandos de reinicio y de identificación de la tarjeta, y recién ahí subir la velocidad del bus |
Para un registro continuo en una SD con FatFs, hay un puñado de prácticas que marcan la diferencia entre un registrador que funciona años y uno que corrompe la tarjeta a las dos semanas:
- Preasignar el archivo (en FatFs,
f_expand) a su tamaño final: las escrituras no tienen que ampliar el archivo ni actualizar la tabla de asignación con cada bloque. - Escribir siempre en múltiplos de 512 B (el tamaño de bloque), alineados: un registro de 64 B se acumula en un buffer de RAM de 512 B (8 registros) y se escribe junto.
- No abrir y cerrar el archivo con cada registro: es lento y desgasta la tabla. Se abre una vez, se escribe de a bloques y se sincroniza (
f_sync) cada tanto, por ejemplo cada 10 s o cada N bloques. El intervalo es un compromiso: más seguido, menos datos perdidos en un corte pero más escrituras y más consumo. - Rotar los archivos (uno por día o por tamaño) en lugar de uno gigante: si se corrompe uno, se pierde un día y no todo.
- Escribir desde una tarea de baja prioridad con una cola que desacopla a quien produce los datos del que espera la tarjeta (tema 21).
05 · Archivos en un microcontrolador
Sistemas de archivos livianos
Un sistema de archivos organiza una memoria en archivos con nombres, y se justifica cuando hay varios archivos, de tamaño variable, que cambian. Para un único registro circular, o para una configuración de pocos parámetros, es un lujo innecesario: el diseño propio de los apartados 6 y del tema 40 es más simple, más pequeño y más fácil de verificar. Pero cuando la cantidad y el tamaño de los datos crecen, un sistema de archivos probado vale lo que cuesta. Los más usados en sistemas embebidos:
| Sistema de archivos | Pensado para | Ventajas | Limitaciones |
|---|---|---|---|
| FAT (FatFs, FAT sobre SD) | Tarjetas SD y memorias USB | Compatible con cualquier PC; muy difundido | Sin nivelación del desgaste ni protección contra cortes; depende de la tarjeta |
| littlefs | Flash NOR/NAND de microcontroladores (interna o externa) | Resistente a cortes de energía (copia en escritura), nivelación del desgaste, poca RAM y uso acotado de memoria | No es legible directamente por una PC; menos velocidad que FAT en algunos casos |
| SPIFFS (y sucesores como LittleFS en ESP32) | Flash SPI en ESP32 y similares | Con nivelación; para pocos archivos | SPIFFS está en desuso en favor de littlefs; sin directorios en su forma original |
| Sistemas de claves y valores (NVS) | Configuración pequeña (tema 40) | Simples; tolerantes a cortes | No son archivos: no sirven para datos grandes ni de longitud variable |
La diferencia que más pesa para un equipo en el campo es la tolerancia a cortes. FAT sobre una tarjeta SD no la ofrece por sí sola: un corte durante la actualización de la tabla puede dejar un sistema de archivos inconsistente. littlefs fue diseñado alrededor de ese problema: usa copia en escritura (nunca pisa un bloque con datos válidos, igual que la doble copia del tema 40) y garantiza que, tras un corte, el sistema de archivos queda en un estado consistente, el anterior o el posterior a la operación. Si el equipo se apaga de golpe con frecuencia, esa garantía vale más que la compatibilidad con la PC.
Un sistema de archivos también ocupa recursos: código (decenas de KB), RAM (buffers del tamaño de un bloque o de una página, a veces varios) y tiempo. Un microcontrolador con 2 KB de RAM no puede con FatFs; uno con 20 KB sí, a costa de una parte considerable. Por eso la pregunta no es «¿qué sistema de archivos?» sino primero «¿hace falta uno?»: muchas veces un registro circular de registros de tamaño fijo (apartado siguiente) resuelve el problema con 200 líneas de código y sin ocupar casi RAM.
06 · El diseño central
El registro circular de datos
El registro circular (circular log) es la estructura más importante de este tema: un anillo de sectores en el que se escriben registros de tamaño fijo, uno detrás de otro, hasta llenar un sector; entonces se pasa al siguiente (que se borra antes de usarse), y cuando se llega al último sector se vuelve al primero, sobrescribiendo lo más viejo. Es la misma idea del registro circular en flash que vimos en el tema 40 para guardar variables, ahora para guardar series de datos, y resuelve sin esfuerzo tres problemas a la vez:
- El desgaste se reparte solo: cada sector se borra una vez por vuelta completa al anillo, así que todos se desgastan por igual (nivelación dinámica gratis).
- Los cortes de energía son inofensivos: cada registro tiene un CRC; el que estaba a medio escribir queda con un CRC inválido y se ignora, y todos los anteriores siguen válidos.
- La memoria nunca se llena: cuando se agota el espacio, se sobrescribe lo más viejo, con una política explícita (apartado siguiente y tema 30).
Cada registro es una unidad autocontenida: un número de secuencia (que crece), el contenido (marca de tiempo y valores) y un CRC. Con 16 bytes, que divide a la página de 256 B (así ninguno cruza un límite) y al sector de 4 KB (256 registros por sector):
typedef struct {
uint32_t seq; /* número de secuencia: crece con cada registro */
uint32_t t; /* marca de tiempo */
int16_t valor[2]; /* los datos medidos */
uint8_t estado; /* banderas */
uint8_t _relleno;
uint16_t crc; /* CRC-16 de los 14 bytes anteriores */
} RegistroLog; /* 16 bytes: divide a la página (256) y al sector (4096) */
#define REG_TAM 16u
#define SECTOR 4096u
#define N_SECTORES 4096u /* flash de 16 MB */
#define LOG_TAM (SECTOR * N_SECTORES)
static uint32_t cabeza; /* dirección donde se escribe el próximo registro: vive en RAM, se recupera al arrancar */
static uint32_t sig_seq;
void log_agregar(const Muestra *m) {
RegistroLog r = { .seq = sig_seq, .t = m->t, .valor = { m->a, m->b }, .estado = m->estado };
r.crc = crc16((const uint8_t *)&r, REG_TAM - 2);
if (cabeza % SECTOR == 0) flash_borrar_sector(cabeza); /* entrar a un sector nuevo: borrarlo ANTES de usarlo */
flash_programar(cabeza, (const uint8_t *)&r, REG_TAM);
cabeza = (cabeza + REG_TAM) % LOG_TAM;
sig_seq++;
}
La posición de escritura vive en RAM y se pierde con la energía, de modo que el arranque tiene que recuperarla. No hace falta recorrer los 16 MB: basta mirar el primer registro de cada sector (4096 lecturas de 16 bytes), quedarse con el sector de mayor secuencia, que es el que se estaba llenando, y dentro de él buscar la primera posición libre:
/* Versión simplificada de la recuperación del arranque */
void log_recuperar(void) {
bool hay = false; uint32_t mejor_seq = 0, mejor_sector = 0;
for (uint32_t s = 0; s < N_SECTORES; s++) { /* solo el primer registro de cada sector */
RegistroLog r; flash_leer(s * SECTOR, &r, sizeof r);
if (registro_valido(&r) && (!hay || (int32_t)(r.seq - mejor_seq) > 0)) { mejor_seq = r.seq; mejor_sector = s; hay = true; }
}
if (!hay) { cabeza = 0; sig_seq = 0; return; } /* memoria en blanco: primer arranque */
uint32_t pos = mejor_sector * SECTOR, fin = pos + SECTOR, ultima = mejor_seq;
for (RegistroLog r; pos < fin; pos += REG_TAM) { /* recorrer los registros válidos del sector actual */
flash_leer(pos, &r, sizeof r);
if (!registro_valido(&r)) break;
ultima = r.seq;
}
while (pos < fin && registro_a_medias_en(pos)) pos += REG_TAM; /* saltar lo que quedó a medias (CRC inválido, no está borrado) */
cabeza = pos % LOG_TAM; /* si el sector se llenó, la cabeza queda al inicio del siguiente */
sig_seq = ultima + 1;
}
La Parte 3 del programa de Python lo prueba con 300 ensayos sobre una flash simulada pequeña (4 sectores, 16 registros): cada ensayo escribe entre 40 y 120 registros (dando varias vueltas al anillo) y corta la energía a mitad del último. En los 300 casos, el arranque recupera la posición correcta, todos los registros completos siguen ahí, sin saltos en la secuencia, y el registro a medias es detectado y descartado por el CRC (300 de 300). Después de recuperar, la escritura sigue sin problemas. El registro a medias no se reutiliza (la flash no permite reprogramarlo sin borrar): queda como un hueco de un registro hasta que su sector se borre en la próxima vuelta.
Qué hacer cuando el anillo se llena
Si los datos no se descargan a tiempo, el anillo da la vuelta y sobrescribe los más viejos. Es una decisión de producto, y no hay una respuesta única:
| Política | Qué se pierde | Cuándo |
|---|---|---|
| Sobrescribir lo más viejo (la del anillo) | Los datos más antiguos no descargados | Telemetría y registros: importa lo reciente |
| Dejar de registrar (anillo lleno: no se escribe más) | Los datos nuevos | Cuando el historial completo desde un evento importa más que lo reciente (una caja negra de un evento) |
| Registrar menos (agregar: mínimo, máximo y promedio por minuto en lugar de cada segundo) | Detalle de alta frecuencia | Cuando la descarga se retrasa y no se quiere perder cobertura temporal: con la capacidad del ejemplo, pasar de 1 por segundo a 1 por minuto extiende el anillo de 12 días a casi 2 años |
| Conservar los marcados como importantes | Lo no importante | Eventos y alarmas deben conservarse; las muestras de rutina, no (dos anillos: uno para cada clase) |
Y para saber qué se descargó, se guarda el número de secuencia del último registro confirmado por el servidor, en un lugar persistente aparte (tema 40). Los registros con una secuencia menor o igual ya pueden sobrescribirse sin pérdida. En el laboratorio de este tema se ve la diferencia entre un registro «subido» y uno «pendiente» y cómo los sobrescritos sin subir se cuentan como pérdida.
07 · El tiempo que la memoria no te deja
Las pausas de la memoria y el buffer en RAM
Hay un tipo de falla que no es de datos sino de tiempo: mientras la memoria está ocupada (borrando un sector, programando una página, haciendo su limpieza interna una tarjeta), el programa no puede escribir, y si las muestras siguen llegando, se pierden a menos que haya un buffer en RAM que las guarde. Un borrado de sector de una flash SPI tarda típicamente 45 ms pero puede tardar 400 ms en el peor caso; una escritura a una tarjeta SD puede tardar hasta 250 ms de golpe, aunque casi siempre sea mucho menos. El buffer necesario es la tasa de datos por la pausa máxima, y la Parte 2 del programa lo calcula:
| Caso | Tasa | RAM mínima |
|---|---|---|
| Flash SPI, borrado de sector típico (45 ms) | 1000 registros/s de 16 B = 16 KB/s | 0,72 KB (46 registros) |
| Flash SPI, borrado de sector en el peor caso (400 ms) | 16 KB/s | 6,40 KB (401 registros) |
| Tarjeta SD, pausa de 250 ms | 100 registros/s de 64 B = 6,4 KB/s | 1,60 KB (26 registros) |
| Tarjeta SD, pausa de 250 ms a 1 kHz | 1000 registros/s de 64 B = 64 KB/s | 16,00 KB (251 registros) |
Los números dan una lección de diseño: un registro de 1 kHz con un peor caso de 400 ms necesita 6,4 KB de RAM solo para las pausas, más que toda la RAM de un microcontrolador chico, y con 64 B por registro y una tarjeta SD, 16 KB. Hay cuatro maneras de bajar la necesidad, y se combinan:
- Borrar por adelantado (pre-erase): en vez de borrar el sector en el momento exacto en que se necesita, borrar el siguiente sector mientras se escribe en el actual, o durante un momento tranquilo. La escritura en el sector nuevo no tiene que esperar el borrado. En el código del apartado 6, la línea que borra al entrar a un sector se reemplaza por un borrado en segundo plano del sector que sigue. Elimina la pausa más larga.
- Escribir desde una tarea separada, con una cola o un buffer circular que desacople a quien produce de quien espera (tema 21), y dimensionar la cola con la fórmula de arriba.
- Reducir la tasa o el tamaño del registro: más compacto (menos bytes por muestra), o agregado (promedios), o disparado por eventos (tema 35).
- Usar DMA para la transferencia por SPI: libera a la CPU mientras se envían los 256 bytes de la página, aunque no acorta el tiempo de programación de la memoria, que es el cuello de botella.
Y siempre hay que decidir qué se hace si el buffer se llena de todos modos: perder las muestras nuevas, perder las más viejas del buffer o frenar el muestreo. En todos los casos, se cuenta y se registra (tema 39): un contador de muestras perdidas por buffer lleno es una señal de salud tan útil como el de errores de comunicación.
08 · El costo escondido
Energía y vida útil
Una memoria externa suma al presupuesto de energía (tema 33), y mucho más si es una tarjeta. Tres líneas del presupuesto:
- Reposo: una flash SPI en deep power-down consume del orden de 1 µA; sin ese comando, unas decenas de µA. Una tarjeta SD, en cambio, consume en reposo de cientos de µA a varios mA según el modelo (la tarjeta no puede apagarse por comando), por lo que en un equipo a batería se la alimenta a través de un interruptor y se la apaga entre grupos de escrituras (a costa de la inicialización de cada encendido, que lleva decenas a cientos de milisegundos).
- Escritura: programar y borrar una flash exige decenas de mA durante milisegundos (el borrado es lo más caro, como se vio en el capacitor de reserva del tema 40); una tarjeta SD, decenas de mA durante la escritura. La energía por registro baja cuando se agrupan los registros en bloques: un solo encendido y una sola escritura de un bloque completo cuestan mucho menos que muchas pequeñas.
- Picos: las corrientes de una tarjeta pueden hundir la tensión de una batería con resistencia interna alta (tema 36) y provocar un brown-out (tema 37) justo cuando más importa no perder datos. Se mitiga con un capacitor cerca de la tarjeta y con una alimentación robusta.
La vida útil de un registro circular en flash NOR rara vez es un problema, y la Parte 1 del programa lo muestra con una flash de 16 MB y registros de 16 B (1 048 576 registros, 4096 sectores): a un registro por segundo el anillo guarda 12,1 días y se desgasta recién a los 3325 años, y a un registro por milisegundo guarda 17,5 minutos y se desgasta en 3 años. El motivo es que el anillo reparte el desgaste entre 4096 sectores: aun a 1 kHz, cada sector se borra una vez cada 17,5 minutos, unas 30 000 veces por año de las 100 000 que aguanta. Es un buen recordatorio de que el desgaste importa mucho para una variable que se reescribe en el lugar (tema 40) y casi nada para un registro circular grande. Lo que sí hay que vigilar en una tarjeta SD es la calidad de la tarjeta y la escritura de la tabla de asignación, no la endurancia teórica de la flash.
09 · Comprobar que funciona
Verificación y diagnóstico
| Qué verificar | Cómo | Resultado esperado |
|---|---|---|
| La conexión con el chip | Leer el JEDEC ID al iniciar | El ID esperado; un 0xFFFFFF o 0x000000 se trata como falla del bus |
| Escritura y lectura | Programar un patrón (incluso cruzando un límite de página) y leerlo | Idéntico al escrito, sin pisar datos vecinos |
| Cortes de energía en cada punto | Flash simulada en la PC que se detiene en cada paso (programación y borrado), como en la Parte 3; y cortes aleatorios en el equipo | Todos los registros completos sobreviven, sin saltos; el parcial se descarta |
| La recuperación del arranque | Arrancar con la memoria en blanco, a medio llenar, llena y dando la vuelta, con un registro parcial, con un CRC dañado en el medio | La cabeza y la secuencia se recuperan siempre; un CRC dañado en el medio no detiene la recuperación |
| Las vueltas del anillo | Escribir muchas veces la capacidad total | Los contadores de borrado son parejos; no hay pérdidas inesperadas |
| Las pausas | Medir el peor tiempo de escritura y de borrado; inyectar pausas largas | No se pierden muestras con el buffer dimensionado; el contador de pérdidas se mantiene en cero |
| El consumo | Medir la corriente en reposo y en cada escritura (tema 36) | Coincide con el presupuesto |
| Extracción o falla de la memoria | Desconectar el chip o retirar la tarjeta durante el registro | No hay cuelgue; se detecta, se registra y se sigue sin la memoria (modo degradado, tema 39) |
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
El JEDEC ID es 0xFFFFFF o 0x000000 | Chip sin alimentación, selección de chip o líneas mal conectadas, modo de SPI incorrecto | Revisar la alimentación, el cableado y el modo (polaridad y fase) del bus |
La escritura «no tiene efecto» y la lectura devuelve 0xFF | Falta el WREN, o el pin de protección contra escritura está activo, o los bits de protección de bloques están puestos | Enviar WREN antes de cada operación; revisar el pin WP y el registro de estado |
| Un dato anterior queda corrompido al guardar uno nuevo | La escritura cruzó un límite de página y dio la vuelta (Parte 4) | Partir las escrituras en la frontera de página; registros que dividan a la página |
| Lectura de datos incorrectos después de escribir | No se esperó a que terminara (WIP) antes del siguiente comando | Consultar el estado con un tiempo máximo |
| Se pierden muestras de forma intermitente | Una pausa de borrado o de la tarjeta superó el buffer | Dimensionar el buffer con el peor caso; borrar por adelantado; escribir desde una tarea |
| La tarjeta SD no inicia o falla al azar | Falta la secuencia de inicialización, resistencias de polarización, tensión o corriente insuficientes, calidad de la tarjeta | Seguir la secuencia; revisar la alimentación con un osciloscopio durante la escritura; probar otra tarjeta |
| La tarjeta aparece corrupta tras un apagado | El archivo estaba abierto sin sincronizar | f_sync periódico, preasignación, archivos rotativos; verificar al arrancar; considerar littlefs |
| Tras un corte falta una parte del registro más reciente | Esperado: los datos en el buffer o sin sincronizar se pierden | Reducir el intervalo de sincronización si importa; guardar al detectar la baja tensión (tema 40) |
10 · Analogía con programación
Probar el modelo en Python
El programa tiene cinco partes: la capacidad y la vida útil de un registro circular en una flash SPI de 16 MB para distintas frecuencias de registro; el buffer de RAM necesario para sobrevivir a las pausas del borrado y de una tarjeta SD; la prueba con 300 cortes de energía de un registro circular sobre una flash simulada; la demostración del error de la frontera de página en el programador de una flash SPI; y el tiempo de escribir 1 MB con distintas velocidades del bus.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar. La simulación de cortes tarda un par de segundos.
# Memorias externas y registro de datos: capacidad y vida útil, buffers para las esperas, un registro circular con cortes y el error de la página.
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: capacidad y vida útil de un registro en una flash SPI de 16 MB ----------------------------------------
FLASH, SECTOR, REG, ENDURANCIA = 16 * 1024 * 1024, 4096, 16, 100_000
regs_total = FLASH // REG
print("Parte 1: flash SPI de 16 MB, sectores de 4 KB, registros de 16 B, endurancia de 100 000 borrados por sector")
print(f" capacidad: {regs_total:,} registros ({FLASH // SECTOR} sectores de {SECTOR // REG} registros)".replace(",", " "))
print(f" {'un registro cada':>18} | {'el anillo guarda':>18} | {'se desgasta tras':>18}")
def duracion(s):
if s < 3600: return f"{s / 60:,.1f} minutos".replace(",", " ")
if s < 86400 * 2: return f"{s / 3600:,.1f} horas".replace(",", " ")
if s < 365 * 86400 * 2: return f"{s / 86400:,.1f} días".replace(",", " ")
return f"{s / (365 * 86400):,.0f} años".replace(",", " ")
for etiqueta, seg in (("1 ms", 0.001), ("1 s", 1), ("1 min", 60), ("1 h", 3600)):
guarda_s = regs_total * seg
vida_s = guarda_s * ENDURANCIA
print(f" {etiqueta:>18} | {duracion(guarda_s):>18} | {duracion(vida_s):>18}")
# ---- Parte 2: cuánta RAM hace falta para sobrevivir a las esperas de la memoria ---------------------------------------
print()
print("Parte 2: buffer en RAM para no perder datos mientras la memoria está ocupada")
casos = (("flash SPI: borrado de sector, típico 45 ms", 1000, 16, 0.045), ("flash SPI: borrado de sector, peor caso 400 ms", 1000, 16, 0.400),
("tarjeta SD: pausa de escritura de hasta 250 ms", 100, 64, 0.250), ("tarjeta SD: pausa de 250 ms a 1 kHz y 64 B", 1000, 64, 0.250))
for nombre, hz, bytes_reg, espera in casos:
tasa = hz * bytes_reg
print(f" {nombre:<48}: {hz:>4} reg/s de {bytes_reg:>2} B = {tasa / 1000:5.1f} KB/s -> RAM mínima {tasa * espera / 1000:5.2f} KB ({int(hz * espera) + 1} registros)")
# ---- Parte 3: un registro circular en flash, con cortes de energía --------------------------------------------------------
SEC, REGS_POR_SEC, SECTORES = 64, 4, 4 # una flash pequeña para simular: 4 sectores de 64 B, registros de 16 B
class Flash:
def __init__(self):
self.m = bytearray(b"\xff" * (SEC * SECTORES)); self.borrados = [0] * SECTORES
def borrar(self, s):
self.m[s * SEC:(s + 1) * SEC] = b"\xff" * SEC; self.borrados[s] += 1
def programar(self, dir_, datos, corte=None):
for i, b in enumerate(datos):
if corte is not None and i >= corte: return False # se cortó la energía: quedó a medias
self.m[dir_ + i] &= b
return True
def armar_registro(seq):
cuerpo = seq.to_bytes(4, "big") + bytes([seq % 251] * 10)
return cuerpo + crc16(cuerpo).to_bytes(2, "big")
def leer_registro(f, i):
r = bytes(f.m[i * REG:(i + 1) * REG])
if r == b"\xff" * REG: return "vacio", None
ok = crc16(r[:-2]) == int.from_bytes(r[-2:], "big")
return ("valido", int.from_bytes(r[:4], "big")) if ok else ("parcial", None)
TOTAL_SLOTS = SECTORES * REGS_POR_SEC
def arrancar(f):
"""Recupera la posición de escritura: después del registro válido de mayor secuencia, saltando lo que quedó a medias."""
mejor, pos = -1, None
for i in range(TOTAL_SLOTS):
est, seq = leer_registro(f, i)
if est == "valido" and seq > mejor: mejor, pos = seq, i
if pos is None: return 0, 0
h = (pos + 1) % TOTAL_SLOTS
while leer_registro(f, h)[0] == "parcial": h = (h + 1) % TOTAL_SLOTS
return h, mejor + 1
def escribir(f, h, seq, corte=None):
if h % REGS_POR_SEC == 0 and any(leer_registro(f, h + k)[0] != "vacio" for k in range(REGS_POR_SEC)): f.borrar(h // REGS_POR_SEC)
return f.programar(h * REG, armar_registro(seq), corte)
print()
print("Parte 3: registro circular de 16 registros (4 sectores); 300 ensayos, cada uno con 40 a 120 registros y un corte de energía en medio del último")
rnd = random.Random(12)
fallos, descartados = 0, 0
for ensayo in range(300):
f = Flash(); h, seq = arrancar(f)
n = rnd.randint(40, 120)
for k in range(n):
escribir(f, h, seq); h = (h + 1) % TOTAL_SLOTS; seq += 1
escribir(f, h, seq, corte=rnd.randint(1, REG - 1)) # el último guardado se corta
h2, seq2 = arrancar(f) # arranque: recuperar
validos = sorted(s for i in range(TOTAL_SLOTS) for e, s in [leer_registro(f, i)] if e == "valido")
parciales = sum(leer_registro(f, i)[0] == "parcial" for i in range(TOTAL_SLOTS))
contiguos = all(b - a == 1 for a, b in zip(validos, validos[1:]))
ultimo_ok = bool(validos) and validos[-1] == seq - 1 # el último registro COMPLETO sobrevivió
if not (contiguos and ultimo_ok and seq2 == seq): fallos += 1
descartados += parciales
for _ in range(5): escribir(f, h2, seq2); h2 = (h2 + 1) % TOTAL_SLOTS; seq2 += 1 # tras recuperar se sigue escribiendo
print(f" ensayos con registros perdidos, saltos en la secuencia o posición mal recuperada: {fallos} de 300")
print(f" registros a medias detectados y descartados por el CRC: {descartados} (uno por ensayo)")
print(" borrados por sector tras el último ensayo:", f.borrados)
# ---- Parte 4: el error de la frontera de página ---------------------------------------------------------------------------
PAG = 256
def programar_pagina_ingenuo(mem, dir_, datos):
"""Una flash SPI NOR: al pasar el final de la página, la escritura DA LA VUELTA al principio de la MISMA página."""
base = dir_ - dir_ % PAG
for i, b in enumerate(datos):
pos = base + (dir_ % PAG + i) % PAG
mem[pos] &= b
def programar_bien(mem, dir_, datos):
i = 0
while i < len(datos):
n = min(PAG - (dir_ + i) % PAG, len(datos) - i) # lo que cabe hasta el final de la página actual
programar_pagina_ingenuo(mem, dir_ + i, datos[i:i + n]); i += n
mem1, mem2 = bytearray(b"\xff" * 1024), bytearray(b"\xff" * 1024)
datos = bytes(range(1, 21)) # 20 bytes escritos a partir del byte 250 de la página (cruzan el final)
programar_pagina_ingenuo(mem1, 250, datos); programar_bien(mem2, 250, datos)
print()
print("Parte 4: escribir 20 bytes a partir de la posición 250 de una página de 256 B")
print(f" ingenuo: bytes 250..255 = {list(mem1[250:256])}, bytes 0..13 de la misma página = {list(mem1[0:14])} (la escritura dio la vuelta y pisó el inicio de la página)")
print(f" correcto: bytes 250..255 = {list(mem2[250:256])}, bytes 256..269 = {list(mem2[256:270])}")
print(f" el ingenuo cumple lo pedido: {bytes(mem1[250:270]) == datos}; el correcto: {bytes(mem2[250:270]) == datos}")
# ---- Parte 5: tiempo de escribir 1 MB en una flash SPI ----------------------------------------------------------------------
print()
print("Parte 5: escribir 1 MB en una flash SPI (páginas de 256 B, programación de 0,7 ms, borrado de sector de 4 KB de 45 ms)")
paginas = (1024 * 1024) // 256
sectores = (1024 * 1024) // 4096
for mhz in (1, 10, 40):
transferencia = (256 + 4) * 8 / (mhz * 1e6) # datos + 4 bytes de comando y dirección
total = paginas * (0.7e-3 + transferencia) + sectores * 45e-3
print(f" SPI a {mhz:>2} MHz: transferencia por página {transferencia * 1000:5.2f} ms; total {total:5.2f} s ({1024 / total:6.0f} KB/s); el borrado aporta {sectores * 45e-3:.1f} s")
Resultado, parte 1: con una flash de 16 MB y registros de 16 B hay 1 048 576 registros en 4096 sectores de 256; el anillo guarda 17,5 minutos a un registro por milisegundo (y se desgasta en 3 años), 12,1 días a uno por segundo (3325 años), 728,2 días a uno por minuto (199 501 años) y 120 años a uno por hora. Parte 2: a 1000 registros por segundo de 16 B (16 KB/s), un borrado de 45 ms pide 0,72 KB de RAM y uno de 400 ms, 6,40 KB; con una tarjeta SD y pausas de 250 ms, 100 registros por segundo de 64 B piden 1,60 KB y 1000 por segundo, 16,00 KB. Parte 3: en 300 ensayos con 40 a 120 registros y un corte en el último, hubo 0 fallos de recuperación (sin pérdidas ni saltos de secuencia) y 300 registros a medio escribir detectados y descartados por el CRC. Parte 4: al escribir 20 bytes a partir de la posición 250, el driver ingenuo deja los bytes 7 a 20 en las posiciones 0 a 13 de la misma página y no cumple lo pedido; el correcto los coloca en la página siguiente y sí. Parte 5: escribir 1 MB lleva 22,91 s a 1 MHz (45 KB/s), 15,24 s a 10 MHz (67 KB/s) y 14,60 s a 40 MHz (70 KB/s), de los cuales 11,5 s son de borrado: subir 40 veces la velocidad del bus ahorra apenas un 36 % del tiempo.
Probá cambiar REG a 24 (un registro que ya no divide a la página) y mirá cómo la prueba de la Parte 3 sigue funcionando pero ahora puede haber registros que crucen un sector, o cambiá el tamaño del anillo (SECTORES) para ver cuántos borrados recibe cada sector. La lección de la Parte 5 es útil: la velocidad del bus importa mucho menos que el tiempo de programación y de borrado de la memoria; para ir más rápido hay que escribir menos, no usar un reloj más rápido.
11 · Exploración interactiva
El anillo de registros
El laboratorio es un registro circular de 6 sectores con 8 registros cada uno (48 ranuras). Escribís registros (cada uno con su número de secuencia), el anillo borra cada sector al entrar a él y, cuando da la vuelta, sobrescribe los más viejos. Podés subir los registros pendientes al servidor (se vuelven azules y pueden sobrescribirse sin pérdida), programar un corte de energía a mitad del próximo registro y hacer el arranque, que recupera la posición de escritura. Los registros sobrescritos sin haberse subido se cuentan como pérdida.
Escribir, subir, cortar y recuperar
Verde: registro válido pendiente (con su número de secuencia) · Azul: válido y ya subido · Rojo rayado: a medias (CRC inválido) · Blanco: vacío · Contorno naranja: dónde se escribe el próximo.
Flash vacía. Escribí algunos registros.
Recorrido guiado
- Apretá «Escribir 10»: se llena el sector 0 (8 registros) y se empieza el 1. Mirá que cada registro tiene su número de secuencia y que el contorno naranja marca dónde se escribe el próximo. Los números entre paréntesis de cada sector cuentan los borrados.
- Apretá «Escribir 50» dos veces: el anillo (48 ranuras) da una vuelta completa y empieza a sobrescribir. Cada vez que entra a un sector lleno, lo borra primero (el contador de borrados sube) y descarta sus registros. Los registros sobrescritos sin haber sido subidos aparecen en «Perdidos».
- Apretá «Reiniciar», luego «Escribir 10» dos veces (20 registros) y «Subir lo pendiente»: los 20 registros se vuelven azules. Escribí 28 más (lo que falta para llenar el anillo de 48 ranuras) y después 16 más: el anillo entra de nuevo a los sectores 0 y 1, cuyos registros ya estaban subidos, y «Perdidos» sigue en 0. Si escribís 8 más, al entrar al sector 2 se borran 4 registros que no se habían subido (los números 21 a 24) y ahora «Perdidos» marca 4: solo se pierde lo que se sobrescribe sin subir. Es la razón de guardar el último número de secuencia confirmado.
- Apretá «Cortar la energía en el próximo registro» y luego «Escribir 1»: el registro queda a medias (rojo rayado, CRC inválido) y el sistema se apaga: la posición de escritura, que estaba en RAM, se perdió. Los registros anteriores siguen intactos.
- Apretá «Arranque y recuperación»: el sistema encuentra el registro válido de mayor secuencia, salta el parcial (no se puede reprogramar sin borrar su sector) y reanuda en la ranura siguiente. Escribí unos registros más: la secuencia continúa sin saltos ni repeticiones. El hueco del parcial desaparece cuando el anillo vuelve a borrar ese sector.
- Programá un corte justo cuando la próxima escritura entra a un sector nuevo (fijate dónde está el contorno naranja: en la primera ranura de un sector): el sector se borra y el registro queda a medias en la primera ranura. El arranque recupera igual: el sector nuevo tiene un parcial y nada válido, y la posición se reanuda detrás del parcial.
Desafío: ¿qué cambiarías para que, aun si el servidor tarda días en responder, no se pierdan registros importantes como las alarmas?
Con dos anillos: uno para las muestras de rutina (que se sobrescribe sin remordimientos, porque importa lo reciente) y otro, más chico, para los eventos importantes (alarmas, fallas, cambios de estado), que ocurren raramente y por eso tardan mucho más en dar la vuelta. El anillo de eventos puede tener capacidad para meses aunque el de muestras guarde solo días. Además, se puede reducir la información de las muestras a medida que se acerca el límite (promediando por minuto en lugar de por segundo, con lo que el mismo espacio dura 60 veces más) y, si la capacidad se agota, reducir la frecuencia de muestreo de forma controlada y avisando. La política se decide al diseñar el producto, no cuando la memoria se llena: qué se pierde primero, qué nunca, y cómo se entera el usuario de que se perdieron datos (un contador de registros perdidos es una señal que el servidor debe recibir).
12 · Método de trabajo
Método: un registro de datos que no falla
- Calculá lo que necesitás antes de elegir el chip: bytes por registro, registros por segundo, cuánto tiempo hay que retener sin descargar, y el peor caso de pausa de la memoria. De ahí salen la capacidad y la RAM del buffer.
- Elegí memoria cruda o gestionada según qué necesitás: flash NOR SPI para registros y recursos con control total; tarjeta SD para gran capacidad y extracción; FRAM para contadores frecuentes. Y preguntate si hace falta un sistema de archivos.
- Escribí un driver con comprobaciones: verificar el JEDEC ID,
WRENantes de cada operación, esperar elWIPcon un tiempo máximo, partir las escrituras en la frontera de página, y toda operación con resultado comprobado (tema 39). - Diseñá el registro con tamaño fijo que divida a la página, con secuencia y CRC en cada uno, en un anillo de sectores; borrá cada sector antes de entrar a él, o mejor, por adelantado.
- Escribí la recuperación del arranque antes que el resto: encontrar la cabeza y la secuencia, saltar el registro parcial, y tolerar memoria en blanco y CRC dañados.
- Desacoplá con un buffer en RAM dimensionado para la pausa más larga, y escribí desde una tarea aparte; contá las muestras perdidas por buffer lleno.
- Decidí la política de anillo lleno (sobrescribir lo viejo, parar, agregar, anillos separados) y guardá el último número de secuencia confirmado por el servidor.
- Si usás una SD: preasignar archivos, escribir bloques de 512 B alineados, sincronizar periódicamente, rotar archivos, alimentarla por un interruptor, y probar con la tarjeta real.
- Presupuestá la energía: reposo de la memoria (deep power-down o interruptor), picos de escritura y agrupación de registros.
- Probá los cortes en cada paso con una flash simulada y con cortes reales aleatorios, y la falla o extracción de la memoria sin colgarse.
13 · Comprensión
Confusiones frecuentes
«Una flash externa se usa igual que la interna, con un puntero»
Es un periférico al otro lado de un bus: cada acceso es un comando con su protocolo, tarda mucho más que uno a RAM y puede fallar. Se la maneja con una capa de acceso con comprobaciones y tiempos máximos, y con un buffer en RAM. Salvo en modo XIP (ejecución directa desde una flash QSPI), no se la mapea como memoria.
«Si la prueba con datos de ejemplo funciona, el driver está bien»
El error de la frontera de página no aparece con datos que no cruzan un límite. Funciona en la mesa durante semanas y falla con el primer registro que cruza, corrompiendo además un dato anterior. Se prueba con escrituras que cruzan la frontera a propósito, y con tamaños de registro que dividan a la página para que no ocurra.
«Un reloj de SPI más rápido hace que registre más rápido»
El bus no es el cuello de botella: lo son el tiempo de programación y de borrado de la memoria. En la Parte 5, subir el reloj de 1 a 40 MHz (40 veces) ahorra solo un 36 % del tiempo de escribir 1 MB. Para registrar más, hay que escribir menos o en bloques mayores, no acelerar el bus.
«Una tarjeta SD siempre responde en pocos milisegundos»
Casi siempre, pero la especificación admite pausas de hasta 250 ms cuando el controlador interno hace su limpieza. Un diseño que no tolera una pausa así, sin buffer ni tarea separada, pierde datos de forma intermitente en el campo, de aquellos fallos que no se reproducen en la mesa.
«Con FAT en la SD, no tengo que preocuparme por los cortes»
FAT no protege contra los cortes: la tabla de asignación se actualiza aparte de los datos, y un corte puede dejarla inconsistente. La tarjeta nivela el desgaste, pero no repara el sistema de archivos. Se mitiga con sincronización periódica, preasignación y archivos rotativos, o se usa un sistema pensado para cortes, como littlefs.
«El desgaste de una flash de 16 MB es un problema grave en un registrador»
Con un registro circular reparte el borrado entre miles de sectores, y los números dan años o siglos: a un registro por segundo, 3325 años. El desgaste importa para una variable reescrita en el lugar (tema 40), no para un anillo grande. Lo que sí hay que vigilar en una SD es su calidad.
«Si se corta la energía durante un guardado, se pierde todo el registro»
Se pierde, como máximo, el registro que se estaba escribiendo. Los demás son válidos, cada uno con su CRC, y el arranque descarta el parcial y continúa. Es una de las grandes ventajas del registro circular de registros de tamaño fijo con CRC.
«Un buffer chico en RAM alcanza porque casi siempre la escritura es rápida»
El buffer se dimensiona para el peor caso, no el típico: un borrado de 45 ms típicos puede ser de 400 ms. A 1000 registros por segundo, la diferencia es de 0,7 KB contra 6,4 KB. Un buffer dimensionado para el caso típico funciona hasta el día del peor caso.
14 · Práctica guiada
Ejercicios
1. ¿Cuánto guarda?
Una flash SPI de 8 MB guarda registros de 32 bytes, uno cada 10 segundos, en un anillo. ¿Cuántos registros caben? ¿Cuánto tiempo de datos guarda?
Ver solución
8 MB = 8 388 608 bytes / 32 = 262 144 registros. A uno cada 10 s: 262 144 × 10 = 2 621 440 s ≈ 30,3 días. Si hay que guardar 90 días, hay que registrar menos seguido (cada 30 s da 91 días), usar registros más chicos o agregar (por ejemplo, promedios por minuto), o una memoria más grande.
2. El buffer para un borrado
Un equipo produce 500 registros por segundo de 24 bytes. El peor caso de borrado de sector es de 400 ms. ¿Cuántos registros y cuántos bytes de RAM hacen falta como mínimo para no perder datos durante el borrado? ¿Cómo se evitaría el problema?
Ver solución
En 400 ms se producen 500 × 0,4 = 200 registros, o sea 200 × 24 = 4800 bytes de RAM, como mínimo (y conviene sumar un margen). Se evita borrando por adelantado: borrar el siguiente sector mientras se escribe en el actual (o en un momento de poca actividad), de modo que cuando el anillo llega a él, ya está listo. Con eso, la pausa de borrado deja de afectar a la escritura, y el buffer solo debe cubrir las esperas de programación de página (del orden de 1 ms, o 3 ms en el peor caso).
3. La frontera de página
Se quiere programar 100 bytes a partir de la dirección 0x1F0 de una flash con páginas de 256 bytes. ¿En cuántas operaciones de Page Program se parte y de qué tamaño? ¿Qué pasaría si se enviara en una sola?
Ver solución
0x1F0 = 496; la posición dentro de la página es 496 mod 256 = 240; hasta el final de esa página (256) quedan 16 bytes. Se parte en dos operaciones: 16 bytes en la dirección 0x1F0 (hasta 0x1FF) y 84 bytes en 0x200 (el principio de la página siguiente). Si se enviara en una sola, los primeros 16 bytes irían bien, y los 84 restantes darían la vuelta y se escribirían en las posiciones 0 a 83 de la misma página (0x100 a 0x153), pisando (o combinando con un AND, porque programar solo baja bits) lo que hubiera ahí.
4. Tiempo de escribir un sector
Con una flash SPI a 10 MHz (0,21 ms de transferencia por página), programación de 0,7 ms por página y borrado de sector de 45 ms, ¿cuánto tarda en reescribirse un sector completo de 4 KB (borrar y programar 16 páginas)? ¿A cuántos KB/s equivale?
Ver solución
Programar 16 páginas: 16 × (0,7 + 0,21) = 14,6 ms. Más el borrado: 14,6 + 45 = 59,6 ms. Equivale a 4096 B / 0,0596 s ≈ 69 KB/s, que coincide con los 67 KB/s de la Parte 5 (que incluye pequeños costos de comando). Observá que el borrado (45 ms) pesa más que programar las 16 páginas (14,6 ms): por eso borrar por adelantado y evitar borrados innecesarios es la mejor manera de acelerar.
5. Una SD en un registrador
Un equipo registra 100 registros por segundo de 64 bytes en una SD de 8 GB (8 × 109 bytes). ¿Cuántos bytes por día? ¿Cuántos días hasta llenarla? Si se acumulan 8 registros en un buffer de 512 B antes de escribir, ¿cada cuánto se escribe?
Ver solución
100 × 64 = 6400 B/s; por día: 6400 × 86 400 = 552 960 000 B (unos 553 MB). 8 × 109 / 552 960 000 ≈ 14,5 días hasta llenarla (después, rotar o sobrescribir). Con 8 registros por bloque de 512 B: 8 / 100 = una escritura cada 80 ms. Con la pausa máxima de 250 ms de la tarjeta, el buffer debe poder retener los registros de al menos 250 ms (25 registros, unos 1,6 KB), o sea, unos 4 bloques de 512 B en cola, no uno solo.
6. Una vuelta del anillo
Con la flash de 16 MB, registros de 16 B y un registro por segundo, el anillo da una vuelta cada 12,1 días. ¿Cuántas veces se borra cada sector por año? ¿Cuántos años tarda en agotar los 100 000 borrados?
Ver solución
Vueltas por año: 365 / 12,14 ≈ 30 vueltas, es decir, unos 30 borrados por sector por año. Para agotar 100 000: 100 000 / 30,07 ≈ 3325 años, el valor de la Parte 1. Conclusión: con un registro circular grande, el desgaste de la flash no es el factor limitante: lo serán la retención de los datos (unos 20 años), la vida del producto o la calidad de la conexión. Si en cambio se reescribiera siempre el mismo sector, el desgaste llegaría en días: lo que protege es el anillo.
15 · Síntesis
Ideas para recordar
- Se usa una memoria externa para registro de datos, recursos grandes, imágenes de firmware o más datos persistentes. Es un periférico al otro lado de un bus: cada acceso es un comando, tarda mucho más que uno a RAM y puede fallar; se accede con comprobaciones y tiempos máximos.
- Hay memoria cruda (EEPROM, FRAM, flash NOR: el programa maneja bytes, borrados y cortes) y gestionada (SD, eMMC: un controlador interno nivela y corrige, pero hace pausas impredecibles). La velocidad del bus importa menos que el tiempo de programar y borrar la memoria.
- La flash NOR SPI se maneja con comandos:
WRENantes de cada escritura o borrado, esperar el bitWIPcon tiempo máximo, programar páginas de 256 B y borrar sectores de 4 KB. La programación da la vuelta en el final de la página: hay que partir las escrituras en la frontera, y elegir registros que dividan a la página. El primer paso del driver es leer el JEDEC ID. - Las tarjetas SD dan capacidad pero con particularidades: pausas de hasta 250 ms, consumo alto, FAT frágil ante cortes. Se mitigan con buffer, tarea separada, bloques de 512 B alineados,
f_syncperiódico, preasignación, archivos rotativos y alimentación por interruptor. Un sistema como littlefs da tolerancia a cortes y nivelación; pero antes hay que preguntarse si hace falta un sistema de archivos. - El registro circular es un anillo de sectores con registros de tamaño fijo con secuencia y CRC: reparte el desgaste solo, un corte pierde como máximo el registro en curso (se descarta por el CRC) y nunca se llena. El arranque recupera la cabeza mirando el primer registro de cada sector y saltando el parcial. En 300 cortes simulados, 0 fallos.
- Las pausas de la memoria exigen un buffer: RAM = tasa × pausa máxima (6,4 KB para 1000 registros/s de 16 B con un borrado de 400 ms). Se baja borrando por adelantado, escribiendo desde una tarea, reduciendo la tasa o el registro, y se cuentan las muestras perdidas.
- Se decide qué hacer cuando el anillo se llena (sobrescribir lo viejo, parar, agregar, anillos separados para las alarmas) y se guarda el último número de secuencia confirmado por el servidor. El desgaste de un anillo grande casi nunca importa (3325 años a 1 registro/s); la energía, sí: reposo con deep power-down o interruptor, picos y agrupación de escrituras.
- Se prueba cortando la energía en cada paso con una flash simulada y con cortes reales, con escrituras que cruzan la frontera de página a propósito, con memoria en blanco, llena, con CRC dañado y con la memoria desconectada sin que el equipo se cuelgue.
¿Listo para el tema siguiente?
Antes de avanzar, calculá el registro de datos de un producto tuyo: bytes por registro, registros por segundo, días que debe retener, pausa máxima de la memoria. De ahí: capacidad, buffer en RAM y tamaño de registro. Después probá, aunque sea con una flash simulada en la PC, el corte de energía en cada paso de un guardado, incluido el de un registro que cruza una frontera de página.
En el próximo tema usamos estas memorias para algo crítico: la actualización de firmware. Cargadores de arranque, particiones A y B, actualización por cable y por aire (OTA), y cómo hacerlo sin que un corte de energía deje al equipo inutilizado.