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
StaticJsonDocumentno stack para payloads pequenos e eviteStringem laços. - “OTA partition could not be found”: a tabela de partições não tem
ota_0/ota_1/otadata. Ajuste opartitions.csve 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.

Deixe um comentário
Entre com Google ou Facebook para comentar. Nome e e-mail vêm da sua conta — sem preencher formulário.
Se o login falhar, configure Google e Facebook em Configurações → Nextend Social Login.
Outras opções de login