
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.
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
Baixe o material deste 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.
Quer receber novos manuais e esquemas industriais? Entre no nosso canal do Telegram / WhatsApp
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:
| 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, |
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:
| 1 | board_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:
| 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 | } |
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:
| 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 | } |
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:
| 1 | CONFIG_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:
| 1 | [env:esp32-ota] |
| 2 | platform = espressif32 |
| 3 | board = esp32dev |
| 4 | framework = arduino |
| 5 | monitor_speed = 115200 |
| 6 | board_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.
| 1 | pio run -t upload |
| 2 | pio 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:
| 1 | pio 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:
| 1 | python -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.
