Diseñ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

text
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

text
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:

text
┌─────────────────────────────────────┐
│                                     │
│  ┌─────────┐      ┌──────────────┐ │
│  │  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

cpp
#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:

text
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:

  1. Codec2 para compresión de audio: Reduce drásticamente el ancho de banda necesario, permitiendo comunicaciones de mayor alcance

  2. Squelch ajustable: Silencia el altavoz cuando no hay señal

  3. Selección de canales: Múltiples frecuencias virtuales

  4. Indicador de batería real: Usar pin ADC del ESP32 para medir voltaje

  5. 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

  1. Pruebas de alcance: ESP-NOW funciona mejor en espacio abierto; las paredes reducen significativamente el alcance

  2. Latencia: Habrá ~0.5 segundos de retraso debido al buffering

  3. Alimentación: El MAX98357 a 5V da mucho más volumen que a 3.3V

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *