Firmware que não trava: FreeRTOS, watchdog, OTA e partições de flash

Ouvir

Carregando voz…

Firmware que não trava: FreeRTOS, watchdog, OTA e partições de flash

Firmware de bancada não é firmware de demo. Um assistente que fica ao lado do teclado o dia inteiro precisa sobreviver a roteador reiniciando, TLS derrubando handshake por falta de memória, OTA que chega pela metade e uma tarefa de áudio que perde o prazo e trava o buffer I2S. Este é o Projeto 9 da série Assistentes de IA de Bancada: transformar um esboço que “funciona na bancada” num binário que aguenta semanas ligado sem intervenção.

Conceito e arquitetura de firmware

O padrão vencedor é uma arquitetura de tarefas sob FreeRTOS com responsabilidades separadas, não um loop() monolítico. Um loop() único mistura áudio, rede e UI no mesmo fio de execução: qualquer chamada HTTP bloqueante faz o buffer de captura I2S estourar e o áudio picota. Quatro tarefas resolvem o problema por construção:

  • audio_task (core 1, prioridade alta) — preenche buffers I2S de captura e enfileira blocos; nunca faz I/O de rede.
  • net_task (core 0) — consome a fila, fala com o host (HTTP/WebSocket) e publica resultados de volta.
  • ui_task (core 1, prioridade baixa) — desenha estado (idle, escutando, pensando, falando) sem bloquear.
  • sys_task (core 0) — reconexão Wi-Fi, gatilho de OTA e manutenção de NVS.

A comunicação entre elas é por filas (dados) e event groups (estado). Áudio nunca espera a rede de forma bloqueante; a rede nunca rouba tempo do I2S. Sobre essa base vêm as três camadas de resiliência: watchdog (detecta tarefa travada e reinicia), reconexão de Wi-Fi com backoff exponencial (sobrevive a queda de roteador) e OTA com partição dual (atualiza sem brickar, com rollback se a imagem nova falhar no boot).

Lista de peças (BOM)

O firmware robusto impõe requisitos de hardware — sobretudo flash e PSRAM para caber duas imagens de aplicação e buffers de TLS/áudio.

Componente Modelo exato Função Faixa de preço
Placa ESP32-S3-DevKitC-1 N16R8 (16 MB flash, 8 MB PSRAM octal) Flash p/ OTA dual-partition + PSRAM p/ buffers de áudio e TLS R$ 70–130
LED de status WS2812B (NeoPixel) 5 mm Estado do sistema (boot, Wi-Fi OK, OTA, erro) sem precisar de display R$ 3–10
Botão Push-button tátil 6×6 mm Gatilho manual de OTA / reset de fábrica (NVS wipe) R$ 2–5
Capacitor de reset 100 nF + 10 kΩ pull-up no EN Reset limpo; evita boot errático R$ 2–5
Conversor USB-serial CP2102 ou CH340 (se a placa não tiver USB-CDC) Flash/console de debug R$ 10–25

Wiring e cuidados elétricos

Sinal Pino ESP32-S3 recomendado Observação
LED de status (dados WS2812B) GPIO 48 Vcc em 3,3 V (ou 5 V com level shifter); resistor de 300–500 Ω na linha de dados.
Botão de OTA / factory reset GPIO 0 (BOOT) É strapping pin: só para download/bootloader. Segurar no reset entra em modo flash.
UART0 (console) GPIO 43 (TX) / GPIO 44 (RX) Não oculte: é o único canal de diagnóstico quando o Wi-Fi cai.
Botão de ação geral GPIO 5 Com pull-up interno, ligado a GND via 100 nF p/ debounce.
Strapping pins a respeitar GPIO 0, 3, 45, 46 Nunca pendure carga que force nível errado no reset; deixe flutuante ou com nível correto.

Dois cuidados evitam suporte eterno: mantenha o console serial acessível mesmo dentro da carcaça (pads expostos), e nunca alimente o botão de OTA por um pino que também sirva de saída — strapping pin em nível errado no boot é uma classe inteira de bugs intermitentes.

Código e firmware

Esqueleto mínimo com duas tarefas orientadas a fila, watchdog por task e criação ancorada por núcleo:

#include <WiFi.h>
#include "esp_task_wdt.h"

QueueHandle_t qAudio;                    // audio_task -> net_task
TaskHandle_t  hAudio, hNet;

void audioTask(void* pv) {
  for (;;) {
    Bloco bloco;
    if (capturaI2S(&bloco)) {            // bloqueia ate preencher um bloco
      if (xQueueSend(qAudio, &bloco, pdMS_TO_TICKS(50)) != pdPASS) {
        Serial.println(" fila cheia: descarta bloco (host lento)");
      }
    }
    esp_task_wdt_reset();                // alimenta o watchdog DESTA task
  }
}

void netTask(void* pv) {
  Bloco bloco;
  for (;;) {
    if (xQueueReceive(qAudio, &bloco, portMAX_DELAY) == pdPASS) {
      enviarParaHost(&bloco);            // HTTP/WS para o host (Ollama/API)
    }
    esp_task_wdt_reset();
  }
}

