Este é o artigo de montagem da série Assistentes de IA de Bancada. Aqui o foco é o caminho mais longo e mais recompensador: um assistente que acorda com uma palavra-chave detectada localmente, no próprio ESP32-S3, sem depender de nuvem para ouvir. Se você ainda não viu o panorama dos repositórios, comece por ele — este texto assume aquela base e desce ao hardware, à pinagem e ao firmware.
(a) Conceito e Arquitetura
O pipeline tem seis estágios. O microfone MEMS entrega amostras por I2S; o AFE do ESP-SR faz a limpeza do sinal (cancelamento de eco, supressão de ruído e controle automático de ganho); o WakeNet observa continuamente esse fluxo e dispara quando reconhece a palavra-chave. A partir daí o áudio do comando é bufferizado e enviado — via Wi-Fi — para um backend de ASR/LLM (nuvem ou Ollama local). A resposta volta em texto, passa por TTS e sai pelo amplificador I2S.
O ponto crítico é que os três primeiros estágios rodam 100% no MCU. A nuvem só entra depois do wake word, o que reduz consumo, custo e exposição de dados. É aqui que este projeto se diferencia de um assistente de voz genérico: o objetivo não é conversar abertamente o tempo todo, e sim ter um companheiro de bancada que responde quando chamado, com latência de gatilho abaixo de 100 ms.
(b) BOM
| Componente (modelo) | Função | Faixa de preço (BRL) |
|---|---|---|
| ESP32-S3-DevKitC-1 (N16R8, 16 MB flash / 8 MB PSRAM) | MCU principal: Wi-Fi/BLE, PSRAM para AFE + WakeNet | R$ 60–120 |
| INMP441 | Microfone MEMS I2S 24-bit, 60 Hz–15 kHz | R$ 15–30 |
| MSM261S4030H0 (alternativa) | Microfone I2S usado em devkits Espressif | R$ 20–40 |
| MAX98357A | Amplificador I2S classe D, 3,2 W | R$ 20–35 |
| Alto-falante 4 Ω / 3 W | Saída de áudio (TTS/retorno) | R$ 10–25 |
| Display ST7789 1,3″ SPI (ou ILI9341) | Feedback visual / UI LVGL (nível de áudio, status) | R$ 35–70 |
| Botão tátil + LED | Push-to-talk e status de estado | R$ 2–5 |
| Protoboard / PCB + jumpers | Montagem e cabeamento | R$ 15–30 |
(c) Wiring & Esquemático
| Sinal | Periférico | GPIO (ESP32-S3) | Observações |
|---|---|---|---|
| VDD | INMP441 | 3V3 | Fonte 3,3 V dedicada; adicione 100 nF perto do pino |
| GND | INMP441 | GND | Referência comum |
| SCK (BCLK) | INMP441 | GPIO14 | Clock de bits, gerado pelo ESP32 (master) |
| WS (LRCLK) | INMP441 | GPIO15 | Seleção de canal/word select |
| SD (DIN) | INMP441 | GPIO32 | Dados do mic para o MCU |
| L/R | INMP441 | GND | GND = canal esquerdo |
| VIN | MAX98357A | 5V | Use o rail 5 V, não o 3V3, em volume alto |
| BCLK | MAX98357A | GPIO26 | Clock de bits do I2S de saída |
| LRC | MAX98357A | GPIO25 | Word select de saída |
| DIN | MAX98357A | GPIO22 | Dados do MCU para o amp |
| SD | MAX98357A | 3V3 | Enable; em nível baixo o amp silencia |
| CS / DC / RST | ST7789 | GPIO5 / GPIO4 / GPIO2 | Barramento SPI do display |
Cuidados elétricos. Wi-Fi somado a PSRAM produz picos de corrente: use fonte de 5 V/2 A e um capacitor de 470 µF–1000 µF no rail de 3,3 V. Não alimente o amplificador pelo 3V3 do devkit se for operar em volume alto. Todos os periféricos aqui são de nível lógico 3,3 V, então não há divisor de nível. Para o botão, basta o pull-up interno do MCU (ou 10 kΩ externo). Atenção aos strapping pins do ESP32-S3 — GPIO0, GPIO3, GPIO45 e GPIO46: não deixe nenhum deles puxado para o nível errado no boot, pois isso altera o modo de inicialização. No I2S, mantenha o BCLK curto e longe das trilhas de RF; o INMP441 não precisa de MCLK, apenas BCLK e WS.
(d) Código e Firmware
Inicialização do I2S de captura (ESP-IDF 5.x):
#include "driver/i2s_std.h"
i2s_chan_handle_t rx_chan;
void i2s_mic_init(void) {
i2s_chan_config_t chan_cfg =
I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.dma_desc_num = 6;
chan_cfg.dma_frame_num = 240; // ~15 ms a 16 kHz
i2s_new_channel(&chan_cfg, NULL, &rx_chan);
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = GPIO_NUM_14,
.ws = GPIO_NUM_15,
.dout = I2S_GPIO_UNUSED,
.din = GPIO_NUM_32,
},
};
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
i2s_channel_init_std_mode(rx_chan, &std_cfg);
i2s_channel_enable(rx_chan);
}
Wake word com ESP-SR (AFE + WakeNet):
#include "esp_afe_sr_iface.h"
#include "esp_afe_sr_models.h"
#include "model_path.h"
static const esp_afe_sr_iface_t *afe = &ESP_AFE_SR_HANDLE;
static esp_afe_sr_data_t *afe_data;
void wakenet_init(void) {
srmodel_list_t *models = esp_srmodel_init("model");
afe_config_t cfg = AFE_CONFIG_DEFAULT();
cfg.wakenet_model_name = esp_srmodel_filter(
models, ESP_WN_PREFIX, NULL); // ex.: "wn9_hilexin"
cfg.aec_init = false;
cfg.pcm_config.total_ch_num = 1;
cfg.pcm_config.mic_num = 1;
cfg.pcm_config.ref_num = 0;
cfg.pcm_config.sample_rate = 16000;
afe_data = afe->create_from_config(&cfg);
}
bool espera_wake_word(void) {
int chunk = afe->get_feed_chunksize(afe_data);
int16_t *buf = heap_caps_malloc(chunk * sizeof(int16_t),
MALLOC_CAP_SPIRAM);
while (1) {
size_t n;
i2s_channel_read(rx_chan, buf, chunk * sizeof(int16_t),
&n, portMAX_DELAY);
afe->feed(afe_data, buf);
afe_fetch_result_t *r = afe->fetch_with_delay(afe_data, 0);
if (r && r->wakeup_state == WAKENET_DETECTED) return true;
}
}
Conexão Wi-Fi (station):
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
void wifi_init_sta(const char *ssid, const char *pass) {
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
wifi_config_t wc = { 0 };
strncpy((char *)wc.sta.ssid, ssid, sizeof(wc.sta.ssid));
strncpy((char *)wc.sta.password, pass, sizeof(wc.sta.password));
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &wc);
esp_wifi_start();
esp_wifi_connect(); // aguarde WIFI_EVENT_STA_GOT_IP
}
Chamada HTTP ao backend (LLM local ou nuvem):
#include "esp_http_client.h"
void pergunta_ao_llm(const char *json_body, char *saida, size_t max) {
esp_http_client_config_t cfg = {
.url = "https://api.exemplo.com/v1/chat",
.method = HTTP_METHOD_POST,
.timeout_ms = 8000,
};
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 SUA_CHAVE");
esp_http_client_set_post_field(c, json_body, strlen(json_body));
esp_http_client_perform(c);
int len = esp_http_client_read_response(c, saida, max - 1);
if (len < 0) len = 0;
saida[len] = '\0';
esp_http_client_cleanup(c);
}
(e) Troubleshooting
- PSRAM: o AFE e o WakeNet exigem memória contínua grande. Habilite PSRAM no menuconfig (CONFIG_SPIRAM) e aloque os buffers com heap_caps_malloc(…, MALLOC_CAP_SPIRAM). Sem PSRAM, o create_from_config falha ou o sistema reinicia por falta de memória.
- Buffer I2S: estalos e cortes quase sempre vêm de DMA mal dimensionado. Ajuste dma_frame_num para múltiplos do chunksize do AFE (~15 ms) e aumente dma_desc_num se houver underrun. Confirme sample_rate = 16000 e slot de 32 bits (o INMP441 devolve dados alinhados à esquerda).
- Falsos positivos: ganho alto demais é a causa número um. O AGC do AFE ajuda, mas calibre o nível de entrada, mantenha distância fixa do microfone e evite fontes de ruído contínuo perto do mic. Em ambiente barulhento, um modelo de wake word específico rende mais que aumentar o limiar.
- Latência: separe a tarefa de áudio da tarefa de rede. Não bloqueie o loop do I2S esperando o HTTP; envie o comando para uma fila e deixe a resposta chegar por evento. Para respostas curtas, streaming por linhas JSON reduz a sensação de espera. Se a latência de nuvem incomodar, o backend local via Ollama elimina o RTT de internet.
(f) Aplicações reais
- Companheiro de bancada que registra comandos de voz durante a montagem, sem tirar as mãos do protótipo.
- Interface de laboratório: acionar roteiros de teste, iniciar gravação de logs ou consultar status por voz.
- Nó de automação doméstica local-first, no qual o gatilho nunca sai do dispositivo.
- Base de acessibilidade auditiva/motora, com retorno por display e áudio em uma única placa.
- Plataforma de estudo de DSP embarcado: trocar modelos de wake word e medir falsos positivos e latência.
Para o panorama dos repositórios e das APIs, volte ao artigo anterior desta série. Para o contexto de voz offline, veja o material sobre assistentes de voz offline e a série J.A.R.V.I.S.; para o runtime de modelo, o texto sobre LLMs locais; e para a integração com hardware, a discussão de Edge AI. Os dois posts formam o par estratégia + montagem de um assistente de bancada.
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