Tocas la pantalla, preguntas en voz alta "¿cuántos grados hay que darle a un servo para que quede al medio?" y unos segundos después la placa te responde hablando, con la pregunta y la respuesta escritas en pantalla. Nada de celular ni parlante inteligente de una marca: un ESP32-S3 que tú programaste, con la personalidad, el idioma y el largo de respuesta que tú decides.

Este tutorial explica cómo funciona un asistente de voz con IA montado sobre la placa Elecrow de 3,5 pulgadas con ESP32-S3, usando los modelos de OpenAI para transcribir, responder y hablar. Más que copiar un proyecto, la idea es que entiendas cada etapa: cómo la placa decide que terminaste de hablar, por qué el audio necesita PSRAM, qué se manda a internet y qué pasa con la clave de la API. Al final vas a saber configurarlo a tu gusto y qué cambiar para llevarlo a un ESP32-S3 genérico con micrófono y amplificador I2S.

La idea en una frase: cinco etapas y una pantalla

Cada interacción (un "turno") pasa siempre por las mismas etapas:

  1. Espera: la pantalla dice "Touch to speak".
  2. Captura: grabas tu pregunta en memoria.
  3. Transcripción: el audio se sube a la API de transcripción y vuelve como texto.
  4. Respuesta: ese texto va al modelo de chat, que escribe la respuesta.
  5. Voz: la respuesta se convierte en audio y suena por el parlante.

Al terminar, la pantalla muestra pregunta y respuesta hasta que la vuelvas a tocar.

Etapas de un turno del asistente de voz

Fíjate en algo importante: el ESP32-S3 no "entiende" nada. Su trabajo es grabar bien, hacer tres pedidos HTTPS en orden y reproducir lo que vuelve. Toda la inteligencia está en los servicios remotos. Por eso el proyecto necesita WiFi y no funciona sin conexión.

El hardware: qué trae la placa y por qué importa

Hardware de la placa Elecrow de 3,5 pulgadas con ESP32-S3

La placa viene con un parlante pequeño, un cable para el conector GPIO de 4 pines y un cable USB-C.

Placa Elecrow de 3,5 pulgadas con el parlante y los cables incluidos

ESP32-S3 N16R8: la "R8" es la que manda

El módulo es un ESP32-S3 de dos núcleos a 240 MHz con 16 MB de flash y 8 MB de PSRAM octal. Para un asistente de voz, esos 8 MB son imprescindibles. Haz la cuenta: audio a 16 kHz y 16 bits ocupa 32 KB por segundo, así que una pregunta de 10 segundos son 320 KB, y la respuesta hablada puede pesar varias veces eso. La RAM interna del ESP32-S3 (512 KB de SRAM, compartida con WiFi, TLS y la pantalla) no alcanza. Los dos buffers grandes de audio viven en la PSRAM.

La radio es solo de 2,4 GHz: tu red WiFi tiene que ofrecer esa banda.

Pantalla y touch

Es una IPS de 3,5" y 320 × 480 con controlador ST77922 por QSPI a 80 MHz, suficiente para mandar un cuadro completo de una vez. El firmware dibuja primero en un buffer y después envía el cuadro terminado: así no se ve el parpadeo de borrar y redibujar por partes. El touch es capacitivo, por I2C, y aunque puede reportar hasta diez dedos, el asistente solo usa "hay un dedo o no".

El audio: codec, micrófono y amplificador

Son tres piezas. Un codec ES8311 convierte entre audio analógico y muestras digitales I2S; un micrófono de alta sensibilidad entra al codec; y un amplificador clase D mono mueve el parlante (hasta 1,5 W en 8 Ω o 2 W en 4 Ω, por un conector de 1,25 mm). El codec trabaja en las dos direcciones con un solo reloj y una sola frecuencia de muestreo, lo que simplifica mucho el código: grabar y reproducir usan el mismo camino.

El amplificador tiene su propio pin de habilitación. El firmware lo enciende solo antes de hablar: así el parlante no hace ruido al arrancar y el amplificador no consume mientras el asistente espera.

Bus I2C compartido: la trampa de la placa

El touch, el codec de audio y el conector I2C externo comparten el mismo bus (IO38 y IO39). Funciona porque cada uno tiene su dirección, pero nunca se debe hablar con dos de ellos al mismo tiempo desde tareas distintas. Si agregas un sensor por el conector externo y lo lees desde otra tarea mientras el código configura el codec, vas a tener errores raros. La regla: todo acceso a ese bus, desde un solo lugar.

Energía, LED y botones

