01 · El permiso para equivocarse
Por qué actualizar y qué puede salir mal
Casi todo producto con firmware necesita actualizarse, por tres razones que se repiten: corregir errores (incluidos los de seguridad: un producto conectado sin forma de parchearse es un riesgo permanente, tema 43), agregar funciones y adaptarse a cambios del entorno (un protocolo nuevo, un servidor que cambió). Un producto que no se puede actualizar tiene que salir perfecto de fábrica, y ninguno lo hace. Por eso, desde el diseño del hardware (la cantidad de flash, el acceso al cargador) hasta el último detalle del firmware, la actualización es una función de primer nivel, no un agregado.
Los canales por los que se actualiza un equipo van desde lo muy controlado hasta lo remoto, y conviene distinguirlos porque tienen riesgos y exigencias muy distintos:
| Canal | Quién lo hace | Ventaja | Riesgo o exigencia |
|---|---|---|---|
| Programador SWD/JTAG | La fábrica o el desarrollador (tema 5) | Control total; recupera un equipo sin cargador | Requiere acceso físico al conector |
| Cargador por cable (UART, USB DFU, CAN) | El técnico en campo o el usuario con una PC | No hace falta un programador; el cargador lo maneja | El cargador debe existir y estar a salvo; el cable puede desconectarse a mitad de camino |
| Actualización por aire (OTA: Wi-Fi, BLE, celular, LoRa) | El propio equipo, a pedido de un servidor | Sin acceso físico; se actualiza una flota entera | El mayor riesgo: no hay nadie al lado si falla. Exige vuelta atrás, autenticación (tema 43) y robustez ante enlaces malos |
La actualización por aire es la más valiosa y la más peligrosa, y define el nivel de diseño. Una regla la resume: un equipo que se puede actualizar por aire tiene que poder fallar la actualización sin perder la capacidad de funcionar y de volver a intentarlo. Los fallos posibles son cuatro, y cada técnica del tema responde a uno:
| Falla | Ejemplo | Defensa |
|---|---|---|
| La transferencia se interrumpe | El enlace se cae a la mitad; se descarga la mitad de la imagen | Bloques con CRC y reanudación; no tocar el firmware vigente hasta tener la imagen completa y verificada |
| El corte de energía durante la escritura | La batería se agota mientras se escribe la flash | No escribir sobre la única copia válida (A/B); encabezado como último paso (commit) |
| La imagen está dañada o es la equivocada | Un bit alterado; una imagen para otro modelo de producto | CRC; encabezado con dirección y producto; (firma: tema 43) |
| La imagen es válida pero tiene un defecto | Arranca y se cuelga; pierde la comunicación y no puede volver a actualizarse | Arranque de prueba, confirmación y vuelta atrás automática; modo de recuperación |
02 · El que arranca primero
El cargador de arranque y el mapa de la flash
El cargador de arranque (bootloader) es un programa pequeño que ocupa el principio de la flash y se ejecuta antes que la aplicación (el tema 8 explicó cómo empieza el arranque; acá lo vemos como el árbitro de las actualizaciones). Su trabajo es decidir qué se ejecuta: si hay una aplicación válida, la lanza; si hay una actualización pendiente, la instala; si algo falló, vuelve atrás o espera una imagen nueva. Como es lo que se ejecuta cuando todo lo demás falla, tiene tres reglas de oro: es pequeño y simple (cuanto menos código, menos errores), está protegido contra escritura (el microcontrolador permite bloquear sus páginas; en los STM32, con bytes de opción) y casi nunca se actualiza, porque un error en la actualización del cargador es el único que no se puede arreglar con otra actualización.
Del mapa se desprende una consecuencia práctica que sorprende al principio: la aplicación ya no empieza en el inicio de la flash, y tiene que saberlo. El enlazador debe ubicar su código desde la dirección de la ranura (en el linker script, el origen de la región FLASH pasa de 0x08000000 a, por ejemplo, 0x08004000), y la tabla de vectores de interrupción tiene que apuntar a la nueva ubicación: en un Cortex-M, el cargador escribe el registro SCB->VTOR antes de saltar. El salto mismo es una secuencia delicada, porque la aplicación espera encontrar el hardware como después de un reset:
#define RAM_INI 0x20000000u
#define RAM_FIN 0x20005000u /* final de la RAM de este chip (20 KB) */
typedef void (*Entrada)(void);
/* Lanzar la aplicación de la ranura que empieza en 'base'. Si no es válida, vuelve (el llamador decide qué hacer). */
void saltar_a_aplicacion(uint32_t base) {
uint32_t sp = *(volatile uint32_t *)base; /* palabra 0 de la tabla de vectores: pila inicial (tema 8) */
uint32_t reset = *(volatile uint32_t *)(base + 4u); /* palabra 1: dirección de Reset_Handler */
if (sp < RAM_INI || sp > RAM_FIN) return; /* la pila no apunta a la RAM: no es una aplicación */
if ((reset & 1u) == 0u) return; /* en Cortex-M la dirección de una función lleva el bit 0 (Thumb) en 1 */
if (reset < base || reset >= base + TAM_RANURA) return; /* y debe estar dentro de la ranura */
__disable_irq();
desinicializar_perifericos(); /* SysTick apagado, interrupciones pendientes limpias, relojes como tras un reset */
SCB->VTOR = base; /* la tabla de vectores ahora es la de la aplicación */
__set_MSP(sp); /* la pila de la aplicación */
((Entrada)reset)(); /* no se vuelve de aquí */
}
El paso desinicializar_perifericos() es una fuente clásica de errores raros: si el cargador deja el SysTick corriendo, un temporizador activo o una interrupción habilitada, la aplicación arranca con una interrupción que se dispara antes de que ella haya instalado su tabla de vectores o inicializado sus variables, y el equipo se cuelga de forma irreproducible. La regla es dejar el microcontrolador, antes de saltar, lo más parecido posible al estado posterior a un reinicio.
03 · Qué se descarga
La imagen y su encabezado
Lo que viaja y se guarda en una actualización no es el archivo del compilador a secas: es una imagen, el binario de la aplicación precedido (o seguido) de un encabezado que lo describe. El encabezado es lo que permite al cargador decidir si una ranura contiene algo ejecutable sin ejecutarlo:
#define MAGIA_IMAGEN 0x464D5731u /* 'FMW1': identifica una imagen de firmware */
typedef struct {
uint32_t magia; /* MAGIA_IMAGEN: distingue una imagen de flash borrada o de basura */
uint32_t formato; /* versión del propio encabezado (para poder evolucionarlo, tema 40) */
uint32_t producto; /* identificador del modelo de producto: no instalar la imagen de otro */
uint32_t version; /* mayor.menor.parche empaquetado en 32 bits */
uint32_t tamano; /* bytes del código */
uint32_t direccion_enlace; /* dirección para la que se enlazó la imagen: debe coincidir con la ranura */
uint32_t crc32; /* CRC-32 del código */
uint32_t flags;
uint32_t crc_encabezado; /* CRC del encabezado mismo (los campos anteriores) */
} EncabezadoImagen;
bool imagen_valida(uint32_t ranura_base, uint32_t tam_ranura) {
const EncabezadoImagen *h = (const EncabezadoImagen *)ranura_base;
if (h->magia != MAGIA_IMAGEN) return false; /* borrada o basura */
if (crc32((const uint8_t *)h, offsetof(EncabezadoImagen, crc_encabezado)) != h->crc_encabezado) return false; /* encabezado dañado */
if (h->producto != PRODUCTO_ID) return false; /* es de otro producto */
if (h->direccion_enlace != ranura_base + sizeof *h) return false; /* enlazada para otra ranura: no puede ejecutarse aquí */
if (h->tamano == 0 || h->tamano > tam_ranura - sizeof *h) return false; /* tamaño imposible */
return crc32((const uint8_t *)(ranura_base + sizeof *h), h->tamano) == h->crc32; /* el código completo está íntegro */
}
El encabezado se escribe al final de la copia a la ranura: mientras no está, la ranura es «borrada o basura» para el cargador, y un corte durante la escritura del código deja una ranura inválida y no una a medias que parezca válida. Es el commit del tema 40 aplicado a una imagen de cientos de KB. La Parte 5 del programa muestra por qué hace falta el CRC de 32 bits de la imagen: sobre una imagen de 2 KB, detectó los 16 384 errores posibles de 1 bit y, de 20 000 corrupciones al azar de hasta 64 bits, no dejó pasar ninguna. Pero un CRC solo prueba que la imagen no se dañó por accidente; no prueba quién la hizo: cualquiera puede calcular el CRC de una imagen maliciosa. Esa garantía (autenticidad) la dan las firmas digitales del tema 43.
Hay un detalle del campo direccion_enlace que decide una elección de arquitectura: en un microcontrolador sin MMU, el código no es reubicable. Una imagen enlazada para ejecutarse desde la ranura A no funciona si se la copia a la ranura B (las direcciones de las funciones y de las constantes están fijas en el binario). Hay dos soluciones, que dan dos familias de estrategias (apartado siguiente): compilar dos imágenes, una para cada ranura, y descargar la que corresponde a la ranura libre; o copiar la imagen nueva a la ranura principal con el cargador, de modo que siempre se ejecute desde la misma dirección. Cada una tiene sus costos.
04 · El diseño de fondo
Estrategias: en el lugar, A/B y ranura de descarga
La pregunta central es dónde se escribe la imagen nueva mientras se está ejecutando la vieja. Hay tres respuestas, con compromisos distintos:
| Estrategia | Cómo funciona | Flash que exige | Ventana de riesgo | Vuelta atrás |
|---|---|---|---|---|
| En el lugar (una sola ranura) | El cargador borra la aplicación y escribe la nueva encima | La mínima: una aplicación | Todo el tiempo que dura el borrado y la escritura (cientos de ms a segundos o minutos) | No existe |
| A/B (dos ranuras de ejecución) | Se recibe la imagen en la ranura inactiva; al terminar y verificarla, se cambia un selector. Cada ranura contiene una imagen enlazada para su dirección | El doble de una aplicación | Ninguna: la ranura activa no se toca hasta conmutar el selector | Sí: la ranura vieja sigue intacta |
| Ranura de descarga con copia (swap) | La imagen nueva se guarda en una ranura secundaria (interna o en una flash externa); el cargador la copia a la ranura principal, y conserva la vieja para volver atrás. La imagen se enlaza siempre para la ranura principal | Dos aplicaciones (ranura principal y de descarga), a veces una más de trabajo; la secundaria puede ir en una flash externa | Mínima si la copia es a prueba de cortes (con marcadores de progreso); la copia tarda en el arranque | Sí: se restaura la vieja guardada |
La Parte 1 del programa de Python compara las dos primeras de forma exhaustiva, igual que en el tema 40: cortando la energía después de cada paso posible de la actualización. En el modelo, la imagen se escribe en 8 bloques:
| Estrategia | Pasos | Puntos de corte | Arranca el firmware viejo | Arranca el nuevo | El equipo queda sin firmware |
|---|---|---|---|---|---|
| En el lugar (una ranura) | 10: borrar, 8 bloques y el encabezado | 11 | 1 | 1 | 9 |
| A/B con selector | 11: borrar B, 8 bloques, encabezado de B y conmutar el selector | 12 | 11 | 1 (en prueba) | 0 |
La asimetría es la de siempre: con una sola ranura, 9 de 11 puntos de corte dejan el equipo sin firmware; con A/B, ninguno. Y el paso que decide, en A/B, es uno solo y atómico: conmutar el selector. Hasta ese paso, el equipo sigue arrancando la ranura A, y la imagen a medio escribir en B es irrelevante (es basura para el cargador, porque no tiene encabezado válido). El selector es un dato pequeño que se guarda con la técnica del tema 40 (doble copia con secuencia y CRC), de modo que su escritura también sea a prueba de cortes.
El precio es el espacio, y la Parte 4 lo cuenta con un microcontrolador de 512 KB, un cargador de 16 KB y 4 KB de configuración:
| Estrategia | Tamaño máximo de la aplicación | Una aplicación de 200 KB | Una aplicación de 300 KB |
|---|---|---|---|
| En el lugar | 492 KB | Entra | Entra |
| A/B interno | 246 KB (la mitad) | Entra (sobran 92 KB) | No entra (haría falta 600 KB) |
| Una ranura interna + imagen nueva en flash externa de 512 KB | 492 KB | Entra | Entra |
La consecuencia de diseño es temprana y costosa de cambiar: decidir la estrategia de actualización antes de elegir el chip. Un producto que debía actualizarse por aire y cuyo firmware creció hasta ocupar el 60 % de la flash no puede usar A/B interno; hay que cambiar a un chip con más flash, agregar una flash externa, o comprimir la imagen (a costa de RAM y código para descomprimir). Es el motivo del 70-75 % de ocupación de flash que recomendaba el tema 2: con A/B, ese límite se aplica a la mitad de la flash.
Y un último punto sobre los datos: la configuración y los datos de usuario (tema 40) no pertenecen a ninguna de las ranuras de aplicación. Viven en su propia zona, que la actualización nunca borra; una actualización que borra la flash «completa» para ganar tiempo borra también las calibraciones de fábrica y el código del usuario.
05 · No creerle a lo nuevo
Probar, confirmar o volver atrás
El A/B resuelve el corte de energía, pero deja un riesgo sin cubrir: una imagen con CRC correcto (llegó íntegra) que, sin embargo, no funciona: arranca y se cuelga, o arranca pero pierde la comunicación y no vuelve a poder actualizarse. Un CRC no dice nada del funcionamiento. Para ese caso se usa el arranque de prueba con confirmación: la imagen nueva se ejecuta «en prueba», y solo se vuelve definitiva cuando demuestra funcionar ejecutando una autoprueba y avisando al cargador. Si no lo hace, el cargador la descarta y vuelve a la anterior.
| Estado | Qué pasa | Transición |
|---|---|---|
| Normal | La ranura activa es la confirmada | Llega una imagen nueva: se recibe en la inactiva y se verifica |
| Pendiente | La imagen nueva está completa y verificada; el selector aún no cambió | Se conmuta el selector y se marca «en prueba» (un solo paso atómico) |
| En prueba | Cada arranque consume un intento; la aplicación nueva debe confirmarse tras su autoprueba | Confirma → normal (definitiva). Se cuelga, reinicia o agota los intentos → vuelta atrás |
| Vuelta atrás | El selector vuelve a la ranura anterior; la nueva se marca como mala (para no reintentarla) | Arranca la anterior; se registra el motivo (tema 37) y se avisa al servidor |
typedef struct { uint8_t activa; uint8_t en_prueba; uint8_t intentos; uint8_t mala[2]; } EstadoArranque; /* se guarda con doble copia y CRC (tema 40) */
#define MAX_INTENTOS 3
/* En el cargador, en cada arranque */
void cargador_decidir(void) {
EstadoArranque e; estado_cargar(&e);
if (e.en_prueba) {
if (e.intentos >= MAX_INTENTOS || !imagen_valida(base_de(e.activa))) {
e.mala[e.activa] = 1; e.activa ^= 1; e.en_prueba = 0; /* VUELTA ATRÁS: la ranura anterior; la nueva queda como mala */
registrar_causa(CAUSA_VUELTA_ATRAS); /* queda evidencia (tema 37) */
} else {
e.intentos++; /* este arranque cuenta, aunque el firmware se cuelgue */
}
estado_guardar(&e); /* ANTES de saltar: así el intento se cuenta aunque la aplicación se cuelgue */
}
if (!imagen_valida(base_de(e.activa))) modo_recuperacion(); /* ni la actual sirve: esperar una imagen por el canal de recuperación */
saltar_a_aplicacion(base_de(e.activa));
}
/* En la aplicación nueva, cuando demostró funcionar */
void actualizacion_confirmar(void) {
EstadoArranque e; estado_cargar(&e);
e.en_prueba = 0; e.intentos = 0; /* la actualización pasa a ser definitiva */
estado_guardar(&e);
}
La Parte 2 del programa simula una imagen con CRC correcto pero defectuosa. Con vuelta atrás: el firmware nuevo se cuelga en los arranques 1, 2 y 3 (el watchdog, tema 38, lo reinicia cada vez) y en el arranque 4 se agotan los 3 intentos y el cargador vuelve al firmware viejo y marca la nueva como mala. Sin vuelta atrás: el equipo se reinicia sin fin, con una imagen válida que no sirve. Con una imagen buena, arranca, pasa su autoprueba y confirma en el primer arranque.
Todo el mecanismo descansa en una decisión de diseño sutil: qué cuenta como «demostró funcionar». Una confirmación demasiado fácil (la aplicación confirma apenas arranca) deja pasar el defecto que aparece más tarde; una demasiado exigente rechaza imágenes buenas. Los criterios habituales, de más débil a más robusto:
- Que arranque y llegue al bucle principal. Detecta solo los cuelgues inmediatos.
- Que las funciones esenciales respondan: los sensores leen, las comunicaciones se conectan al servidor y se puede recibir otra actualización. Este último criterio es el más importante: una imagen que funciona pero no puede actualizarse es una imagen que, si tiene un defecto, no se puede corregir.
- Que se mantenga estable un tiempo mínimo (por ejemplo, 10 minutos sin reinicios ni fallas), con un tiempo mayor que el que tarda en aparecer el defecto típico.
Un ejemplo de lo que puede salir mal: una aplicación que confirma a los 2 minutos de arrancar, con un defecto que la cuelga cada 5 minutos, confirma antes de fallar, y el cargador ya no puede volver atrás; con confirmación a los 10 minutos, la imagen nunca confirmaría, agotaría los intentos y se descartaría. El criterio de confirmación es parte de la especificación de la actualización, y se prueba como cualquier otra.
Y siempre debe existir un modo de recuperación: si ninguna ranura tiene una imagen válida (una falla de la flash, una actualización del propio cargador que salió mal), el cargador no puede hacer más que esperar una imagen, y para eso necesita un canal propio: por UART, por USB, y para equipos remotos, un cargador capaz de usar la red (con el costo de código que eso implica en una zona protegida y casi inmutable). Muchos productos agregan además un pin o una combinación de botones que fuerza el modo de recuperación.
06 · Llegar al equipo
Llevar la imagen al equipo: cable y OTA
Transmitir una imagen de cientos de KB por un enlace imperfecto es un problema de protocolo con sus propias reglas. Un buen protocolo de actualización tiene cinco propiedades, que conviene exigir sin importar el medio:
- Bloques pequeños con CRC y confirmación: la imagen se parte en bloques (256 B a 1 KB), cada uno con su número de posición y su CRC. El receptor valida cada uno, lo escribe en la flash y confirma; si falta o llega dañado, pide el reenvío.
- Reanudación: el receptor guarda en memoria no volátil cuántos bytes lleva (tema 40), de modo que un corte del enlace o de la energía no obligue a empezar de cero. En un enlace lento (LoRa), empezar de cero tras un corte puede significar perder horas.
- Verificación final completa: al terminar, el receptor calcula el CRC (y la firma, tema 43) de toda la imagen y recién entonces declara la imagen lista. Los CRC de cada bloque no sustituyen al de la imagen entera.
- La imagen vigente no se toca durante la transferencia: se escribe en otra ranura (A/B) o en una flash externa, y solo después de verificarla se pide la instalación.
- Condiciones previas: antes de empezar, se comprueba que haya batería suficiente (un umbral alto, tema 37: una actualización a medias con una batería que se agota es el peor momento), espacio en la ranura y que la versión sea aceptable. Durante la transferencia, el equipo sigue haciendo su trabajo normal (por ejemplo, el control de un calefactor), con una tarea separada para la actualización.
typedef struct { uint32_t esperado; uint32_t total; bool activa; } Transferencia;
static Transferencia t;
/* Se llama por cada bloque recibido: (posición, datos, CRC-16 del bloque) */
Resultado ota_recibir_bloque(uint32_t posicion, const uint8_t *datos, size_t n, uint16_t crc_rx) {
if (!t.activa) return R_NO_HAY_TRANSFERENCIA;
if (crc16(datos, n) != crc_rx) return R_CRC; /* bloque dañado: se pide de nuevo */
if (posicion != t.esperado) return R_FUERA_DE_ORDEN; /* se avisa cuál se espera: el emisor retrocede */
if (posicion + n > t.total) return R_EXCEDE_EL_TAMANO;
if (!flash_programar(BASE_RANURA_B + sizeof(EncabezadoImagen) + posicion, datos, n)) return R_FLASH;
t.esperado += (uint32_t)n;
progreso_guardar(t.esperado); /* persistente (tema 40): permite reanudar tras un corte */
return R_OK; /* el emisor envía el siguiente bloque */
}
/* Al llegar t.esperado == t.total: verificar CRC de toda la imagen, escribir el encabezado (commit) y recién entonces pedir la instalación. */
El medio de transporte fija cuánto tarda, y la diferencia entre medios es de varios órdenes de magnitud. La Parte 3 del programa lo calcula con rendimientos efectivos supuestos (están indicados en cada línea y dependen de cada instalación):
| Enlace (rendimiento supuesto) | Completa de 256 KB | Comprimida a 160 KB | Diferencial de 24 KB |
|---|---|---|---|
| UART a 115 200 baudios (11,5 KB/s) | 23 s | 14 s | 2,1 s |
| BLE (10 KB/s prácticos) | 26 s | 16 s | 2,5 s |
| Wi-Fi con HTTPS (400 KB/s) | 0,7 s | 0,4 s | 0,1 s |
| LoRa con ciclo de trabajo del 1 % (5 B/s) | 14,6 h | 9,1 h | 81,9 min |
La tabla muestra por qué la técnica de la imagen cambia con el medio. Con Wi-Fi, mandar los 256 KB completos es trivial. Con un enlace lento como LoRa, es la diferencia entre inviable y práctico: 14,6 horas para una imagen completa contra 82 minutos para una diferencial. Hay dos técnicas para achicar lo que se envía:
- Compresión: se envía la imagen comprimida y el cargador la descomprime al escribirla. Reduce lo que viaja (160 KB en el ejemplo), pero exige código de descompresión en el cargador y RAM para su ventana (un descompresor liviano puede necesitar unos pocos KB).
- Actualización diferencial (delta): se envía solo la diferencia entre la versión instalada y la nueva (24 KB en el ejemplo). Es la que más reduce, pero tiene dos exigencias estrictas: el cargador debe tener el código para aplicar el parche, y el parche solo vale para una versión base exacta: si el equipo tiene una versión distinta de la esperada, hay que enviar el parche correspondiente o la imagen completa. En una flota con versiones dispersas, eso multiplica el trabajo del servidor.
Otro dato que viene de la Parte 3: el tiempo por UART (23 s) es en realidad el menor de los problemas, porque se suma el tiempo de la flash (tema 28): borrar 128 páginas de 2 KB a unos 25 ms por página son 3,2 s, y programar lleva su tiempo. En una actualización por aire, el tiempo total lo domina el enlace, pero hay que programar el borrado para que no bloquee al resto del firmware: borrar la ranura de a una página por vez, entre otras tareas, no de golpe.
07 · Antes y después
Compatibilidad entre versiones
Una actualización no cambia solo el código: cambia el comportamiento frente a todo lo que el equipo ya tiene guardado y frente a todo con lo que se comunica. Tres compatibilidades se rompen con frecuencia:
| Qué | Cómo se rompe | Qué hacer |
|---|---|---|
| Los datos guardados (tema 40) | El firmware nuevo cambia el formato de la configuración; si se vuelve atrás, el viejo no entiende el formato nuevo | Versión en el bloque; las versiones nuevas leen las viejas; mientras la actualización está «en prueba», seguir escribiendo en un formato que la anterior entienda, y migrar al formato nuevo recién al confirmar |
| La interfaz entre el cargador y la aplicación | El cargador espera una cosa del encabezado o del selector y la aplicación nueva deja otra | Versionar la interfaz; el cargador no se actualiza, de modo que la interfaz debe ser mínima y estable de por vida |
| El protocolo con el servidor y otros equipos | El firmware nuevo habla una versión del protocolo que el servidor no entiende todavía (o al revés) | Desplegar el servidor primero, que acepte las dos versiones; actualizar los equipos después; retirar la versión vieja al final |
La primera es la más traicionera en relación con la vuelta atrás: un firmware v2 que migra la configuración al formato nuevo apenas arranca, y que después se cuelga y es descartado, deja una configuración que el firmware v1 no puede leer. Tras la vuelta atrás, el equipo arranca con valores de fábrica y el usuario pierde su configuración, justamente por un mecanismo diseñado para protegerlo. La regla es posponer toda migración irreversible hasta la confirmación, y mientras tanto, escribir de forma que ambas versiones entiendan lo guardado.
También hay una cuestión de política de versiones: ¿se permite instalar una versión anterior a la actual (downgrade)? Tiene usos legítimos (volver a una versión estable) y un riesgo de seguridad serio (reinstalar una versión con una vulnerabilidad conocida). Se decide al diseñar; el tema 43 trata la protección contra el retroceso.
08 · Comprobar que funciona
Verificación y diagnóstico
| Prueba | Cómo | Resultado esperado |
|---|---|---|
| Corte de energía en cada paso | Cortar tras cada paso de la actualización (con un modelo en la PC y con cortes reales aleatorios) | El equipo siempre arranca una versión válida (la vieja o la nueva) |
| Imagen dañada | Cambiar un bit de la imagen recibida, del encabezado, del CRC | La imagen se rechaza; el equipo sigue con la vigente |
| Imagen de otro producto o para otra dirección | Instalar una imagen con otro identificador de producto o enlazada para otra ranura | El cargador la rechaza antes de ejecutarla |
| Imagen válida pero defectuosa | Una imagen que se cuelga al arrancar; una que arranca pero nunca confirma | Se agotan los intentos y se vuelve atrás; la nueva queda como mala y no se reintenta |
| Transferencia interrumpida | Cortar el enlace y el equipo a mitad de la descarga; reordenar, duplicar y dañar bloques | Se reanuda desde donde quedó; la imagen final es idéntica a la original |
| Batería baja | Pedir una actualización con la batería por debajo del umbral | El equipo la rechaza y avisa |
| Falla de las dos ranuras | Dañar ambas ranuras a propósito | El cargador entra en el modo de recuperación; se puede reinstalar por el canal de recuperación |
| Compatibilidad de datos | Actualizar con datos de la versión anterior, volver atrás, y actualizar de nuevo | La configuración se conserva en todo el recorrido |
| Todo el recorrido en la flota | Actualizar primero un grupo pequeño y observar antes de seguir (despliegue escalonado) | Los equipos informan versión y resultado; un problema se detecta con pocas unidades |
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Algunos equipos quedan inutilizables tras la actualización | Actualización en el lugar con un corte, o sin vuelta atrás ante una imagen defectuosa | A/B o ranura de descarga; commit al final; prueba con confirmación |
| La aplicación nueva se cuelga solo después de saltar desde el cargador | El cargador dejó periféricos o interrupciones activos, o no ajustó VTOR | Desinicializar antes de saltar; verificar VTOR y la pila |
| La imagen copiada a la otra ranura no arranca | No es reubicable: está enlazada para la otra ranura | Compilar una imagen por ranura, o usar la ranura de descarga con copia |
| La actualización por aire «funciona» pero el equipo nunca confirma | El criterio de confirmación no se cumple (no llega a conectarse, o la autoprueba es muy estricta) | Revisar el criterio y los tiempos; registrar el motivo de la no confirmación |
| Tras volver atrás, el usuario pierde su configuración | La versión nueva migró los datos de forma irreversible antes de confirmar | Migrar recién al confirmar; escribir en un formato que ambas versiones entiendan |
| La actualización por LoRa o celular nunca termina | Imagen demasiado grande para el enlace, o sin reanudación | Compresión o actualización diferencial; reanudación con progreso persistente |
| El equipo se reinicia durante la descarga | El borrado o la escritura de flash bloquean el watchdog o la tensión cae (temas 37 y 38) | Borrar por páginas entre otras tareas; alimentar el watchdog de forma controlada; verificar la batería antes de empezar |
| Una actualización del cargador deja equipos inutilizados | El cargador se actualizó sin una copia de respaldo | Casi nunca actualizar el cargador; si es imprescindible, un cargador secundario o un esquema con dos copias y una verificación mucho más estricta |
09 · Analogía con programación
Probar el modelo en Python
El programa tiene cinco partes: la prueba exhaustiva de cortes de energía en cada paso para la actualización en el lugar y para el esquema A/B; la simulación de una imagen con CRC correcto pero defectuosa, con y sin vuelta atrás; el tiempo de transferencia de una imagen completa, comprimida y diferencial por cuatro enlaces; el espacio de flash que exige cada estrategia; y la detección de imágenes dañadas con un CRC-32.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar. La parte del CRC tarda unos segundos.
# Actualización de firmware: cortes en cada paso, prueba y vuelta atrás, tiempos de transferencia, espacio de flash y detección de imágenes dañadas.
import random, zlib
# ---- Parte 1: cortar la energía en cada paso de una actualización --------------------------------------------------
N_BLOQUES = 8 # la imagen se escribe en 8 bloques
def pasos_en_lugar(): return ["borrar la aplicación"] + [f"bloque {i + 1}" for i in range(N_BLOQUES)] + ["escribir el encabezado (commit)"]
def pasos_ab(): return ["borrar la ranura B"] + [f"bloque {i + 1} en B" for i in range(N_BLOQUES)] + ["encabezado de B (imagen válida)", "conmutar el selector a B (en prueba)"]
def arranque_en_lugar(k):
"""Tras completar k pasos de actualizar en el lugar, ¿qué arranca?"""
n = len(pasos_en_lugar())
if k == 0: return "firmware viejo"
if k >= n: return "firmware nuevo"
return "NADA (aplicación inválida)"
def arranque_ab(k):
n = len(pasos_ab())
if k >= n: return "firmware nuevo (en prueba)"
return "firmware viejo" # hasta conmutar el selector, el cargador sigue arrancando A
print("Parte 1: un corte de energía después de cada paso posible de la actualización")
for nombre, pasos, f in (("en el lugar (una sola ranura)", pasos_en_lugar(), arranque_en_lugar), ("A/B con selector", pasos_ab(), arranque_ab)):
resultados = [f(k) for k in range(len(pasos) + 1)]
cuenta = {}
for r in resultados: cuenta[r] = cuenta.get(r, 0) + 1
print(f" {nombre:<30} {len(resultados)} puntos de corte: " + "; ".join(f"{r}: {n}" for r, n in cuenta.items()))
# ---- Parte 2: prueba y vuelta atrás ---------------------------------------------------------------------------------
def arrancar_con_prueba(imagen_defectuosa, max_intentos=3, con_vuelta_atras=True, reinicios=8):
"""El selector apunta a la imagen nueva (en prueba). Cada arranque consume un intento si la app no confirma."""
intentos, linea = 0, []
for n in range(1, reinicios + 1):
if con_vuelta_atras and intentos >= max_intentos:
linea.append(f"arranque {n}: se agotaron los {max_intentos} intentos -> VUELTA ATRÁS al firmware viejo; la nueva se marca como mala")
return linea
intentos += 1
if imagen_defectuosa: linea.append(f"arranque {n}: el firmware nuevo se cuelga y el watchdog reinicia (intento {intentos})")
else:
linea.append(f"arranque {n}: el firmware nuevo arranca, pasa su autoprueba y CONFIRMA")
return linea
linea.append(f"... el equipo sigue reiniciándose sin fin (sin vuelta atrás)")
return linea
print()
print("Parte 2: una imagen nueva con CRC correcto pero que se cuelga al arrancar")
for titulo, defec, vuelta in (("imagen buena", False, True), ("imagen defectuosa, CON vuelta atrás", True, True), ("imagen defectuosa, SIN vuelta atrás", True, False)):
print(f" {titulo}:")
for l in arrancar_con_prueba(defec, con_vuelta_atras=vuelta, reinicios=6): print(" " + l)
# ---- Parte 3: cuánto tarda en llegar la imagen -------------------------------------------------------------------------
print()
print("Parte 3: tiempo de transferencia de una imagen según el enlace (rendimientos efectivos supuestos: ver cada línea)")
ENLACES = (("UART a 115 200 baudios (10 bits por byte)", 115_200 / 10), ("BLE (rendimiento práctico supuesto de 10 KB/s)", 10_000),
("Wi-Fi con HTTPS (supuesto de 400 KB/s)", 400_000), ("LoRa con ciclo de trabajo del 1 % (supuesto de 5 B/s)", 5))
IMAGENES = (("completa de 256 KB", 256 * 1024), ("comprimida a 160 KB", 160 * 1024), ("diferencial de 24 KB", 24 * 1024))
def legible(s):
if s < 10: return f"{s:.1f} s".replace(".", ",")
if s < 120: return f"{s:,.0f} s".replace(",", " ")
if s < 7200: return f"{s / 60:,.1f} min".replace(",", " ").replace(".", ",")
return f"{s / 3600:,.1f} h".replace(",", " ").replace(".", ",")
print(f" {'enlace':<54} | " + " | ".join(f"{n:>20}" for n, _ in IMAGENES))
for nombre, bps in ENLACES:
print(f" {nombre:<54} | " + " | ".join(f"{legible(b / bps):>20}" for _, b in IMAGENES))
# ---- Parte 4: cuánta flash cuesta cada estrategia ----------------------------------------------------------------------
print()
print("Parte 4: un microcontrolador de 512 KB con cargador de 16 KB y 4 KB de configuración")
LIBRE = 512 - 16 - 4
print(f" una sola ranura (actualización en el lugar): la aplicación puede ocupar hasta {LIBRE} KB")
print(f" dos ranuras A/B internas: cada aplicación puede ocupar hasta {LIBRE // 2} KB")
for app in (200, 300):
if app <= LIBRE // 2: print(f" una aplicación de {app} KB: entra en A/B interno ({LIBRE - 2 * app} KB sobran)")
else: print(f" una aplicación de {app} KB: NO entra en A/B interno (hacen falta {2 * app} KB); con una ranura interna y la imagen nueva en una flash externa de 512 KB sí")
# ---- Parte 5: detectar una imagen dañada ------------------------------------------------------------------------------
rnd = random.Random(21)
imagen = bytes(rnd.randrange(256) for _ in range(2048))
crc0 = zlib.crc32(imagen)
detectados = 0
for bit in range(len(imagen) * 8):
d = bytearray(imagen); d[bit // 8] ^= 1 << (bit % 8)
detectados += zlib.crc32(bytes(d)) != crc0
sin = 0
for _ in range(20_000):
d = bytearray(imagen)
for _ in range(64): i = rnd.randrange(len(d)); d[i] ^= 1 << rnd.randrange(8)
if bytes(d) != imagen and zlib.crc32(bytes(d)) == crc0: sin += 1
print()
print("Parte 5: CRC-32 sobre una imagen de 2 KB")
print(f" errores de 1 bit: {detectados} de {len(imagen) * 8} detectados; 20 000 corrupciones al azar de hasta 64 bits: {sin} sin detectar")
print(" un CRC detecta daños accidentales, pero no demuestra QUIÉN hizo la imagen: para eso hace falta una firma (tema 43)")
Resultado, parte 1: actualizando en el lugar, de los 11 puntos de corte, 1 deja el firmware viejo, 1 el nuevo y 9 dejan al equipo sin firmware; con A/B, de 12 puntos de corte, 11 dejan el viejo y 1 arranca el nuevo en prueba, y ninguno deja al equipo inutilizado. Parte 2: una imagen defectuosa con vuelta atrás se cuelga en los arranques 1, 2 y 3, y en el 4.º se agotan los 3 intentos y vuelve al firmware viejo; sin vuelta atrás, reinicia sin fin. Parte 3: la imagen de 256 KB tarda 23 s por UART, 26 s por BLE, 0,7 s por Wi-Fi y 14,6 h por LoRa; comprimida a 160 KB, 14 s, 16 s, 0,4 s y 9,1 h; diferencial de 24 KB, 2,1 s, 2,5 s, 0,1 s y 81,9 min. Parte 4: en 512 KB con un cargador de 16 KB y 4 KB de configuración, una sola ranura admite 492 KB y A/B interno 246 KB; una aplicación de 200 KB entra en A/B (sobran 92 KB) y una de 300 KB no (necesitaría 600 KB). Parte 5: el CRC-32 detectó los 16 384 errores de 1 bit de una imagen de 2 KB, y ninguna de 20 000 corrupciones aleatorias pasó sin detectar.
Probá cambiar N_BLOQUES en la Parte 1 y mirá que la proporción de cortes que dejan al equipo sin firmware en el esquema en el lugar crece con el tamaño de la imagen (la ventana es más larga), o max_intentos en la Parte 2 para ver cuánto tarda la vuelta atrás. La lección del programa es que la seguridad de una actualización se puede demostrar: se recorren todos los puntos de corte y se comprueba que ninguno deja al equipo inutilizado.
10 · Exploración interactiva
Actualizar con cortes
El laboratorio simula una actualización de v1 a v2 con dos estrategias: en el lugar (una sola ranura) y A/B con prueba y confirmación. Avanzás paso a paso, cortás la energía donde quieras y hacés el arranque para ver qué decide el cargador. Con la casilla marcás que la imagen nueva tiene un defecto (se cuelga al arrancar) para ver la vuelta atrás.
Paso a paso, con cortes y con una imagen defectuosa
Registro
—
Elegí una estrategia y avanzá paso a paso.
Recorrido guiado
- Con «En el lugar», apretá «Siguiente paso» una vez («borrar la aplicación») y después «Cortar la energía ahora» y «Arranque»: no hay aplicación válida, y el equipo queda sin firmware. Es el peor caso: un corte en la ventana del borrado.
- Apretá «Reiniciar» y avanzá hasta el paso 3 (dos bloques escritos) antes de cortar: la ranura queda a medias y el resultado es el mismo. En esta estrategia, cualquier corte entre el primer y el último paso (9 de 11 en la simulación) deja al equipo inutilizado.
- Apretá «Reiniciar», completá los 6 pasos y hacé el arranque: arranca la v2. Marcá ahora «La imagen nueva tiene un defecto», repetí y arrancá: el firmware v2 se cuelga y se vuelve a colgar en cada arranque, porque no hay otra copia ni vuelta atrás.
- Cambiá a «A/B con prueba y confirmación» (con el defecto desmarcado), avanzá algunos pasos y cortá la energía en cualquier punto antes del último, y hacé el arranque: sigue arrancando la v1, siempre. La ranura B a medias es irrelevante. Probá cortar en todos los puntos: nunca se pierde el firmware.
- Completá los 7 pasos (el último conmuta el selector a B «en prueba») y hacé el arranque: arranca la v2, pasa su autoprueba y confirma: el selector queda definitivo.
- Con «La imagen nueva tiene un defecto» marcado, completá los 7 pasos y arrancá varias veces: la v2 se cuelga en los intentos 1, 2 y 3; al cuarto arranque el cargador vuelve atrás a la v1 y marca la ranura B como mala. El equipo siguió funcionando durante todo el intento.
Desafío: ¿qué pasaría con A/B si la imagen nueva arranca bien, confirma, y el defecto aparece una hora después?
La vuelta atrás automática ya no sirve: la imagen quedó confirmada y el cargador no la ve como sospechosa. Hay tres líneas de defensa que se suman. Primera: un criterio de confirmación más exigente, con un período de estabilidad mayor al tiempo en que suelen aparecer los defectos (en vez de confirmar al arrancar, confirmar tras varias horas de funcionamiento normal con la comunicación activa). Segunda: el despliegue escalonado en la flota: actualizar primero un grupo pequeño (1 %) y esperar un día antes de seguir. Los equipos informan versión, causas de reinicio (tema 37) y contadores de fallas, y si el defecto aparece en ese grupo, se detiene el despliegue; es la defensa más eficaz contra un defecto lento. Tercera: la vuelta atrás ordenada por el servidor: como la ranura anterior sigue intacta (hasta que se sobrescriba con la siguiente actualización), el servidor puede indicar a los equipos que vuelvan a ella, o enviar una versión corregida. Por eso conviene no sobrescribir la ranura anterior de inmediato tras confirmar: conservarla un tiempo da margen para una vuelta atrás manual.
11 · Método de trabajo
Método: una actualización que no falla
- Decidí la estrategia al elegir el chip: cuánta flash exige la actualización (en el lugar, A/B o ranura de descarga con flash externa) y qué canal se usa (cable, OTA). Es la decisión más cara de revertir.
- Diseñá el mapa de la flash: cargador protegido, configuración y selector con doble copia (tema 40), ranuras, y datos de usuario fuera de toda zona que la actualización borre.
- Mantené el cargador pequeño, simple y protegido, con una interfaz mínima y estable, y un modo de recuperación por un canal propio. Casi nunca se actualiza.
- Definí la imagen con un encabezado: magia, producto, versión, tamaño, dirección de enlace y CRC; se escribe al final (commit) y se valida antes de ejecutarla.
- Resolvé la reubicación: una imagen por ranura, o una ranura principal fija con copia. El código no es reubicable en un microcontrolador sin MMU.
- Nunca toques la imagen vigente hasta que la nueva esté completa y verificada; la conmutación es un único paso atómico.
- Usá arranque de prueba con confirmación y vuelta atrás: un contador de intentos guardado antes de saltar, y un criterio de confirmación robusto que incluya la capacidad de recibir otra actualización.
- Diseñá el transporte con bloques con CRC, confirmación, reanudación persistente y verificación final; comprobá batería y espacio antes de empezar; elegí compresión o diferencia según el enlace.
- Cuidá la compatibilidad: versión en los datos, migración irreversible solo tras confirmar, interfaz estable con el cargador, y servidor compatible con ambas versiones durante el despliegue.
- Probá cada punto de corte, cada imagen dañada y cada defecto con un modelo en la PC y con el equipo real; desplegá de forma escalonada y registrá versión y resultado de cada equipo.
12 · Comprensión
Confusiones frecuentes
«Si el CRC de la imagen es correcto, la actualización es segura»
El CRC prueba que la imagen llegó íntegra, no que funciona (una imagen defectuosa tiene un CRC perfecto) y mucho menos quién la hizo (cualquiera puede calcular el CRC de una imagen maliciosa). Hacen falta además el arranque de prueba con vuelta atrás y, para la autenticidad, una firma digital (tema 43).
«Actualizar en el lugar es más simple y alcanza»
Es más simple y usa la mitad de flash, pero deja una ventana de riesgo de segundos o más en la que un corte inutiliza el equipo: en la simulación, 9 de 11 puntos de corte. Para un equipo que se actualiza por aire en el campo, sin nadie que lo pueda recuperar, no es una opción. Es aceptable si siempre hay un cable y una persona cerca.
«Puedo copiar la imagen de la ranura A a la B y arrancarla desde ahí»
En un microcontrolador sin MMU, el código no es reubicable: las direcciones de las funciones y las constantes están fijas en el binario, y una imagen enlazada para la ranura A no funciona en la B. Se compila una imagen por ranura, o se usa una ranura principal fija a la que el cargador copia la imagen nueva.
«El cargador puede actualizarse como cualquier otra parte del firmware»
Es lo único que no se puede actualizar de forma segura con otra actualización: un error en ese proceso deja al equipo sin cargador y sin forma de recibir una imagen. Se lo mantiene pequeño, protegido contra escritura y se lo toca lo menos posible; si hay que actualizarlo, se hace con un esquema redundante y una verificación mucho más estricta.
«Si la imagen nueva arranca, ya está confirmada»
Arrancar no es funcionar. Una imagen que arranca y se cuelga a los 5 minutos, o que pierde la comunicación y no puede recibir otra actualización, arranca. La confirmación debe esperar a que se demuestre lo esencial (sensores, comunicación, capacidad de actualizarse) y a un tiempo de estabilidad mayor que el de aparición del defecto típico.
«Tras la actualización, borro la versión vieja para ganar espacio»
Conservar la ranura anterior un tiempo después de confirmar es la forma de poder volver atrás si el defecto aparece tarde. Se sobrescribe cuando llega la siguiente actualización, no antes. La seguridad de la vuelta atrás depende de que lo anterior siga intacto.
«Mi enlace es lento pero siempre llega, no hace falta reanudación»
En un enlace lento, un corte a las 13 horas de 14,6 sin reanudación obliga a empezar de nuevo. Con bloques confirmados y el progreso guardado de forma persistente, se continúa desde donde quedó. Cuanto más lento el enlace, más importante la reanudación, y más conviene reducir la imagen (compresión o diferencia).
«Una actualización diferencial sirve para cualquier equipo de la flota»
Un parche diferencial solo aplica sobre una versión base exacta. Si un equipo tiene una versión distinta, el parche produce basura (que el CRC final detecta, y se descarta). El servidor debe saber qué versión tiene cada equipo, tener un parche por versión base o caer a la imagen completa, y el cargador debe conservar la vigente hasta verificar el resultado.
13 · Práctica guiada
Ejercicios
1. ¿Entra en A/B?
Un microcontrolador tiene 1 MB de flash, con un cargador de 32 KB y 8 KB de configuración. ¿Cuánto puede ocupar cada aplicación con A/B interno? ¿Entra una aplicación de 450 KB? ¿Y una de 520 KB?
Ver solución
Libre: 1024 − 32 − 8 = 984 KB; con dos ranuras, 492 KB cada una. Una aplicación de 450 KB entra (sobran 42 KB por ranura, un 8 %: poco margen de crecimiento). Una de 520 KB no entra en A/B interno (haría falta 1040 KB). Opciones: una ranura interna (hasta 984 KB) con la imagen nueva en una flash externa y copia; o comprimir la imagen en una flash interna más pequeña; o reducir la aplicación (tema 32).
2. La ventana de riesgo
Se actualiza en el lugar una imagen de 200 KB por UART a 115 200 baudios (11 520 bytes por segundo), en una flash con páginas de 2 KB que se borran en 25 ms cada una. Si el cargador borra primero toda la aplicación y recibe después, ¿cuánto dura la ventana en la que un corte inutiliza el equipo? ¿Y si usa A/B?
Ver solución
Borrado: 200 KB / 2 KB = 100 páginas × 25 ms = 2,5 s. Transferencia: 204 800 B / 11 520 B/s ≈ 17,8 s. Si la ventana va desde el borrado hasta el encabezado, dura unos 20,3 s, y en cualquier instante de ese lapso un corte deja al equipo sin firmware. Con A/B, la ranura activa no se toca durante esos 20 s: la ventana de riesgo se reduce a cero (el único paso crítico es conmutar el selector, de microsegundos, y está protegido con doble copia, tema 40). Lo que sí ocurre en A/B es que el equipo sigue funcionando normalmente durante toda la descarga.
3. El criterio de confirmación
Una imagen nueva tiene un defecto que la hace reiniciar cada 5 minutos. Los intentos máximos son 3. ¿Qué pasa si la aplicación confirma a los 2 minutos de arrancar? ¿Y si confirma tras 10 minutos de funcionamiento estable?
Ver solución
Si confirma a los 2 minutos, lo hace antes de que aparezca el defecto (a los 5): el cargador la da por buena, los intentos se ponen a cero y la vuelta atrás ya no es posible. El equipo queda reiniciando cada 5 minutos con la imagen confirmada. Si exige 10 minutos de funcionamiento estable, el reinicio de los 5 minutos ocurre antes de confirmar: se consume un intento por arranque, y en el cuarto arranque (tras unos 15 minutos) el cargador vuelve a la imagen anterior. Moraleja: el período de estabilidad debe ser mayor que el tiempo típico de aparición de los defectos que se quieren detectar.
4. Por LoRa
Un equipo recibe por LoRa con un rendimiento efectivo de 5 B/s. ¿Cuánto tarda una imagen completa de 256 KB? ¿Y una diferencial de 24 KB? ¿Qué requisito tiene que cumplir el equipo para que la diferencial sea aplicable?
Ver solución
Completa: 262 144 / 5 = 52 429 s ≈ 14,6 horas. Diferencial: 24 576 / 5 = 4915 s ≈ 82 minutos (81,9). La diferencial exige que el equipo tenga exactamente la versión base para la que se generó el parche, que el cargador tenga el código para aplicarlo (con la RAM y la flash de trabajo necesarias) y que, tras aplicarlo, se verifique el CRC (y la firma) de la imagen resultante antes de instalarla. Y como el enlace es tan lento, es imprescindible reanudar la transferencia tras un corte.
5. ¿Qué rechaza el cargador?
Una ranura de 246 KB contiene una imagen con encabezado que dice: producto = 7 (el equipo es el 5), tamaño = 300 KB. Otra tiene producto = 5, tamaño = 100 KB, dirección de enlace = la de la ranura A pero está en la B. Una tercera tiene todo correcto pero un bit cambiado en el código. ¿Qué verificación rechaza cada una?
Ver solución
La primera se rechaza por producto (7 ≠ 5) y, además, por tamaño (300 KB no cabe en una ranura de 246 KB): cualquiera de las dos basta. La segunda, por dirección de enlace: fue enlazada para ejecutarse desde la ranura A, y en la B no funcionaría aunque su CRC sea correcto. La tercera, por el CRC del código (un bit cambiado cambia el CRC-32; se detectan todos los errores de 1 bit). Observá que las tres se rechazan sin ejecutar nada de la imagen: la validación se hace sobre el encabezado y los bytes, antes de saltar.
6. Volver atrás sin perder la configuración
La versión 2 de un firmware migra la configuración a un formato nuevo apenas arranca. La actualización se descarta por un defecto y se vuelve a la versión 1. ¿Qué pasa con la configuración del usuario? ¿Cómo se evita?
Ver solución
La versión 1 encuentra un bloque de configuración con una versión de formato desconocida (la 2): según el tema 40, no debe interpretarlo y usa valores de fábrica, de modo que el usuario pierde su configuración por una actualización que ni siquiera quedó instalada. Se evita posponiendo la migración irreversible hasta la confirmación: mientras la imagen está «en prueba», la versión 2 sigue guardando en el formato 1 (o guarda los campos nuevos en un registro aparte que la 1 ignora), y solo al confirmar pasa al formato 2. Así la vuelta atrás encuentra datos que entiende. Es un ejemplo de que la compatibilidad no se resuelve en la actualización sino en el diseño del formato de los datos.
14 · Síntesis
Ideas para recordar
- Actualizar permite corregir errores y agregar funciones sin retirar el producto, pero es la operación más peligrosa del equipo: puede fallar la transferencia, la energía, la imagen (dañada o equivocada) o el firmware nuevo (defectuoso). Cada técnica responde a una de esas fallas, y una actualización segura es la que puede fallar en cualquier paso sin inutilizar al equipo.
- El cargador de arranque se ejecuta primero y decide qué se lanza: es pequeño, simple, protegido contra escritura y casi nunca se actualiza. La aplicación se enlaza para su ranura y el cargador salta a ella ajustando
VTORy la pila, tras dejar el hardware como después de un reset. - La imagen lleva un encabezado (magia, producto, versión, tamaño, dirección de enlace y CRC) que se escribe al final, como commit. El CRC detecta daños accidentales (los 16 384 errores de 1 bit de una imagen de 2 KB), pero no prueba quién la hizo: para eso, la firma (tema 43). El código no es reubicable en un microcontrolador sin MMU.
- Tres estrategias: en el lugar (poca flash, pero una ventana de riesgo: 9 de 11 cortes inutilizan el equipo), A/B (el doble de flash, ventana nula: la ranura activa no se toca hasta conmutar un selector atómico) y ranura de descarga con copia (la imagen, a veces en una flash externa, se copia a la ranura principal). La decisión se toma al elegir el chip.
- El arranque de prueba con confirmación cubre la imagen válida pero defectuosa: cada arranque consume un intento (guardado antes de saltar); si la aplicación no confirma, al agotarse los intentos se vuelve a la anterior y la nueva se marca como mala. El criterio de confirmación debe incluir la capacidad de recibir otra actualización y un período de estabilidad mayor que el de aparición de los defectos. Siempre existe un modo de recuperación.
- El transporte usa bloques con CRC y confirmación, reanudación persistente y verificación final; comprueba batería y espacio antes de empezar y no toca la imagen vigente. El enlace fija el tiempo (14,6 h por LoRa contra 0,7 s por Wi-Fi para 256 KB): para los lentos, compresión o actualización diferencial (que exige una versión base exacta).
- La compatibilidad se cuida en tres frentes: los datos guardados (migrar de forma irreversible solo tras confirmar, para que la vuelta atrás no pierda la configuración), la interfaz con el cargador (mínima y estable) y el protocolo con el servidor (que acepte las dos versiones durante el despliegue).
- Se prueba cortando en cada paso, con imágenes dañadas, de otro producto y defectuosas, con transferencias interrumpidas, batería baja y las dos ranuras dañadas, y se despliega de forma escalonada con el resultado de cada equipo registrado. Conviene conservar la ranura anterior un tiempo después de confirmar.
¿Listo para el tema siguiente?
Antes de avanzar, dibujá el mapa de flash de un producto tuyo (o imaginario) y recorré la actualización paso a paso preguntando, en cada uno: ¿qué pasa si se corta la energía aquí?, ¿qué pasa si la imagen llega dañada?, ¿qué pasa si es válida pero se cuelga?, ¿cómo vuelve atrás el equipo? Si alguna respuesta es «queda inutilizado», ahí está el diseño que falta.
En el próximo tema cerramos el bloque con la pregunta que este deja abierta: ¿quién dice que esta imagen es legítima? El firmware seguro: verificación con firmas, arranque seguro, protección contra el retroceso y protección del código.