(a) Conceito e Arquitetura
Um assistente de IA de bancada feito com ESP32 não roda o modelo: ele é o corpo do assistente, enquanto o cérebro fica atrás de uma API HTTPS. O microcontrolador captura áudio ou texto, monta uma requisição JSON, envia por TLS e renderiza a resposta em um display, fala por um amplificador I2S ou aciona uma GPIO. Esse desenho é o oposto do “LLM local” em hardware próprio — aqui a inferência é remota e o ESP32-S3 atua como orquestrador de rede.
O fluxo típico de uma rodada é: microfone I2S → (opcional) transcrição por API → LLM (OpenAI, Gemini ou Claude) → (opcional) síntese de voz → amplificador I2S. Texto puro pelo display/botão também é perfeitamente válido e elimina duas chamadas de rede, o que reduz custo e latência.
Existem três topologias. (1) Direto do MCU para a API comercial: menos peças, menor latência, mas expõe a chave de API no dispositivo — só é aceitável em bancada, nunca em produto em campo. (2) Proxy/gateway local: um serviço em Raspberry Pi ou PC (por exemplo, o endpoint compatível com OpenAI exposto pelo ollama/ollama) guarda a chave, aplica rate limit, cache e logging. (3) Híbrida: comandos de bancada vão para o proxy local e consultas abertas vão para a nuvem. Este artigo cobre a topologia 1 do ponto de vista de TLS e JSON, deixando a topologia 2 como recomendação de produção.
Requisitos mínimos: TLS 1.2+, heap livre confortável (o handshake TLS sozinho consome dezenas de KB), PSRAM recomendada para buffers de resposta e um relógio confiável — sem hora correta o certificado é rejeitado.
(b) BOM (Bill of Materials)
| Componente | Modelo sugerido | Função | Observação |
|---|---|---|---|
| Placa MCU | ESP32-S3-DevKitC-1 (N16R8) | CPU, Wi-Fi, TLS, I2S | 16 MB flash + 8 MB PSRAM para buffers |
| Display | ST7789 240×240 SPI | Status e resposta textual | Barramento SPI compartilhado, CS dedicado |
| Microfone | INMP441 (I2S digital) | Entrada de voz | I2S0, sem ADC externo |
| Amplificador | MAX98357A (I2S) | Saída de áudio | Alto-falante 4 Ω / 3 W |
| Regulador | Buck/Linear 3,3 V ≥ 1 A | Alimentação estável | Picos de TX Wi-Fi ~500 mA |
| Capacitores | 470 µF eletrolítico + 100 nF cerâmico | Filtro/desacoplamento | Colocar perto do pino 3V3 |
| Botão | Push momentary | Push-to-talk / reset | Pull-up interno + RC antirruído |
| Resistores | 10 kΩ, 100 Ω (série I2S) | Pull-up e casamento | Opcional para integridade de sinal |
(c) Wiring e Esquemático (alimentação e estabilidade)
O ponto que mais derruba projetos de assistente é a alimentação. O rádio Wi-Fi do ESP32 puxa correntes de pico entre 350 e 500 mA durante as transmissões, e um regulador fraco ou fiação fina provocam o clássico brownout reset — sintoma típico de “cai justamente na hora de chamar a API”. Diretrizes práticas:
- Use regulador de 3,3 V com capacidade ≥ 1 A e boa resposta a transientes.
- Coloque um capacitor bulk de 470 µF em paralelo com um cerâmico de 100 nF o mais próximo possível do pino 3V3 do módulo.
- Evite alimentar o ESP32 pelo USB e por fonte externa ao mesmo tempo; se necessário, use um diodo de isolamento.
- Adote ground em estrela e mantenha as trilhas de clock I2S curtas para não injetar ruído no áudio.
- Pino EN com circuito RC (10 kΩ + 1 µF) garante reset limpo na energização.
| Sinal | GPIO (sugerido) | Periférico |
|---|---|---|
| I2S0 BCLK / WS / DIN | GPIO 4 / 5 / 6 | Microfone INMP441 |
| I2S1 BCLK / WS / DOUT | GPIO 15 / 16 / 7 | Amplificador MAX98357A |
| SPI MOSI / SCLK / CS / DC / RST | GPIO 11 / 12 / 10 / 9 / 8 | Display ST7789 |
(d) Código — TLS, certificados, POST JSON, streaming e parse
O primeiro passo é sincronizar o relógio. Sem isso a validação de certificado falha porque a CA parece “do futuro”.
#include "esp_sntp.h"
#include <time.h>
// Obrigatorio antes de qualquer handshake TLS valido:
// certificados sao checados contra o relogio do sistema.
static void sync_time(void) {
setenv("TZ", "<-03>3", 1); // America/Sao_Paulo
tzset();
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_init();
time_t now = 0;
while (time(&now) < 1700000000) { // espera o relogio passar de ~2023
vTaskDelay(pdMS_TO_TICKS(500));
}
}
Com o relógio certo, o cliente HTTPS usa o bundle de CAs do ESP-IDF (esp_crt_bundle_attach), evitando embutir certificados PEM manualmente. O mesmo esqueleto vale para OpenAI, Gemini e Claude — muda só a URL, o cabeçalho de autenticação e o nome dos campos JSON.
#include "esp_http_client.h"
#include "esp_crt_bundle.h" // bundle de CAs do ESP-IDF
static char s_buf[4096];
static esp_err_t on_data(esp_http_client_event_t *e) {
if (e->event_id == HTTP_EVENT_ON_DATA) {
// streaming: cada chunk da resposta (SSE) chega aqui
int n = e->data_len < (int)sizeof(s_buf) - 1 ? e->data_len : (int)sizeof(s_buf) - 1;
memcpy(s_buf, e->data, n);
s_buf[n] = 0;
// aqui voce pode fazer parse incremental do campo "delta"
}
return ESP_OK;
}
esp_err_t chat_post(const char *bearer, const char *body_json) {
esp_http_client_config_t cfg = {};
cfg.url = "https://api.openai.com/v1/chat/completions";
cfg.method = HTTP_METHOD_POST;
cfg.crt_bundle_attach = esp_crt_bundle_attach; // valida a cadeia de CAs
cfg.event_handler = on_data;
cfg.timeout_ms = 20000;
esp_http_client_handle_t c = esp_http_client_init(&cfg);
esp_http_client_set_header(c, "Content-Type", "application/json");
esp_http_client_set_header(c, "Authorization", bearer); // "Bearer sk-..."
esp_http_client_open(c, strlen(body_json));
esp_http_client_write(c, body_json, strlen(body_json));
esp_http_client_fetch_headers(c);
int status = esp_http_client_get_status_code(c);
while (esp_http_client_read(c, s_buf, sizeof(s_buf)) > 0) { /* drena SSE */ }
esp_http_client_close(c);
esp_http_client_cleanup(c);
return status == 200 ? ESP_OK : ESP_FAIL;
}
O streaming é importante para a sensação de tempo real: em vez de esperar a resposta inteira, cada chunk (formato Server-Sent Events, data: {...}) é tratado dentro do event handler e o texto pode ser exibido progressivamente no display. Por fim, o parse com ArduinoJson, usando um filtro para carregar só os campos úteis e diminuir o consumo de RAM.
#include <ArduinoJson.h>
// Filtro: mantem apenas os campos usados -> economiza RAM
const char *filter =
"{\"choices\":[{\"message\":{\"content\":true}}],"
" \"usage\":{\"total_tokens\":true}}";
StaticJsonDocument<768> doc; // doc pequeno, sem heap frag
auto err = deserializeJson(doc, s_buf,
DeserializationOption::Filter(filter));
if (!err) {
const char *txt = doc["choices"][0]["message"]["content"] | "";
Serial.printf("resposta: %s\n", txt);
int tokens = doc["usage"]["total_tokens"] | 0; // custo da interacao
Serial.printf("tokens: %d\n", tokens);
}
(e) Troubleshooting
- Handshake TLS falha: na maioria dos casos o relógio está errado. Sincronize por SNTP antes da primeira chamada. Verifique também o bundle de CAs atualizado e a URL exata do endpoint.
- Memória: o handshake TLS pode precisar de 40–60 KB de heap; respostas JSON longas fragmentam a heap. Habilite PSRAM, aumente o buffer e use filtros no ArduinoJson.
- Timeouts: ajuste
timeout_ms(20 s é um bom começo) e implemente retry com backoff. Requisições que passam do limite devem ser canceladas e repetidas. - Custo por interação: tokens de entrada e saída contam; o modo streaming não reduz preço. Leia
usage.total_tokenspara telemetria e considere modelos “mini” para tarefas de bancada. Cacheie respostas repetidas. - Limites de cota: erros
429exigem respeitar o cabeçalhoRetry-After. Um proxy local (topologia 2) centraliza esse controle.
(f) Aplicações reais
Com a base de TLS e JSON funcionando, as aplicações aparecem rápido: um terminal de voz sobre a bancada que mostra a resposta no ST7789; um painel doméstico que resume leituras de sensores via LLM; um tutor de código que responde por alto-falante; e um monitor que narra alertas de telemetria. O projeto 78/xiaozhi-esp32 é a melhor referência pública de firmware completo (voz + display + MCP) para ESP32-S3 e serve de ponto de partida para quem não quer começar do zero. No próximo artigo da série, o foco sai da rede e vai para a memória: como dar contexto e continuidade ao assistente usando NVS, littlefs e uma camada leve de RAG local.
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