
Actualización OTA (Over-The-Air) en el ESP32
Dos particiones, otadata y el estado pendiente de verificación: cómo actualizar el firmware por red sin convertir un error en una visita técnica.
Requisitos de hardware:
- Placa ESP32 ou ESP32-S3 com pelo menos 4 MB de flash
- Rede Wi-Fi de 2,4 GHz
- Um servidor HTTP para hospedar o binário, mesmo que local
Descarga el material de este tutorial:
- Projeto completo (ZIP)160 KBFirmware Arduino com atualização por pull, verificação do byte mágico, comparação de tamanho e autodiagnóstico com rollback. Tabela de partições, README e manual em PDF.
- Manual em PDF (8 páginas)193 KBPor que duas partições, push contra pull, o firmware que sobe e não funciona, o estado pendente de verificação e o teste de falha proposital. Checklist de treze itens.
¿Quieres recibir nuevos manuales y esquemas industriales? Únete a nuestro canal de Telegram / WhatsApp
El problema que resuelve OTA y el que crea
Grabar firmware por cable es sencillo cuando la placa está en la mesa. Cuando está dentro de un cuadro eléctrico cerrado, en la tercera planta de una nave o detrás de una máquina en funcionamiento, cada actualización se convierte en una visita técnica.
OTA, actualización por el aire, resuelve eso. Pero crea un riesgo nuevo, y este es el punto que separa una implementación de tutorial de una que aceptarías en campo: si la actualización sale mal, el dispositivo puede quedar inaccesible exactamente por el motivo que llevó a usar OTA.
Este artículo trata las dos partes. Primero cómo actualizar. Después, y sobre todo, cómo asegurar que una actualización defectuosa no cueste la visita técnica que querías evitar.
Por qué deben existir dos particiones
Lo primero es entender que el ESP32 no sobrescribe el firmware que está ejecutando. Sería imposible: el código que está funcionando es el mismo que se estaría borrando.
La solución es tener dos espacios de aplicación. El firmware se ejecuta en uno, escribe el nuevo en el otro y solo después marca cuál debe utilizarse en el siguiente arranque. Una tercera partición pequeña, llamada otadata, guarda esa marca.
Esto tiene una consecuencia excelente: un corte de energía durante la actualización es seguro. Se escribe el slot inactivo, el slot activo permanece intacto y otadata sigue apuntando a él. La placa reinicia el firmware antiguo como si nada hubiera ocurrido.
También tiene una consecuencia incómoda: pierdes la mitad de la flash. En una placa de 4 MB cada slot queda en unos 1,9 MB. Es el precio y no hay forma de evitarlo.
La tabla de particiones
Esta es la tabla que usa el firmware del repetidor en una placa de 16 MB:
| 1 | # Name, Type, SubType, Offset, Size, Flags |
| 2 | nvs, data, nvs, 0x9000, 0x5000, |
| 3 | otadata, data, ota, 0xe000, 0x2000, |
| 4 | app0, app, ota_0, 0x10000, 0x300000, |
| 5 | app1, app, ota_1, , 0x300000, |
| 6 | spiffs, data, spiffs, , 0x900000, |
Lee los campos importantes. otadata ocupa 8 KB y registra qué slot es válido. app0 y app1 son los dos espacios de aplicación, de 3 MB cada uno. spiffs conserva los 9 MB restantes para configuración y archivos.
Observa que app1 y spiffs no declaran desplazamiento: el generador lo calcula a partir del tamaño anterior. Solo la primera partición necesita una dirección fija.
Si tu placa tiene 4 MB, utiliza la tabla preparada min_spiffs, que reserva unos 1,9 MB por slot. Es lo más cercano a un diseño razonable para OTA que ofrece Arduino.
Indica la tabla en platformio.ini:
| 1 | board_build.partitions = partitions.csv |
Sin esto, el proyecto usa la tabla predeterminada de una sola partición y la actualización falla en el primer intento con un error de partición inexistente.
Push o pull: quién inicia la conversación
Existen dos modelos y la elección cambia la arquitectura.
Con push, el dispositivo expone un endpoint HTTP y tú le envías el binario. Es el modelo de ArduinoOTA y de las bibliotecas OTA web. Es sencillo de probar y funciona bien cuando tienes acceso a la red local del equipo.
Con pull, el dispositivo consulta periódicamente un servidor, compara la versión y descarga la novedad si existe. Requiere más infraestructura, pero es el único que escala: actualizar cincuenta gateways por push significa cincuenta operaciones manuales; por pull significa publicar un archivo.
Para un producto, pull. Para la mesa y el mantenimiento puntual, push. Este tutorial implementa pull, el caso más difícil y más útil.
La actualización en sí es la parte fácil
El código de grabación sorprende por su sencillez. La biblioteca Update, que ya viene en el core, se ocupa de lo esencial:
| 1 | bool aplicarAtualizacao(Stream &origem, size_t tamanho) { |
| 2 | if (!Update.begin(tamanho)) { |
| 3 | Serial.printf("Espaco insuficiente: %s\\n", Update.errorString()); |
| 4 | return false; |
| 5 | } |
| 6 | size_t escritos = Update.writeStream(origem); |
| 7 | if (escritos != tamanho) { |
| 8 | Serial.printf("Escreveu %u de %u bytes\\n", (unsigned)escritos, (unsigned)tamanho); |
| 9 | Update.abort(); |
| 10 | return false; |
| 11 | } |
| 12 | if (!Update.end(true)) { |
| 13 | Serial.printf("Falha ao finalizar: %s\\n", Update.errorString()); |
| 14 | return false; |
| 15 | } |
| 16 | return true; |
| 17 | } |
Update.begin elige el slot inactivo y comprueba si cabe el binario. writeStream escribe directamente desde el flujo HTTP, sin un búfer intermedio del tamaño del firmware; sería imposible porque el binario es mayor que la RAM. end(true) valida la imagen y actualiza otadata.
Si falla cualquier etapa, nada cambia: otadata sigue apuntando al slot que está funcionando.
El firmware que arranca y no funciona
Aquí está el riesgo real y la razón de existir de este artículo.
El caso que todos imaginan es que la actualización falle a mitad. Es el caso fácil; ya vimos que es seguro por construcción.
El caso peligroso es otro: la actualización termina correctamente, el binario es válido, la placa reinicia con el firmware nuevo y entonces descubre un error que impide conectarse al Wi-Fi. O se bloquea en setup. O consume memoria hasta reiniciarse en bucle.
Desde el punto de vista de OTA todo salió bien. En la práctica, el dispositivo está inaccesible y has perdido precisamente la capacidad de enviar la corrección. Es la visita técnica que OTA quería evitar, agravada porque tú mismo la provocaste.
Ninguna comprobación de integridad del binario detecta esto. El archivo está intacto; lo incorrecto es su comportamiento.
Rollback: el estado pendiente de verificación
La solución es no confiar de inmediato en el firmware nuevo.
Con el rollback activado, una imagen recién grabada entra en un estado especial: pendiente de verificación. El bootloader la ejecuta con una condición: si la placa reinicia antes de que el firmware declare que funciona, el bootloader vuelve automáticamente a la imagen anterior.
El firmware nuevo debe demostrar que funciona:
| 1 | #include "esp_ota_ops.h" |
| 2 | void confirmarSeSaudavel() { |
| 3 | const esp_partition_t *rodando = esp_ota_get_running_partition(); |
| 4 | esp_ota_img_states_t estado; |
| 5 | if (esp_ota_get_state_partition(rodando, &estado) != ESP_OK) { |
| 6 | return; |
| 7 | } |
| 8 | if (estado != ESP_OTA_IMG_PENDING_VERIFY) { |
| 9 | return; |
| 10 | } |
| 11 | if (autodiagnosticoOk()) { |
| 12 | esp_ota_mark_app_valid_cancel_rollback(); |
| 13 | Serial.println("[ota] imagem confirmada"); |
| 14 | } else { |
| 15 | Serial.println("[ota] autodiagnostico falhou, voltando ao anterior"); |
| 16 | esp_ota_mark_app_invalid_rollback_and_reboot(); |
| 17 | } |
| 18 | } |
Qué cuenta como autodiagnóstico es una decisión de diseño y ahí está el valor. Conectarse al Wi-Fi en un máximo de sesenta segundos suele ser el mínimo. Si el dispositivo depende de MQTT o de un bus Modbus, exige también eso; no sirve confirmar una imagen que conecta a la red pero no habla con el equipo.
Llama a la confirmación después de que el diagnóstico haya tenido tiempo de ejecutarse, no al principio de setup. Confirmar demasiado pronto anula todo el mecanismo.
El detalle que invalida todo
El estado pendiente de verificación solo existe si el bootloader se compiló con rollback habilitado. Es una opción del bootloader, no del código:
| 1 | CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y |
En ESP-IDF añádelo al sdkconfig y recompila. En Arduino el bootloader viene precompilado y la opción no está activada por defecto; llamar a las funciones anteriores no da error, pero tampoco protege nada, porque ninguna imagen entra en estado pendiente.
Es la misma frontera que NAPT: Arduino sirve para el camino sencillo y el producto que necesita garantías debe migrar a ESP-IDF. Conviene saberlo antes de prometer rollback a un cliente.
Verificación de integridad
Con independencia del rollback, valida lo que llegó. Lo mínimo es comparar el tamaño anunciado con el escrito, algo que ya hace el código anterior.
Un paso mejor es comprobar el primer byte: todo binario de aplicación del ESP32 empieza con el valor 0xE9. Si el servidor devolvió una página HTML de error en lugar del archivo, se detecta antes de escribir nada.
El paso definitivo es un SHA-256 publicado junto al binario y comprobado después de grabarlo. Cuesta unos segundos y elimina toda la clase de corrupción silenciosa en una red defectuosa.
Tutorial: de cero a la primera actualización por el aire
Paso 1: preparar la tabla de particiones
Crea el archivo partitions.csv en la raíz del proyecto con la tabla anterior, ajustando los tamaños a tu flash. Indícalo en platformio.ini:
| 1 | [env:esp32-ota] |
| 2 | platform = espressif32 |
| 3 | board = esp32dev |
| 4 | framework = arduino |
| 5 | monitor_speed = 115200 |
| 6 | board_build.partitions = partitions.csv |
Compila y comprueba el informe de tamaño. Si aparece un error de imagen demasiado grande, los slots son pequeños para tu firmware.
Paso 2: grabar la primera versión por cable
La primera grabación siempre es por USB; no existe OTA sin un firmware funcionando. Graba una versión con el número de versión visible en el log de arranque, porque así confirmarás el cambio.
| 1 | pio run -t upload |
| 2 | pio device monitor -b 115200 |
Paso 3: publicar el binario nuevo
Cambia el número de versión, compila sin grabar y localiza el archivo:
| 1 | pio run |
El binario queda en .pio/build/esp32-ota/firmware.bin. Cópialo a un servidor HTTP accesible desde la red de la placa. Para probar, basta el servidor integrado de Python:
| 1 | python -m http.server 8000 |
Paso 4: iniciar la actualización
Con el firmware del kit, indica la URL del binario y observa el monitor serie. Debes ver el tamaño anunciado, el progreso de la grabación y la confirmación antes del reinicio.
Si falla la descarga, prueba la URL en el navegador de un ordenador de la misma red; un error de ruta es mucho más común que un error de código.
Paso 5: confirmar el cambio
Después del reinicio, el log de arranque debe mostrar el número de versión nuevo. Si muestra el antiguo, la grabación no terminó o no se actualizó otadata.
Paso 6: probar el fallo a propósito
Este paso casi nadie lo hace y es el que da confianza en el mecanismo.
Compila una versión deliberadamente rota, por ejemplo con la contraseña Wi-Fi equivocada, y actualiza a ella. Con rollback activo, la placa debe intentarlo, fallar el autodiagnóstico y volver sola a la versión anterior.
Si se queda en la versión rota, el rollback no funciona y es mucho mejor descubrirlo en la mesa.
Errores comunes y cómo resolverlos
Error de partición inexistente. El proyecto usa la tabla predeterminada de una sola partición. Indica partitions.csv en platformio.ini.
Sketch too big después de habilitar OTA. Es normal: tienes la mitad de la flash. En una placa de 4 MB usa min_spiffs; si todavía no cabe, considera una placa mayor.
La actualización termina pero la placa vuelve a la versión antigua. No se actualizó otadata; casi siempre falló end(true). Lee errorString.
Descarga interrumpida en una red mala. Implementa reintentos y compara el tamaño anunciado en la cabecera con el escrito. Sin esa comparación, una descarga truncada se convierte en una imagen inválida grabada como si hubiera tenido éxito.
Se grabó una página HTML como firmware. El servidor devolvió un error y el código grabó la respuesta. Comprobar el primer byte 0xE9 lo detecta antes.
Llamé a las funciones de rollback y no pasa nada. El bootloader no se compiló con la opción activada. Con el bootloader estándar de Arduino el rollback no está disponible.
La placa entró en un ciclo de reinicio después de la actualización. Sin rollback la recuperación es por cable. Con rollback, eso es precisamente lo que existe para evitar; por eso hay que probar el fallo a propósito antes de ir a campo.
