Notifications
Clear all

ESP32 + nRF24LO1 BASED walkie talkie PROJECT not working yet!

13 Posts
3 Users
0 Reactions
461 Views
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  

I am building a walkie-talkie using ESP32-WROOM-32D, INMP441, nRF24LO1+LA+PNA, MAX9857A, Speaker, batteries...

this is my prototype circuit(still on breadbroad) wiring connections for the two cct.
── Wiring ───
* nRF24: CE=4 CSN=5 SCK=18 MOSI=23 MISO=19 (+100µF cap VCC-GND)
* INMP441: WS=15 SCK=14 SD=32 L/R=GND
* MAX98357A: BCLK=25 LRC=26 DIN=22 SD_MODE=33
* PTT=GPIO27→GND LED=GPIO2

my current problem is nRF24 audio packet handling, I am getting a lot of packet losses, I don't why the packet is getting lost, I have also tried using ESP-NOW the code worked fine but the range was not satisfactory, hence the need of trying nRF24LO1 transceiver module for better range capability.

I tried to use the AudioTools library by Phil Schatzmann(@pschatzmann), and it is still giving the packet loss on the receiver cct.

this is the flow in mind:
PTT released → RECEIVE: nRF24 → ring buffer → I2S → MAX98357A → Speaker
PTT pressed → TRANSMIT: INMP441 → I2S → nRF24

but my current result is a continuous-pausing whoshing sound, more like (shi -500ms silince- shi -500ms silince- shi -500ms silince- shi) - I hope you understand

WHAT COULD I BE MISSING OR NOT DOING RIGHT FOR THIS TO WORK?

I have attached my current code for it!

#include <Arduino.h>
#include <SPI.h>
#include <RF24.h>
#include <AudioTools.h>
#include <rom/gpio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>

#define PIN_PTT       27
#define PIN_LED       2
#define PIN_AMP_SD    33
#define PIN_RF_CE     4
#define PIN_RF_CSN    5
#define PIN_MIC_SCK   14
#define PIN_MIC_WS    15
#define PIN_MIC_SD    32
#define PIN_SPK_BCLK  25
#define PIN_SPK_LRC   26
#define PIN_SPK_DIN   22

#define SAMPLE_RATE      8000
#define PACKET_SAMPLES   32
#define PACKET_BYTES     64
#define TX_QUEUE_DEPTH   30        // mic packets buffered for radio
#define RING_BYTES       16000
#define RF_CHANNEL       76

const uint8_t RF_ADDR[6] = "WKTLK";

AudioInfo   audioFmt(SAMPLE_RATE, 1, 16);   // 8kHz, mono, 16-bit

I2SStream   spkStream;   // MAX98357A output
I2SStream   micStream;   // INMP441 input

RingBuffer<uint8_t> rxRing(RING_BYTES);


//  TX AUDIO QUEUE - micTask → txQueue → radioTask → nRF24

typedef struct { int16_t samples[PACKET_SAMPLES]; } TxPacket;
static QueueHandle_t txQueue = NULL;


//  RF
RF24 radio(PIN_RF_CE, PIN_RF_CSN);
static volatile bool pttPressed  = false;
static volatile bool spkReady    = false;

void ampOn()  { digitalWrite(PIN_AMP_SD, HIGH); }
void ampOff() { digitalWrite(PIN_AMP_SD, LOW);  }


//  INIT SPEAKER (MAX98357A on I2S_NUM_1)
bool initSpeaker() {
    auto cfg      = spkStream.defaultConfig(TX_MODE);
    cfg.copyFrom(audioFmt);
    cfg.port_no   = 1;
    cfg.pin_bck   = PIN_SPK_BCLK;
    cfg.pin_ws    = PIN_SPK_LRC;
    cfg.pin_data  = PIN_SPK_DIN;
    cfg.pin_mck   = -1;
    if (!spkStream.begin(cfg)) {
        Serial.println("[SPK] FAIL"); return false;
    }
    Serial.println("[SPK] OK");
    return true;
}