Se alimenta por USB-C o con una batería LiPo de una celda por un conector de 1,25 mm de 2 pines. El cargador limita a 500 mA (en la práctica, unos 290 mA) y es normal que la placa se entibie al cargar. Un divisor de voltaje permite medir la batería. Hay dos reguladores de 3,3 V: uno para lo digital y otro más limpio para el audio analógico, un detalle que se nota en el ruido de fondo del micrófono.

Trae además una ranura microSD en modo de 4 bits, un LED RGB direccionable y dos botones: reset y BOOT, que también sirve como tecla normal (un candidato para "apretar para hablar").

Pines que te van a importar

Función Pines
Pantalla QSPI IO10 (CS), IO12 (reloj), IO11, IO13, IO14, IO9 (datos)
Luz de fondo IO41
I2C compartido (touch, codec, conector) IO38 (SDA), IO39 (SCL)
Touch: reset e interrupción IO48, IO47
Audio I2S IO17 (MCLK), IO18 (BCLK), IO21 (LRCK), IO16 (salida), IO15 (entrada)
Habilitación del amplificador IO1 (activo en bajo)
LED RGB IO40
Batería (ADC) IO8
Pines libres del conector IO45, IO46

Cómo sabe que terminaste de hablar

Tocas la pantalla una sola vez, y el equipo decide solo cuándo terminó tu pregunta. Lo hace con detección de actividad de voz (VAD), en su versión más simple: mirar el volumen.

  • Empieza a grabar en serio cuando el volumen supera un umbral (VAD_THRESHOLD).
  • Termina cuando, después de haber oído voz, viene un silencio corto.
  • Corta por límite si llega a la duración máxima (MAX_RECORD_SECS), aunque sigas hablando.
  • Se rinde si no oye nada durante unos segundos tras el toque: muestra "No speech detected" y no sube nada. Así un toque accidental no deja el equipo colgado ni gasta una llamada a la API.

Por qué a veces se come la primera palabra. Si empiezas a hablar muy suave, las primeras sílabas quedan bajo el umbral y no se guardan. La solución clásica es un pequeño "pre buffer": guardar siempre el último medio segundo y, cuando se cruza el umbral, incluirlo en la grabación. Si no quieres tocar código, baja VAD_THRESHOLD o empieza con una palabra más fuerte.

El umbral depende de tu pieza. Con un ventilador o un aire acondicionado al lado, el ruido de fondo puede quedar sobre el umbral y la grabación no termina nunca (hasta el límite máximo). Prueba primero en silencio, mira en el monitor serie los niveles que reporta y ajusta.

Lo que ves en pantalla

La pantalla es el único indicador del equipo, así que nombra cada estado con palabras:

Texto Qué está pasando
Touch to speak Esperando (o mostrando el último turno)
Listening… Grabando tu pregunta
Transcribing… Subiendo el audio a la transcripción
Thinking… Esperando la respuesta del modelo
Speaking… Reproduciendo la respuesta
No speech detected Grabación en silencio, no se subió nada
Transcription failed La transcripción falló o vino inutilizable
No answer El modelo no devolvió nada útil
Audio not ready El audio no arrancó al encender
WiFi offline No se pudo conectar a la red

El software, por dentro

El código está en el repositorio del autor (enlace en Recursos) y se compila con PlatformIO en VS Code. Cada archivo lleva el nombre de la etapa que resuelve, no de la tecnología:

Archivo Qué hace
src/main.cpp Arranca todo y corre un turno de principio a fin
src/app_config.h Configuración: modelos, pines, tiempos, volumen, endpoints
src/display_ui.* Pantalla, buffer, touch, diseño y ajuste de texto
src/audio_io.* Grabar y reproducir por un solo bus I2S y un codec
src/openai_http.* Transporte HTTPS: clave, tiempos de espera, formatos de pedido
src/stt.* Voz a texto: arma la subida y lee la transcripción
src/llm.* Chat: arma el pedido y lee la respuesta
src/tts.* Texto a voz: pide el audio, lo guarda y se lo pasa al audio
src/latin1_font.h Fuente con todo Latin-1, para que se vean tildes y eñes

En lib/ vienen los drivers locales (ES8311, ST77922, ST77922_TOUCH) y una copia modificada de la librería de audio audio-tools. Esa modificación importa: si la reemplazas por una descarga nueva, se pierde el gancho que lee una de las opciones de compilación (más abajo) y el audio sale mal sin dar error.

Dónde cambiar qué