void setup() {
  Serial.begin(115200);
  qAudio = xQueueCreate(8, sizeof(Bloco));
  xTaskCreatePinnedToCore(audioTask, "audio", 8192,  NULL, 5, &hAudio, 1);
  xTaskCreatePinnedToCore(netTask,   "net",   12288, NULL, 4, &hNet,   0);

  // Watchdog: 8 s, panic -> reinicia em vez de ficar pendurado
  esp_task_wdt_config_t cfg = { .timeout_ms = 8000, .idle_core_mask = 0, .trigger_panic = true };
  esp_task_wdt_init(&cfg);
  esp_task_wdt_add(hNet);                // registra as tasks criticas
}

Reconexão de Wi-Fi resiliente com backoff exponencial (o sleep vai numa task de sistema, nunca dentro do callback de evento, que roda no contexto do driver):

uint8_t tentativas = 0;

void onWifiEvent(WiFiEvent_t e) {
  if (e == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
    tentativas = min(tentativas + 1, 6);
    uint32_t espera = min(30000u, 500u * (1u << tentativas));  // ~1s..30s
    xTaskNotifyGive(hSys);               // a sys_task aplica o backoff
  } else if (e == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
    tentativas = 0;
    Serial.print("[net] IP: "); Serial.println(WiFi.localIP());
  }
}

OTA sobre HTTPS com verificação de integridade e reinício só no fim:

#include <Update.h>
#include <HTTPClient.h>

void doOTA(const char* url) {
  HTTPClient http; http.begin(url);
  int code = http.GET();
  if (code != 200) { Serial.printf("[ota] HTTP %d\n", code); return; }
  int len = http.getSize();
  if (!Update.begin(len)) {                      // grava na particao ota_1
    Serial.printf("[ota] begin falhou: %s\n", Update.errorString()); return;
  }
  size_t w = Update.writeStream(*http.getStreamPtr());
  if (w != (size_t)len) Serial.println("[ota] escrita parcial");
  if (!Update.end() || !Update.isFinished()) {
    Serial.printf("[ota] fim falhou: %s\n", Update.errorString());
  } else { Serial.println("[ota] OK, reiniciando"); ESP.restart(); }
}

A tabela de partições precisa reservar duas partições de aplicação mais otadata, senão o OTA nem compila:

# Name,   Type, SubType, Offset,   Size,     Flags
nvs,      data, nvs,     0x9000,   0x5000,
otadata,  data, ota,     0xe000,   0x2000,
app0,     app,  ota_0,   0x10000,  0x1E0000,
app1,     app,  ota_1,   0x1F0000, 0x1E0000,
spiffs,   data, spiffs,  0x3D0000, 0x30000,

Resolução de problemas

  • Task watchdog dispara: alguma task não chega ao esp_task_wdt_reset() — quase sempre por I/O bloqueante dentro dela. Mova a rede para a própria task e alimente o watchdog no laço.
  • Stack overflow / Guru Meditation: meça com uxTaskGetStackHighWaterMark() e redimensione. Parse de JSON e TLS pedem pilha maior que o imaginado.
  • Heap fragmentado após dias: aloque buffers no boot, prefira StaticJsonDocument no stack para payloads pequenos e evite String em laços.
  • “OTA partition could not be found”: a tabela de partições não tem ota_0/ota_1/otadata. Ajuste o partitions.csv e regrave.
  • Rollback após OTA ruim: com CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE, a imagem nova só é confirmada depois de marcar-se válida. Não marque antes de checar Wi-Fi e hello-world com o host.
  • Brownout durante a escrita do OTA: flash exige corrente estável; garanta alimentação e evite rodar em bateria no fim da carga.
  • Wi-Fi que não volta: callbacks de evento não devem dormir; use notificação para uma task e aplique o backoff lá.

Aplicações reais

Com essa base, o assistente vira infraestrutura: você o deixa na mesa e esquece que ele existe até precisar. O OTA pelo navegador da LAN elimina o cabo USB do dia a dia — comportamento de produto, não de protótipo. O watchdog transforma travamentos silenciosos em reinícios observáveis (contados no LED e no log), e a reconexão com backoff faz o aparelho voltar sozinho quando o roteador pisca. É a diferença entre um brinquedo que você desliga no fim do dia e uma ferramenta que fica no ar.

O capítulo anterior tratou da energia que sustenta esse firmware; o próximo muda a fronteira de responsabilidade e mostra por que dividir o trabalho com um host que roda o LLM é, na prática, a arquitetura mais robusta. Para o lado “cérebro” dessa divisão, veja LLMs Locais: Sua IA, Seu Hardware, Suas Regras. Hub da editoria: tiolu.com.br/nucleo-hits.

Não some depois da matéria

Radar do hub: Núcleo Hits, Além do Oculto, Bobinho. Sem spray, sem lista comprada.

Radar do hub

Um e-mail quando sair matéria que importa — sem spray de newsletter genérica.

Comentários

Deixe um comentário