Walkie-talkie ESP32 con ESP-NOW
6 días hace adminDiseño completo basado en ESP-NOW (ideal por su bajo consumo y no depender de WiFi) con filtro de voz, batería de litio y visualización OLED.
Visión general del proyecto
Walkie-talkie ESP32 con ESP-NOW
| Característica | Descripción |
|---|---|
| Comunicación | ESP-NOW (protocolo peer-to-peer de Espressif) |
| Alcance típico | 50-100 metros (línea de vista) |
| Filtro de voz | Pasa banda 300Hz – 3.4kHz (mejora inteligibilidad) |
| Alimentación | Batería Li-Po 3.7V con TP4056 |
| Visualización | OLED 128×64 (niveles de voz y estado) |
| Control | Pulsador PTT (Push-to-Talk) y botón de encendido |
Componentes necesarios (por unidad)
| Componente | Modelo recomendado | Cantidad |
|---|---|---|
| Microcontrolador | ESP32 (NodeMCU-32S o similar) | 1 |
| Micrófono | INMP441 (I2S MEMS) | 1 |
| Amplificador | MAX98357A (I2S 3W class D) | 1 |
| Altavoz | 4Ω 3W (o 8Ω 2W) | 1 |
| Pantalla OLED | 0.96″ 128×64 I2C | 1 |
| Módulo de carga | TP4056 con protección | 1 |
| Batería | Li-Po 3.7V 2000mAh | 1 |
| Regulador | MT3608 (step-up a 5V) | 1 |
| Pulsadores | 2x táctiles (PTT + encendido) | 2 |
| PCB o protoboard | – | – |
Diagrama de conexiones
Conexiones principales
ESP32 INMP441 (MIC) ------ -------- 3.3V ────────────────► VDD GND ────────────────► GND GND ────────────────► L/R (canal izquierdo) GPIO32 ◄──────────────── DOUT GPIO26 ────────────────► BCLK GPIO25 ────────────────► WS ESP32 MAX98357 (AMP) ------ -------- 5V ────────────────► VIN GND ────────────────► GND GPIO26 ────────────────► BCLK (compartido) GPIO25 ────────────────► LRC (compartido) GPIO33 ────────────────► DIN GND ────────────────► GAIN (12dB) ESP32 OLED 0.96" ------ -------- 3.3V ────────────────► VCC GND ────────────────► GND GPIO22 ────────────────► SCL GPIO21 ────────────────► SDA ESP32 Controles ------ -------- GPIO34 ────────────────► Botón PTT (con pull-up) GPIO35 ────────────────► Botón Encendido
Sistema de alimentación con batería Li-Po
Batería Li-Po 3.7V
│
▼
┌─────────┐
│ TP4056 │───► Salida 4.2V
│ Cargador│
└─────────┘
│
├───────────────────► ESP32 (alimentación directa si soporta 4.2V)
│
▼
┌─────────┐
│ MT3608 │───► 5V ────► MAX98357
│ Step-Up │
└─────────┘
Nota importante: El MAX98357 funciona mejor a 5V para máxima potencia. El ESP32 puede alimentarse directamente desde la batería Li-Po (3.7V – 4.2V) si tu placa tiene regulador interno.
Diseño de PCB sugerido (puedes fabricarlo en JLCPCB o similar)
El proyecto de atomic14 que encontré en los resultados de búsqueda muestra un diseño compacto ideal:
┌─────────────────────────────────────┐ │ │ │ ┌─────────┐ ┌──────────────┐ │ │ │ OLED │ │ Antena │ │ │ │ 0.96" │ │ │ │ │ └─────────┘ └──────────────┘ │ │ │ │ ┌─────────┐ ┌──────────────┐ │ │ │ ESP32 │ │ MAX98357 │ │ │ │ │ │ Amplificador│ │ │ └─────────┘ └──────────────┘ │ │ │ │ ┌─────────┐ ┌──────────────┐ │ │ │ INMP441 │ │ TP4056 │ │ │ │ Micrófono │ Cargador │ │ │ └─────────┘ └──────────────┘ │ │ │ │ [Botón PTT] [Botón Encendido] │ └─────────────────────────────────────┘
Código completo para walkie-talkie ESP-NOW
#include <Arduino.h> #include <driver/i2s.h> #include <esp_now.h> #include <WiFi.h> #include <Adafruit_SSD1306.h> #include <Adafruit_GFX.h> // ============================================ // CONFIGURACIÓN DE PINES // ============================================ // Micrófono INMP441 #define I2S_MIC_WS 25 #define I2S_MIC_SD 32 #define I2S_BCK 26 // Amplificador MAX98357 #define I2S_AMP_DIN 33 // Botones #define PTT_BUTTON 34 #define POWER_BUTTON 35 // OLED #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // ============================================ // CONFIGURACIÓN DE AUDIO // ============================================ #define SAMPLE_RATE 16000 // 16kHz es ideal para voz #define AUDIO_BUFFER_SIZE 256 // Muestras por paquete #define I2S_PORT_MIC I2S_NUM_0 #define I2S_PORT_AMP I2S_NUM_1 // Buffers int16_t micBuffer[AUDIO_BUFFER_SIZE]; int16_t ampBuffer[AUDIO_BUFFER_SIZE]; // ============================================ // CONFIGURACIÓN ESP-NOW // ============================================ // Dirección MAC del otro walkie-talkie // Reemplazar con la MAC de tu otro dispositivo uint8_t peerMac[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Estructura de datos para enviar typedef struct { uint16_t sampleCount; int16_t audioData[AUDIO_BUFFER_SIZE]; uint32_t timestamp; } audioPacket_t; audioPacket_t txPacket; audioPacket_t rxPacket; // ============================================ // FILTRO PASA BANDA PARA VOZ (300Hz - 3.4kHz) // Coeficientes generados con MATLAB // ============================================ #define FILTER_ORDER 32 float fir_coeff[FILTER_ORDER + 1] = { // Coeficientes para filtro FIR pasa banda 300Hz-3400Hz @ 16kHz // GENERAR CON MATLAB Y PEGAR AQUÍ // (Por espacio, se muestran valores de ejemplo) -0.0012, -0.0025, 0.0018, 0.0082, 0.0125, 0.0089, -0.0035, -0.0182, -0.0245, -0.0152, 0.0085, 0.0352, 0.0485, 0.0382, 0.0085, -0.0285, -0.0525, -0.0482, -0.0125, 0.0352, 0.0725, 0.0785, 0.0482, -0.0085, -0.0685, -0.1025, -0.0925, -0.0385, 0.0352, 0.0985, 0.1250, 0.0985, 0.0450 }; float filterBuffer[FILTER_ORDER + 1] = {0}; int filterIndex = 0; // ============================================ // VARIABLES DE ESTADO // ============================================ bool pttPressed = false; bool isTransmitting = false; bool isReceiving = false; bool powerOn = true; unsigned long lastPacketTime = 0; unsigned long displayUpdateTime = 0; float audioLevel = 0; // ============================================ // CONFIGURACIÓN I2S // ============================================ void setupI2S() { // Configuración para micrófono (entrada) const i2s_config_t i2s_config_in = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = 128, .use_apll = false }; // Configuración para amplificador (salida) const i2s_config_t i2s_config_out = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = 128, .use_apll = false, .tx_desc_auto_clear = true }; // Pines para micrófono const i2s_pin_config_t pin_config_in = { .bck_io_num = I2S_BCK, .ws_io_num = I2S_MIC_WS, .data_out_num = -1, .data_in_num = I2S_MIC_SD }; // Pines para amplificador (comparten BCK y WS) const i2s_pin_config_t pin_config_out = { .bck_io_num = I2S_BCK, .ws_io_num = I2S_MIC_WS, .data_out_num = I2S_AMP_DIN, .data_in_num = -1 }; i2s_driver_install(I2S_PORT_MIC, &i2s_config_in, 0, NULL); i2s_driver_install(I2S_PORT_AMP, &i2s_config_out, 0, NULL); i2s_set_pin(I2S_PORT_MIC, &pin_config_in); i2s_set_pin(I2S_PORT_AMP, &pin_config_out); } // ============================================ // FILTRO FIR PARA VOZ // ============================================ int16_t applyVoiceFilter(int16_t input) { // Normalizar entrada float input_f = (float)input / 32768.0f; // Buffer circular filterIndex = (filterIndex + 1) % (FILTER_ORDER + 1); filterBuffer[filterIndex] = input_f; // Convolución float output_f = 0; int idx = filterIndex; for (int i = 0; i <= FILTER_ORDER; i++) { output_f += fir_coeff[i] * filterBuffer[idx]; idx = (idx - 1 + FILTER_ORDER + 1) % (FILTER_ORDER + 1); } // Desnormalizar y limitar int32_t output = (int32_t)(output_f * 32768.0f); if (output > 32767) output = 32767; if (output < -32768) output = -32768; return (int16_t)output; } // ============================================ // CALLBACKS ESP-NOW // ============================================ void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { Serial.print("Envío: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Éxito" : "Fallo"); } void onDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) { memcpy(&rxPacket, incomingData, sizeof(rxPacket)); isReceiving = true; lastPacketTime = millis(); // Reproducir audio recibido size_t bytesWritten; i2s_write(I2S_PORT_AMP, rxPacket.audioData, rxPacket.sampleCount * sizeof(int16_t), &bytesWritten, portMAX_DELAY); } // ============================================ // CONFIGURACIÓN ESP-NOW // ============================================ void setupESPNOW() { WiFi.mode(WIFI_STA); WiFi.disconnect(); if (esp_now_init() != ESP_OK) { Serial.println("Error iniciando ESP-NOW"); return; } esp_now_register_send_cb(onDataSent); esp_now_register_recv_cb(onDataRecv); esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, peerMac, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Error añadiendo peer"); return; } } // ============================================ // ACTUALIZACIÓN DE PANTALLA OLED // ============================================ void updateDisplay() { display.clearDisplay(); // Título display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.print("WALKIE-TALKIE"); // Estado display.setCursor(0, 10); if (pttPressed) { display.print("> TRANSMITIENDO"); } else if (isReceiving && (millis() - lastPacketTime < 1000)) { display.print("> RECIBIENDO"); } else { display.print("> EN ESPERA"); } // Nivel de audio (barra simple) display.setCursor(0, 25); display.print("Nivel:"); int barLength = map(audioLevel, 0, 3000, 0, 80); if (barLength > 80) barLength = 80; display.fillRect(40, 25, barLength, 6, SSD1306_WHITE); display.drawRect(40, 25, 80, 6, SSD1306_WHITE); // Indicador de batería (simulado) display.setCursor(0, 40); display.print("Bateria:"); display.fillRect(50, 40, 40, 6, SSD1306_WHITE); display.drawRect(50, 40, 40, 6, SSD1306_WHITE); // Dirección MAC (útil para debug) display.setCursor(0, 55); display.print("MAC: "); display.print(peerMac[0], HEX); display.print(":"); display.print(peerMac[5], HEX); display.display(); } // ============================================ // SETUP // ============================================ void setup() { Serial.begin(115200); // Configurar pines de botones pinMode(PTT_BUTTON, INPUT_PULLUP); pinMode(POWER_BUTTON, INPUT_PULLUP); // Inicializar I2S setupI2S(); Serial.println("✅ I2S listo"); // Inicializar OLED if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("❌ Error OLED"); } else { display.clearDisplay(); display.display(); Serial.println("✅ OLED listo"); } // Inicializar ESP-NOW setupESPNOW(); Serial.println("✅ ESP-NOW listo"); // Mensaje de inicio en OLED display.clearDisplay(); display.setCursor(0, 0); display.println("Walkie-Talkie"); display.println("Iniciando..."); display.println("MAC: " + WiFi.macAddress()); display.display(); delay(2000); } // ============================================ // LOOP PRINCIPAL // ============================================ void loop() { // Leer botones bool currentPTT = !digitalRead(PTT_BUTTON); // Pull-up, LOW = presionado // Control de encendido if (!digitalRead(POWER_BUTTON)) { // Botón de encendido presionado - entrar en deep sleep display.clearDisplay(); display.setCursor(20, 20); display.println("Apagando..."); display.display(); delay(1000); esp_deep_sleep_start(); } // Actualizar pantalla cada 100ms if (millis() - displayUpdateTime > 100) { updateDisplay(); displayUpdateTime = millis(); } if (currentPTT && !pttPressed) { // PTT recién presionado - comenzar transmisión pttPressed = true; isTransmitting = true; Serial.println("PTT ON - Transmitiendo"); } else if (!currentPTT && pttPressed) { // PTT liberado - detener transmisión pttPressed = false; isTransmitting = false; Serial.println("PTT OFF"); } if (isTransmitting) { // Modo transmisión - leer micrófono y enviar size_t bytesRead; // Leer del micrófono i2s_read(I2S_PORT_MIC, &micBuffer, sizeof(micBuffer), &bytesRead, portMAX_DELAY); int samplesRead = bytesRead / sizeof(int16_t); // Calcular nivel de audio para visualización audioLevel = 0; for (int i = 0; i < samplesRead; i++) { int16_t absVal = abs(micBuffer[i]); if (absVal > audioLevel) audioLevel = absVal; } // Aplicar filtro pasa banda a las muestras for (int i = 0; i < samplesRead; i++) { micBuffer[i] = applyVoiceFilter(micBuffer[i]); } // Preparar paquete txPacket.sampleCount = samplesRead; txPacket.timestamp = millis(); memcpy(txPacket.audioData, micBuffer, samplesRead * sizeof(int16_t)); // Enviar por ESP-NOW esp_now_send(peerMac, (uint8_t *)&txPacket, sizeof(txPacket)); // Pequeña pausa para no saturar delay(5); } // Limpiar estado de recepción si no hay paquetes por 2 segundos if (isReceiving && (millis() - lastPacketTime > 2000)) { isReceiving = false; } }
Gestión de batería y consumo
Consumo estimado
| Modo | Corriente | Autonomía con 2000mAh |
|---|---|---|
| Deep Sleep | ~10µA | > 20 años (teórico) |
| Espera | ~80mA | 25 horas |
| Transmitiendo | ~250mA | 8 horas |
| Recibiendo | ~150mA | 13 horas |
Circuito de carga TP4056
El módulo TP4056 es ideal para este proyecto:
Batería Li-Po ──┬──► TP4056 BAT+
└──► ESP32 (alimentación directa)
USB 5V ──────────► TP4056 IN+
Mejoras opcionales
Basado en los resultados de búsqueda, puedes considerar:
-
Codec2 para compresión de audio: Reduce drásticamente el ancho de banda necesario, permitiendo comunicaciones de mayor alcance
-
Squelch ajustable: Silencia el altavoz cuando no hay señal
-
Selección de canales: Múltiples frecuencias virtuales
-
Indicador de batería real: Usar pin ADC del ESP32 para medir voltaje
-
Mesh networking: Para comunicación entre múltiples dispositivos
Referencias y proyectos existentes
El proyecto de atomic14 en GitHub (https://github.com/atomic14/esp32-walkie-talkie) es una excelente referencia y base para este desarrollo. También hay implementaciones exitosas usando M5Stack y otros hardware.
Consideraciones importantes
-
Pruebas de alcance: ESP-NOW funciona mejor en espacio abierto; las paredes reducen significativamente el alcance
-
Latencia: Habrá ~0.5 segundos de retraso debido al buffering
-
Alimentación: El MAX98357 a 5V da mucho más volumen que a 3.3V