//  INIT MIC (INMP441 on I2S_NUM_0)
bool initMic() {
    gpio_matrix_out(0, 0x100, false, false);   // prevent MCLK on GPIO0

    AudioInfo mic32fmt(SAMPLE_RATE, 1, 32);

    auto cfg          = micStream.defaultConfig(RX_MODE);
    cfg.copyFrom(mic32fmt);
    cfg.port_no       = 0;
    cfg.pin_bck       = PIN_MIC_SCK;
    cfg.pin_ws        = PIN_MIC_WS;
    cfg.pin_data      = PIN_MIC_SD;
    cfg.pin_mck       = -1;
    cfg.channels      = 1;

    bool ok = micStream.begin(cfg);
    gpio_matrix_out(0, 0x100, false, false);   // re-disconnect GPIO0 after begin

    if (!ok) { Serial.println("[MIC] FAIL"); return false; }
    Serial.println("[MIC] OK");
    return true;
}

//  INIT RADIO
bool initRadio() {
    if (!radio.begin()) { Serial.println("[RF] FAIL"); return false; }
    radio.setChannel(RF_CHANNEL);
    radio.setDataRate(RF24_1MBPS);
    radio.setPALevel(RF24_PA_MIN);
    radio.setPayloadSize(PACKET_BYTES);
    radio.setAutoAck(false);
    radio.setCRCLength(RF24_CRC_16);
    radio.setRetries(0, 0); // MODIFIED;
    radio.openWritingPipe(RF_ADDR);
    radio.openReadingPipe(1, RF_ADDR);
    radio.startListening();
    Serial.printf("[RF] OK ch%d\n", RF_CHANNEL);
    return true;
}


//  TASK 1 — MIC CAPTURE  (Core 1)
void micTask(void* param) {
    Serial.println("[TASK] micTask Core 1");

    // Each 32-bit frame = 4 bytes from AudioTools
    // We read PACKET_SAMPLES frames at a time = PACKET_SAMPLES * 4 bytes
    const int  FRAME_BYTES = PACKET_SAMPLES * 4;
    uint8_t    rawBuf[FRAME_BYTES];
    TxPacket   pkt;
    uint8_t    drainBuf[64];

    uint32_t t = millis();
    while (millis() - t < 100) {
        micStream.readBytes(drainBuf, sizeof(drainBuf));
    }

    while (true) {
        if (!pttPressed) {
            // Drain mic DMA continuously when not transmitting
            micStream.readBytes(drainBuf, sizeof(drainBuf));
            vTaskDelay(pdMS_TO_TICKS(1));
            continue;
        }

        // Read exactly PACKET_SAMPLES 32-bit frames
        size_t got = micStream.readBytes(rawBuf, FRAME_BYTES);
        if (got < (size_t)FRAME_BYTES) {
            vTaskDelay(pdMS_TO_TICKS(1));
            continue;
        }

        // Convert 32-bit INMP441 frames → 16-bit signed PCM
        // INMP441: 24-bit signed audio left-justified in 32-bit word
        // bits[31:8] = audio data, bits[7:0] = 0
        // Reinterpret 4 bytes as int32_t, shift right 8 → int16_t
        for (int i = 0; i < PACKET_SAMPLES; i++) {
            int32_t raw;
            memcpy(&raw, &rawBuf[i * 4], 4);
            pkt.samples[i] = (int16_t)(raw >> 8); 
        }

        // Push to queue — drop oldest if full to keep audio fresh
        if (xQueueSend(txQueue, &pkt, 0) != pdTRUE) {
            TxPacket discard;
            xQueueReceive(txQueue, &discard, 0);
            xQueueSend(txQueue, &pkt, 0);
        }
    }
}


