iiOTESP32 Brasil
Modo de contingência: páginas públicas e downloads seguem disponíveis; login, comunidade e dados do servidor podem ficar indisponíveis.
Voltar para catálogo de tutoriais
Ilustração: placa ESP32 recebendo dados de uma nuvem com um arquivo de firmware, ao lado de dois chips que representam duas partições, uma ativa e outra em atualização
Imagem ilustrativa gerada por IA
IntermediárioOTALeitura: 14 min

Atualização OTA (Over-The-Air) no ESP32

Duas partições, otadata e o estado pendente de verificação: como atualizar firmware pela rede sem transformar um bug em visita técnica.

Tecnologias:#ESP32#OTA#Update.h#esp_ota_ops#Rollback#Partições

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

O problema que OTA resolve, e o que ele cria

Gravar firmware por cabo é simples quando a placa está na bancada. Quando ela está num painel elétrico trancado, no terceiro andar de um galpão, atrás de uma máquina em operação, cada atualização vira uma visita técnica.

OTA — atualização pelo ar — resolve isso. Mas cria um risco novo, e é esse risco que separa uma implementação de tutorial de uma que você aceita colocar em campo: se a atualização der errado, o dispositivo pode ficar inacessível exatamente do jeito que motivou o OTA.

Este artigo trata das duas partes. Primeiro como atualizar. Depois, e principalmente, como garantir que uma atualização ruim não custe a visita técnica que você queria evitar.

Por que precisam existir duas partições

A primeira coisa a entender é que o ESP32 não sobrescreve o firmware em execução. Seria impossível: o código que está rodando é o mesmo que estaria sendo apagado.

A solução é ter dois espaços de aplicação. O firmware roda de um deles, grava o novo no outro, e só então marca qual deve ser usado no próximo boot. Quem guarda essa marcação é uma terceira partição, pequena, chamada otadata.

Isso tem uma consequência excelente: queda de energia no meio da atualização é segura. O que está sendo escrito é o slot inativo; o slot ativo continua intacto, e o otadata ainda aponta para ele. A placa reinicia no firmware antigo como se nada tivesse acontecido.

E tem uma consequência incômoda: você perde metade da flash. Numa placa de 4 MB, cada slot fica com cerca de 1,9 MB. É o preço, e não há como fugir dele.

A tabela de partições

Esta é a tabela usada no firmware do repetidor, numa placa de 16 MB:

code.cpp
cpp
1# Name, Type, SubType, Offset, Size, Flags
2nvs, data, nvs, 0x9000, 0x5000,
3otadata, data, ota, 0xe000, 0x2000,
4app0, app, ota_0, 0x10000, 0x300000,
5app1, app, ota_1, , 0x300000,
6spiffs, data, spiffs, , 0x900000,

Leia os campos que importam. O otadata ocupa 8 KB e é onde fica registrado qual slot é o válido. O app0 e o app1 são os dois espaços de aplicação, com 3 MB cada. E o spiffs fica com os 9 MB restantes, para configuração e arquivos.

Repare que app1 e spiffs não declaram deslocamento: o gerador calcula a partir do tamanho do anterior. Só o primeiro precisa do endereço fixo.

Se a sua placa tem 4 MB, use a tabela pronta chamada min_spiffs, que reserva cerca de 1,9 MB por slot. É o que o Arduino oferece de mais próximo do razoável para OTA.

Aponte a tabela no platformio.ini:

code.cpp
cpp
1board_build.partitions = partitions.csv

Sem isso, o projeto usa a tabela padrão de partição única e a atualização falha na primeira tentativa com erro de partição não encontrada.

Push ou pull: quem começa a conversa

Existem dois modelos, e a escolha muda a arquitetura.

No push, o dispositivo expõe um endpoint HTTP e você envia o binário para ele. É o modelo do ArduinoOTA e das bibliotecas de OTA web. Simples de testar, e funciona bem quando você tem acesso à rede local do equipamento.

No pull, o dispositivo consulta periodicamente um servidor, compara a versão e baixa se houver novidade. Exige mais infraestrutura, mas é o único que escala: atualizar cinquenta gateways por push significa cinquenta operações manuais; por pull, significa publicar um arquivo.

Para produto, pull. Para bancada e manutenção pontual, push. Este tutorial implementa o pull, que é o caso mais difícil e o mais útil.

A atualização em si é a parte fácil

O código de gravação surpreende pela simplicidade. A biblioteca Update, que já vem no core, cuida do essencial:

code.cpp
cpp
1bool 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}

O Update.begin escolhe sozinho o slot inativo e verifica se o binário cabe. O writeStream grava direto do fluxo HTTP, sem precisar de buffer intermediário do tamanho do firmware — o que seria impossível, já que o binário é maior que a RAM. E o end(true) valida a imagem e atualiza o otadata.

Se qualquer etapa falhar, nada muda: o otadata continua apontando para o slot que está rodando.

O firmware que sobe e não funciona

Aqui está o risco de verdade, e a razão de este artigo existir.

O caso que todo mundo imagina é a atualização falhar no meio. Esse é o caso fácil — já vimos que ele é seguro por construção.

O caso perigoso é outro: a atualização conclui com sucesso, o binário é válido, a placa reinicia no firmware novo, e aí descobre-se que ele tem um bug que impede conectar no Wi-Fi. Ou que trava no setup. Ou que consome memória até reiniciar em ciclo.

Do ponto de vista do OTA, deu tudo certo. Do ponto de vista prático, o dispositivo está inacessível e você perdeu justamente a capacidade de enviar a correção. É a visita técnica que o OTA existia para evitar, agora com o agravante de que você mesmo causou.

Nenhuma verificação de integridade do binário detecta isso. O arquivo está íntegro; o comportamento é que está errado.