Quiero cambiar… Edito
Personalidad, tono o idioma LLM_SYSTEM_PROMPT en src/app_config.h
La voz TTS_VOICE
El largo de las respuestas LLM_SYSTEM_PROMPT y LLM_MAX_TOKENS
Cuánto puede durar una pregunta MAX_RECORD_SECS
Sensibilidad del micrófono VAD_THRESHOLD
Volumen OUTPUT_VOLUME_PERCENT
Los modelos TRANSCRIBE_MODEL, LLM_MODEL y TTS_MODEL
Proveedor o endpoint OPENAI_HOST y las tres rutas
WiFi y clave de la API include/credentials.h

Las opciones de compilación que pueden arruinarte el audio

En platformio.ini hay tres opciones de compilación. Dos son conocidas (ARDUINO_USB_MODE=1 y ARDUINO_USB_CDC_ON_BOOT=1, para que el monitor serie funcione desde el arranque). La tercera es la que más problemas da:

AUDIO_TOOLS_I2S_MCLK_MULTIPLE=I2S_MCLK_MULTIPLE_384

El reloj maestro I2S (MCLK) tiene que ser un múltiplo exacto de la frecuencia de muestreo. La librería de audio usa 256 por defecto, pero el driver del codec lo configura para 384. A 16 kHz eso es 6,144 MHz. Si los dos no coinciden, el codec recibe un reloj que no esperaba y la voz sale estirada, apurada o como de robot. Si alguna vez oyes eso, revisa esta opción antes que cualquier otra cosa.

Las opciones de memoria también importan: board_build.arduino.memory_type = qio_opi es la combinación que deja flash y PSRAM disponibles a la vez. Si está mal, los buffers de audio no se pueden reservar y el equipo muestra un error al arrancar.

Por qué un solo buffer contiguo

La transcripción se sube como un único pedido HTTP con el audio dentro de un contenedor WAV. El firmware reserva en la PSRAM un solo bloque continuo para todo el cuerpo del pedido, con el audio ya envuelto en su contenedor, y lo manda de una vez. Es más simple y más rápido que mandar en trozos, y evita fragmentar la memoria con muchas reservas chicas.

Respuestas sin memoria

Cada pregunta va sola al modelo, con la instrucción de sistema y nada más. El asistente no recuerda lo que preguntaste antes. Es una decisión de diseño: mantiene el pedido chico, rápido y barato. Si quieres conversación con contexto, hay que guardar los últimos turnos y mandarlos junto con la pregunta nueva, sabiendo que cada pedido se vuelve más largo.

Configurarlo: credenciales e instrucción de sistema

La clave de la API

WiFi, contraseña y clave de OpenAI van en include/credentials.h, un archivo que no está en el repositorio a propósito (el proyecto no compila hasta que lo crees). El tutorial original trae la plantilla: tres constantes, una para el nombre de la red, otra para su contraseña y otra para la clave.

La clave de la API es una credencial pagada: cada transcripción, respuesta y audio se cobra a la cuenta dueña. Tres reglas:

  • No la compartas ni la subas a un repositorio. Si se filtra, revócala y crea otra.
  • Crea una clave solo para este proyecto y ponle límite de gasto a la cuenta. Si algo se queda en bucle, el daño queda acotado.
  • Recuerda que vive dentro del firmware. Cualquiera con la placa en la mano y paciencia puede leerla de la flash. Para un aparato de escritorio propio está bien; para algo que vas a regalar o dejar en un lugar público, no.

Hacerlo hablar en español de Chile

La personalidad entera está en LLM_SYSTEM_PROMPT. Una instrucción como "Eres un asistente de electrónica. Responde en español de Chile, con tuteo, en dos o tres oraciones como máximo, sin listas ni formato" cambia por completo la experiencia. Respuestas cortas no son solo más cómodas: el audio pesa menos, se descarga antes y la espera total baja. Ajusta también LLM_MAX_TOKENS como tope duro.

La transcripción reconoce español sin configuración extra, y la voz de síntesis lee en español si el texto viene en español. Cambia TTS_VOICE hasta encontrar una que te suene natural.

El volumen

OUTPUT_VOLUME_PERCENT controla la ganancia digital. El parlante incluido es chico: si subes demasiado, distorsiona antes de sonar más fuerte. Si necesitas más volumen, cambia el parlante por uno más grande (el amplificador entrega hasta 2 W en 4 Ω) en vez de subir el porcentaje.

Sobre los certificados

El cliente HTTPS del proyecto no valida el certificado del servidor. La conexión va cifrada igual, pero en teoría alguien en tu misma red podría hacerse pasar por el servidor y capturar la clave. Para mejorarlo, carga el certificado raíz de la API en el firmware y actívale la validación al cliente TLS.