//  TASK 2 — SPEAKER PLAYBACK  (Core 1)
void speakerTask(void* param) {
    Serial.println("[TASK] speakerTask Core 1");

    const int CHUNK          = 256;    // bytes per write call
    const int START_THRESHOLD = 2000;  // bytes — ~125ms at 8kHz 16-bit mono
    uint8_t   buf[CHUNK];
    bool      buffering = true;

    // Pre-fill I2S DMA with silence before enabling amp
    memset(buf, 0, sizeof(buf));
    for (int i = 0; i < 16; i++) spkStream.write(buf, sizeof(buf));
    ampOn();
    spkReady = true;
    Serial.println("[SPK] Ready");

    uint32_t lastLog = 0;

    while (true) {

        // During TX — output silence, reset buffering state
        if (pttPressed) {
            memset(buf, 0, sizeof(buf));
            spkStream.write(buf, sizeof(buf));
            buffering = true;
            continue;
        }

        int avail = rxRing.available();

        // Wait for enough data before starting playback
        if (buffering) {
            if (avail >= START_THRESHOLD) {
                buffering = false;
                Serial.printf("[SPK] Start — ring=%d\n", avail);
            } else {
                memset(buf, 0, sizeof(buf));
                spkStream.write(buf, sizeof(buf));
                continue;
            }
        }

        // Ring ran dry — go back to buffering
        if (avail == 0) {
            buffering = true;
            memset(buf, 0, sizeof(buf));
            spkStream.write(buf, sizeof(buf));
            continue;
        }

        // Read from ring into output buffer
        int toRead = (avail >= CHUNK) ? CHUNK : avail;
        memset(buf, 0, sizeof(buf));
        for (int i = 0; i < toRead; i++) {
            uint8_t b;
            rxRing.read(b);
            buf[i] = b;
        }
        spkStream.write(buf, sizeof(buf));

        if (millis() - lastLog > 3000) {
            Serial.printf("[SPK] ring=%d\n", rxRing.available());
            lastLog = millis();
        }
    }
}


//  TASK 3 — RADIO MANAGER  (Core 0)
void radioTask(void* param) {
    Serial.println("[TASK] radioTask Core 0");

    TxPacket pkt;
    uint8_t  rxBuf[PACKET_BYTES];
    bool     lastPtt  = false;
    uint32_t txCount  = 0;
    uint32_t rxCount  = 0;
    uint32_t rxMissed = 0;

    while (true) {
        bool ptt = pttPressed;

        // ── Mode transition ───
        if (ptt != lastPtt) {
            if (ptt) {
                // Switch to TX
                radio.stopListening();
                xQueueReset(txQueue);
                Serial.println("[RF] → TX");
            } else {
                { uint8_t b; while (rxRing.available()) rxRing.read(b); }
                radio.startListening();
                Serial.println("[RF] → RX");
            }
            lastPtt = ptt;
        }

        // ── TX PATH ──
        if (ptt) {
            // Block up to 5ms for a mic packet
            if (xQueueReceive(txQueue, &pkt, pdMS_TO_TICKS(5)) == pdTRUE) {
                bool ok = radio.write(pkt.samples, PACKET_BYTES);
                txCount++;
                if (txCount % 500 == 0) {
                    Serial.printf("[TX] %lu pkts ok=%d\n", txCount, ok);
                }
            }
        }

        // ── RX PATH ──
        else {
            if (spkReady && radio.available()) {
                radio.read(rxBuf, PACKET_BYTES);
                rxCount++;

                if (rxCount % 500 == 0) {
                    Serial.printf("[RX] %lu pkts, ring=%d, missed=%lu\n", 
                                  rxCount, rxRing.available(), rxMissed);
                }

                // Push to ring — drop oldest byte if full
                for (int i = 0; i < PACKET_BYTES; i++) {
                    if (rxRing.availableForWrite() > 0) {
                        rxRing.write(rxBuf[i]);
                    } else {
                        uint8_t b; rxRing.read(b);   // drop oldest
                        rxRing.write(rxBuf[i]);
                    }
                }
            } else {
                if (spkReady) {
                    rxMissed++;
                    if (rxMissed % 10000 == 0) {
                        Serial.printf("[RX] Missed checks: %lu\n", rxMissed);
                    }
                }
            }
            vTaskDelay(pdMS_TO_TICKS(1));
        }
    }
}


