Notifications
Clear all

Need help with sending audio over nRF24

4 Posts
2 Users
0 Reactions
104 Views
JimG
 JimG
(@jimgilliland)
Member
Joined: 1 year ago
Posts: 53
Topic starter  

A couple of years ago, I had an idea that I wanted to try to build a pair of digital walkie-talkies using NRF24 transceivers. I worked on it for a while, but never really had much success. Eventually I set it aside.

This summer I thought I would give it another try. Bill's video on I2S convinced me to try to let the hardware do more of the work, so I decided to use an I2S ADC with a microphone and an I2S DAC with a small amplifier and speaker. The NRF24 uses SPI, so I actually needed two I2S channels plus SPI. I chose an ESP32S3 microprocessor.

It took me some trial and error to find some success, but eventually I was able to get a voice sent from one radio to the other.

Actually, the first thing I did was just to make sure I could send audio from the ADC out to the DAC and have it work. That part turned out to be easy. I would have thought that it might take a bit more to coordinate the data rather than just reading from the mic and sending it to the speaker, but it turned out that there wasn't much more to it.

And then when I separated the input from the output into separate units that were connected by the radios, it still gave me at least OK results. But not perfect. For one thing there is a lot of "static" accompanying the voice. Obviously, it's not really signal noise. I'm assuming that it is due to clocking and synchronization issues, but I'm not at all certain of that.

I could probably live with the "static" - after all this isn't intended to be a device that anyone would actually use, just a fun little proof-of-concept to see if I could actually make it work. But there is a bigger problem. After some period of time, the audio changes from reasonably clear to badly distorted, and it generally stays that way until the device is reset and restarted.

I'm assuming that (maybe) this, too, is a question of getting the data packaged properly (for lack of a better word) for the DAC. But again, I really don't know. So if there is anyone here who knows more about how to process this stream from one end to the other without damaging it, I'd love to hear your thoughts.

The same code runs on both units, and they are identical except for one pin which is pulled low on one and high on the other so that one can be unit 0 and the other unit 1. The code is my usual mix of stolen ideas from many other authors, so don't expect it to be pretty. I've tried to clean out as much of the extraneous stuff as possible, but there may be some unneeded remnants hidden in there somewhere. It's not that long a program anyway, and it's all built using the Arduino IDE.

As you can see, I'm using most of the available pins on the board.  Two I2S channels, SPI for the radio, a Push to talk switch, one to set the radio number, and the two onboard LEDs to show data being transmitted and received.

Components:

E01-ML01DP5 Wireless Transmission Module nRF24L01P+PA+LNA 2.4G Wireless Transceiver Module
Max98357 I2S 3W Audio Amplifier Breakout Interface I2s Dac Decoder Module
INMP441 Omnidirectional Microphone Module MEMS I2S Interface
ESP32 S3 Development Board, ESP32-S3 MCU with 8MB PSRAM, 16MB Flash and 512K SRAM

ESP32S3 board
This topic was modified 1 month ago by JimG

   
Quote
JimG
 JimG
(@jimgilliland)
Member
Joined: 1 year ago
Posts: 53
Topic starter  

I don't know why, but the site is refusing to show any post from me that has a block of code in it.  So I will break with etiquette and post my code as plain text here.  If someone can tell me how to get the code block to work, I will revise it (assuming it's still within the time limit for editing).

#include <SPI.h>
#include "printf.h"
#include "RF24.h"

#define SCK 18
#define MOSI 17
#define MISO 16
#define CE 15
#define CS 14

SPIClass* vspi = new SPIClass();

// SPIClass* spiBus = &vspi;

// instantiate an object for the nRF24L01 transceiver
RF24 radio(CE, CS);

// Let these addresses be used for the pair
uint8_t address[][6] = { "1Node", "2Node" };

bool radioNumber;  // 0 uses address[0] to transmit, 1 uses address[1] to transmit

// Used to control whether this node is sending or receiving
bool role = false;  // true = TX node, false = RX node

#define SIZE 64             // this is the maximum for this example. (minimum is 1)

// Define input buffer length
const uint8_t bufferLen = SIZE/2;

union Buffer {
    char cBuffer[SIZE];      // for the RX node
    int16_t iBuffer[bufferLen];
} buffer;

uint8_t counter = 0;        // for counting the number of received payloads
//void makePayload(uint8_t);  // prototype to construct a payload dynamically