Compilar, subir y probar

  1. Instala VS Code y la extensión PlatformIO IDE. La primera compilación descarga varios cientos de MB (plataforma ESP32 y framework Arduino): paciencia.
  2. Clona el repositorio y ábrelo en VS Code.
  3. Crea include/credentials.h con tus datos.
  4. Conecta la placa por USB-C (cable de datos), elige el puerto y sube el firmware.
  5. Abre el monitor serie a 115200. Al arrancar deberías ver la conexión a la red y el audio listo. Durante los turnos el registro queda callado salvo que algo falle: solo se anotan los errores, para no llenar el monitor.

Un turno completo suele tardar unos segundos, y casi todo ese tiempo es red: subir el audio, esperar la respuesta y bajar la voz.

Errores comunes

  • Error al arrancar por memoria. La PSRAM no está habilitada: revisa qio_opi en platformio.ini.
  • La voz suena estirada o como robot. El múltiplo del MCLK no calza (la opción I2S_MCLK_MULTIPLE_384) o reemplazaste la copia modificada de audio-tools.
  • "WiFi offline". Red de 5 GHz, contraseña mal escrita o señal débil.
  • "Transcription failed" o "No answer". Clave revocada, cuenta sin saldo o límite alcanzado. El monitor serie muestra el código de error HTTP.
  • No termina de grabar. Ruido de fondo sobre el umbral: sube VAD_THRESHOLD.
  • Se come la primera palabra. Umbral alto o falta de pre buffer (ver arriba).

Variantes y mejoras

  • Llevarlo a un ESP32-S3 genérico. El proyecto está atado a la Elecrow por el codec ES8311, la pantalla ST77922 y sus pines, pero la arquitectura por etapas se porta bien. Con una XIAO ESP32 S3 (también trae 8 MB de PSRAM), un micrófono I2S INMP441 y un amplificador I2S MAX98357A con un parlante de 8 Ω, el cambio se concentra en tres lugares. En src/app_config.h, los pines nuevos. En src/audio_io.*, la configuración: ya no hay codec que configurar por I2C ni MCLK, porque los dos módulos son I2S puros. Pueden compartir reloj de bit y selección de canal, con el micrófono en la entrada de datos y el amplificador en la salida, a la misma frecuencia de 16 kHz. Ojo con que el INMP441 entrega muestras de 24 bits en ranuras de 32, así que hay que bajarlas a 16 bits antes de armar el WAV, y su pin L/R a GND lo deja en el canal izquierdo. Y en src/display_ui.*, reemplazar la pantalla por mensajes en el monitor serie o una OLED chica, con un botón en vez del touch.
  • Apretar para hablar. Usa el botón BOOT como disparador en vez de la pantalla. Es más rápido y evita toques accidentales.
  • Indicador sin mirar. Usa el LED RGB para el estado: azul escuchando, amarillo pensando, verde hablando, rojo error. Sirve cuando la placa queda lejos o de costado.
  • Respuestas con datos tuyos. Pon un sensor de temperatura en el conector I2C (desde la misma tarea que maneja el bus) y agrega la lectura a la instrucción de sistema en cada turno. Así "¿hace frío en el taller?" tiene respuesta real.

Personalización para Chile

En MechatronicStore consigues lo que acompaña a la placa y las piezas para llevarla a un ESP32-S3 genérico:

  • Cable USB Tipo C a USB Tipo A 1mt (SKU B-101): para subir el firmware y ver el monitor serie. Tiene que ser de datos.
  • Batería LiPo 852540 3.7V 600mAh (SKU GE1-11): para usarlo sin cable. Revisa el conector antes de enchufar: la placa usa uno de 1,25 mm de 2 pines y la polaridad tiene que coincidir; si no calza, cambia el conector en vez de forzarlo.
  • Seeed Studio XIAO ESP32 S3 (SKU GS1-3): la base de la versión genérica, con 8 MB de PSRAM para los buffers de audio y WiFi.
  • Módulo Micrófono I2S INMP441 (SKU GL3-12): el micrófono digital de la versión genérica.
  • Parlante altavoz 8 Ohm 1W 50mm (SKU G-107): un parlante algo más grande que el incluido, para la versión genérica o para reemplazar el de la Elecrow.

La placa Elecrow de 3,5" no está en el catálogo, y el amplificador I2S MAX98357A de la versión genérica tampoco. Si quieres una pantalla táctil con ESP32 del catálogo para otros proyectos, está la ESP32 WROOM con Pantalla LCD Táctil 2.8" LVGL (SKU D-517), pero ojo: es un ESP32 clásico sin la PSRAM que este asistente necesita, así que no sirve para este proyecto tal cual.

Recursos

Versión chilena basada en el tutorial de Makerguides, con componentes en stock local.