void setup() {
    Serial.begin(115200);
    delay(2000);
    WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
    Serial.println("══════════════════════════════════");
    Serial.println(" ESP32 Walkie-Talkie — AudioTools v2");
    Serial.println("══════════════════════════════════");

    pinMode(PIN_LED,    OUTPUT);
    pinMode(PIN_AMP_SD, OUTPUT);
    pinMode(PIN_PTT,    INPUT_PULLUP);
    digitalWrite(PIN_LED, LOW);
    ampOff();

    if (!initSpeaker()) { while(true){} }
    if (!initMic())     { while(true){} }
    if (!initRadio())   { while(true){} }

    txQueue = xQueueCreate(TX_QUEUE_DEPTH, sizeof(TxPacket));
    if (!txQueue) { Serial.println("[BOOT] Queue FAIL"); while(true){} }

    // speakerTask and micTask share Core 1 (time-sliced)
    // radioTask owns Core 0 exclusively — sole radio owner
    xTaskCreatePinnedToCore(speakerTask, "spk",   8192, NULL, 3, NULL, 1);
    xTaskCreatePinnedToCore(micTask,     "mic",   8192, NULL, 3, NULL, 1);
    xTaskCreatePinnedToCore(radioTask,   "radio", 8192, NULL, 4, NULL, 0);

    Serial.println("[BOOT] Ready — hold PTT to talk, release to listen");
}

//  LOOP — PTT debounce + LED
void loop() {
    static bool     lastRaw   = HIGH;
    static uint32_t lastMs    = 0;
    static bool     debounced = false;

    bool raw = digitalRead(PIN_PTT);
    if (raw != lastRaw) { lastMs = millis(); lastRaw = raw; }

    if (millis() - lastMs > 25) {
        bool pressed = (raw == LOW);
        if (pressed != debounced) {
            debounced  = pressed;
            pttPressed = pressed;
            digitalWrite(PIN_LED, pressed ? HIGH : LOW);
            Serial.printf("[PTT] %s\n", pressed ? "TX" : "RX");
        }
    }

    vTaskDelay(pdMS_TO_TICKS(10));
}


   
Quote
noweare
(@noweare)
Member
Joined: 6 years ago
Posts: 216
 

I think i remember the nrf240l has had power problems in the past with people having to solder on capacitors to keep the voltage stable. I would look at that first. There are adapters the you plug then board into to solve the power problem also.



   
ReplyQuote
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  

@noweare I am still using breadboard, so I just connected the capacitor across the VCC and the GND, 

So what do you think about the code?



   
ReplyQuote
noweare
(@noweare)
Member
Joined: 6 years ago
Posts: 216
 

I really don't know about the software. I normally use the idf not arduino so I am not familiar with the code base.  But I would break the code down to sections if you need to troubleshoot it.  Get one part working then the next part. I like that you are using freeRtos and queues. Did you write the code or did you get it from someplace ?



   
ReplyQuote
(@lucky_pos)
Member
Joined: 1 month ago
Posts: 7
 

I plugged your code into who I call gemi , a version of a jailbreak gemini,       ------>

Correction: Actually, the INMP441 outputs data in the high 24 bits of the 32-bit word. If you shift right by 8, you are leaving 16 bits of audio. However, standard I2S for this mic is often "Left Justified." If your audio sounds extremely quiet or like static, try shifting by 14 or 16, or masking the sign bit properly.

 

​3. The "Buffering" Strategy

​The START_THRESHOLD (2000 bytes) in your speakerTask is a smart move. It prevents "motorboating" (rapid clicking) caused by the speaker playing faster than the radio can deliver packets.