// Include I2S driver
#include <driver/i2s.h>

#define RADIO_SEL_PIN 12

#define PTT 13

// Connections to INMP441 I2S microphone
#define ADC_WS 5
#define ADC_SD 3
#define ADC_SCK 4

// Use I2S Processor 1
#define ADC_PORT I2S_NUM_1

#define DAC_WS 9
#define DAC_SD 7
#define DAC_SCK 8

// Use I2S Processor 0
#define DAC_PORT I2S_NUM_0
void adc_install() {
  // Set up I2S Processor configuration
  const i2s_config_t adc_config = {
    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 11025,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 8,
    .dma_buf_len = bufferLen,
    .use_apll = true
  };

  i2s_driver_install(ADC_PORT, &adc_config, 0, NULL);
}

void adc_setpin() {
  // Set I2S pin configuration
  const i2s_pin_config_t dac_pin_config = {
    .bck_io_num = ADC_SCK,
    .ws_io_num = ADC_WS,
    .data_out_num = -1,
    .data_in_num = ADC_SD
  };

  i2s_set_pin(ADC_PORT, &dac_pin_config);
}

void dac_install() {
  // Set up I2S Processor configuration
  const i2s_config_t dac_config = {
    .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = 11025,
    .bits_per_sample = i2s_bits_per_sample_t(16),
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 8,
    .dma_buf_len = bufferLen,
    .use_apll = true
  };

  i2s_driver_install(DAC_PORT, &dac_config, 0, NULL);
}

void dac_setpin() {
  // Set I2S pin configuration
  const i2s_pin_config_t dac_pin_config = {
    .bck_io_num = DAC_SCK,
    .ws_io_num = DAC_WS,
    .data_out_num = DAC_SD,
    .data_in_num = -1
  };

  i2s_set_pin(DAC_PORT, &dac_pin_config);
}

void setup() {

  Serial.begin(115200);
 
  pinMode(10, OUTPUT);  
  pinMode(11, OUTPUT);

  pinMode(RADIO_SEL_PIN, INPUT_PULLUP);

  pinMode(PTT, INPUT_PULLUP);

  vspi->begin(SCK, MISO, MOSI, CS);
 
    // initialize the transceiver on the SPI bus
  if (!radio.begin(vspi)) {
    Serial.println(F("radio hardware is not responding!!"));
    while (1) {}  // hold in infinite loop
  }

  // print example's introductory prompt
  Serial.println(F("RF24/examples/StreamingData"));

  // To set the radioNumber on startup
 
  radioNumber = digitalRead(RADIO_SEL_PIN);
  Serial.print(F("radioNumber = "));
  Serial.println((int)radioNumber);

  // Set the PA Level low to try preventing power supply related problems
  // because these examples are likely run with nodes in close proximity to
  // each other.
  radio.setPALevel(RF24_PA_LOW);  // RF24_PA_MAX is default.

  // save on transmission time by setting the radio to only transmit the
  // number of bytes we need to transmit
  radio.setPayloadSize(SIZE);  // default value is the maximum 32 bytes

  // set the TX address of the RX node into the TX pipe
  radio.openWritingPipe(address[radioNumber]);  // always uses pipe 0

  // set the RX address of the TX node into a RX pipe
  radio.openReadingPipe(1, address[!radioNumber]);  // using pipe 1

  // additional setup specific to the node's role
  if (role) {
    radio.stopListening();  // put radio in TX mode
  } else {
    radio.startListening();  // put radio in RX mode

    // Set up I2S
    adc_install();
    adc_setpin();
    i2s_start(ADC_PORT);

    dac_install();
    dac_setpin();
    i2s_start(DAC_PORT);

  delay(500);
  }

}  // setup()
void loop() {

  if (role) {
    // device is in TX node

    radio.flush_tx();
    uint8_t i = 0;
    uint8_t failures = 0;
    digitalWrite(11, LOW);
    unsigned long start_timer = micros();  // start the timer
    while (i < SIZE) {
     
      size_t bytesIn = 0;
      esp_err_t TxResult = i2s_read(ADC_PORT, &buffer.iBuffer, bufferLen, &bytesIn, portMAX_DELAY);
      digitalWrite(11, HIGH);
      bitClear(bytesIn,0);
      bitClear(bytesIn,1);
      if (!radio.writeFast(&buffer.cBuffer, bytesIn)) {
        failures++;
        radio.reUseTX();
      } else {
        i++;
      }

      if (failures >= 100) {
        Serial.print(F("Too many failures detected. Aborting at payload "));
        break;
      }
    }
    unsigned long end_timer = micros();  // end the timer

  } else {
    // device is in RX node

      size_t bytesIn = 0;

      if (radio.available()) {      // is there a payload?
        bytesIn = radio.getDynamicPayloadSize();
        radio.read(&buffer.cBuffer, bytesIn);  // fetch payload from FIFO
       
        digitalWrite(10, HIGH);
        esp_err_t RxResult = i2s_write(DAC_PORT, &buffer.iBuffer, bufferLen, &bytesIn, portMAX_DELAY);
        digitalWrite(10, LOW);  
      }
  }  // role

    bool ptt = !digitalRead(PTT);
    if (ptt && !role) {
      // Become the TX node

      role = true;
      counter = 0;  //reset the RX node's counter
      radio.stopListening();
      digitalWrite(10, LOW);
      digitalWrite(11, LOW);

    } else if (!ptt && role) {
      // Become the RX node

      role = false;
      radio.startListening();
      digitalWrite(10, LOW);
      digitalWrite(11, LOW);
    }

}  // loop


   
ReplyQuote
Ron
 Ron