Rollback: o estado pendente de verificação

A solução é não confiar no firmware novo de imediato.

Com o rollback habilitado, uma imagem recém-gravada entra num estado especial: pendente de verificação. O bootloader a executa, mas com uma condição — se a placa reiniciar sem que o firmware tenha declarado que está funcionando, o bootloader volta sozinho para a imagem anterior.

O firmware novo precisa então provar que funciona:

code.cpp
cpp
1#include "esp_ota_ops.h"
2void 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}

O que conta como autodiagnóstico é decisão de projeto, e é onde mora o valor. Conectar no Wi-Fi em até sessenta segundos costuma ser o mínimo. Se o seu dispositivo depende de MQTT ou de um barramento Modbus, exija também isso — de nada adianta confirmar uma imagem que conecta na rede e não fala com o equipamento.

Chame a confirmação depois de o diagnóstico ter tido tempo de rodar, não no início do setup. Confirmar cedo demais anula o mecanismo inteiro.

O detalhe que invalida tudo

O estado pendente de verificação só existe se o bootloader foi compilado com rollback habilitado. É uma opção do bootloader, não do seu código:

code.cpp
cpp
1CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y

Em ESP-IDF você acrescenta isso ao sdkconfig e recompila. Em Arduino, o bootloader vem pré-compilado, e a opção não está ligada por padrão — chamar as funções acima não dá erro, mas também não protege nada, porque nenhuma imagem entra em estado pendente.

É a mesma fronteira do NAPT: o Arduino serve para o caminho simples, e o produto que precisa de garantia migra para ESP-IDF. Vale saber disso antes de prometer rollback a um cliente.

Verificação de integridade

Independente do rollback, valide o que chegou. O mínimo é comparar o tamanho anunciado com o gravado, o que o código acima já faz.

Um passo melhor é verificar o primeiro byte: todo binário de aplicação do ESP32 começa com o valor 0xE9. Se o servidor devolveu uma página de erro HTML em vez do arquivo, isso é detectado antes de gravar qualquer coisa.

E o passo definitivo é o SHA-256 publicado junto do binário, conferido depois da gravação. Custa poucos segundos e elimina a classe inteira de corrupção silenciosa em rede ruim.

Tutorial: do zero à primeira atualização pelo ar

Passo 1: preparar a tabela de partições

Crie o arquivo partitions.csv na raiz do projeto com a tabela mostrada acima, ajustando os tamanhos à sua flash. Aponte no platformio.ini:

code.cpp
cpp
1[env:esp32-ota]
2platform = espressif32
3board = esp32dev
4framework = arduino
5monitor_speed = 115200
6board_build.partitions = partitions.csv

Compile e confira o relatório de tamanho. Se aparecer erro de imagem grande demais, os slots estão pequenos para o seu firmware.

Passo 2: gravar a primeira versão pelo cabo

A primeira gravação é sempre por USB — não existe OTA sem um firmware já rodando. Grave uma versão com o número de versão bem visível no log de boot, porque é assim que você vai confirmar a troca.

code.cpp
cpp
1pio run -t upload
2pio device monitor -b 115200

Passo 3: publicar o binário novo

Altere o número da versão, compile sem gravar e localize o arquivo:

code.cpp
cpp
1pio run

O binário fica em .pio/build/esp32-ota/firmware.bin. Copie para um servidor HTTP acessível pela rede da placa. Para teste, o servidor embutido do Python resolve:

code.cpp
cpp
1python -m http.server 8000

Passo 4: disparar a atualização

Com o firmware do kit, informe a URL do binário e observe o monitor serial. Você deve ver o tamanho anunciado, o progresso da gravação e a confirmação antes do reinício.

Se o download falhar, teste a URL pelo navegador de um computador na mesma rede: erro de caminho é bem mais comum que erro de código.

Passo 5: confirmar a troca

Depois do reinício, o log de boot deve mostrar o número de versão novo. Se mostrar o antigo, a gravação não concluiu ou o otadata não foi atualizado.

Passo 6: testar a falha de propósito

Este é o passo que quase ninguém faz, e é o que dá confiança no mecanismo.

Compile uma versão deliberadamente quebrada — com a senha do Wi-Fi errada, por exemplo — e atualize para ela. Com rollback ativo, a placa deve tentar, falhar no autodiagnóstico e voltar sozinha à versão anterior.

Se ela ficar presa na versão quebrada, o rollback não está funcionando, e é muito melhor descobrir isso na bancada.

Erros comuns e como resolver

Erro de partição não encontrada. O projeto está usando a tabela padrão, de partição única. Aponte o partitions.csv no platformio.ini.

Sketch too big depois de habilitar OTA. Esperado: você tem metade da flash. Numa placa de 4 MB, use a tabela min_spiffs; se ainda não couber, considere uma placa maior.

A atualização conclui mas a placa volta na versão antiga. O otadata não foi atualizado — quase sempre o end(true) falhou. Leia o errorString.

Download interrompido em rede ruim. Implemente repetição, e confira o tamanho anunciado no cabeçalho contra o gravado. Sem essa comparação, um download truncado vira uma imagem inválida gravada com sucesso.

Gravou uma página HTML como firmware. O servidor devolveu erro e o código gravou a resposta. A verificação do primeiro byte 0xE9 pega isso antes.

Chamei as funções de rollback e nada acontece. O bootloader não foi compilado com a opção habilitada. Em Arduino com bootloader padrão, o rollback não está disponível.

A placa entrou em ciclo de reinício depois da atualização. Sem rollback, a recuperação é pelo cabo. Com rollback, é isso que ele existe para evitar — e é o motivo de testar a falha de propósito antes de ir para campo.

Ver mais tutoriais técnicosIIoT ESP32 Brasil — Documentação Prática