​⚠️ Potential Issues & Suggestions

​1. The "Ring Buffer" Race Condition

​You are using RingBuffer<uint8_t> rxRing(RING_BYTES);.

  • The Risk: If this is a standard non-thread-safe buffer, you might experience memory corruption or crashes because radioTask (Core 0) is writing while speakerTask (Core 1) is reading.
  • The Fix: Use a FreeRTOS Stream Buffer (xStreamBufferCreate) instead. It is designed exactly for this: one producer, one consumer, across different cores, with built-in thread safety.


   
ReplyQuote
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  

@noweare I used Claude AI to get the code. I haven't seen any tutorial that used nRF24 for audio transmision, so I tried using Claude to come up with a code that could work. 

 

As for the power issue could it be because the capacitor isn't soldered to the nRF24 module VCC and GND Pins?

Although on the breadboard it is connected. 



   
ReplyQuote
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  

@lucky_pos Thanks for your response, I have tried changing the shifting to 10 and 16, it was still outputting the same pulsating hissing sounds... 

I will try to use the FreeRTOS Stream Buffer to see if it works. 



   
ReplyQuote
noweare
(@noweare)
Member
Joined: 6 years ago
Posts: 216
 

@maximuz4  When you were working with esp now did you use an external antenna ? I remember my frame rate was terrible using the internal antenna when playing around with esp32 camera.  It made a huge difference changing to an external antenna. I think adding the 24nrf just adds another layer of complexity to be honest but I think it would be fun to be able to get that working. As long a the cap is physicall close to the NRF24L01 VCC an ground it should help.



   
ReplyQuote
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  
@noweare No I didn't add an external antenna to the esp32, although the range of the walkie talkie using esp-now wasn't that great till I started getting audio packet losses. But it was a huge relief seeing that the mic, esp32, and speaker wasn't the main culprit. 🙂

 

But I don't know why the nrf24 is not transmitting and receiving the audio packet properly. 

Thank you @noweare for your response. 

I really wish I could get this nRf24 transceiver module to work 😣 


This post was modified 4 weeks ago by maximuz4

   
ReplyQuote
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  

Also today I got to learn that ESP32 boards of same spec are not the same in how they work, 

Using the ESP-NOW and wi-fi take a lot of current and while one of my board worked fine, the other board kept crashing at the point of wi-fi initialisation "brownout  detector was triggered " error. I had to add a 470uF capacitor to the 3.3v and GND of that board, and had to ignore the brownout error from the code for it to stabilise and run. 



   
ReplyQuote
noweare
(@noweare)
Member
Joined: 6 years ago
Posts: 216
 

I would maybe just try sending some small text file over to the other esp32 via nrf24l01 and see if that works. You kind of want to isolate the nrf24L  and just get that working. I am not too familiar with the radio, though i do have some modules but have not used them. 



   
ReplyQuote
(@maximuz4)
Member
Joined: 1 month ago
Posts: 11
Topic starter  

@noweare Thanks a lot for your response ❤️ . I was able to blink the LED on the other ESP32 board using the nRF24L01 module, but I couldn’t successfully transmit audio packets yet. I didn't try transmitting text file though.(Didn't see your res on time)

To meet the project submission deadline, I decided to use the ESP-NOW protocol with Wi-Fi for the walkie-talkie project instead.

I still hope to try the nRF24L01 setup again later when I get a new set of ESP32 boards.

It would also be great if someone could successfully implement the nRF24L01 + ESP32 audio transmission project and share a working video on YouTube.



   
ReplyQuote
noweare
(@noweare)
Member
Joined: 6 years ago
Posts: 216
 

I'm glad you got it working. It would be a good excercise to see if you could get it working with an 24L01. The packet size on the 24L01 is only 32 bytes so thats only two 16 bit audio samples. I think there will be a lot of overhead and you will be interrupting  the processor quite frequently to send packets on one side and recieve packets on the other. 



   
ReplyQuote