(@zander)
Father of a miniature Wookie
Joined: 4 years ago
Posts: 7590
 

@jimgilliland I just copied your code and tried to paste it. No preview, followed by no post. I think there is a bug Bill @dronebot-workshop

First computer 1959. Retired from my own computer company 2004.
Hardware - Expert in 1401, and 360, fairly knowledge in PC plus numerous MPU's and MCU's
Major Languages - Machine language, 360 Macro Assembler, Intel Assembler, PL/I and PL1, Pascal, Basic, C plus numerous job control and scripting languages.
My personal scorecard is now 1 PC hardware fix (circa 1982), 1 open source fix (at age 82), and 2 zero day bugs in a major OS.


   
ReplyQuote
JimG
 JimG
(@jimgilliland)
Member
Joined: 1 year ago
Posts: 53
Topic starter  

Thanks, Ron.  Is the problem only with my code?  Maybe there is a length limitation?  

Anyway, I had an idea today about what might be wrong.  If I am trying to send data faster than the radios can handle it, I'm going to have some corruption.  This RF24 library is pretty sophisticated, and I don't understand it nearly as well as I may need to.  But there was one thing that I could try that didn't take much effort.  I lowered the sample rate for the I2S devices.  I had it set at 11025 (one fourth of CD quality 44100), but I2S can support a sampling rate of 8000.  8000 would be pretty terrible for music, but for basic human voice it's not bad at all.

And it solved the distortion problem and most of the crackling/static problem.  Which is great, right?  Because those are the only problems I mentioned.  🙂 

But there was one more problem that I needed to fix.  When neither radio was transmitting, the DAC produced a random hum or buzzing.  The actual character of the noise changed each time a transmission ended.  I solved that by setting up a mechanism that only turns the DAC on when data is received.  If 100ms goes by with no new data, the DAC gets turned off until it is needed again.  Problem solved.

So all of the known problems were fixed.  Of course, that meant that a new one had to present itself.  Occasionally, the units would get into a state where one would transmit, but the other did not receive.  And once this happened, it tended to stay that way until I reset them.  After some testing, I realized that I only had to reset the one that was transmitting to make it work again.  

The example code that I used for the radios had some logic that would let it retry a transmission up to 100 times before reporting a failure.  Turns out that it was failing that way from time to time, and it never recovered from it.  That's when I started to dig into the library.  I found a function that would flush the transmit buffers, and that allowed the transmitter to recover.  And I reduced the retries to 25.  So now it still fails occasionally, but it quickly recovers.  But the bottom line is that the RF24 library has a lot of features and choices, and I could probably find a much better way to use it for my purposes.  

It also seems to me that a dropped packet of 32 bytes isn't going to matter very much.  Trying to resend it 25 times is probably going to cause more trouble than just letting it go.  But right now the transceivers are only about a foot apart.  Finding a good way to handle transmission errors will probably become a lot more important when they are in a more real world situation.

So I made a lot of progress today.  It seems that the hardware is all working exactly as expected, but I will probably have to keep tinkering with the RF24 library to make it work over reasonable distances.

This post was modified 1 month ago by JimG

   
ReplyQuote